Jay Lee ed19f877a5
Some checks failed
Build and test GAM / build (build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (build, 10, Build Intel Windows, windows-2022) (push) Has been cancelled
Build and test GAM / build (build, 11, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (build, 7, Build Intel MacOS, macos-13) (push) Has been cancelled
Build and test GAM / build (build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (test, 12, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (test, 13, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (test, 14, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (test, 15, Test Python 3.14-dev, ubuntu-24.04, 3.14-dev) (push) Has been cancelled
Build and test GAM / merge (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-apis (push) Has been cancelled
Improved checkconn
checkconn now works from a base set of hosts and then builds it's full
list based on our discovery APIs and the host in their discovery document.
2025-04-24 18:55:45 +00:00

77322 lines
3.3 MiB
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# GAM7
#
# Copyright 2024, All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
GAM is a command line tool which allows Administrators to control their Google Workspace domain and accounts.
For more information, see:
https://github.com/GAM-team/GAM
https://github.com/GAM-team/GAM/wiki
"""
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.06.10'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
#pylint: disable=wrong-import-position
import base64
import calendar as calendarlib
import codecs
import collections
import configparser
import csv
import datetime
from email.charset import add_charset, QP
from email.generator import Generator
from email.header import decode_header, Header
from email import message_from_string
from email.mime.application import MIMEApplication
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
from email.policy import SMTP as policySMTP
import hashlib
from html.entities import name2codepoint
from html.parser import HTMLParser
import http.client as http_client
import importlib
from importlib.metadata import version as lib_version
import io
import ipaddress
import json
import logging
from logging.handlers import RotatingFileHandler
import mimetypes
import multiprocessing
import os
import platform
import queue
import random
import re
from secrets import SystemRandom
import shlex
import signal
import smtplib
import socket
import sqlite3
import ssl
import string
import struct
import subprocess
import sys
from tempfile import TemporaryFile
try:
import termios
except ImportError:
# termios does not exist for Windows
pass
import threading
import time
from traceback import print_exc
import types
from urllib.parse import quote, quote_plus, unquote, urlencode, urlparse, parse_qs
import uuid
import warnings
import webbrowser
import wsgiref.simple_server
import wsgiref.util
import zipfile
# disable legacy stuff we don't use and isn't secure
os.environ['CRYPTOGRAPHY_OPENSSL_NO_LEGACY'] = "1"
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
# 10/2024 - I don't recall why we did this but PyInstaller
# 6.10.0+ does not like it. Only run this when we're not
# Frozen.
if not getattr(sys, 'frozen', False):
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
from dateutil.relativedelta import relativedelta
from pathvalidate import sanitize_filename, sanitize_filepath
import google.oauth2.credentials
import google.oauth2.id_token
import google.auth
from google.auth.jwt import Credentials as JWTCredentials
import google.oauth2.service_account
import google_auth_oauthlib.flow
import google_auth_httplib2
import httplib2
httplib2.RETRIES = 5
from passlib.hash import sha512_crypt
from filelock import FileLock
if platform.system() == 'Linux':
import distro
from gamlib import glaction
from gamlib import glapi as API
from gamlib import glcfg as GC
from gamlib import glclargs
from gamlib import glentity
from gamlib import glgapi as GAPI
from gamlib import glgdata as GDATA
from gamlib import glglobals as GM
from gamlib import glindent
from gamlib import glmsgs as Msg
from gamlib import glskus as SKU
from gamlib import gluprop as UProp
from gamlib import glverlibs
import gdata.apps.service
import gdata.apps.audit
import gdata.apps.audit.service
import gdata.apps.contacts
import gdata.apps.contacts.service
# Import local library, does not include discovery documents
import googleapiclient
import googleapiclient.discovery
import googleapiclient.errors
import googleapiclient.http
from iso8601 import iso8601
IS08601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%:z'
RFC2822_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S %z'
def ISOformatTimeStamp(timestamp):
return timestamp.isoformat('T', 'seconds')
def currentISOformatTimeStamp(timespec='milliseconds'):
return datetime.datetime.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec)
Act = glaction.GamAction()
Cmd = glclargs.GamCLArgs()
Ent = glentity.GamEntity()
Ind = glindent.GamIndent()
# Finding path method varies between Python source, PyInstaller and StaticX
if os.environ.get('STATICX_PROG_PATH', False):
# StaticX static executable
GM.Globals[GM.GAM_PATH] = os.path.dirname(os.environ['STATICX_PROG_PATH'])
GM.Globals[GM.GAM_TYPE] = 'staticx'
elif getattr(sys, 'frozen', False):
# Pyinstaller executable
GM.Globals[GM.GAM_PATH] = os.path.dirname(sys.executable)
GM.Globals[GM.GAM_TYPE] = 'pyinstaller'
else:
# Source code
GM.Globals[GM.GAM_PATH] = os.path.dirname(os.path.realpath(__file__))
GM.Globals[GM.GAM_TYPE] = 'pythonsource'
GIT_USER = 'GAM-team'
GAM = 'GAM'
GAM_URL = f'https://github.com/{GIT_USER}/{GAM}'
GAM_USER_AGENT = (f'{GAM} {__version__} - {GAM_URL} / '
f'{__author__} / '
f'Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {sys.version_info[3]} / '
f'{platform.platform()} {platform.machine()} /'
)
GAM_RELEASES = f'https://github.com/{GIT_USER}/{GAM}/releases'
GAM_WIKI = f'https://github.com/{GIT_USER}/{GAM}/wiki'
GAM_LATEST_RELEASE = f'https://api.github.com/repos/{GIT_USER}/{GAM}/releases/latest'
GAM_PROJECT_CREATION = 'GAM Project Creation'
GAM_PROJECT_CREATION_CLIENT_ID = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com'
TRUE = 'true'
FALSE = 'false'
TRUE_VALUES = [TRUE, 'on', 'yes', 'enabled', '1']
FALSE_VALUES = [FALSE, 'off', 'no', 'disabled', '0']
TRUE_FALSE = [TRUE, FALSE]
ERROR = 'ERROR'
ERROR_PREFIX = ERROR+': '
WARNING = 'WARNING'
WARNING_PREFIX = WARNING+': '
ONE_KILO_10_BYTES = 1000
ONE_MEGA_10_BYTES = 1000000
ONE_GIGA_10_BYTES = 1000000000
ONE_KILO_BYTES = 1024
ONE_MEGA_BYTES = 1048576
ONE_GIGA_BYTES = 1073741824
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 3600
SECONDS_PER_DAY = 86400
SECONDS_PER_WEEK = 604800
MAX_GOOGLE_SHEET_CELLS = 10000000 # See https://support.google.com/drive/answer/37603
MAX_LOCAL_GOOGLE_TIME_OFFSET = 30
SHARED_DRIVE_MAX_FILES_FOLDERS = 500000
UTF8 = 'utf-8'
UTF8_SIG = 'utf-8-sig'
EV_GAMCFGDIR = 'GAMCFGDIR'
EV_GAMCFGSECTION = 'GAMCFGSECTION'
EV_OLDGAMPATH = 'OLDGAMPATH'
FN_GAM_CFG = 'gam.cfg'
FN_LAST_UPDATE_CHECK_TXT = 'lastupdatecheck.txt'
FN_GAMCOMMANDS_TXT = 'GamCommands.txt'
MY_DRIVE = 'My Drive'
TEAM_DRIVE = 'Drive'
ROOT = 'root'
ROOTID = 'rootid'
ORPHANS = 'Orphans'
SHARED_WITHME = 'SharedWithMe'
SHARED_DRIVES = 'SharedDrives'
LOWERNUMERIC_CHARS = string.ascii_lowercase+string.digits
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS+string.ascii_uppercase
URL_SAFE_CHARS = ALPHANUMERIC_CHARS+'-._~'
PASSWORD_SAFE_CHARS = ALPHANUMERIC_CHARS+'!#$%&()*-./:;<=>?@[\\]^_{|}~'
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS+'-_.() '
CHAT_MESSAGEID_CHARS = string.ascii_lowercase+string.digits+'-'
ADMIN_ACCESS_OPTIONS = {'adminaccess', 'asadmin'}
OWNER_ACCESS_OPTIONS = {'owneraccess', 'asowner'}
# Python 3 values
DEFAULT_CSV_READ_MODE = 'r'
DEFAULT_FILE_APPEND_MODE = 'a'
DEFAULT_FILE_READ_MODE = 'r'
DEFAULT_FILE_WRITE_MODE = 'w'
# Google API constants
APPLICATION_VND_GOOGLE_APPS = 'application/vnd.google-apps.'
MIMETYPE_GA_DOCUMENT = f'{APPLICATION_VND_GOOGLE_APPS}document'
MIMETYPE_GA_DRAWING = f'{APPLICATION_VND_GOOGLE_APPS}drawing'
MIMETYPE_GA_FILE = f'{APPLICATION_VND_GOOGLE_APPS}file'
MIMETYPE_GA_FOLDER = f'{APPLICATION_VND_GOOGLE_APPS}folder'
MIMETYPE_GA_FORM = f'{APPLICATION_VND_GOOGLE_APPS}form'
MIMETYPE_GA_FUSIONTABLE = f'{APPLICATION_VND_GOOGLE_APPS}fusiontable'
MIMETYPE_GA_JAM = f'{APPLICATION_VND_GOOGLE_APPS}jam'
MIMETYPE_GA_MAP = f'{APPLICATION_VND_GOOGLE_APPS}map'
MIMETYPE_GA_PRESENTATION = f'{APPLICATION_VND_GOOGLE_APPS}presentation'
MIMETYPE_GA_SCRIPT = f'{APPLICATION_VND_GOOGLE_APPS}script'
MIMETYPE_GA_SCRIPT_JSON = f'{APPLICATION_VND_GOOGLE_APPS}script+json'
MIMETYPE_GA_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}shortcut'
MIMETYPE_GA_3P_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}drive-sdk'
MIMETYPE_GA_SITE = f'{APPLICATION_VND_GOOGLE_APPS}site'
MIMETYPE_GA_SPREADSHEET = f'{APPLICATION_VND_GOOGLE_APPS}spreadsheet'
MIMETYPE_TEXT_CSV = 'text/csv'
MIMETYPE_TEXT_HTML = 'text/html'
MIMETYPE_TEXT_PLAIN = 'text/plain'
GOOGLE_NAMESERVERS = ['8.8.8.8', '8.8.4.4']
GOOGLE_TIMECHECK_LOCATION = 'admin.googleapis.com'
NEVER_DATE = '1970-01-01'
NEVER_DATETIME = '1970-01-01 00:00'
NEVER_TIME = '1970-01-01T00:00:00.000Z'
NEVER_TIME_NOMS = '1970-01-01T00:00:00Z'
NEVER_END_DATE = '1969-12-31'
NEVER_START_DATE = NEVER_DATE
PROJECTION_CHOICE_MAP = {'basic': 'BASIC', 'full': 'FULL'}
REFRESH_EXPIRY = '1970-01-01T00:00:01Z'
REPLACE_GROUP_PATTERN = re.compile(r'\\(\d+)')
UNKNOWN = 'Unknown'
# Queries
ME_IN_OWNERS = "'me' in owners"
ME_IN_OWNERS_AND = ME_IN_OWNERS+" and "
AND_ME_IN_OWNERS = " and "+ME_IN_OWNERS
NOT_ME_IN_OWNERS = "not "+ME_IN_OWNERS
NOT_ME_IN_OWNERS_AND = NOT_ME_IN_OWNERS+" and "
AND_NOT_ME_IN_OWNERS = " and "+NOT_ME_IN_OWNERS
ANY_FOLDERS = "mimeType = '"+MIMETYPE_GA_FOLDER+"'"
MY_FOLDERS = ME_IN_OWNERS_AND+ANY_FOLDERS
NON_TRASHED = "trashed = false"
WITH_PARENTS = "'{0}' in parents"
ANY_NON_TRASHED_WITH_PARENTS = "trashed = false and '{0}' in parents"
ANY_NON_TRASHED_FOLDER_NAME = "mimeType = '"+MIMETYPE_GA_FOLDER+"' and name = '{0}' and trashed = false"
MY_NON_TRASHED_FOLDER_NAME = ME_IN_OWNERS_AND+ANY_NON_TRASHED_FOLDER_NAME
MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS = ME_IN_OWNERS_AND+"mimeType = '"+MIMETYPE_GA_FOLDER+"' and name = '{0}' and trashed = false and '{1}' in parents"
ANY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS = "mimeType = '"+MIMETYPE_GA_FOLDER+"' and name = '{0}' and trashed = false and '{1}' in parents"
WITH_ANY_FILE_NAME = "name = '{0}'"
WITH_MY_FILE_NAME = ME_IN_OWNERS_AND+WITH_ANY_FILE_NAME
WITH_OTHER_FILE_NAME = NOT_ME_IN_OWNERS_AND+WITH_ANY_FILE_NAME
AND_NOT_SHORTCUT = " and mimeType != '"+MIMETYPE_GA_SHORTCUT+"'"
# Program return codes
UNKNOWN_ERROR_RC = 1
USAGE_ERROR_RC = 2
SOCKET_ERROR_RC = 3
GOOGLE_API_ERROR_RC = 4
NETWORK_ERROR_RC = 5
FILE_ERROR_RC = 6
MEMORY_ERROR_RC = 7
KEYBOARD_INTERRUPT_RC = 8
HTTP_ERROR_RC = 9
SCOPES_NOT_AUTHORIZED_RC = 10
DATA_ERROR_RC = 11
API_ACCESS_DENIED_RC = 12
CONFIG_ERROR_RC = 13
SYSTEM_ERROR_RC = 14
NO_SCOPES_FOR_API_RC = 15
CLIENT_SECRETS_JSON_REQUIRED_RC = 16
OAUTH2SERVICE_JSON_REQUIRED_RC = 16
OAUTH2_TXT_REQUIRED_RC = 16
INVALID_JSON_RC = 17
JSON_ALREADY_EXISTS_RC = 17
AUTHENTICATION_TOKEN_REFRESH_ERROR_RC = 18
HARD_ERROR_RC = 19
# Information
ENTITY_IS_A_USER_RC = 20
ENTITY_IS_A_USER_ALIAS_RC = 21
ENTITY_IS_A_GROUP_RC = 22
ENTITY_IS_A_GROUP_ALIAS_RC = 23
ENTITY_IS_AN_UNMANAGED_ACCOUNT_RC = 24
ORGUNIT_NOT_EMPTY_RC = 25
CHECK_USER_GROUPS_ERROR_RC = 29
ORPHANS_COLLECTED_RC = 30
# Warnings/Errors
ACTION_FAILED_RC = 50
ACTION_NOT_PERFORMED_RC = 51
INVALID_ENTITY_RC = 52
BAD_REQUEST_RC = 53
ENTITY_IS_NOT_UNIQUE_RC = 54
DATA_NOT_AVALIABLE_RC = 55
ENTITY_DOES_NOT_EXIST_RC = 56
ENTITY_DUPLICATE_RC = 57
ENTITY_IS_NOT_AN_ALIAS_RC = 58
ENTITY_IS_UKNOWN_RC = 59
NO_ENTITIES_FOUND_RC = 60
INVALID_DOMAIN_RC = 61
INVALID_DOMAIN_VALUE_RC = 62
INVALID_TOKEN_RC = 63
JSON_LOADS_ERROR_RC = 64
MULTIPLE_DELETED_USERS_FOUND_RC = 65
MULTIPLE_PROJECT_FOLDERS_FOUND_RC = 65
STDOUT_STDERR_ERROR_RC = 66
INSUFFICIENT_PERMISSIONS_RC = 67
REQUEST_COMPLETED_NO_RESULTS_RC = 71
REQUEST_NOT_COMPLETED_RC = 72
SERVICE_NOT_APPLICABLE_RC = 73
TARGET_DRIVE_SPACE_ERROR_RC = 74
USER_REQUIRED_TO_CHANGE_PASSWORD_ERROR_RC = 75
USER_SUSPENDED_ERROR_RC = 76
NO_CSV_DATA_TO_UPLOAD_RC = 77
NO_SA_ACCESS_CONTEXT_MANAGER_EDITOR_ROLE_RC = 78
ACCESS_POLICY_ERROR_RC = 79
YUBIKEY_CONNECTION_ERROR_RC = 80
YUBIKEY_INVALID_KEY_TYPE_RC = 81
YUBIKEY_INVALID_SLOT_RC = 82
YUBIKEY_INVALID_PIN_RC = 83
YUBIKEY_APDU_ERROR_RC = 84
YUBIKEY_VALUE_ERROR_RC = 85
YUBIKEY_MULTIPLE_CONNECTED_RC = 86
YUBIKEY_NOT_FOUND_RC = 87
# Multiprocessing lock
mplock = None
# stdin/stdout/stderr
def readStdin(prompt):
return input(prompt)
def stdErrorExit(e):
try:
sys.stderr.write(f'\n{ERROR_PREFIX}{str(e)}\n')
except IOError:
pass
sys.exit(STDOUT_STDERR_ERROR_RC)
def writeStdout(data):
try:
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).write(data)
except IOError as e:
stdErrorExit(e)
def flushStdout():
try:
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).flush()
except IOError as e:
stdErrorExit(e)
def writeStderr(data):
flushStdout()
try:
GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr).write(data)
except IOError as e:
stdErrorExit(e)
def flushStderr():
try:
GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr).flush()
except IOError as e:
stdErrorExit(e)
# Error messages
def setSysExitRC(sysRC):
GM.Globals[GM.SYSEXITRC] = sysRC
def stderrErrorMsg(message):
writeStderr(f'\n{ERROR_PREFIX}{message}\n')
def stderrWarningMsg(message):
writeStderr(f'\n{WARNING_PREFIX}{message}\n')
def systemErrorExit(sysRC, message):
if message:
stderrErrorMsg(message)
sys.exit(sysRC)
def printErrorMessage(sysRC, message):
setSysExitRC(sysRC)
writeStderr(f'\n{Ind.Spaces()}{ERROR_PREFIX}{message}\n')
def printWarningMessage(sysRC, message):
setSysExitRC(sysRC)
writeStderr(f'\n{Ind.Spaces()}{WARNING_PREFIX}{message}\n')
def supportsColoredText():
"""Determines if the current terminal environment supports colored text.
Returns:
Bool, True if the current terminal environment supports colored text via
ANSI escape characters.
"""
# Make a rudimentary check for Windows. Though Windows does seem to support
# colorization with VT100 emulation, it is disabled by default. Therefore,
# we'll simply disable it in GAM on Windows for now.
return not sys.platform.startswith('win')
def createColoredText(text, color):
"""Uses ANSI escape characters to create colored text in supported terminals.
See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
Args:
text: String, The text to colorize using ANSI escape characters.
color: String, An ANSI escape sequence denoting the color of the text to be
created. See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
Returns:
The input text with appropriate ANSI escape characters to create
colorization in a supported terminal environment.
"""
END_COLOR_SEQUENCE = '\033[0m' # Ends the applied color formatting
if supportsColoredText():
return color + text + END_COLOR_SEQUENCE
return text # Hand back the plain text, uncolorized.
def createRedText(text):
"""Uses ANSI encoding to create red colored text if supported."""
return createColoredText(text, '\033[91m')
def createGreenText(text):
"""Uses ANSI encoding to create green colored text if supported."""
return createColoredText(text, '\u001b[32m')
def createYellowText(text):
"""Uses ANSI encoding to create yellow text if supported."""
return createColoredText(text, '\u001b[33m')
def executeBatch(dbatch):
dbatch.execute()
if GC.Values[GC.INTER_BATCH_WAIT] > 0:
time.sleep(GC.Values[GC.INTER_BATCH_WAIT])
def _stripControlCharsFromName(name):
for cc in ['\x00', '\r', '\n']:
name = name.replace(cc, '')
return name
class LazyLoader(types.ModuleType):
"""Lazily import a module, mainly to avoid pulling in large dependencies.
`contrib`, and `ffmpeg` are examples of modules that are large and not always
needed, and this allows them to only be loaded when they are used.
"""
# The lint error here is incorrect.
def __init__(self, local_name, parent_module_globals, name):
self._local_name = local_name
self._parent_module_globals = parent_module_globals
super().__init__(name)
def _load(self):
# Import the target module and insert it into the parent's namespace
module = importlib.import_module(self.__name__)
self._parent_module_globals[self._local_name] = module
# Update this object's dict so that if someone keeps a reference to the
# LazyLoader, lookups are efficient (__getattr__ is only called on lookups
# that fail).
self.__dict__.update(module.__dict__)
return module
def __getattr__(self, item):
module = self._load()
return getattr(module, item)
def __dir__(self):
module = self._load()
return dir(module)
yubikey = LazyLoader('yubikey', globals(), 'gam.gamlib.yubikey')
# gam yubikey resetpvi [yubikey_serialnumber <String>]
def doResetYubiKeyPIV():
new_data = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'yubikeyserialnumber':
new_data['yubikey_serial_number'] = getInteger()
else:
unknownArgumentExit()
yk = yubikey.YubiKey(new_data)
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
class _DeHTMLParser(HTMLParser): #pylint: disable=abstract-method
def __init__(self):
HTMLParser.__init__(self)
self.__text = []
def handle_data(self, data):
self.__text.append(data)
def handle_charref(self, name):
self.__text.append(chr(int(name[1:], 16)) if name.startswith('x') else chr(int(name)))
def handle_entityref(self, name):
cp = name2codepoint.get(name)
if cp:
self.__text.append(chr(cp))
else:
self.__text.append('&'+name)
def handle_starttag(self, tag, attrs):
if tag == 'p':
self.__text.append('\n\n')
elif tag == 'br':
self.__text.append('\n')
elif tag == 'a':
for attr in attrs:
if attr[0] == 'href':
self.__text.append(f'({attr[1]}) ')
break
elif tag == 'div':
if not attrs:
self.__text.append('\n')
elif tag in {'http:', 'https'}:
self.__text.append(f' ({tag}//{attrs[0][0]}) ')
def handle_startendtag(self, tag, attrs):
if tag == 'br':
self.__text.append('\n\n')
def text(self):
return re.sub(r'\n{2}\n+', '\n\n', re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
def dehtml(text):
parser = _DeHTMLParser()
parser.feed(str(text))
parser.close()
return parser.text()
def currentCount(i, count):
return f' ({i}/{count})' if (count > GC.Values[GC.SHOW_COUNTS_MIN]) else ''
def currentCountNL(i, count):
return f' ({i}/{count})\n' if (count > GC.Values[GC.SHOW_COUNTS_MIN]) else '\n'
# Format a key value list
# key, value -> "key: value" + ", " if not last item
# key, '' -> "key:" + ", " if not last item
# key, None -> "key" + " " if not last item
def formatKeyValueList(prefixStr, kvList, suffixStr):
msg = prefixStr
i = 0
l = len(kvList)
while i < l:
if isinstance(kvList[i], (bool, float, int)):
msg += str(kvList[i])
else:
msg += kvList[i]
i += 1
if i < l:
val = kvList[i]
if (val is not None) or (i == l-1):
msg += ':'
if (val is not None) and (not isinstance(val, str) or val):
msg += ' '
if isinstance(val, (bool, float, int)):
msg += str(val)
else:
msg += val
i += 1
if i < l:
msg += ', '
else:
i += 1
if i < l:
msg += ' '
msg += suffixStr
return msg
# Something's wrong with CustomerID??
def accessErrorMessage(cd, errMsg=None):
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
try:
callGAPI(cd.customers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerKey=GC.Values[GC.CUSTOMER_ID], fields='id')
except (GAPI.badRequest, GAPI.invalidInput):
return formatKeyValueList('',
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
Msg.INVALID],
'')
except GAPI.resourceNotFound:
return formatKeyValueList('',
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
Msg.DOES_NOT_EXIST],
'')
except GAPI.forbidden:
return formatKeyValueList('',
Ent.FormatEntityValueList([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID],
Ent.DOMAIN, GC.Values[GC.DOMAIN],
Ent.USER, GM.Globals[GM.ADMIN]])+[Msg.ACCESS_FORBIDDEN],
'')
if errMsg:
return formatKeyValueList('',
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
errMsg],
'')
return None
def accessErrorExit(cd, errMsg=None):
systemErrorExit(INVALID_DOMAIN_RC, accessErrorMessage(cd or buildGAPIObject(API.DIRECTORY), errMsg))
def accessErrorExitNonDirectory(api, errMsg):
systemErrorExit(API_ACCESS_DENIED_RC,
formatKeyValueList('',
Ent.FormatEntityValueList([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID],
Ent.DOMAIN, GC.Values[GC.DOMAIN],
Ent.API, api])+[errMsg],
''))
def ClientAPIAccessDeniedExit(errMsg=None):
stderrErrorMsg(Msg.API_ACCESS_DENIED)
if errMsg:
stderrErrorMsg(errMsg)
missingScopes = API.getClientScopesSet(GM.Globals[GM.CURRENT_CLIENT_API])-GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
if missingScopes:
writeStderr(Msg.API_CHECK_CLIENT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2_CLIENT_ID],
','.join(sorted(missingScopes))))
systemErrorExit(API_ACCESS_DENIED_RC, None)
def SvcAcctAPIAccessDenied():
_getSvcAcctData()
if (GM.Globals[GM.CURRENT_SVCACCT_API] == API.GMAIL and
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] and
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES][0] == API.GMAIL_SEND_SCOPE):
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED)
stderrErrorMsg(Msg.API_ACCESS_DENIED)
apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]))
writeStderr(Msg.API_CHECK_SVCACCT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'],
apiOrScopes,
GM.Globals[GM.CURRENT_SVCACCT_USER] or _getAdminEmail()))
def SvcAcctAPIAccessDeniedExit():
SvcAcctAPIAccessDenied()
systemErrorExit(API_ACCESS_DENIED_RC, None)
def SvcAcctAPIDisabledExit():
if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]:
ClientAPIAccessDeniedExit()
if GM.Globals[GM.CURRENT_SVCACCT_API]:
stderrErrorMsg(Msg.SERVICE_ACCOUNT_API_DISABLED.format(API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API])))
systemErrorExit(API_ACCESS_DENIED_RC, None)
systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED)
def APIAccessDeniedExit():
if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]:
ClientAPIAccessDeniedExit()
if GM.Globals[GM.CURRENT_SVCACCT_API]:
SvcAcctAPIAccessDeniedExit()
systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED)
def checkEntityDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0):
message = accessErrorMessage(cd)
if message:
systemErrorExit(INVALID_DOMAIN_RC, message)
entityDoesNotExistWarning(entityType, entityName, i, count)
def checkEntityAFDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0):
message = accessErrorMessage(cd)
if message:
systemErrorExit(INVALID_DOMAIN_RC, message)
entityActionFailedWarning([entityType, entityName], Msg.DOES_NOT_EXIST, i, count)
def checkEntityItemValueAFDNEorAccessErrorExit(cd, entityType, entityName, itemType, itemValue, i=0, count=0):
message = accessErrorMessage(cd)
if message:
systemErrorExit(INVALID_DOMAIN_RC, message)
entityActionFailedWarning([entityType, entityName, itemType, itemValue], Msg.DOES_NOT_EXIST, i, count)
def invalidClientSecretsJsonExit(errMsg):
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.CLIENT_SECRETS_JSON_FILE), GC.Values[GC.CLIENT_SECRETS_JSON], errMsg))
writeStderr(Msg.INSTRUCTIONS_CLIENT_SECRETS_JSON)
systemErrorExit(CLIENT_SECRETS_JSON_REQUIRED_RC, None)
def invalidOauth2serviceJsonExit(errMsg):
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.OAUTH2SERVICE_JSON_FILE), GC.Values[GC.OAUTH2SERVICE_JSON], errMsg))
writeStderr(Msg.INSTRUCTIONS_OAUTH2SERVICE_JSON)
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, None)
def invalidOauth2TxtExit(errMsg):
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.OAUTH2_TXT_FILE), GC.Values[GC.OAUTH2_TXT], errMsg))
writeStderr(Msg.EXECUTE_GAM_OAUTH_CREATE)
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, None)
def expiredRevokedOauth2TxtExit():
stderrErrorMsg(Msg.IS_EXPIRED_OR_REVOKED.format(Ent.Singular(Ent.OAUTH2_TXT_FILE), GC.Values[GC.OAUTH2_TXT]))
writeStderr(Msg.EXECUTE_GAM_OAUTH_CREATE)
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, None)
def invalidDiscoveryJsonExit(fileName, errMsg):
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.DISCOVERY_JSON_FILE), fileName, errMsg))
systemErrorExit(INVALID_JSON_RC, None)
def entityActionFailedExit(entityValueList, errMsg, i=0, count=0):
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), errMsg],
currentCountNL(i, count)))
def entityDoesNotExistExit(entityType, entityName, i=0, count=0, errMsg=None):
Cmd.Backup()
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
systemErrorExit(ENTITY_DOES_NOT_EXIST_RC, formatKeyValueList(Ind.Spaces(),
[Ent.Singular(entityType), entityName, errMsg or Msg.DOES_NOT_EXIST],
currentCountNL(i, count)))
def entityDoesNotHaveItemExit(entityValueList, i=0, count=0):
Cmd.Backup()
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
systemErrorExit(ENTITY_DOES_NOT_EXIST_RC, formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
currentCountNL(i, count)))
def entityIsNotUniqueExit(entityType, entityName, valueType, valueList, i=0, count=0):
Cmd.Backup()
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
systemErrorExit(ENTITY_IS_NOT_UNIQUE_RC, formatKeyValueList(Ind.Spaces(),
[Ent.Singular(entityType), entityName, Msg.IS_NOT_UNIQUE.format(Ent.Plural(valueType), ','.join(valueList))],
currentCountNL(i, count)))
def usageErrorExit(message, extraneous=False):
writeStderr(Cmd.CommandLineWithBadArgumentMarked(extraneous))
stderrErrorMsg(message)
writeStderr(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
writeStderr(Msg.HELP_WIKI.format(GAM_WIKI))
sys.exit(USAGE_ERROR_RC)
def csvFieldErrorExit(fieldName, fieldNames, backupArg=False, checkForCharset=False):
if backupArg:
Cmd.Backup()
if checkForCharset and Cmd.Previous() == 'charset':
Cmd.Backup()
Cmd.Backup()
usageErrorExit(Msg.HEADER_NOT_FOUND_IN_CSV_HEADERS.format(fieldName, ','.join(fieldNames)))
def csvDataAlreadySavedErrorExit():
Cmd.Backup()
usageErrorExit(Msg.CSV_DATA_ALREADY_SAVED)
# The last thing shown is unknown
def unknownArgumentExit():
Cmd.Backup()
usageErrorExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1])
# Argument describes what's expected
def expectedArgumentExit(problem, argument):
usageErrorExit(f'{problem}: {Msg.EXPECTED} <{argument}>')
def blankArgumentExit(argument):
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_BLANK][1], f'{Msg.NON_BLANK} {argument}')
def emptyArgumentExit(argument):
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_EMPTY][1], f'{Msg.NON_EMPTY} {argument}')
def invalidArgumentExit(argument):
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1], argument)
def missingArgumentExit(argument):
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_MISSING][1], argument)
def deprecatedArgument(argument):
Cmd.Backup()
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
Cmd.Advance()
stderrWarningMsg(f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_DEPRECATED][1]}: {Msg.IGNORED} <{argument}>')
def deprecatedArgumentExit(argument):
usageErrorExit(f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_DEPRECATED][1]}: <{argument}>')
def deprecatedCommandExit():
systemErrorExit(USAGE_ERROR_RC, Msg.SITES_COMMAND_DEPRECATED.format(Cmd.CommandDeprecated()))
# Choices is the valid set of choices that was expected
def formatChoiceList(choices):
choiceList = [c if c else "''" for c in choices]
if len(choiceList) <= 5:
return '|'.join(choiceList)
return '|'.join(sorted(choiceList))
def invalidChoiceExit(choice, choices, backupArg):
if backupArg:
Cmd.Backup()
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID_CHOICE][1].format(choice), formatChoiceList(choices))
def missingChoiceExit(choices):
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_MISSING][1], formatChoiceList(choices))
# Check if argument present
def checkArgumentPresent(choices, required=False):
choiceList = choices if isinstance(choices, (list, set)) else [choices]
if Cmd.ArgumentsRemaining():
choice = Cmd.Current().strip().lower().replace('_', '')
if choice:
if choice in choiceList:
Cmd.Advance()
return True
if not required:
return False
invalidChoiceExit(choice, choiceList, False)
elif not required:
return False
missingChoiceExit(choiceList)
# Check that there are no extraneous arguments at the end of the command line
def checkForExtraneousArguments():
if Cmd.ArgumentsRemaining():
usageErrorExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_EXTRANEOUS][[1, 0][Cmd.MultipleArgumentsRemaining()]], extraneous=True)
# Check that an argument remains, get an argument, downshift, delete underscores
def checkGetArgument():
if Cmd.ArgumentsRemaining():
argument = Cmd.Current().lower()
if argument:
Cmd.Advance()
return argument.replace('_', '')
missingArgumentExit(Cmd.OB_ARGUMENT)
# Get an argument, downshift, delete underscores
def getArgument():
argument = Cmd.Current().lower()
if argument:
Cmd.Advance()
return argument.replace('_', '')
missingArgumentExit(Cmd.OB_ARGUMENT)
# Get an argument, downshift, delete underscores
# An empty argument is allowed
def getArgumentEmptyAllowed():
argument = Cmd.Current().lower()
Cmd.Advance()
return argument.replace('_', '')
def getACLRoles(aclRolesMap):
roles = []
for role in getString(Cmd.OB_ROLE_LIST, minLen=0).strip().lower().replace(',', ' ').split():
if role == 'all':
for arole in aclRolesMap:
roles.append(aclRolesMap[arole])
elif role in aclRolesMap:
roles.append(aclRolesMap[role])
else:
invalidChoiceExit(role, aclRolesMap, True)
return set(roles)
def getBoolean(defaultValue=True):
if Cmd.ArgumentsRemaining():
boolean = Cmd.Current().strip().lower()
if boolean in TRUE_VALUES:
Cmd.Advance()
return True
if boolean in FALSE_VALUES:
Cmd.Advance()
return False
if defaultValue is not None:
if not Cmd.Current().strip(): # If current argument is empty, skip over it
Cmd.Advance()
return defaultValue
invalidChoiceExit(boolean, TRUE_FALSE, False)
if defaultValue is not None:
return defaultValue
missingChoiceExit(TRUE_FALSE)
def getCharSet():
if checkArgumentPresent('charset'):
return getString(Cmd.OB_CHAR_SET)
return GC.Values[GC.CHARSET]
DEFAULT_CHOICE = 'defaultChoice'
CHOICE_ALIASES = 'choiceAliases'
MAP_CHOICE = 'mapChoice'
NO_DEFAULT = 'NoDefault'
def getChoice(choices, **opts):
if Cmd.ArgumentsRemaining():
choice = Cmd.Current().strip().lower()
if choice or '' in choices:
if choice in opts.get(CHOICE_ALIASES, []):
choice = opts[CHOICE_ALIASES][choice]
if choice not in choices:
choice = choice.replace('_', '').replace('-', '')
if choice in opts.get(CHOICE_ALIASES, []):
choice = opts[CHOICE_ALIASES][choice]
if choice in choices:
Cmd.Advance()
return choice if not opts.get(MAP_CHOICE, False) else choices[choice]
if opts.get(DEFAULT_CHOICE, NO_DEFAULT) != NO_DEFAULT:
return opts[DEFAULT_CHOICE]
invalidChoiceExit(choice, choices, False)
elif opts.get(DEFAULT_CHOICE, NO_DEFAULT) != NO_DEFAULT:
return opts[DEFAULT_CHOICE]
missingChoiceExit(choices)
def getChoiceAndValue(item, choices, delimiter):
if not Cmd.ArgumentsRemaining() or Cmd.Current().find(delimiter) == -1:
return (None, None)
choice, value = Cmd.Current().strip().split(delimiter, 1)
choice = choice.strip().lower()
value = value.strip()
if choice in choices:
if value:
Cmd.Advance()
return (choice, value)
missingArgumentExit(item)
invalidChoiceExit(choice, choices, False)
SUSPENDED_ARGUMENTS = {'notsuspended', 'suspended', 'issuspended'}
SUSPENDED_CHOICE_MAP = {'notsuspended': False, 'suspended': True}
def _getIsSuspended(myarg):
if myarg in SUSPENDED_CHOICE_MAP:
return SUSPENDED_CHOICE_MAP[myarg]
return getBoolean()
ARCHIVED_ARGUMENTS = {'notarchived', 'archived', 'isarchived'}
ARCHIVED_CHOICE_MAP = {'notarchived': False, 'archived': True}
def _getIsArchived(myarg):
if myarg in ARCHIVED_CHOICE_MAP:
return ARCHIVED_CHOICE_MAP[myarg]
return getBoolean()
def _getOptionalIsSuspendedIsArchived():
isSuspended = isArchived = None
while True:
if Cmd.PeekArgumentPresent(SUSPENDED_ARGUMENTS):
isSuspended = getChoice(SUSPENDED_CHOICE_MAP, defaultChoice=None, mapChoice=True)
if isSuspended is None:
isSuspended = getBoolean()
elif Cmd.PeekArgumentPresent(ARCHIVED_ARGUMENTS):
isArchived = getChoice(ARCHIVED_CHOICE_MAP, defaultChoice=None, mapChoice=True)
if isArchived is None:
isArchived = getBoolean()
else:
break
return isSuspended, isArchived
CALENDAR_COLOR_MAP = {
'amethyst': 24, 'avocado': 10, 'banana': 12, 'basil': 8, 'birch': 20, 'blueberry': 16,
'cherryblossom': 22, 'citron': 11, 'cobalt': 15, 'cocoa': 1, 'eucalyptus': 7, 'flamingo': 2,
'grape': 23, 'graphite': 19, 'lavender': 17, 'mango': 6, 'peacock': 14, 'pistachio': 9,
'pumpkin': 5, 'radicchio': 21, 'sage': 13, 'tangerine': 4, 'tomato': 3, 'wisteria': 18,
}
CALENDAR_EVENT_COLOR_MAP = {
'banana': 5, 'basil': 10, 'blueberry': 9, 'flamingo': 4, 'graphite': 8, 'grape': 3,
'lavender': 1, 'peacock': 7, 'sage': 2, 'tangerine': 6, 'tomato': 11,
}
GOOGLE_COLOR_MAP = {
'asparagus': '#7bd148', 'bluevelvet': '#9a9cff', 'bubblegum': '#f691b2', 'cardinal': '#f83a22',
'chocolateicecream': '#ac725e', 'denim': '#9fc6e7', 'desertsand': '#fbe983', 'earthworm': '#cca6ac',
'macaroni': '#fad165', 'marsorange': '#ff7537', 'mountaingray': '#cabdbf', 'mountaingrey': '#cabdbf',
'mouse': '#8f8f8f', 'oldbrickred': '#d06b64', 'pool': '#9fe1e7', 'purpledino': '#b99aff',
'purplerain': '#cd74e6', 'rainysky': '#4986e7', 'seafoam': '#92e1c0', 'slimegreen': '#b3dc6c',
'spearmint': '#42d692', 'toyeggplant': '#a47ae2', 'vernfern': '#16a765', 'wildstrawberries': '#fa573c',
'yellowcab': '#ffad46',
}
WEB_COLOR_MAP = {
'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff', 'aquamarine': '#7fffd4',
'azure': '#f0ffff', 'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000',
'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2', 'brown': '#a52a2a',
'burlywood': '#deb887', 'cadetblue': '#5f9ea0', 'chartreuse': '#7fff00', 'chocolate': '#d2691e',
'coral': '#ff7f50', 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc', 'crimson': '#dc143c',
'cyan': '#00ffff', 'darkblue': '#00008b', 'darkcyan': '#008b8b', 'darkgoldenrod': '#b8860b',
'darkgray': '#a9a9a9', 'darkgrey': '#a9a9a9', 'darkgreen': '#006400', 'darkkhaki': '#bdb76b',
'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc',
'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b',
'darkslategray': '#2f4f4f', 'darkslategrey': '#2f4f4f', 'darkturquoise': '#00ced1', 'darkviolet': '#9400d3',
'deeppink': '#ff1493', 'deepskyblue': '#00bfff', 'dimgray': '#696969', 'dimgrey': '#696969',
'dodgerblue': '#1e90ff', 'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22',
'fuchsia': '#ff00ff', 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700',
'goldenrod': '#daa520', 'gray': '#808080', 'grey': '#808080', 'green': '#008000',
'greenyellow': '#adff2f', 'honeydew': '#f0fff0', 'hotpink': '#ff69b4', 'indianred': '#cd5c5c',
'indigo': '#4b0082', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa',
'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd', 'lightblue': '#add8e6',
'lightcoral': '#f08080', 'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2', 'lightgray': '#d3d3d3',
'lightgrey': '#d3d3d3', 'lightgreen': '#90ee90', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a',
'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899', 'lightslategrey': '#778899',
'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32',
'linen': '#faf0e6', 'magenta': '#ff00ff', 'maroon': '#800000', 'mediumaquamarine': '#66cdaa',
'mediumblue': '#0000cd', 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370db', 'mediumseagreen': '#3cb371',
'mediumslateblue': '#7b68ee', 'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc', 'mediumvioletred': '#c71585',
'midnightblue': '#191970', 'mintcream': '#f5fffa', 'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5',
'navajowhite': '#ffdead', 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000',
'olivedrab': '#6b8e23', 'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6',
'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee', 'palevioletred': '#db7093',
'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9', 'peru': '#cd853f', 'pink': '#ffc0cb',
'plum': '#dda0dd', 'powderblue': '#b0e0e6', 'purple': '#800080', 'red': '#ff0000',
'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513', 'salmon': '#fa8072',
'sandybrown': '#f4a460', 'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d',
'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090',
'slategrey': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4',
'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347',
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32',
}
COLORHEX_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$')
COLORHEX_FORMAT_REQUIRED = 'ColorName|ColorHex'
def getColor():
if Cmd.ArgumentsRemaining():
color = Cmd.Current().strip().lower()
if color in GOOGLE_COLOR_MAP:
Cmd.Advance()
return GOOGLE_COLOR_MAP[color]
if color in WEB_COLOR_MAP:
Cmd.Advance()
return WEB_COLOR_MAP[color]
tg = COLORHEX_PATTERN.match(color)
if tg:
Cmd.Advance()
return tg.group(0)
invalidArgumentExit(COLORHEX_FORMAT_REQUIRED)
missingArgumentExit(COLORHEX_FORMAT_REQUIRED)
LABEL_COLORS = [
'#000000', '#076239', '#0b804b', '#149e60', '#16a766', '#1a764d', '#1c4587', '#285bac',
'#2a9c68', '#3c78d8', '#3dc789', '#41236d', '#434343', '#43d692', '#44b984', '#4a86e8',
'#653e9b', '#666666', '#68dfa9', '#6d9eeb', '#822111', '#83334c', '#89d3b2', '#8e63ce',
'#999999', '#a0eac9', '#a46a21', '#a479e2', '#a4c2f4', '#aa8831', '#ac2b16', '#b65775',
'#b694e8', '#b9e4d0', '#c6f3de', '#c9daf8', '#cc3a21', '#cccccc', '#cf8933', '#d0bcf1',
'#d5ae49', '#e07798', '#e4d7f5', '#e66550', '#eaa041', '#efa093', '#efefef', '#f2c960',
'#f3f3f3', '#f691b3', '#f6c5be', '#f7a7c0', '#fad165', '#fb4c2f', '#fbc8d9', '#fcda83',
'#fcdee8', '#fce8b3', '#fef1d1', '#ffad47', '#ffbc6b', '#ffd6a2', '#ffe6c7', '#ffffff',
]
LABEL_BACKGROUND_COLORS = [
'#16a765', '#2da2bb', '#42d692', '#4986e7', '#98d7e4', '#a2dcc1',
'#b3efd3', '#b6cff5', '#b99aff', '#c2c2c2', '#cca6ac', '#e3d7ff',
'#e7e7e7', '#ebdbde', '#f2b2a8', '#f691b2', '#fb4c2f', '#fbd3e0',
'#fbe983', '#fdedc1', '#ff7537', '#ffad46', '#ffc8af', '#ffdeb5',
]
LABEL_TEXT_COLORS = [
'#04502e', '#094228', '#0b4f30', '#0d3472', '#0d3b44', '#3d188e',
'#464646', '#594c05', '#662e37', '#684e07', '#711a36', '#7a2e0b',
'#7a4706', '#8a1c0a', '#994a64', '#ffffff',
]
def getLabelColor(colorType):
if Cmd.ArgumentsRemaining():
color = Cmd.Current().strip().lower()
tg = COLORHEX_PATTERN.match(color)
if tg:
color = tg.group(0)
if color in colorType or color in LABEL_COLORS:
Cmd.Advance()
return color
elif color.startswith('custom:'):
tg = COLORHEX_PATTERN.match(color[7:])
if tg:
Cmd.Advance()
return tg.group(0)
invalidArgumentExit('|'.join(colorType))
missingArgumentExit(Cmd.OB_LABEL_COLOR_HEX)
# Language codes used in Drive Labels/Youtube
BCP47_LANGUAGE_CODES_MAP = {
'ar-sa': 'ar-SA', 'cs-cz': 'cs-CZ', 'da-dk': 'da-DK', 'de-de': 'de-DE', #Arabic Saudi Arabia, Czech Czech Republic, Danish Denmark, German Germany
'el-gr': 'el-GR', 'en-au': 'en-AU', 'en-gb': 'en-GB', 'en-ie': 'en-IE', #Modern Greek Greece, English Australia, English United Kingdom, English Ireland
'en-us': 'en-US', 'en-za': 'en-ZA', 'es-es': 'es-ES', 'es-mx': 'es-MX', #English United States, English South Africa, Spanish Spain, Spanish Mexico
'fi-fi': 'fi-FI', 'fr-ca': 'fr-CA', 'fr-fr': 'fr-FR', 'he-il': 'he-IL', #Finnish Finland, French Canada, French France, Hebrew Israel
'hi-in': 'hi-IN', 'hu-hu': 'hu-HU', 'id-id': 'id-ID', 'it-it': 'it-IT', #Hindi India, Hungarian Hungary, Indonesian Indonesia, Italian Italy
'ja-jp': 'ja-JP', 'ko-kr': 'ko-KR', 'nl-be': 'nl-BE', 'nl-nl': 'nl-NL', #Japanese Japan, Korean Republic of Korea, Dutch Belgium, Dutch Netherlands
'no-no': 'no-NO', 'pl-pl': 'pl-PL', 'pt-br': 'pt-BR', 'pt-pt': 'pt-PT', #Norwegian Norway, Polish Poland, Portuguese Brazil, Portuguese Portugal
'ro-ro': 'ro-RO', 'ru-ru': 'ru-RU', 'sk-sk': 'sk-SK', 'sv-se': 'sv-SE', #Romanian Romania, Russian Russian Federation, Slovak Slovakia, Swedish Sweden
'th-th': 'th-TH', 'tr-tr': 'tr-TR', 'zh-cn': 'zh-CN', 'zh-hk': 'zh-HK', #Thai Thailand, Turkish Turkey, Chinese China, Chinese Hong Kong
'zh-tw': 'zh-TW' #Chinese Taiwan
}
# Valid language codes
LANGUAGE_CODES_MAP = {
'ach': 'ach', 'af': 'af', 'ag': 'ga', 'ak': 'ak', 'am': 'am', 'ar': 'ar', 'az': 'az', #Luo, Afrikaans, Irish, Akan, Amharic, Arabica, Azerbaijani
'be': 'be', 'bem': 'bem', 'bg': 'bg', 'bn': 'bn', 'br': 'br', 'bs': 'bs', 'ca': 'ca', #Belarusian, Bemba, Bulgarian, Bengali, Breton, Bosnian, Catalan
'chr': 'chr', 'ckb': 'ckb', 'co': 'co', 'crs': 'crs', 'cs': 'cs', 'cy': 'cy', 'da': 'da', #Cherokee, Kurdish (Sorani), Corsican, Seychellois Creole, Czech, Welsh, Danish
'de': 'de', 'ee': 'ee', 'el': 'el', 'en': 'en', 'en-ca': 'en-CA', 'en-gb': 'en-GB', 'en-us': 'en-US', 'eo': 'eo', #German, Ewe, Greek, English, English (CA), English (UK), English (US), Esperanto
'es': 'es', 'es-419': 'es-419', 'et': 'et', 'eu': 'eu', 'fa': 'fa', 'fi': 'fi', 'fil': 'fil', 'fo': 'fo', #Spanish, Spanish (Latin American), Estonian, Basque, Persian, Finnish, Filipino, Faroese
'fr': 'fr', 'fr-ca': 'fr-CA', 'fy': 'fy', 'ga': 'ga', 'gaa': 'gaa', 'gd': 'gd', 'gl': 'gl', #French, French (Canada), Frisian, Irish, Ga, Scots Gaelic, Galician
'gn': 'gn', 'gu': 'gu', 'ha': 'ha', 'haw': 'haw', 'he': 'he', 'hi': 'hi', 'hr': 'hr', #Guarani, Gujarati, Hausa, Hawaiian, Hebrew, Hindi, Croatian
'ht': 'ht', 'hu': 'hu', 'hy': 'hy', 'ia': 'ia', 'id': 'id', 'ig': 'ig', 'in': 'in', #Haitian Creole, Hungarian, Armenian, Interlingua, Indonesian, Igbo, in
'is': 'is', 'it': 'it', 'iw': 'iw', 'ja': 'ja', 'jw': 'jw', 'ka': 'ka', 'kg': 'kg', #Icelandic, Italian, Hebrew, Japanese, Javanese, Georgian, Kongo
'kk': 'kk', 'km': 'km', 'kn': 'kn', 'ko': 'ko', 'kri': 'kri', 'k': 'k', 'ky': 'ky', #Kazakh, Khmer, Kannada, Korean, Krio (Sierra Leone), Kurdish, Kyrgyz
'la': 'la', 'lg': 'lg', 'ln': 'ln', 'lo': 'lo', 'loz': 'loz', 'lt': 'lt', 'lua': 'lua', #Latin, Luganda, Lingala, Laothian, Lozi, Lithuanian, Tshiluba
'lv': 'lv', 'mfe': 'mfe', 'mg': 'mg', 'mi': 'mi', 'mk': 'mk', 'ml': 'ml', 'mn': 'mn', #Latvian, Mauritian Creole, Malagasy, Maori, Macedonian, Malayalam, Mongolian
'mo': 'mo', 'mr': 'mr', 'ms': 'ms', 'mt': 'mt', 'my': 'my', 'ne': 'ne', 'nl': 'nl', #Moldavian, Marathi, Malay, Maltese, Burmese, Nepali, Dutch
'nn': 'nn', 'no': 'no', 'nso': 'nso', 'ny': 'ny', 'nyn': 'nyn', 'oc': 'oc', 'om': 'om', #Norwegian (Nynorsk), Norwegian, Northern Sotho, Chichewa, Runyakitara, Occitan, Oromo
'or': 'or', 'pa': 'pa', 'pcm': 'pcm', 'pl': 'pl', 'ps': 'ps', 'pt-br': 'pt-BR', 'pt-pt': 'pt-PT', #Oriya, Punjabi, Nigerian Pidgin, Polish, Pashto, Portuguese (Brazil), Portuguese (Portugal)
'q': 'q', 'rm': 'rm', 'rn': 'rn', 'ro': 'ro', 'ru': 'ru', 'rw': 'rw', 'sd': 'sd', #Quechua, Romansh, Kirundi, Romanian, Russian, Kinyarwanda, Sindhi
'sh': 'sh', 'si': 'si', 'sk': 'sk', 'sl': 'sl', 'sn': 'sn', 'so': 'so', 'sq': 'sq', #Serbo-Croatian, Sinhalese, Slovak, Slovenian, Shona, Somali, Albanian
'sr': 'sr', 'sr-me': 'sr-ME', 'st': 'st', 'su': 'su', 'sv': 'sv', 'sw': 'sw', 'ta': 'ta', #Serbian, Montenegrin, Sesotho, Sundanese, Swedish, Swahili, Tamil
'te': 'te', 'tg': 'tg', 'th': 'th', 'ti': 'ti', 'tk': 'tk', 'tl': 'tl', 'tn': 'tn', #Telugu, Tajik, Thai, Tigrinya, Turkmen, Tagalog, Setswana
'to': 'to', 'tr': 'tr', 'tt': 'tt', 'tum': 'tum', 'tw': 'tw', 'ug': 'ug', 'uk': 'uk', #Tonga, Turkish, Tatar, Tumbuka, Twi, Uighur, Ukrainian
'ur': 'ur', 'uz': 'uz', 'vi': 'vi', 'wo': 'wo', 'xh': 'xh', 'yi': 'yi', 'yo': 'yo', #Urdu, Uzbek, Vietnamese, Wolof, Xhosa, Yiddish, Yoruba
'zh-cn': 'zh-CN', 'zh-hk': 'zh-HK', 'zh-tw': 'zh-TW', 'zu': 'zu', #Chinese (Simplified), Chinese (Hong Kong/Traditional), Chinese (Taiwan/Traditional), Zulu
}
LOCALE_CODES_MAP = {
'': '',
'ar-eg': 'ar_EG', #Arabic, Egypt
'az-az': 'az_AZ', #Azerbaijani, Azerbaijan
'be-by': 'be_BY', #Belarusian, Belarus
'bg-bg': 'bg_BG', #Bulgarian, Bulgaria
'bn-in': 'bn_IN', #Bengali, India
'ca-es': 'ca_ES', #Catalan, Spain
'cs-cz': 'cs_CZ', #Czech, Czech Republic
'cy-gb': 'cy_GB', #Welsh, United Kingdom
'da-dk': 'da_DK', #Danish, Denmark
'de-ch': 'de_CH', #German, Switzerland
'de-de': 'de_DE', #German, Germany
'el-gr': 'el_GR', #Greek, Greece
'en-au': 'en_AU', #English, Australia
'en-ca': 'en_CA', #English, Canada
'en-gb': 'en_GB', #English, United Kingdom
'en-ie': 'en_IE', #English, Ireland
'en-us': 'en_US', #English, U.S.A.
'es-ar': 'es_AR', #Spanish, Argentina
'es-bo': 'es_BO', #Spanish, Bolivia
'es-cl': 'es_CL', #Spanish, Chile
'es-co': 'es_CO', #Spanish, Colombia
'es-ec': 'es_EC', #Spanish, Ecuador
'es-es': 'es_ES', #Spanish, Spain
'es-mx': 'es_MX', #Spanish, Mexico
'es-py': 'es_PY', #Spanish, Paraguay
'es-uy': 'es_UY', #Spanish, Uruguay
'es-ve': 'es_VE', #Spanish, Venezuela
'fi-fi': 'fi_FI', #Finnish, Finland
'fil-ph': 'fil_PH', #Filipino, Philippines
'fr-ca': 'fr_CA', #French, Canada
'fr-fr': 'fr_FR', #French, France
'gu-in': 'gu_IN', #Gujarati, India
'hi-in': 'hi_IN', #Hindi, India
'hr-hr': 'hr_HR', #Croatian, Croatia
'hu-hu': 'hu_HU', #Hungarian, Hungary
'hy-am': 'hy_AM', #Armenian, Armenia
'in-id': 'in_ID', #Indonesian, Indonesia
'it-it': 'it_IT', #Italian, Italy
'iw-il': 'iw_IL', #Hebrew, Israel
'ja-jp': 'ja_JP', #Japanese, Japan
'ka-ge': 'ka_GE', #Georgian, Georgia
'kk-kz': 'kk_KZ', #Kazakh, Kazakhstan
'kn-in': 'kn_IN', #Kannada, India
'ko-kr': 'ko_KR', #Korean, Korea
'lt-lt': 'lt_LT', #Lithuanian, Lithuania
'lv-lv': 'lv_LV', #Latvian, Latvia
'ml-in': 'ml_IN', #Malayalam, India
'mn-mn': 'mn_MN', #Mongolian, Mongolia
'mr-in': 'mr_IN', #Marathi, India
'my-mn': 'my_MN', #Burmese, Myanmar
'nl-nl': 'nl_NL', #Dutch, Netherlands
'nn-no': 'nn_NO', #Nynorsk, Norway
'no-no': 'no_NO', #Bokmal, Norway
'pa-in': 'pa_IN', #Punjabi, India
'pl-pl': 'pl_PL', #Polish, Poland
'pt-br': 'pt_BR', #Portuguese, Brazil
'pt-pt': 'pt_PT', #Portuguese, Portugal
'ro-ro': 'ro_RO', #Romanian, Romania
'ru-ru': 'ru_RU', #Russian, Russia
'sk-sk': 'sk_SK', #Slovak, Slovakia
'sl-si': 'sl_SI', #Slovenian, Slovenia
'sr-rs': 'sr_RS', #Serbian, Serbia
'sv-se': 'sv_SE', #Swedish, Sweden
'ta-in': 'ta_IN', #Tamil, India
'te-in': 'te_IN', #Telugu, India
'th-th': 'th_TH', #Thai, Thailand
'tr-tr': 'tr_TR', #Turkish, Turkey
'uk-ua': 'uk_UA', #Ukrainian, Ukraine
'vi-vn': 'vi_VN', #Vietnamese, Vietnam
'zh-cn': 'zh_CN', #Simplified Chinese, China
'zh-hk': 'zh_HK', #Traditional Chinese, Hong Kong SAR China
'zh-tw': 'zh_TW', #Traditional Chinese, Taiwan
}
def getLanguageCode(languageCodeMap):
if Cmd.ArgumentsRemaining():
choice = Cmd.Current().strip().lower().replace('_', '-')
if choice in languageCodeMap:
Cmd.Advance()
return languageCodeMap[choice]
invalidChoiceExit(choice, languageCodeMap, False)
missingChoiceExit(languageCodeMap)
def addCourseIdScope(courseId):
if not courseId.isdigit() and courseId[:2] not in {'d:', 'p:'}:
return f'd:{courseId}'
return courseId
def removeCourseIdScope(courseId):
if courseId.startswith('d:'):
return courseId[2:]
return courseId
def addCourseAliasScope(alias):
if alias[:2] not in {'d:', 'p:'}:
return f'd:{alias}'
return alias
def removeCourseAliasScope(alias):
if alias.startswith('d:'):
return alias[2:]
return alias
def getCourseAlias():
if Cmd.ArgumentsRemaining():
courseAlias = Cmd.Current()
if courseAlias:
Cmd.Advance()
return addCourseAliasScope(courseAlias)
missingArgumentExit(Cmd.OB_COURSE_ALIAS)
DELIVERY_SETTINGS_UNDEFINED = 'DSU'
GROUP_DELIVERY_SETTINGS_MAP = {
'allmail': 'ALL_MAIL',
'abridged': 'DAILY',
'daily': 'DAILY',
'digest': 'DIGEST',
'disabled': 'DISABLED',
'none': 'NONE',
'nomail': 'NONE',
}
def getDeliverySettings():
if checkArgumentPresent(['delivery', 'deliverysettings']):
return getChoice(GROUP_DELIVERY_SETTINGS_MAP, mapChoice=True)
return getChoice(GROUP_DELIVERY_SETTINGS_MAP, defaultChoice=DELIVERY_SETTINGS_UNDEFINED, mapChoice=True)
UID_PATTERN = re.compile(r'u?id: ?(.+)', re.IGNORECASE)
PEOPLE_PATTERN = re.compile(r'people/([0-9]+)$', re.IGNORECASE)
def validateEmailAddressOrUID(emailAddressOrUID, checkPeople=True, ciGroupsAPI=False):
cg = UID_PATTERN.match(emailAddressOrUID)
if cg:
return cg.group(1)
if checkPeople:
cg = PEOPLE_PATTERN.match(emailAddressOrUID)
if cg:
return cg.group(1)
if ciGroupsAPI and emailAddressOrUID.startswith('groups/'):
return emailAddressOrUID
return emailAddressOrUID.find('@') != 0 and emailAddressOrUID.count('@') <= 1
# Normalize user/group email address/uid
# uid:12345abc -> 12345abc
# foo -> foo@domain
# foo@ -> foo@domain
# foo@bar.com -> foo@bar.com
# @domain -> domain
def normalizeEmailAddressOrUID(emailAddressOrUID, noUid=False, checkForCustomerId=False, noLower=False, ciGroupsAPI=False):
if checkForCustomerId and (emailAddressOrUID == GC.Values[GC.CUSTOMER_ID]):
return emailAddressOrUID
if not noUid:
cg = UID_PATTERN.match(emailAddressOrUID)
if cg:
return cg.group(1)
cg = PEOPLE_PATTERN.match(emailAddressOrUID)
if cg:
return cg.group(1)
if ciGroupsAPI and emailAddressOrUID.startswith('groups/'):
return emailAddressOrUID
atLoc = emailAddressOrUID.find('@')
if atLoc == 0:
return emailAddressOrUID[1:].lower() if not noLower else emailAddressOrUID[1:]
if (atLoc == -1) or (atLoc == len(emailAddressOrUID)-1) and GC.Values[GC.DOMAIN]:
if atLoc == -1:
emailAddressOrUID = f'{emailAddressOrUID}@{GC.Values[GC.DOMAIN]}'
else:
emailAddressOrUID = f'{emailAddressOrUID}{GC.Values[GC.DOMAIN]}'
return emailAddressOrUID.lower() if not noLower else emailAddressOrUID
# Normalize student/guardian email address/uid
# 12345678 -> 12345678
# - -> -
# Otherwise, same results as normalizeEmailAddressOrUID
def normalizeStudentGuardianEmailAddressOrUID(emailAddressOrUID, allowDash=False):
if emailAddressOrUID.isdigit() or (allowDash and emailAddressOrUID == '-'):
return emailAddressOrUID
return normalizeEmailAddressOrUID(emailAddressOrUID)
def getEmailAddress(noUid=False, minLen=1, optional=False, returnUIDprefix=''):
if Cmd.ArgumentsRemaining():
emailAddress = Cmd.Current().strip().lower()
if emailAddress:
cg = UID_PATTERN.match(emailAddress)
if cg:
if not noUid:
if cg.group(1):
Cmd.Advance()
return f'{returnUIDprefix}{cg.group(1)}'
else:
invalidArgumentExit('name@domain')
else:
atLoc = emailAddress.find('@')
if atLoc == -1:
if GC.Values[GC.DOMAIN]:
emailAddress = f'{emailAddress}@{GC.Values[GC.DOMAIN]}'
Cmd.Advance()
return emailAddress
if atLoc != 0:
if (atLoc == len(emailAddress)-1) and GC.Values[GC.DOMAIN]:
emailAddress = f'{emailAddress}{GC.Values[GC.DOMAIN]}'
Cmd.Advance()
return emailAddress
invalidArgumentExit('name@domain')
if optional:
Cmd.Advance()
return None
if minLen == 0:
Cmd.Advance()
return ''
elif optional:
return None
missingArgumentExit([Cmd.OB_EMAIL_ADDRESS_OR_UID, Cmd.OB_EMAIL_ADDRESS][noUid])
def getFilename():
filename = os.path.expanduser(getString(Cmd.OB_FILE_NAME))
if os.path.isfile(filename):
return filename
entityDoesNotExistExit(Ent.FILE, filename)
def getPermissionId():
if Cmd.ArgumentsRemaining():
emailAddress = Cmd.Current().strip()
if emailAddress:
cg = UID_PATTERN.match(emailAddress)
if cg:
Cmd.Advance()
return (False, cg.group(1))
emailAddress = emailAddress.lower()
atLoc = emailAddress.find('@')
if atLoc == -1:
if emailAddress == 'anyone':
Cmd.Advance()
return (False, emailAddress)
if emailAddress == 'anyonewithlink':
Cmd.Advance()
return (False, 'anyoneWithLink')
if GC.Values[GC.DOMAIN]:
emailAddress = f'{emailAddress}@{GC.Values[GC.DOMAIN]}'
Cmd.Advance()
return (True, emailAddress)
if atLoc != 0:
if (atLoc == len(emailAddress)-1) and GC.Values[GC.DOMAIN]:
emailAddress = f'{emailAddress}{GC.Values[GC.DOMAIN]}'
Cmd.Advance()
return (True, emailAddress)
invalidArgumentExit('name@domain')
missingArgumentExit(Cmd.OB_DRIVE_FILE_PERMISSION_ID)
def getGoogleProduct():
if Cmd.ArgumentsRemaining():
product = Cmd.Current().strip()
if product:
status, productId = SKU.normalizeProductId(product)
if not status:
invalidChoiceExit(productId, SKU.getSortedProductList(), False)
Cmd.Advance()
return productId
missingArgumentExit(Cmd.OB_PRODUCT_ID)
def getGoogleProductList():
if Cmd.ArgumentsRemaining():
productsList = []
for product in Cmd.Current().split(','):
status, productId = SKU.normalizeProductId(product)
if not status:
invalidChoiceExit(productId, SKU.getSortedProductList(), False)
if productId not in productsList:
productsList.append(productId)
Cmd.Advance()
return productsList
missingArgumentExit(Cmd.OB_PRODUCT_ID_LIST)
def getGoogleSKU():
if Cmd.ArgumentsRemaining():
sku = Cmd.Current().strip()
if sku:
Cmd.Advance()
return SKU.getProductAndSKU(sku)
missingArgumentExit(Cmd.OB_SKU_ID)
def getGoogleSKUList(allowUnknownProduct=False):
if Cmd.ArgumentsRemaining():
skusList = []
for sku in Cmd.Current().split(','):
productId, sku = SKU.getProductAndSKU(sku)
if not productId and not allowUnknownProduct:
invalidChoiceExit(sku, SKU.getSortedSKUList(), False)
if (productId, sku) not in skusList:
skusList.append((productId, sku))
Cmd.Advance()
return skusList
missingArgumentExit(Cmd.OB_SKU_ID_LIST)
def floatLimits(minVal, maxVal, item='float'):
if (minVal is not None) and (maxVal is not None):
return f'{item} {minVal:.3f}<=x<={maxVal:.3f}'
if minVal is not None:
return f'{item} x>={minVal:.3f}'
if maxVal is not None:
return f'{item} x<={maxVal:.3f}'
return f'{item} x'
def getFloat(minVal=None, maxVal=None):
if Cmd.ArgumentsRemaining():
try:
number = float(Cmd.Current().strip())
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
Cmd.Advance()
return number
except ValueError:
pass
invalidArgumentExit(floatLimits(minVal, maxVal))
missingArgumentExit(floatLimits(minVal, maxVal))
def integerLimits(minVal, maxVal, item='integer'):
if (minVal is not None) and (maxVal is not None):
return f'{item} {minVal}<=x<={maxVal}'
if minVal is not None:
return f'{item} x>={minVal}'
if maxVal is not None:
return f'{item} x<={maxVal}'
return f'{item} x'
def getInteger(minVal=None, maxVal=None, default=None):
if Cmd.ArgumentsRemaining():
try:
number = int(Cmd.Current().strip())
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
Cmd.Advance()
return number
except ValueError:
if default is not None:
if not Cmd.Current().strip(): # If current argument is empty, skip over it
Cmd.Advance()
return default
invalidArgumentExit(integerLimits(minVal, maxVal))
elif default is not None:
return default
missingArgumentExit(integerLimits(minVal, maxVal))
def getIntegerEmptyAllowed(minVal=None, maxVal=None, default=0):
if Cmd.ArgumentsRemaining():
number = Cmd.Current().strip()
if not number:
Cmd.Advance()
return default
try:
number = int(number)
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
Cmd.Advance()
return number
except ValueError:
pass
invalidArgumentExit(integerLimits(minVal, maxVal))
return default
SORTORDER_CHOICE_MAP = {'ascending': 'ASCENDING', 'descending': 'DESCENDING'}
class OrderBy():
def __init__(self, choiceMap, ascendingKeyword='', descendingKeyword='desc'):
self.choiceMap = choiceMap
self.ascendingKeyword = ascendingKeyword
self.descendingKeyword = descendingKeyword
self.items = []
def GetChoice(self):
fieldName = getChoice(self.choiceMap, mapChoice=True)
fieldNameAscending = fieldName
if self.ascendingKeyword:
fieldNameAscending += f' {self.ascendingKeyword}'
if fieldNameAscending in self.items:
self.items.remove(fieldNameAscending)
fieldNameDescending = fieldName
if self.descendingKeyword:
fieldNameDescending += f' {self.descendingKeyword}'
if fieldNameDescending in self.items:
self.items.remove(fieldNameDescending)
if getChoice(SORTORDER_CHOICE_MAP, defaultChoice=None, mapChoice=True) != 'DESCENDING':
self.items.append(fieldNameAscending)
else:
self.items.append(fieldNameDescending)
def SetItems(self, itemList):
self.items = itemList.split(',')
@property
def orderBy(self):
return ','.join(self.items)
def getOrderBySortOrder(choiceMap, defaultSortOrderChoice='ASCENDING', mapSortOrderChoice=True):
return (getChoice(choiceMap, mapChoice=True),
getChoice(SORTORDER_CHOICE_MAP, defaultChoice=defaultSortOrderChoice, mapChoice=mapSortOrderChoice))
def orgUnitPathQuery(path, isSuspended):
query = "orgUnitPath='{0}'".format(path.replace("'", "\\'")) if path != '/' else ''
if isSuspended is not None:
query += f' isSuspended={isSuspended}'
return query
def makeOrgUnitPathAbsolute(path):
if path == '/':
return path
if path.startswith('/'):
if not path.endswith('/'):
return path
return path[:-1]
if path.startswith('id:'):
return path
if path.startswith('uid:'):
return path[1:]
if not path.endswith('/'):
return '/'+path
return '/'+path[:-1]
def makeOrgUnitPathRelative(path):
if path == '/':
return path
if path.startswith('/'):
if not path.endswith('/'):
return path[1:]
return path[1:-1]
if path.startswith('id:'):
return path
if path.startswith('uid:'):
return path[1:]
if not path.endswith('/'):
return path
return path[:-1]
def encodeOrgUnitPath(path):
# 6.22.19 - Encoding doesn't work
# % no longer needs encoding and + is handled incorrectly in API with or without encoding
return path
# if path.find('+') == -1 and path.find('%') == -1:
# return path
# encpath = ''
# for c in path:
# if c == '+':
# encpath += '%2B'
# elif c == '%':
# encpath += '%25'
# else:
# encpath += c
# return encpath
def getOrgUnitItem(pathOnly=False, absolutePath=True, cd=None):
if Cmd.ArgumentsRemaining():
path = Cmd.Current().strip()
if path:
if pathOnly and (path.startswith('id:') or path.startswith('uid:')) and cd is not None:
try:
result = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=path,
fields='orgUnitPath')
Cmd.Advance()
if absolutePath:
return makeOrgUnitPathAbsolute(result['orgUnitPath'])
return makeOrgUnitPathRelative(result['orgUnitPath'])
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, path)
invalidArgumentExit(Cmd.OB_ORGUNIT_PATH)
Cmd.Advance()
if absolutePath:
return makeOrgUnitPathAbsolute(path)
return makeOrgUnitPathRelative(path)
missingArgumentExit([Cmd.OB_ORGUNIT_ITEM, Cmd.OB_ORGUNIT_PATH][pathOnly])
def getTopLevelOrgId(cd, parentOrgUnitPath):
if parentOrgUnitPath != '/':
try:
result = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(parentOrgUnitPath)),
fields='orgUnitId')
return result['orgUnitId']
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
return None
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, parentOrgUnitPath)
return None
try:
result = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath='/', type='allIncludingParent',
fields='organizationUnits(orgUnitId,orgUnitPath)')
for orgUnit in result.get('organizationUnits', []):
if orgUnit['orgUnitPath'] == '/':
return orgUnit['orgUnitId']
return None
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
return None
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, parentOrgUnitPath)
return None
def getOrgUnitId(cd=None, orgUnit=None):
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
if orgUnit is None:
orgUnit = getOrgUnitItem()
try:
if orgUnit == '/':
result = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath='/', type='children',
fields='organizationUnits(parentOrgUnitId,parentOrgUnitPath)')
if result.get('organizationUnits', []):
return (result['organizationUnits'][0]['parentOrgUnitPath'], result['organizationUnits'][0]['parentOrgUnitId'])
topLevelOrgId = getTopLevelOrgId(cd, '/')
if topLevelOrgId:
return (orgUnit, topLevelOrgId)
return (orgUnit, '/') #Bogus but should never happen
result = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnit)),
fields='orgUnitId,orgUnitPath')
return (result['orgUnitPath'], result['orgUnitId'])
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, orgUnit)
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
accessErrorExit(cd)
def getAllParentOrgUnitsForUser(cd, user):
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=user, fields='orgUnitPath', projection='basic')
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden):
entityDoesNotExistExit(Ent.USER, user)
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
accessErrorExit(cd)
parentPath = result['orgUnitPath']
if parentPath == '/':
orgUnitPath, orgUnitId = getOrgUnitId(cd, '/')
return {orgUnitId: orgUnitPath}
parentPath = encodeOrgUnitPath(makeOrgUnitPathRelative(parentPath))
orgUnits = {}
while True:
try:
result = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=parentPath,
fields='orgUnitId,orgUnitPath,parentOrgUnitId')
orgUnits[result['orgUnitId']] = result['orgUnitPath']
if 'parentOrgUnitId' not in result:
break
parentPath = result['parentOrgUnitId']
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, parentPath)
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
accessErrorExit(cd)
return orgUnits
def validateREPattern(patstr, flags=0):
try:
return re.compile(patstr, flags)
except re.error as e:
Cmd.Backup()
usageErrorExit(f'{Cmd.OB_RE_PATTERN} {Msg.ERROR}: {e}')
def getREPattern(flags=0):
if Cmd.ArgumentsRemaining():
patstr = Cmd.Current()
if patstr:
Cmd.Advance()
return validateREPattern(patstr, flags)
missingArgumentExit(Cmd.OB_RE_PATTERN)
def validateREPatternSubstitution(pattern, replacement):
try:
re.sub(pattern, replacement, '')
return (pattern, replacement)
except re.error as e:
Cmd.Backup()
usageErrorExit(f'{Cmd.OB_RE_SUBSTITUTION} {Msg.ERROR}: {e}')
def getREPatternSubstitution(flags=0):
pattern = getREPattern(flags)
replacement = getString(Cmd.OB_RE_SUBSTITUTION, minLen=0)
return validateREPatternSubstitution(pattern, replacement)
def getSheetEntity(allowBlankSheet):
if Cmd.ArgumentsRemaining():
sheet = Cmd.Current()
if sheet or allowBlankSheet:
cg = UID_PATTERN.match(sheet)
if cg:
if cg.group(1).isdigit():
Cmd.Advance()
return {'sheetType': Ent.SHEET_ID, 'sheetValue': int(cg.group(1)), 'sheetId': int(cg.group(1)), 'sheetTitle': ''}
else:
Cmd.Advance()
return {'sheetType': Ent.SHEET, 'sheetValue': sheet, 'sheetId': None, 'sheetTitle': sheet}
missingArgumentExit(Cmd.OB_SHEET_ENTITY)
def getSheetIdFromSheetEntity(spreadsheet, sheetEntity):
if sheetEntity['sheetType'] == Ent.SHEET_ID:
for sheet in spreadsheet['sheets']:
if sheetEntity['sheetId'] == sheet['properties']['sheetId']:
return sheet['properties']['sheetId']
else:
sheetTitleLower = sheetEntity['sheetTitle'].lower()
for sheet in spreadsheet['sheets']:
if sheetTitleLower == sheet['properties']['title'].lower():
return sheet['properties']['sheetId']
return None
def protectedSheetId(spreadsheet, sheetId):
for sheet in spreadsheet['sheets']:
for protectedRange in sheet.get('protectedRanges', []):
if protectedRange.get('range', {}).get('sheetId', -1) == sheetId and not protectedRange.get('requestingUserCanEdit', False):
return True
return False
def getString(item, checkBlank=False, optional=False, minLen=1, maxLen=None):
if Cmd.ArgumentsRemaining():
argstr = Cmd.Current()
if argstr:
if checkBlank:
if argstr.isspace():
blankArgumentExit(item)
if (len(argstr) >= minLen) and ((maxLen is None) or (len(argstr) <= maxLen)):
Cmd.Advance()
return argstr
invalidArgumentExit(f'{integerLimits(minLen, maxLen, Msg.STRING_LENGTH)} for {item}')
if optional or (minLen == 0):
Cmd.Advance()
return ''
emptyArgumentExit(item)
elif optional:
return ''
missingArgumentExit(item)
def escapeCRsNLs(value):
return value.replace('\r', '\\r').replace('\n', '\\n')
def unescapeCRsNLs(value):
return value.replace('\\r', '\r').replace('\\n', '\n')
def getStringWithCRsNLs():
return unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=0))
def getStringReturnInList(item):
argstr = getString(item, minLen=0).strip()
if argstr:
return [argstr]
return []
SORF_SIG_ARGUMENTS = {'signature', 'sig', 'textsig', 'htmlsig'}
SORF_MSG_ARGUMENTS = {'message', 'textmessage', 'htmlmessage'}
SORF_FILE_ARGUMENTS = {'file', 'textfile', 'htmlfile', 'gdoc', 'ghtml', 'gcsdoc', 'gcshtml'}
SORF_HTML_ARGUMENTS = {'htmlsig', 'htmlmessage', 'htmlfile', 'ghtml', 'gcshtml'}
SORF_TEXT_ARGUMENTS = {'text', 'textfile', 'gdoc', 'gcsdoc'}
SORF_SIG_FILE_ARGUMENTS = SORF_SIG_ARGUMENTS.union(SORF_FILE_ARGUMENTS)
SORF_MSG_FILE_ARGUMENTS = SORF_MSG_ARGUMENTS.union(SORF_FILE_ARGUMENTS)
def getStringOrFile(myarg, minLen=0, unescapeCRLF=False):
if myarg in SORF_SIG_ARGUMENTS:
if checkArgumentPresent(SORF_FILE_ARGUMENTS):
myarg = Cmd.Previous().strip().lower().replace('_', '')
html = myarg in SORF_HTML_ARGUMENTS
if myarg in SORF_FILE_ARGUMENTS:
if myarg in {'file', 'textfile', 'htmlfile'}:
filename = getString(Cmd.OB_FILE_NAME)
encoding = getCharSet()
return (readFile(filename, encoding=encoding), encoding, html)
if myarg in {'gdoc', 'ghtml'}:
f = getGDocData(myarg)
data = f.read()
f.close()
return (data, UTF8, html)
return (getStorageFileData(myarg), UTF8, html)
if not unescapeCRLF:
return (getString(Cmd.OB_STRING, minLen=minLen), UTF8, html)
return (unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=minLen)), UTF8, html)
def getStringWithCRsNLsOrFile():
if checkArgumentPresent(SORF_FILE_ARGUMENTS):
return getStringOrFile(Cmd.Previous().strip().lower().replace('_', ''), minLen=0)
return (unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=0)), UTF8, False)
def todaysDate():
return datetime.datetime(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
tzinfo=GC.Values[GC.TIMEZONE])
def todaysTime():
return datetime.datetime(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
GM.Globals[GM.DATETIME_NOW].hour, GM.Globals[GM.DATETIME_NOW].minute,
tzinfo=GC.Values[GC.TIMEZONE])
def getDelta(argstr, pattern):
if argstr == 'NOW':
return todaysTime()
if argstr == 'TODAY':
return todaysDate()
tg = pattern.match(argstr.lower())
if tg is None:
return None
sign = tg.group(1)
delta = int(tg.group(2))
unit = tg.group(3)
if unit == 'y':
deltaTime = datetime.timedelta(days=delta*365)
elif unit == 'w':
deltaTime = datetime.timedelta(weeks=delta)
elif unit == 'd':
deltaTime = datetime.timedelta(days=delta)
elif unit == 'h':
deltaTime = datetime.timedelta(hours=delta)
elif unit == 'm':
deltaTime = datetime.timedelta(minutes=delta)
baseTime = todaysDate()
if unit in {'h', 'm'}:
baseTime = baseTime+datetime.timedelta(hours=GM.Globals[GM.DATETIME_NOW].hour, minutes=GM.Globals[GM.DATETIME_NOW].minute)
if sign == '-':
return baseTime-deltaTime
return baseTime+deltaTime
DELTA_DATE_PATTERN = re.compile(r'^([+-])(\d+)([dwy])$')
DELTA_DATE_FORMAT_REQUIRED = '(+|-)<Number>(d|w|y)'
def getDeltaDate(argstr):
deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
if deltaDate is None:
invalidArgumentExit(DELTA_DATE_FORMAT_REQUIRED)
return deltaDate
DELTA_TIME_PATTERN = re.compile(r'^([+-])(\d+)([mhdwy])$')
DELTA_TIME_FORMAT_REQUIRED = '(+|-)<Number>(m|h|d|w|y)'
def getDeltaTime(argstr):
deltaTime = getDelta(argstr, DELTA_TIME_PATTERN)
if deltaTime is None:
invalidArgumentExit(DELTA_TIME_FORMAT_REQUIRED)
return deltaTime
YYYYMMDD_FORMAT = '%Y-%m-%d'
YYYYMMDD_FORMAT_REQUIRED = 'yyyy-mm-dd'
TODAY_NOW = {'TODAY', 'NOW'}
PLUS_MINUS = {'+', '-'}
def getYYYYMMDD(minLen=1, returnTimeStamp=False, returnDateTime=False, alternateValue=None):
if Cmd.ArgumentsRemaining():
argstr = Cmd.Current().strip().upper()
if argstr:
if alternateValue is not None and argstr == alternateValue.upper():
Cmd.Advance()
return None
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
if argstr == 'NOW':
argstr = 'TODAY'
argstr = getDeltaDate(argstr).strftime(YYYYMMDD_FORMAT)
elif argstr == 'NEVER':
argstr = NEVER_DATE
try:
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
Cmd.Advance()
if returnTimeStamp:
return time.mktime(dateTime.timetuple())*1000
if returnDateTime:
return dateTime
return argstr
except ValueError:
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
elif minLen == 0:
Cmd.Advance()
return ''
missingArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
HHMM_FORMAT = '%H:%M'
HHMM_FORMAT_REQUIRED = 'hh:mm'
def getHHMM():
if Cmd.ArgumentsRemaining():
argstr = Cmd.Current().strip().upper()
if argstr:
try:
datetime.datetime.strptime(argstr, HHMM_FORMAT)
Cmd.Advance()
return argstr
except ValueError:
invalidArgumentExit(HHMM_FORMAT_REQUIRED)
missingArgumentExit(HHMM_FORMAT_REQUIRED)
YYYYMMDD_HHMM_FORMAT = '%Y-%m-%d %H:%M'
YYYYMMDD_HHMM_FORMAT_REQUIRED = 'yyyy-mm-dd hh:mm'
def getYYYYMMDD_HHMM():
if Cmd.ArgumentsRemaining():
argstr = Cmd.Current().strip().upper()
if argstr:
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
argstr = getDeltaTime(argstr).strftime(YYYYMMDD_HHMM_FORMAT)
elif argstr == 'NEVER':
argstr = NEVER_DATETIME
argstr = argstr.replace('T', ' ')
try:
datetime.datetime.strptime(argstr, YYYYMMDD_HHMM_FORMAT)
Cmd.Advance()
return argstr
except ValueError:
invalidArgumentExit(YYYYMMDD_HHMM_FORMAT_REQUIRED)
missingArgumentExit(YYYYMMDD_HHMM_FORMAT_REQUIRED)
YYYYMMDDTHHMMSSZ_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
YYYYMMDD_PATTERN = re.compile(r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$')
def getDateOrDeltaFromNow(returnDateTime=False):
if Cmd.ArgumentsRemaining():
argstr = Cmd.Current().strip().upper()
if argstr:
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
if argstr == 'NOW':
argstr = 'TODAY'
argDate = getDeltaDate(argstr)
elif argstr == 'NEVER':
argDate = datetime.datetime.strptime(NEVER_DATE, YYYYMMDD_FORMAT)
elif YYYYMMDD_PATTERN.match(argstr):
try:
argDate = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
except ValueError:
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
else:
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
Cmd.Advance()
if not returnDateTime:
return argDate.strftime(YYYYMMDD_FORMAT)
return (datetime.datetime(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE]),
GC.Values[GC.TIMEZONE], argDate.strftime(YYYYMMDD_FORMAT))
missingArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
YYYYMMDDTHHMMSS_FORMAT_REQUIRED = 'yyyy-mm-ddThh:mm:ss[.fff](Z|(+|-(hh:mm)))'
TIMEZONE_FORMAT_REQUIRED = 'Z|(+|-(hh:mm))'
def getTimeOrDeltaFromNow(returnDateTime=False):
if Cmd.ArgumentsRemaining():
argstr = Cmd.Current().strip().upper()
if argstr:
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
argstr = ISOformatTimeStamp(getDeltaTime(argstr))
elif argstr == 'NEVER':
argstr = NEVER_TIME
elif YYYYMMDD_PATTERN.match(argstr):
try:
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
except ValueError:
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
try:
argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=GC.Values[GC.TIMEZONE]))
except OverflowError:
pass
try:
fullDateTime, tz = iso8601.parse_date(argstr)
Cmd.Advance()
if not returnDateTime:
return argstr.replace(' ', 'T')
return (fullDateTime, tz, argstr.replace(' ', 'T'))
except (iso8601.ParseError, OverflowError):
pass
invalidArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
missingArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
def getRowFilterDateOrDeltaFromNow(argstr):
argstr = argstr.strip().upper()
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
if argstr == 'NOW':
argstr = 'TODAY'
deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
if deltaDate is None:
return (False, DELTA_DATE_FORMAT_REQUIRED)
argstr = ISOformatTimeStamp(deltaDate.replace(tzinfo=iso8601.UTC))
elif argstr == 'NEVER' or YYYYMMDD_PATTERN.match(argstr):
if argstr == 'NEVER':
argstr = NEVER_DATE
try:
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
except ValueError:
return (False, YYYYMMDD_FORMAT_REQUIRED)
argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=iso8601.UTC))
try:
iso8601.parse_date(argstr)
return (True, argstr.replace(' ', 'T'))
except (iso8601.ParseError, OverflowError):
return (False, YYYYMMDD_FORMAT_REQUIRED)
def getRowFilterTimeOrDeltaFromNow(argstr):
argstr = argstr.strip().upper()
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
deltaTime = getDelta(argstr, DELTA_TIME_PATTERN)
if deltaTime is None:
return (False, DELTA_TIME_FORMAT_REQUIRED)
argstr = ISOformatTimeStamp(deltaTime)
elif argstr == 'NEVER':
argstr = NEVER_TIME
elif YYYYMMDD_PATTERN.match(argstr):
try:
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
except ValueError:
return (False, YYYYMMDD_FORMAT_REQUIRED)
argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=GC.Values[GC.TIMEZONE]))
try:
iso8601.parse_date(argstr)
return (True, argstr.replace(' ', 'T'))
except (iso8601.ParseError, OverflowError):
return (False, YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
def mapQueryRelativeTimes(query, keywords):
QUOTES = '\'"'
for kw in keywords:
pattern = re.compile(rf'({kw})\s*([<>]=?|=|!=)\s*[{QUOTES}]?(now|today|[+-]\d+[mhdwy])', re.IGNORECASE)
pos = 0
while True:
mg = pattern.search(query, pos)
if not mg:
break
if mg.groups()[2] is not None:
deltaTime = getDelta(mg.group(3).upper(), DELTA_TIME_PATTERN)
if deltaTime:
query = query[:mg.start(3)]+ISOformatTimeStamp(deltaTime)+query[mg.end(3):]
pos = mg.end()
return query
class StartEndTime():
def __init__(self, startkw='starttime', endkw='endtime', mode='time'):
self.startTime = self.endTime = self.startDateTime = self.endDateTime = None
self._startkw = startkw
self._endkw = endkw
self._getValueOrDeltaFromNow = getTimeOrDeltaFromNow if mode == 'time' else getDateOrDeltaFromNow
def Get(self, myarg):
if myarg in {'start', self._startkw}:
self.startDateTime, _, self.startTime = self._getValueOrDeltaFromNow(True)
elif myarg in {'end', self._endkw}:
self.endDateTime, _, self.endTime = self._getValueOrDeltaFromNow(True)
elif myarg == 'yesterday':
currDate = todaysDate()
self.startDateTime = currDate+datetime.timedelta(days=-1)
self.startTime = ISOformatTimeStamp(self.startDateTime)
self.endDateTime = currDate+datetime.timedelta(seconds=-1)
self.endTime = ISOformatTimeStamp(self.endDateTime)
elif myarg == 'today':
currDate = todaysDate()
self.startDateTime = currDate
self.startTime = ISOformatTimeStamp(self.startDateTime)
elif myarg == 'range':
self.startDateTime, _, self.startTime = self._getValueOrDeltaFromNow(True)
self.endDateTime, _, self.endTime = self._getValueOrDeltaFromNow(True)
else: #elif myarg in {'thismonth', 'previousmonths'}
if myarg == 'thismonth':
firstMonth = 0
else:
firstMonth = getInteger(minVal=1, maxVal=6)
currDate = todaysDate()
self.startDateTime = currDate+relativedelta(months=-firstMonth, day=1, hour=0, minute=0, second=0, microsecond=0)
self.startTime = ISOformatTimeStamp(self.startDateTime)
if myarg == 'thismonth':
self.endDateTime = todaysTime()
else:
self.endDateTime = currDate+relativedelta(day=1, hour=23, minute=59, second=59, microsecond=0)+relativedelta(days=-1)
self.endTime = ISOformatTimeStamp(self.endDateTime)
if self.startDateTime and self.endDateTime and self.endDateTime < self.startDateTime:
Cmd.Backup()
usageErrorExit(Msg.INVALID_DATE_TIME_RANGE.format(self._endkw, self.endTime, self._startkw, self.startTime))
EVENTID_PATTERN = re.compile(r'^[a-v0-9]{5,1024}$')
EVENTID_FORMAT_REQUIRED = '[a-v0-9]{5,1024}'
def getEventID():
if Cmd.ArgumentsRemaining():
tg = EVENTID_PATTERN.match(Cmd.Current().strip())
if tg:
Cmd.Advance()
return tg.group(0)
invalidArgumentExit(EVENTID_FORMAT_REQUIRED)
missingArgumentExit(EVENTID_FORMAT_REQUIRED)
EVENT_TIME_FORMAT_REQUIRED = 'allday yyyy-mm-dd | '+YYYYMMDDTHHMMSS_FORMAT_REQUIRED
def getEventTime():
if Cmd.ArgumentsRemaining():
if Cmd.Current().strip().lower() == 'allday':
Cmd.Advance()
return {'date': getYYYYMMDD()}
return {'dateTime': getTimeOrDeltaFromNow()}
missingArgumentExit(EVENT_TIME_FORMAT_REQUIRED)
AGE_TIME_PATTERN = re.compile(r'^(\d+)([mhdw])$')
AGE_TIME_FORMAT_REQUIRED = '<Number>(m|h|d|w)'
def getAgeTime():
if Cmd.ArgumentsRemaining():
tg = AGE_TIME_PATTERN.match(Cmd.Current().strip().lower())
if tg:
age = int(tg.group(1))
age_unit = tg.group(2)
now = int(time.time())
if age_unit == 'm':
age = now-(age*SECONDS_PER_MINUTE)
elif age_unit == 'h':
age = now-(age*SECONDS_PER_HOUR)
elif age_unit == 'd':
age = now-(age*SECONDS_PER_DAY)
else: # age_unit == 'w':
age = now-(age*SECONDS_PER_WEEK)
Cmd.Advance()
return age*1000
invalidArgumentExit(AGE_TIME_FORMAT_REQUIRED)
missingArgumentExit(AGE_TIME_FORMAT_REQUIRED)
CALENDAR_REMINDER_METHODS = ['email', 'popup']
CALENDAR_REMINDER_MAX_MINUTES = 40320
def getCalendarReminder(allowClearNone=False):
methods = CALENDAR_REMINDER_METHODS[:]
if allowClearNone:
methods += Cmd.CLEAR_NONE_ARGUMENT
if Cmd.ArgumentsRemaining():
method = Cmd.Current().strip()
if not method.isdigit():
method = getChoice(methods)
minutes = getInteger(minVal=0, maxVal=CALENDAR_REMINDER_MAX_MINUTES)
else:
minutes = getInteger(minVal=0, maxVal=CALENDAR_REMINDER_MAX_MINUTES)
method = getChoice(methods)
return {'method': method, 'minutes': minutes}
missingChoiceExit(methods)
def getCharacter():
if Cmd.ArgumentsRemaining():
argstr = codecs.escape_decode(bytes(Cmd.Current(), UTF8))[0].decode(UTF8)
if argstr:
if len(argstr) == 1:
Cmd.Advance()
return argstr
invalidArgumentExit(f'{integerLimits(1, 1, Msg.STRING_LENGTH)} for {Cmd.OB_CHARACTER}')
emptyArgumentExit(Cmd.OB_CHARACTER)
missingArgumentExit(Cmd.OB_CHARACTER)
def getDelimiter():
if not checkArgumentPresent('delimiter'):
return None
return getCharacter()
def getJSON(deleteFields):
if not checkArgumentPresent('file'):
encoding = getCharSet()
if not Cmd.ArgumentsRemaining():
missingArgumentExit(Cmd.OB_JSON_DATA)
argstr = Cmd.Current()
# argstr = Cmd.Current().replace(r'\\"', r'\"')
Cmd.Advance()
try:
if encoding == UTF8:
jsonData = json.loads(argstr)
else:
jsonData = json.loads(argstr.encode(encoding).decode(UTF8))
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
Cmd.Backup()
usageErrorExit(f'{str(e)}: {argstr if encoding == UTF8 else argstr.encode(encoding).decode(UTF8)}')
else:
filename = getString(Cmd.OB_FILE_NAME)
encoding = getCharSet()
try:
jsonData = json.loads(readFile(filename, encoding=encoding))
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
Cmd.Backup()
usageErrorExit(Msg.JSON_ERROR.format(str(e), filename))
for field in deleteFields:
jsonData.pop(field, None)
return jsonData
def getMatchSkipFields(fieldNames):
matchFields = {}
skipFields = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'matchfield', 'skipfield'}:
matchField = getString(Cmd.OB_FIELD_NAME).strip('~')
if (not matchField) or (matchField not in fieldNames):
csvFieldErrorExit(matchField, fieldNames, backupArg=True)
if myarg == 'matchfield':
matchFields[matchField] = getREPattern()
else:
skipFields[matchField] = getREPattern()
else:
Cmd.Backup()
break
return (matchFields, skipFields)
def checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
for matchField, matchPattern in iter(matchFields.items()):
if (matchField not in row) or not matchPattern.search(row[matchField]):
return False
for skipField, matchPattern in iter(skipFields.items()):
if (skipField in row) and matchPattern.search(row[skipField]):
return False
if fieldnames and (GC.Values[GC.CSV_INPUT_ROW_FILTER] or GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER]):
return RowFilterMatch(row, fieldnames,
GC.Values[GC.CSV_INPUT_ROW_FILTER], GC.Values[GC.CSV_INPUT_ROW_FILTER_MODE],
GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER_MODE])
return True
def checkSubkeyField():
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
Cmd.Backup()
usageErrorExit(Msg.NO_CSV_FILE_SUBKEYS_SAVED)
chkSubkeyField = getString(Cmd.OB_FIELD_NAME, checkBlank=True)
if chkSubkeyField != GM.Globals[GM.CSV_SUBKEY_FIELD]:
Cmd.Backup()
usageErrorExit(Msg.SUBKEY_FIELD_MISMATCH.format(chkSubkeyField, GM.Globals[GM.CSV_SUBKEY_FIELD]))
def checkDataField():
if not GM.Globals[GM.CSV_DATA_FIELD]:
Cmd.Backup()
usageErrorExit(Msg.NO_CSV_FILE_DATA_SAVED)
chkDataField = getString(Cmd.OB_FIELD_NAME, checkBlank=True)
if chkDataField != GM.Globals[GM.CSV_DATA_FIELD]:
Cmd.Backup()
usageErrorExit(Msg.DATA_FIELD_MISMATCH.format(chkDataField, GM.Globals[GM.CSV_DATA_FIELD]))
MAX_MESSAGE_BYTES_PATTERN = re.compile(r'^(\d+)([mkb]?)$')
MAX_MESSAGE_BYTES_FORMAT_REQUIRED = '<Number>[m|k|b]'
def getMaxMessageBytes(oneKiloBytes, oneMegaBytes):
if Cmd.ArgumentsRemaining():
tg = MAX_MESSAGE_BYTES_PATTERN.match(Cmd.Current().strip().lower())
if tg:
mmb = int(tg.group(1))
mmb_unit = tg.group(2)
if mmb_unit == 'm':
mmb *= oneMegaBytes
elif mmb_unit == 'k':
mmb *= oneKiloBytes
Cmd.Advance()
return mmb
invalidArgumentExit(MAX_MESSAGE_BYTES_FORMAT_REQUIRED)
missingArgumentExit(MAX_MESSAGE_BYTES_FORMAT_REQUIRED)
# Get domain from email address
def getEmailAddressDomain(emailAddress):
atLoc = emailAddress.find('@')
if atLoc == -1:
return GC.Values[GC.DOMAIN]
return emailAddress[atLoc+1:].lower()
# Get user name from email address
def getEmailAddressUsername(emailAddress):
atLoc = emailAddress.find('@')
if atLoc == -1:
return emailAddress.lower()
return emailAddress[:atLoc].lower()
# Split email address into user and domain
def splitEmailAddress(emailAddress):
atLoc = emailAddress.find('@')
if atLoc == -1:
return (emailAddress.lower(), GC.Values[GC.DOMAIN])
return (emailAddress[:atLoc].lower(), emailAddress[atLoc+1:].lower())
def formatFileSize(fileSize):
if fileSize == 0:
return '0kb'
if fileSize < ONE_KILO_10_BYTES:
return '1kb'
if fileSize < ONE_MEGA_10_BYTES:
return f'{fileSize//ONE_KILO_10_BYTES}kb'
if fileSize < ONE_GIGA_10_BYTES:
return f'{fileSize//ONE_MEGA_10_BYTES}mb'
return f'{fileSize//ONE_GIGA_10_BYTES}gb'
def formatLocalTime(dateTimeStr):
if dateTimeStr in {NEVER_TIME, NEVER_TIME_NOMS}:
return GC.Values[GC.NEVER_TIME]
try:
timestamp, _ = iso8601.parse_date(dateTimeStr)
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
if GM.Globals[GM.CONVERT_TO_LOCAL_TIME]:
return ISOformatTimeStamp(timestamp.astimezone(GC.Values[GC.TIMEZONE]))
return timestamp.strftime(YYYYMMDDTHHMMSSZ_FORMAT)
if GM.Globals[GM.CONVERT_TO_LOCAL_TIME]:
return timestamp.astimezone(GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
return timestamp.strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
except (iso8601.ParseError, OverflowError):
return dateTimeStr
def formatLocalSecondsTimestamp(timestamp):
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]))
return datetime.datetime.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
def formatLocalTimestamp(timestamp):
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]))
return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
def formatLocalTimestampUTC(timestamp):
return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp)//1000, iso8601.UTC))
def formatLocalDatestamp(timestamp):
try:
if not GC.Values[GC.OUTPUT_DATEFORMAT]:
return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(YYYYMMDD_FORMAT)
return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_DATEFORMAT])
except OverflowError:
return NEVER_DATE
def formatMaxMessageBytes(maxMessageBytes, oneKiloBytes, oneMegaBytes):
if maxMessageBytes < oneKiloBytes:
return maxMessageBytes
if maxMessageBytes < oneMegaBytes:
return f'{maxMessageBytes//oneKiloBytes}K'
return f'{maxMessageBytes//oneMegaBytes}M'
def formatMilliSeconds(millis):
seconds, millis = divmod(millis, 1000)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
return f'{hours:02d}:{minutes:02d}:{seconds:02d}'
def getPhraseDNEorSNA(email):
return Msg.DOES_NOT_EXIST if getEmailAddressDomain(email) == GC.Values[GC.DOMAIN] else Msg.SERVICE_NOT_APPLICABLE
def formatHTTPError(http_status, reason, message):
return f'{http_status}: {reason} - {message}'
def getHTTPError(responses, http_status, reason, message):
if reason in responses:
return responses[reason]
return formatHTTPError(http_status, reason, message)
# Warnings
def badRequestWarning(entityType, itemType, itemValue):
printWarningMessage(BAD_REQUEST_RC,
f'{Msg.GOT} 0 {Ent.Plural(entityType)}: {Msg.INVALID} {Ent.Singular(itemType)} - {itemValue}')
def emptyQuery(query, entityType):
return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.NO_ENTITIES_FOUND.format(Ent.Plural(entityType))}'
def invalidQuery(query):
return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.INVALID}'
def invalidMember(query):
if query:
badRequestWarning(Ent.GROUP, Ent.QUERY, invalidQuery(query))
return True
return False
def invalidUserSchema(schema):
if isinstance(schema, list):
return f'{Ent.Singular(Ent.USER_SCHEMA)} ({",".join(schema)}) {Msg.INVALID}'
return f'{Ent.Singular(Ent.USER_SCHEMA)} {schema}) {Msg.INVALID}'
def userServiceNotEnabledWarning(entityName, service, i=0, count=0):
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(Ent.USER), entityName, Msg.SERVICE_NOT_ENABLED.format(service)],
currentCountNL(i, count)))
def userAlertsServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Alerts', i, count)
def userAnalyticsServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Alerts', i, count)
def userCalServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Calendar', i, count)
def userChatServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Chat', i, count)
def userContactDelegateServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Contact Delegate', i, count)
def userDriveServiceNotEnabledWarning(user, errMessage, i=0, count=0):
# if errMessage.find('Drive apps') == -1 and errMessage.find('Active session is invalid') == -1:
# entityServiceNotApplicableWarning(Ent.USER, user, i, count)
if errMessage.find('Drive apps') >= 0 or errMessage.find('Active session is invalid') >= 0:
userServiceNotEnabledWarning(user, 'Drive', i, count)
else:
entityActionNotPerformedWarning([Ent.USER, user], errMessage, i, count)
def userKeepServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Keep', i, count)
def userGmailServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Gmail', i, count)
def userLookerStudioServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Looker Studio', i, count)
def userPeopleServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'People', i, count)
def userTasksServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'Tasks', i, count)
def userYouTubeServiceNotEnabledWarning(entityName, i=0, count=0):
userServiceNotEnabledWarning(entityName, 'YouTube', i, count)
def entityServiceNotApplicableWarning(entityType, entityName, i=0, count=0):
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(entityType), entityName, Msg.SERVICE_NOT_APPLICABLE],
currentCountNL(i, count)))
def entityDoesNotExistWarning(entityType, entityName, i=0, count=0):
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(entityType), entityName, Msg.DOES_NOT_EXIST],
currentCountNL(i, count)))
def entityListDoesNotExistWarning(entityValueList, i=0, count=0):
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
currentCountNL(i, count)))
def entityUnknownWarning(entityType, entityName, i=0, count=0):
domain = getEmailAddressDomain(entityName)
if (domain.endswith(GC.Values[GC.DOMAIN])) or (domain.endswith('google.com')):
entityDoesNotExistWarning(entityType, entityName, i, count)
else:
entityServiceNotApplicableWarning(entityType, entityName, i, count)
def entityOrEntityUnknownWarning(entity1Type, entity1Name, entity2Type, entity2Name, i=0, count=0):
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[f'{Msg.EITHER} {Ent.Singular(entity1Type)}', entity1Name, getPhraseDNEorSNA(entity1Name), None,
f'{Msg.OR} {Ent.Singular(entity2Type)}', entity2Name, getPhraseDNEorSNA(entity2Name)],
currentCountNL(i, count)))
def entityDoesNotHaveItemWarning(entityValueList, i=0, count=0):
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
currentCountNL(i, count)))
def duplicateAliasGroupUserWarning(cd, entityValueList, i=0, count=0):
email = entityValueList[1]
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=email, fields='id,primaryEmail')
if (result['primaryEmail'].lower() == email) or (result['id'] == email):
kvList = [Ent.USER, email]
else:
kvList = [Ent.USER_ALIAS, email, Ent.USER, result['primaryEmail']]
except (GAPI.userNotFound, GAPI.badRequest,
GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError, GAPI.systemError):
try:
result = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
groupKey=email, fields='id,email')
if (result['email'].lower() == email) or (result['id'] == email):
kvList = [Ent.GROUP, email]
else:
kvList = [Ent.GROUP_ALIAS, email, Ent.GROUP, result['email']]
except (GAPI.groupNotFound,
GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
kvList = [Ent.EMAIL, email]
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+
[Act.Failed(), Msg.DUPLICATE]+
Ent.FormatEntityValueList(kvList),
currentCountNL(i, count)))
setSysExitRC(ENTITY_DUPLICATE_RC)
return kvList[0]
def entityDuplicateWarning(entityValueList, i=0, count=0):
setSysExitRC(ENTITY_DUPLICATE_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), Msg.DUPLICATE],
currentCountNL(i, count)))
def entityActionFailedWarning(entityValueList, errMessage, i=0, count=0):
setSysExitRC(ACTION_FAILED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), errMessage],
currentCountNL(i, count)))
def entityModifierItemValueListActionFailedWarning(entityValueList, modifier, infoTypeValueList, errMessage, i=0, count=0):
setSysExitRC(ACTION_FAILED_RC)
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList)+[Act.Failed(), errMessage],
currentCountNL(i, count)))
def entityModifierActionFailedWarning(entityValueList, modifier, errMessage, i=0, count=0):
setSysExitRC(ACTION_FAILED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', Act.Failed(), errMessage],
currentCountNL(i, count)))
def entityModifierNewValueActionFailedWarning(entityValueList, modifier, newValue, errMessage, i=0, count=0):
setSysExitRC(ACTION_FAILED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue, Act.Failed(), errMessage],
currentCountNL(i, count)))
def entityNumEntitiesActionFailedWarning(entityType, entityName, itemType, itemCount, errMessage, i=0, count=0):
setSysExitRC(ACTION_FAILED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(entityType), entityName,
Ent.Choose(itemType, itemCount), itemCount,
Act.Failed(), errMessage],
currentCountNL(i, count)))
def entityActionNotPerformedWarning(entityValueList, errMessage, i=0, count=0):
setSysExitRC(ACTION_NOT_PERFORMED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMessage],
currentCountNL(i, count)))
def entityItemValueListActionNotPerformedWarning(entityValueList, infoTypeValueList, errMessage, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), '']+Ent.FormatEntityValueList(infoTypeValueList)+[errMessage],
currentCountNL(i, count)))
def entityModifierItemValueListActionNotPerformedWarning(entityValueList, modifier, infoTypeValueList, errMessage, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.NotPerformed()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList)+[errMessage],
currentCountNL(i, count)))
def entityNumEntitiesActionNotPerformedWarning(entityValueList, itemType, itemCount, errMessage, i=0, count=0):
setSysExitRC(ACTION_NOT_PERFORMED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Ent.Choose(itemType, itemCount), itemCount, Act.NotPerformed(), errMessage],
currentCountNL(i, count)))
def entityBadRequestWarning(entityValueList, errMessage, i=0, count=0):
setSysExitRC(BAD_REQUEST_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[ERROR, errMessage],
currentCountNL(i, count)))
# Getting ... utilities
def printGettingAllAccountEntities(entityType, query='', qualifier='', accountType=Ent.ACCOUNT):
if GC.Values[GC.SHOW_GETTINGS]:
if query:
Ent.SetGettingQuery(entityType, query)
elif qualifier:
Ent.SetGettingQualifier(entityType, qualifier)
else:
Ent.SetGetting(entityType)
writeStderr(f'{Msg.GETTING_ALL} {Ent.PluralGetting()}{Ent.GettingPreQualifier()}{Ent.MayTakeTime(accountType)}\n')
def printGotAccountEntities(count):
if GC.Values[GC.SHOW_GETTINGS]:
writeStderr(f'{Msg.GOT} {count} {Ent.ChooseGetting(count)}{Ent.GettingPostQualifier()}\n')
def setGettingAllEntityItemsForWhom(entityItem, forWhom, query='', qualifier=''):
if GC.Values[GC.SHOW_GETTINGS]:
if query:
Ent.SetGettingQuery(entityItem, query)
elif qualifier:
Ent.SetGettingQualifier(entityItem, qualifier)
else:
Ent.SetGetting(entityItem)
Ent.SetGettingForWhom(forWhom)
def printGettingAllEntityItemsForWhom(entityItem, forWhom, i=0, count=0, query='', qualifier='', entityType=None):
if GC.Values[GC.SHOW_GETTINGS]:
setGettingAllEntityItemsForWhom(entityItem, forWhom, query=query, qualifier=qualifier)
writeStderr(f'{Msg.GETTING_ALL} {Ent.PluralGetting()}{Ent.GettingPreQualifier()} {Msg.FOR} {forWhom}{Ent.MayTakeTime(entityType)}{currentCountNL(i, count)}')
def printGotEntityItemsForWhom(count):
if GC.Values[GC.SHOW_GETTINGS]:
writeStderr(f'{Msg.GOT} {count} {Ent.ChooseGetting(count)}{Ent.GettingPostQualifier()} {Msg.FOR} {Ent.GettingForWhom()}\n')
def printGettingEntityItem(entityType, entityItem, i=0, count=0):
if GC.Values[GC.SHOW_GETTINGS]:
writeStderr(f'{Msg.GETTING} {Ent.Singular(entityType)} {entityItem}{currentCountNL(i, count)}')
def printGettingEntityItemForWhom(entityItem, forWhom, i=0, count=0):
if GC.Values[GC.SHOW_GETTINGS]:
Ent.SetGetting(entityItem)
Ent.SetGettingForWhom(forWhom)
writeStderr(f'{Msg.GETTING} {Ent.PluralGetting()} {Msg.FOR} {forWhom}{currentCountNL(i, count)}')
def stderrEntityMessage(entityValueList, message, i=0, count=0):
writeStderr(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[message],
currentCountNL(i, count)))
FIRST_ITEM_MARKER = '%%first_item%%'
LAST_ITEM_MARKER = '%%last_item%%'
TOTAL_ITEMS_MARKER = '%%total_items%%'
def getPageMessage(showFirstLastItems=False, showDate=None):
if not GC.Values[GC.SHOW_GETTINGS]:
return None
pageMessage = f'{Msg.GOT} {TOTAL_ITEMS_MARKER} {{0}}'
if showDate:
pageMessage += f' on {showDate}'
if showFirstLastItems:
pageMessage += f': {FIRST_ITEM_MARKER} - {LAST_ITEM_MARKER}'
else:
pageMessage += '...'
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
pageMessage += '\n'
else:
GM.Globals[GM.LAST_GOT_MSG_LEN] = 0
return pageMessage
def getPageMessageForWhom(forWhom=None, showFirstLastItems=False, showDate=None, clearLastGotMsgLen=True):
if not GC.Values[GC.SHOW_GETTINGS]:
return None
if forWhom:
Ent.SetGettingForWhom(forWhom)
pageMessage = f'{Msg.GOT} {TOTAL_ITEMS_MARKER} {{0}}{Ent.GettingPostQualifier()} {Msg.FOR} {Ent.GettingForWhom()}'
if showDate:
pageMessage += f' on {showDate}'
if showFirstLastItems:
pageMessage += f': {FIRST_ITEM_MARKER} - {LAST_ITEM_MARKER}'
else:
pageMessage += '...'
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
pageMessage += '\n'
elif clearLastGotMsgLen:
GM.Globals[GM.LAST_GOT_MSG_LEN] = 0
return pageMessage
def printLine(message):
writeStdout(message+'\n')
def printBlankLine():
writeStdout('\n')
def printKeyValueList(kvList):
writeStdout(formatKeyValueList(Ind.Spaces(), kvList, '\n'))
def printKeyValueListWithCount(kvList, i, count):
writeStdout(formatKeyValueList(Ind.Spaces(), kvList, currentCountNL(i, count)))
def printKeyValueDict(kvDict):
for key, value in iter(kvDict.items()):
writeStdout(formatKeyValueList(Ind.Spaces(), [key, value], '\n'))
def printKeyValueWithCRsNLs(key, value):
if value.find('\n') >= 0 or value.find('\r') >= 0:
if GC.Values[GC.SHOW_CONVERT_CR_NL]:
printKeyValueList([key, escapeCRsNLs(value)])
else:
printKeyValueList([key, ''])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(value)])
Ind.Decrement()
else:
printKeyValueList([key, value])
def printJSONKey(key):
writeStdout(formatKeyValueList(Ind.Spaces(), [key, None], ''))
def printJSONValue(value):
writeStdout(formatKeyValueList(' ', [value], '\n'))
def printEntity(entityValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList),
currentCountNL(i, count)))
def printEntityMessage(entityValueList, message, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[message],
currentCountNL(i, count)))
def printEntitiesCount(entityType, entityList):
writeStdout(formatKeyValueList(Ind.Spaces(),
[Ent.Plural(entityType), None if entityList is None else f'({len(entityList)})'],
'\n'))
def printEntityKVList(entityValueList, infoKVList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+infoKVList,
currentCountNL(i, count)))
def performAction(entityType, entityValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
[f'{Act.ToPerform()} {Ent.Singular(entityType)} {entityValue}'],
currentCountNL(i, count)))
def performActionNumItems(itemCount, itemType, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)}'],
currentCountNL(i, count)))
def performActionModifierNumItems(modifier, itemCount, itemType, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
[f'{Act.ToPerform()} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
currentCountNL(i, count)))
def actionPerformedNumItems(itemCount, itemType, i=0, count=0):
writeStderr(formatKeyValueList(Ind.Spaces(),
[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Performed()} '],
currentCountNL(i, count)))
def actionFailedNumItems(itemCount, itemType, errMessage, i=0, count=0):
writeStderr(formatKeyValueList(Ind.Spaces(),
[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Failed()}: {errMessage} '],
currentCountNL(i, count)))
def actionNotPerformedNumItemsWarning(itemCount, itemType, errMessage, i=0, count=0):
setSysExitRC(ACTION_NOT_PERFORMED_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[Ent.Choose(itemType, itemCount), itemCount, Act.NotPerformed(), errMessage],
currentCountNL(i, count)))
def entityPerformAction(entityValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()}'],
currentCountNL(i, count)))
def entityPerformActionNumItems(entityValueList, itemCount, itemType, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)}'],
currentCountNL(i, count)))
def entityPerformActionModifierNumItems(entityValueList, modifier, itemCount, itemType, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
currentCountNL(i, count)))
def entityPerformActionNumItemsModifier(entityValueList, itemCount, itemType, modifier, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier}'],
currentCountNL(i, count)))
def entityPerformActionSubItemModifierNumItems(entityValueList, subitemType, modifier, itemCount, itemType, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {Ent.Plural(subitemType)} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
currentCountNL(i, count)))
def entityPerformActionSubItemModifierNumItemsModifierNewValue(entityValueList, subitemType, modifier1, itemCount, itemType, modifier2, newValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+
[f'{Act.ToPerform()} {Ent.Plural(subitemType)} {modifier1} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier2}', newValue],
currentCountNL(i, count)))
def entityPerformActionModifierNumItemsModifier(entityValueList, modifier1, itemCount, itemType, modifier2, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier1} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier2}'],
currentCountNL(i, count)))
def entityPerformActionModifierItemValueList(entityValueList, modifier, infoTypeValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList),
currentCountNL(i, count)))
def entityPerformActionModifierNewValue(entityValueList, modifier, newValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue],
currentCountNL(i, count)))
def entityPerformActionModifierNewValueItemValueList(entityValueList, modifier, newValue, infoTypeValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue]+Ent.FormatEntityValueList(infoTypeValueList),
currentCountNL(i, count)))
def entityPerformActionItemValue(entityValueList, itemType, itemValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.ToPerform(), None, Ent.Singular(itemType), itemValue],
currentCountNL(i, count)))
def entityPerformActionInfo(entityValueList, infoValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.ToPerform(), infoValue],
currentCountNL(i, count)))
def entityActionPerformed(entityValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.Performed()],
currentCountNL(i, count)))
def entityActionPerformedMessage(entityValueList, message, i=0, count=0):
if message:
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.Performed(), message],
currentCountNL(i, count)))
else:
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.Performed()],
currentCountNL(i, count)))
def entityNumItemsActionPerformed(entityValueList, itemCount, itemType, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Performed()}'],
currentCountNL(i, count)))
def entityModifierActionPerformed(entityValueList, modifier, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', None],
currentCountNL(i, count)))
def entityModifierItemValueListActionPerformed(entityValueList, modifier, infoTypeValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList),
currentCountNL(i, count)))
def entityModifierNewValueActionPerformed(entityValueList, modifier, newValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue],
currentCountNL(i, count)))
def entityModifierNewValueItemValueListActionPerformed(entityValueList, modifier, newValue, infoTypeValueList, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue]+Ent.FormatEntityValueList(infoTypeValueList),
currentCountNL(i, count)))
def entityModifierNewValueKeyValueActionPerformed(entityValueList, modifier, newValue, infoKey, infoValue, i=0, count=0):
writeStdout(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue, infoKey, infoValue],
currentCountNL(i, count)))
def cleanFilename(filename):
return sanitize_filename(filename, '_')
def setFilePath(fileName):
if fileName.startswith('./') or fileName.startswith('.\\'):
fileName = os.path.join(os.getcwd(), fileName[2:])
else:
fileName = os.path.expanduser(fileName)
if not os.path.isabs(fileName):
fileName = os.path.join(GC.Values[GC.DRIVE_DIR], fileName)
return fileName
def uniqueFilename(targetFolder, filetitle, overwrite, extension=None):
filename = filetitle
y = 0
while True:
if extension is not None and filename.lower()[-len(extension):] != extension.lower():
filename += extension
filepath = os.path.join(targetFolder, filename)
if overwrite or not os.path.isfile(filepath):
return (filepath, filename)
y += 1
filename = f'({y})-{filetitle}'
def cleanFilepath(filepath):
return sanitize_filepath(filepath, platform='auto')
def fileErrorMessage(filename, e, entityType=Ent.FILE):
return f'{Ent.Singular(entityType)}: {filename}, {str(e)}'
def fdErrorMessage(f, defaultFilename, e):
return fileErrorMessage(getattr(f, 'name') if hasattr(f, 'name') else defaultFilename, e)
# Set file encoding to handle UTF8 BOM
def setEncoding(mode, encoding):
if 'b' in mode:
return {}
if not encoding:
encoding = GM.Globals[GM.SYS_ENCODING]
if 'r' in mode and encoding.lower().replace('-', '') == 'utf8':
encoding = UTF8_SIG
return {'encoding': encoding}
def StringIOobject(initbuff=None):
if initbuff is None:
return io.StringIO()
return io.StringIO(initbuff)
# Open a file
def openFile(filename, mode=DEFAULT_FILE_READ_MODE, encoding=None, errors=None, newline=None,
continueOnError=False, displayError=True, stripUTFBOM=False):
try:
if filename != '-':
kwargs = setEncoding(mode, encoding)
f = open(os.path.expanduser(filename), mode, errors=errors, newline=newline, **kwargs)
if stripUTFBOM:
if 'b' in mode:
if f.read(3) != b'\xef\xbb\xbf':
f.seek(0)
elif not kwargs['encoding'].lower().startswith('utf'):
if f.read(3).encode('iso-8859-1', 'replace') != codecs.BOM_UTF8:
f.seek(0)
else:
if f.read(1) != '\ufeff':
f.seek(0)
return f
if 'r' in mode:
return StringIOobject(str(sys.stdin.read()))
if 'b' not in mode:
return sys.stdout
return os.fdopen(os.dup(sys.stdout.fileno()), 'wb')
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
if continueOnError:
if displayError:
stderrWarningMsg(fileErrorMessage(filename, e))
setSysExitRC(FILE_ERROR_RC)
return None
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
# Close a file
def closeFile(f, forceFlush=False):
try:
if forceFlush:
# Necessary to make sure file is flushed by both Python and OS
# https://stackoverflow.com/a/13762137/1503886
f.flush()
os.fsync(f.fileno())
f.close()
return True
except IOError as e:
stderrErrorMsg(fdErrorMessage(f, UNKNOWN, e))
setSysExitRC(FILE_ERROR_RC)
return False
# Read a file
def readFile(filename, mode=DEFAULT_FILE_READ_MODE, encoding=None, newline=None,
continueOnError=False, displayError=True):
try:
if filename != '-':
kwargs = setEncoding(mode, encoding)
with open(os.path.expanduser(filename), mode, newline=newline, **kwargs) as f:
return f.read()
return str(sys.stdin.read())
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
if continueOnError:
if displayError:
stderrWarningMsg(fileErrorMessage(filename, e))
setSysExitRC(FILE_ERROR_RC)
return None
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
# Write a file
def writeFile(filename, data, mode=DEFAULT_FILE_WRITE_MODE,
continueOnError=False, displayError=True):
try:
if filename != '-':
kwargs = setEncoding(mode, None)
with open(os.path.expanduser(filename), mode, **kwargs) as f:
f.write(data)
return True
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).write(data)
return True
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
if continueOnError:
if displayError:
stderrErrorMsg(fileErrorMessage(filename, e))
setSysExitRC(FILE_ERROR_RC)
return False
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
# Write a file, return error
def writeFileReturnError(filename, data, mode=DEFAULT_FILE_WRITE_MODE):
try:
kwargs = {'encoding': GM.Globals[GM.SYS_ENCODING]} if 'b' not in mode else {}
with open(os.path.expanduser(filename), mode, **kwargs) as f:
f.write(data)
return (True, None)
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
return (False, e)
# Delete a file
def deleteFile(filename, continueOnError=False, displayError=True):
if os.path.isfile(filename):
try:
os.remove(filename)
except OSError as e:
if continueOnError:
if displayError:
stderrWarningMsg(fileErrorMessage(filename, e))
return
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
def getGDocSheetDataRetryWarning(entityValueList, errMsg, i=0, count=0):
action = Act.Get()
Act.Set(Act.RETRIEVE_DATA)
stderrWarningMsg(formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg, 'Retry', ''],
currentCountNL(i, count)))
Act.Set(action)
def getGDocSheetDataFailedExit(entityValueList, errMsg, i=0, count=0):
Act.Set(Act.RETRIEVE_DATA)
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg],
currentCountNL(i, count)))
GDOC_FORMAT_MIME_TYPES = {
'gcsv': MIMETYPE_TEXT_CSV,
'gdoc': MIMETYPE_TEXT_PLAIN,
'ghtml': MIMETYPE_TEXT_HTML,
}
# gdoc <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity>
def getGDocData(gformat):
mimeType = GDOC_FORMAT_MIME_TYPES[gformat]
user = getEmailAddress()
fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
user, drive, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity)
if not drive:
sys.exit(GM.Globals[GM.SYSEXITRC])
if jcount == 0:
getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
if jcount > 1:
getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
fileId = fileIdEntity['list'][0]
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,mimeType,exportLinks',
supportsAllDrives=True)
# Google Doc
if 'exportLinks' in result:
if mimeType not in result['exportLinks']:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
Msg.INVALID_MIMETYPE.format(result['mimeType'], mimeType))
f = TemporaryFile(mode='w+', encoding=UTF8)
_, content = drive._http.request(uri=result['exportLinks'][mimeType], method='GET')
f.write(content.decode(UTF8_SIG))
f.seek(0)
return f
# Drive File
if result['mimeType'] != mimeType:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
Msg.INVALID_MIMETYPE.format(result['mimeType'], mimeType))
fb = TemporaryFile(mode='wb+')
request = drive.files().get_media(fileId=fileId)
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
done = False
while not done:
_, done = downloader.next_chunk()
f = TemporaryFile(mode='w+', encoding=UTF8)
fb.seek(0)
f.write(fb.read().decode(UTF8_SIG))
fb.close()
f.seek(0)
return f
except GAPI.fileNotFound:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DOCUMENT, fileId], Msg.DOES_NOT_EXIST)
except (IOError, httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if f:
f.close()
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DOCUMENT, fileId], str(e))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e))
sys.exit(GM.Globals[GM.SYSEXITRC])
HTML_TITLE_PATTERN = re.compile(r'.*<title>(.+)</title>')
# gsheet <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity> <SheetEntity>
def getGSheetData():
user = getEmailAddress()
fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
sheetEntity = getSheetEntity(False)
user, drive, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity)
if not drive:
sys.exit(GM.Globals[GM.SYSEXITRC])
if jcount == 0:
getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
if jcount > 1:
getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
_, sheet = buildGAPIServiceObject(API.SHEETS, user)
if not sheet:
sys.exit(GM.Globals[GM.SYSEXITRC])
fileId = fileIdEntity['list'][0]
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,mimeType', supportsAllDrives=True)
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
Msg.INVALID_MIMETYPE.format(result['mimeType'], MIMETYPE_GA_SPREADSHEET))
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))')
sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
if sheetId is None:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], Msg.NOT_FOUND)
spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?format=csv&id={fileId}&gid={sheetId}'
f = TemporaryFile(mode='w+', encoding=UTF8)
if GC.Values[GC.DEBUG_LEVEL] > 0:
sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n')
triesLimit = 3
for n in range(1, triesLimit+1):
_, content = drive._http.request(uri=spreadsheetUrl, method='GET')
# Check for HTML error message instead of data
if content[0:15] != b'<!DOCTYPE html>':
break
tg = HTML_TITLE_PATTERN.match(content[0:600].decode('utf-8'))
errMsg = tg.group(1) if tg else 'Unknown error'
getGDocSheetDataRetryWarning([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg, n, triesLimit)
time.sleep(20)
else:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg)
f.write(content.decode(UTF8_SIG))
f.seek(0)
return f
except GAPI.fileNotFound:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId], Msg.DOES_NOT_EXIST)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId, sheetEntity['sheetType'], sheetEntity['sheetValue']], str(e))
except (IOError, httplib2.HttpLib2Error) as e:
if f:
f.close()
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId, sheetEntity['sheetType'], sheetEntity['sheetValue']], str(e))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e))
sys.exit(GM.Globals[GM.SYSEXITRC])
BUCKET_OBJECT_PATTERNS = [
{'pattern': re.compile(r'https://storage.(?:googleapis|cloud.google).com/(.+?)/(.+)'), 'unquote': True},
{'pattern': re.compile(r'gs://(.+?)/(.+)'), 'unquote': False},
{'pattern': re.compile(r'(.+?)/(.+)'), 'unquote': False},
]
def getBucketObjectName():
uri = getString(Cmd.OB_STRING)
for pattern in BUCKET_OBJECT_PATTERNS:
mg = re.search(pattern['pattern'], uri)
if mg:
bucket = mg.group(1)
s_object = mg.group(2) if not pattern['unquote'] else unquote(mg.group(2))
return (bucket, s_object, f'{bucket}/{s_object}')
systemErrorExit(ACTION_NOT_PERFORMED_RC, f'Invalid <StorageBucketObjectName>: {uri}')
GCS_FORMAT_MIME_TYPES = {
'gcscsv': MIMETYPE_TEXT_CSV,
'gcsdoc': MIMETYPE_TEXT_PLAIN,
'gcshtml': MIMETYPE_TEXT_HTML,
}
# gcscsv|gcshtml|gcsdoc <StorageBucketObjectName>
def getStorageFileData(gcsformat, returnData=True):
mimeType = GCS_FORMAT_MIME_TYPES[gcsformat]
bucket, s_object, bucketObject = getBucketObjectName()
s = buildGAPIObject(API.STORAGEREAD)
try:
result = callGAPI(s.objects(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
bucket=bucket, object=s_object, projection='noAcl', fields='contentType')
except GAPI.notFound:
entityDoesNotExistExit(Ent.CLOUD_STORAGE_FILE, bucketObject)
except GAPI.forbidden as e:
entityActionFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject], str(e))
if result['contentType'] != mimeType:
getGDocSheetDataFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject],
Msg.INVALID_MIMETYPE.format(result['contentType'], mimeType))
fb = TemporaryFile(mode='wb+')
try:
request = s.objects().get_media(bucket=bucket, object=s_object)
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
done = False
while not done:
_, done = downloader.next_chunk()
fb.seek(0)
if returnData:
data = fb.read().decode(UTF8)
fb.close()
return data
f = TemporaryFile(mode='w+', encoding=UTF8)
f.write(fb.read().decode(UTF8_SIG))
fb.close()
f.seek(0)
return f
except googleapiclient.http.HttpError as e:
mg = HTTP_ERROR_PATTERN.match(str(e))
getGDocSheetDataFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject], mg.group(1) if mg else str(e))
# <CSVFileInput>
def openCSVFileReader(filename, fieldnames=None):
filenameLower = filename.lower()
if filenameLower == 'gsheet':
f = getGSheetData()
getCharSet()
elif filenameLower in {'gcsv', 'gdoc'}:
f = getGDocData(filenameLower)
getCharSet()
elif filenameLower in {'gcscsv', 'gcsdoc'}:
f = getStorageFileData(filenameLower, False)
getCharSet()
else:
encoding = getCharSet()
f = openFile(filename, mode=DEFAULT_CSV_READ_MODE, encoding=encoding)
if checkArgumentPresent('warnifnodata'):
loc = f.tell()
try:
if not f.readline() or not f.readline():
stderrWarningMsg(fileErrorMessage(filename, Msg.NO_CSV_FILE_DATA_FOUND))
sys.exit(NO_ENTITIES_FOUND_RC)
f.seek(loc)
except (IOError, UnicodeDecodeError, UnicodeError) as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
if checkArgumentPresent('columndelimiter'):
columnDelimiter = getCharacter()
else:
columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
if checkArgumentPresent('noescapechar'):
noEscapeChar = getBoolean()
else:
noEscapeChar = GC.Values[GC.CSV_INPUT_NO_ESCAPE_CHAR]
if checkArgumentPresent('quotechar'):
quotechar = getCharacter()
else:
quotechar = GC.Values[GC.CSV_INPUT_QUOTE_CHAR]
if not checkArgumentPresent('endcsv') and checkArgumentPresent('fields'):
fieldnames = shlexSplitList(getString(Cmd.OB_FIELD_NAME_LIST))
try:
csvFile = csv.DictReader(f, fieldnames=fieldnames,
delimiter=columnDelimiter,
escapechar='\\' if not noEscapeChar else None,
quotechar=quotechar)
return (f, csvFile, csvFile.fieldnames if csvFile.fieldnames is not None else [])
except (csv.Error, UnicodeDecodeError, UnicodeError) as e:
systemErrorExit(FILE_ERROR_RC, e)
def incrAPICallsRetryData(errMsg, delta):
GM.Globals[GM.API_CALLS_RETRY_DATA].setdefault(errMsg, [0, 0.0])
GM.Globals[GM.API_CALLS_RETRY_DATA][errMsg][0] += 1
GM.Globals[GM.API_CALLS_RETRY_DATA][errMsg][1] += delta
def initAPICallsRateCheck():
GM.Globals[GM.RATE_CHECK_COUNT] = 0
GM.Globals[GM.RATE_CHECK_START] = time.time()
def checkAPICallsRate():
GM.Globals[GM.RATE_CHECK_COUNT] += 1
if GM.Globals[GM.RATE_CHECK_COUNT] >= GC.Values[GC.API_CALLS_RATE_LIMIT]:
current = time.time()
delta = int(current-GM.Globals[GM.RATE_CHECK_START])
if 0 <= delta < 60:
delta = (60-delta)+3
error_message = f'API calls per 60 seconds limit {GC.Values[GC.API_CALLS_RATE_LIMIT]} exceeded'
writeStderr(f'{WARNING_PREFIX}{error_message}: Backing off: {delta} seconds\n')
flushStderr()
time.sleep(delta)
if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]:
incrAPICallsRetryData(error_message, delta)
GM.Globals[GM.RATE_CHECK_START] = time.time()
else:
GM.Globals[GM.RATE_CHECK_START] = current
GM.Globals[GM.RATE_CHECK_COUNT] = 0
def openGAMCommandLog(Globals, name):
try:
Globals[GM.CMDLOG_LOGGER] = logging.getLogger(name)
Globals[GM.CMDLOG_LOGGER].setLevel(logging.INFO)
Globals[GM.CMDLOG_HANDLER] = RotatingFileHandler(GC.Values[GC.CMDLOG],
maxBytes=1024*GC.Values[GC.CMDLOG_MAX_KILO_BYTES],
backupCount=GC.Values[GC.CMDLOG_MAX_BACKUPS],
encoding=GC.Values[GC.CHARSET])
Globals[GM.CMDLOG_LOGGER].addHandler(Globals[GM.CMDLOG_HANDLER])
except Exception as e:
Globals[GM.CMDLOG_LOGGER] = None
systemErrorExit(CONFIG_ERROR_RC, Msg.LOGGING_INITIALIZATION_ERROR.format(str(e)))
def writeGAMCommandLog(Globals, logCmd, sysRC):
Globals[GM.CMDLOG_LOGGER].info(f'{currentISOformatTimeStamp()},{sysRC},{logCmd}')
def closeGAMCommandLog(Globals):
try:
Globals[GM.CMDLOG_HANDLER].flush()
Globals[GM.CMDLOG_HANDLER].close()
Globals[GM.CMDLOG_LOGGER].removeHandler(Globals[GM.CMDLOG_HANDLER])
except Exception:
pass
Globals[GM.CMDLOG_LOGGER] = None
# Set global variables from config file
# Return True if there are additional commands on the command line
def SetGlobalVariables():
def _stringInQuotes(value):
return (len(value) > 1) and (((value.startswith('"') and value.endswith('"'))) or ((value.startswith("'") and value.endswith("'"))))
def _stripStringQuotes(value):
if _stringInQuotes(value):
return value[1:-1]
return value
def _quoteStringIfLeadingTrailingBlanks(value):
if not value:
return "''"
if _stringInQuotes(value):
return value
if (value[0] != ' ') and (value[-1] != ' '):
return value
return f"'{value}'"
def _getDefault(itemName, itemEntry, oldGamPath):
if GC.VAR_SIGFILE in itemEntry:
GC.Defaults[itemName] = itemEntry[GC.VAR_SFFT][os.path.isfile(os.path.join(oldGamPath, itemEntry[GC.VAR_SIGFILE]))]
elif GC.VAR_ENVVAR in itemEntry:
value = os.environ.get(itemEntry[GC.VAR_ENVVAR], GC.Defaults[itemName])
if itemEntry[GC.VAR_TYPE] in [GC.TYPE_INTEGER, GC.TYPE_FLOAT]:
try:
number = int(value) if itemEntry[GC.VAR_TYPE] == GC.TYPE_INTEGER else float(value)
minVal, maxVal = itemEntry[GC.VAR_LIMITS]
if (minVal is not None) and (number < minVal):
number = minVal
elif (maxVal is not None) and (number > maxVal):
number = maxVal
except ValueError:
number = GC.Defaults[itemName]
value = str(number)
elif itemEntry[GC.VAR_TYPE] == GC.TYPE_STRING:
value = _quoteStringIfLeadingTrailingBlanks(value)
GC.Defaults[itemName] = value
def _selectSection():
value = getString(Cmd.OB_SECTION_NAME, minLen=0)
if (not value) or (value.upper() == configparser.DEFAULTSECT):
return configparser.DEFAULTSECT
if GM.Globals[GM.PARSER].has_section(value):
return value
Cmd.Backup()
usageErrorExit(formatKeyValueList('', [Ent.Singular(Ent.SECTION), value, Msg.NOT_FOUND], ''))
def _showSections():
printKeyValueList([Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE]])
Ind.Increment()
for section in [configparser.DEFAULTSECT]+sorted(GM.Globals[GM.PARSER].sections()):
printKeyValueList([f'{section}{" *" if section == sectionName else ""}'])
Ind.Decrement()
def _checkMakeDir(itemName):
if not os.path.isdir(GC.Defaults[itemName]):
try:
os.makedirs(GC.Defaults[itemName])
printKeyValueList([Act.PerformedName(Act.CREATE), GC.Defaults[itemName]])
except OSError as e:
if not os.path.isdir(GC.Defaults[itemName]):
systemErrorExit(FILE_ERROR_RC, e)
def _copyCfgFile(srcFile, targetDir, oldGamPath):
if (not srcFile) or os.path.isabs(srcFile):
return
dstFile = os.path.join(GC.Defaults[targetDir], srcFile)
if os.path.isfile(dstFile):
return
srcFile = os.path.join(oldGamPath, srcFile)
if not os.path.isfile(srcFile):
return
data = readFile(srcFile, continueOnError=True, displayError=False)
if (data is not None) and writeFile(dstFile, data, continueOnError=True):
printKeyValueList([Act.PerformedName(Act.COPY), srcFile, Msg.TO, dstFile])
def _printValueError(sectionName, itemName, value, errMessage, sysRC=CONFIG_ERROR_RC):
kvlMsg = formatKeyValueList('',
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.SECTION), sectionName,
Ent.Singular(Ent.ITEM), itemName,
Ent.Singular(Ent.VALUE), value,
errMessage],
'')
if sysRC != 0:
status['errors'] = True
printErrorMessage(sysRC, kvlMsg)
else:
writeStderr(formatKeyValueList(Ind.Spaces(), [WARNING, kvlMsg], '\n'))
def _getCfgBoolean(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName).lower()
if value in TRUE_VALUES:
return True
if value in FALSE_VALUES:
return False
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {formatChoiceList(TRUE_FALSE)}')
return False
def _getCfgCharacter(sectionName, itemName):
value = codecs.escape_decode(bytes(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)), UTF8))[0].decode(UTF8)
if not value and (itemName == 'csv_output_field_delimiter'):
return ' '
if not value and (itemName in {'csv_input_escape_char', 'csv_output_escape_char'}):
return None
if len(value) == 1:
return value
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {integerLimits(1, 1, Msg.STRING_LENGTH)}')
return ''
def _getCfgChoice(sectionName, itemName):
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)).lower()
choices = GC.VAR_INFO[itemName][GC.VAR_CHOICES]
if value in choices:
return choices[value]
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {",".join(choices)}')
return ''
def _getCfgLocale(sectionName, itemName):
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)).lower().replace('_', '-')
if value in LOCALE_CODES_MAP:
return LOCALE_CODES_MAP[value]
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {",".join(LOCALE_CODES_MAP)}')
return ''
def _getCfgNumber(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
minVal, maxVal = GC.VAR_INFO[itemName][GC.VAR_LIMITS]
try:
number = int(value) if GC.VAR_INFO[itemName][GC.VAR_TYPE] == GC.TYPE_INTEGER else float(value)
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
return number
if (minVal is not None) and (number < minVal):
number = minVal
else:
number = maxVal
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {integerLimits(minVal, maxVal)}, {Msg.USED}: {number}', sysRC=0)
return number
except ValueError:
pass
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {integerLimits(minVal, maxVal)}')
return 0
def _getCfgHeaderFilter(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
headerFilters = []
if not value or (len(value) == 2 and _stringInQuotes(value)):
return headerFilters
splitStatus, filters = shlexSplitListStatus(value)
if splitStatus:
for filterStr in filters:
try:
headerFilters.append(re.compile(filterStr, re.IGNORECASE))
except re.error as e:
_printValueError(sectionName, itemName, f'"{filterStr}"', f'{Msg.INVALID_RE}: {e}')
else:
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.INVALID_LIST}: {filters}')
return headerFilters
def _getCfgHeaderFilterFromForce(sectionName, itemName):
headerFilters = []
for filterStr in GC.Values[itemName]:
try:
headerFilters.append(re.compile(fr'^{filterStr}$'))
except re.error as e:
_printValueError(sectionName, itemName, f'"{filterStr}"', f'{Msg.INVALID_RE}: {e}')
return headerFilters
ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE)
ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE)
ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE)
ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE)
ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE)
ROW_FILTER_TEXTRANGE_PATTERN = re.compile(r'^(textrange)(=|!=)(.*)/(.*)$', re.IGNORECASE)
ROW_FILTER_RE_PATTERN = re.compile(r'^(regex|regexcs|notregex|notregexcs):(.*)$', re.IGNORECASE)
ROW_FILTER_DATA_PATTERN = re.compile(r'^(data|notdata):(list|file|csvfile) +(.+)$', re.IGNORECASE)
REGEX_CHARS = '^$*+|$[{('
def _getCfgRowFilter(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
rowFilters = []
if not value:
return rowFilters
if value.startswith('{'):
try:
filterDict = json.loads(value.encode('unicode-escape').decode(UTF8))
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.FAILED_TO_PARSE_AS_JSON}: {str(e)}')
return rowFilters
else:
filterDict = {}
status, filterList = shlexSplitListStatus(value)
if not status:
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.FAILED_TO_PARSE_AS_LIST}: {str(filterList)}')
return rowFilters
for filterVal in filterList:
if not filterVal:
continue
try:
filterTokens = shlexSplitList(filterVal, ':')
column = filterTokens[0]
filterStr = ':'.join(filterTokens[1:])
except ValueError:
_printValueError(sectionName, itemName, f'"{filterVal}"', f'{Msg.EXPECTED}: column:filter')
continue
filterDict[column] = filterStr
for column, filterStr in iter(filterDict.items()):
for c in REGEX_CHARS:
if c in column:
columnPat = column
break
else:
columnPat = f'^{column}$'
try:
columnPat = re.compile(columnPat, re.IGNORECASE)
except re.error as e:
_printValueError(sectionName, itemName, f'"{column}"', f'{Msg.INVALID_RE}: {e}')
continue
anyMatch = True
mg = ROW_FILTER_ANY_ALL_PATTERN.match(filterStr)
if mg:
anyMatch = mg.group(1).lower() == 'any:'
filterStr = mg.group(2)
mg = ROW_FILTER_COMP_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
if filterType in {'date', 'time'}:
if filterType == 'date':
valid, filterValue = getRowFilterDateOrDeltaFromNow(mg.group(3))
else:
valid, filterValue = getRowFilterTimeOrDeltaFromNow(mg.group(3))
if valid:
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue))
else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue}')
else: # filterType in {'count', 'length'}:
if mg.group(3).isdigit():
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3))))
else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Number>')
continue
mg = ROW_FILTER_TEXT_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), mg.group(3)))
continue
mg = ROW_FILTER_TEXTRANGE_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), mg.group(3), mg.group(4)))
continue
mg = ROW_FILTER_RANGE_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
if filterType in {'daterange', 'timerange'}:
if filterType == 'daterange':
valid1, filterValue1 = getRowFilterDateOrDeltaFromNow(mg.group(3))
valid2, filterValue2 = getRowFilterDateOrDeltaFromNow(mg.group(4))
else:
valid1, filterValue1 = getRowFilterTimeOrDeltaFromNow(mg.group(3))
valid2, filterValue2 = getRowFilterTimeOrDeltaFromNow(mg.group(4))
if valid1 and valid2:
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue1, filterValue2))
else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue1}/{filterValue2}')
else: #countrange|lengthrange
if mg.group(3).isdigit() and mg.group(4).isdigit():
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)), int(mg.group(4))))
else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Number>/<Number>')
continue
mg = ROW_FILTER_TIMEOFDAYRANGE_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
startHour = int(mg.group(3))
startMinute = int(mg.group(4))
endHour = int(mg.group(5))
endMinute = int(mg.group(6))
if startHour > 23 or startMinute > 59 or endHour > 23 or endMinute > 59 or \
endHour < startHour or (endHour == startHour and endMinute < startMinute):
Cmd.Backup()
usageErrorExit(Msg.INVALID_TIMEOFDAY_RANGE.format(f'{startHour:02d}:{startMinute:02d}', f'{endHour:02d}:{endMinute:02d}'))
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), f'{startHour:02d}:{startMinute:02d}', f'{endHour:02d}:{endMinute:02d}'))
continue
mg = ROW_FILTER_BOOL_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
filterValue = mg.group(2).lower()
if filterValue in TRUE_VALUES:
rowFilters.append((columnPat, anyMatch, filterType, True))
elif filterValue in FALSE_VALUES:
rowFilters.append((columnPat, anyMatch, filterType, False))
else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Boolean>')
continue
mg = ROW_FILTER_RE_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
try:
if filterType.endswith('cs'):
filterType = filterType[0:-2]
flags = 0
else:
flags = re.IGNORECASE
rowFilters.append((columnPat, anyMatch, filterType, re.compile(mg.group(2), flags)))
except re.error as e:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.INVALID_RE}: {e}')
continue
mg = ROW_FILTER_DATA_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
filterSubType = mg.group(2).lower()
if filterSubType == 'list':
rowFilters.append((columnPat, anyMatch, filterType, set(shlexSplitList(mg.group(3)))))
continue
Cmd.MergeArguments(shlexSplitList(mg.group(3), ' '))
if filterSubType == 'file':
rowFilters.append((columnPat, anyMatch, filterType, getEntitiesFromFile(False, returnSet=True)))
else: #elif filterSubType == 'csvfile':
rowFilters.append((columnPat, anyMatch, filterType, getEntitiesFromCSVFile(False, returnSet=True)))
Cmd.RestoreArguments()
continue
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <RowValueFilter>')
return rowFilters
def _getCfgSection(sectionName, itemName):
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName))
if (not value) or (value.upper() == configparser.DEFAULTSECT):
return configparser.DEFAULTSECT
if GM.Globals[GM.PARSER].has_section(value):
return value
_printValueError(sectionName, itemName, value, Msg.NOT_FOUND)
return configparser.DEFAULTSECT
def _getCfgPassword(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
if isinstance(value, bytes):
return value
value = _stripStringQuotes(value)
if value.startswith("b'") and value.endswith("'"):
return bytes(value[2:-1], UTF8)
if value:
return value
return ''
def _validateLicenseSKUs(sectionName, itemName, skuList):
GM.Globals[GM.LICENSE_SKUS] = []
for sku in skuList.split(','):
if '/' not in sku:
productId, sku = SKU.getProductAndSKU(sku)
if not productId:
_printValueError(sectionName, itemName, sku, f'{Msg.EXPECTED}: {",".join(SKU.getSortedSKUList())}')
else:
(productId, sku) = sku.split('/')
if (productId, sku) not in GM.Globals[GM.LICENSE_SKUS]:
GM.Globals[GM.LICENSE_SKUS].append((productId, sku))
def _getCfgString(sectionName, itemName):
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName))
if itemName == GC.DOMAIN:
value = value.strip()
minLen, maxLen = GC.VAR_INFO[itemName].get(GC.VAR_LIMITS, (None, None))
if ((minLen is None) or (len(value) >= minLen)) and ((maxLen is None) or (len(value) <= maxLen)):
if itemName == GC.LICENSE_SKUS and value:
_validateLicenseSKUs(sectionName, itemName, value)
return value
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {integerLimits(minLen, maxLen, Msg.STRING_LENGTH)}')
return ''
def _getCfgStringList(sectionName, itemName):
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
stringlist = []
if not value or (len(value) == 2 and _stringInQuotes(value)):
return stringlist
splitStatus, stringlist = shlexSplitListStatus(value)
if not splitStatus:
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.INVALID_LIST}: {stringlist}')
return stringlist
def _getCfgTimezone(sectionName, itemName):
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName).lower())
if value == 'utc':
GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = False
return iso8601.UTC
GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = True
if value == 'local':
return iso8601.Local
try:
return iso8601.parse_timezone_str(value)
except (iso8601.ParseError, OverflowError):
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {TIMEZONE_FORMAT_REQUIRED}')
GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = False
return iso8601.UTC
def _getCfgDirectory(sectionName, itemName):
dirPath = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)))
if (not dirPath) and (itemName in {GC.GMAIL_CSE_INCERT_DIR, GC.GMAIL_CSE_INKEY_DIR}):
return dirPath
if (not dirPath) or (not os.path.isabs(dirPath) and dirPath != '.'):
if (sectionName != configparser.DEFAULTSECT) and (GM.Globals[GM.PARSER].has_option(sectionName, itemName)):
dirPath = os.path.join(os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, itemName))), dirPath)
if not os.path.isabs(dirPath):
dirPath = os.path.join(GM.Globals[GM.GAM_CFG_PATH], dirPath)
return dirPath
def _getCfgFile(sectionName, itemName):
value = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)))
if value and not os.path.isabs(value):
value = os.path.expanduser(os.path.join(_getCfgDirectory(sectionName, GC.CONFIG_DIR), value))
elif not value and itemName == GC.CACERTS_PEM:
if hasattr(sys, '_MEIPASS'):
value = os.path.join(sys._MEIPASS, GC.FN_CACERTS_PEM) #pylint: disable=no-member
else:
value = os.path.join(GM.Globals[GM.GAM_PATH], GC.FN_CACERTS_PEM)
return value
def _readGamCfgFile(config, fileName):
try:
with open(fileName, DEFAULT_FILE_READ_MODE, encoding=GM.Globals[GM.SYS_ENCODING]) as f:
config.read_file(f)
except (configparser.DuplicateOptionError, configparser.DuplicateSectionError,
configparser.MissingSectionHeaderError, configparser.ParsingError) as e:
systemErrorExit(CONFIG_ERROR_RC, formatKeyValueList('',
[Ent.Singular(Ent.CONFIG_FILE), fileName,
Msg.INVALID, str(e)],
''))
except IOError as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(fileName, e, Ent.CONFIG_FILE))
def _writeGamCfgFile(config, fileName, action):
GM.Globals[GM.SECTION] = None # No need to save section for inner gams
try:
with open(fileName, DEFAULT_FILE_WRITE_MODE, encoding=GM.Globals[GM.SYS_ENCODING]) as f:
config.write(f)
printKeyValueList([Ent.Singular(Ent.CONFIG_FILE), fileName, Act.PerformedName(action)])
except IOError as e:
stderrErrorMsg(fileErrorMessage(fileName, e, Ent.CONFIG_FILE))
def _verifyValues(sectionName, inputFilterSectionName, outputFilterSectionName):
printKeyValueList([Ent.Singular(Ent.SECTION), sectionName]) # Do not use printEntity
Ind.Increment()
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
sectName = sectionName
if itemName in GC.CSV_INPUT_ROW_FILTER_ITEMS:
if inputFilterSectionName:
sectName = inputFilterSectionName
elif itemName in GC.CSV_OUTPUT_ROW_FILTER_ITEMS:
if outputFilterSectionName:
sectName = outputFilterSectionName
cfgValue = GM.Globals[GM.PARSER].get(sectName, itemName)
varType = itemEntry[GC.VAR_TYPE]
if varType == GC.TYPE_CHOICE:
for choice, value in iter(itemEntry[GC.VAR_CHOICES].items()):
if cfgValue == value:
cfgValue = choice
break
elif varType not in [GC.TYPE_BOOLEAN, GC.TYPE_INTEGER, GC.TYPE_FLOAT, GC.TYPE_PASSWORD]:
cfgValue = _quoteStringIfLeadingTrailingBlanks(cfgValue)
if varType == GC.TYPE_FILE:
expdValue = _getCfgFile(sectName, itemName)
if cfgValue not in ("''", expdValue):
cfgValue = f'{cfgValue} ; {expdValue}'
elif varType == GC.TYPE_DIRECTORY:
expdValue = _getCfgDirectory(sectName, itemName)
if cfgValue not in ("''", expdValue):
cfgValue = f'{cfgValue} ; {expdValue}'
elif (itemName == GC.SECTION) and (sectName != configparser.DEFAULTSECT):
continue
printLine(f'{Ind.Spaces()}{itemName} = {cfgValue}')
Ind.Decrement()
def _chkCfgDirectories(sectionName):
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
if itemEntry[GC.VAR_TYPE] == GC.TYPE_DIRECTORY:
dirPath = GC.Values[itemName]
if (not dirPath) and (itemName in {GC.GMAIL_CSE_INCERT_DIR, GC.GMAIL_CSE_INKEY_DIR}):
return
if (itemName != GC.CACHE_DIR or not GC.Values[GC.NO_CACHE]) and not os.path.isdir(dirPath):
writeStderr(formatKeyValueList(WARNING_PREFIX,
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.SECTION), sectionName,
Ent.Singular(Ent.ITEM), itemName,
Ent.Singular(Ent.VALUE), dirPath,
Msg.INVALID_PATH],
'\n'))
def _chkCfgFiles(sectionName):
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
if itemEntry[GC.VAR_TYPE] == GC.TYPE_FILE:
fileName = GC.Values[itemName]
if (not fileName) and (itemName in {GC.EXTRA_ARGS, GC.CMDLOG}):
continue
if itemName == GC.CLIENT_SECRETS_JSON: # Added 6.57.01
continue
if GC.Values[GC.ENABLE_DASA] and itemName == GC.OAUTH2_TXT:
continue
if not os.path.isfile(fileName):
writeStderr(formatKeyValueList([WARNING_PREFIX, ERROR_PREFIX][itemName == GC.CACERTS_PEM],
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.SECTION), sectionName,
Ent.Singular(Ent.ITEM), itemName,
Ent.Singular(Ent.VALUE), fileName,
Msg.NOT_FOUND],
'\n'))
if itemName == GC.CACERTS_PEM:
status['errors'] = True
elif not os.access(fileName, itemEntry[GC.VAR_ACCESS]):
if itemEntry[GC.VAR_ACCESS] == os.R_OK | os.W_OK:
accessMsg = Msg.NEED_READ_WRITE_ACCESS
elif itemEntry[GC.VAR_ACCESS] == os.R_OK:
accessMsg = Msg.NEED_READ_ACCESS
else:
accessMsg = Msg.NEED_WRITE_ACCESS
writeStderr(formatKeyValueList(ERROR_PREFIX,
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.SECTION), sectionName,
Ent.Singular(Ent.ITEM), itemName,
Ent.Singular(Ent.VALUE), fileName,
accessMsg],
'\n'))
status['errors'] = True
def _setCSVFile(fileName, mode, encoding, writeHeader, multi):
if fileName != '-':
fileName = setFilePath(fileName)
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = fileName
GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE] = mode
GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING] = encoding
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = writeHeader
GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] = multi
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None
def _setSTDFile(stdtype, fileName, mode, multi):
if stdtype == GM.STDOUT:
GM.Globals[GM.SAVED_STDOUT] = None
GM.Globals[stdtype][GM.REDIRECT_STD] = False
if fileName == 'null':
GM.Globals[stdtype][GM.REDIRECT_FD] = open(os.devnull, mode, encoding=UTF8)
elif fileName == '-':
GM.Globals[stdtype][GM.REDIRECT_STD] = True
if stdtype == GM.STDOUT:
GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup(sys.stdout.fileno()), mode, encoding=GM.Globals[GM.SYS_ENCODING])
else:
GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup(sys.stderr.fileno()), mode, encoding=GM.Globals[GM.SYS_ENCODING])
else:
fileName = setFilePath(fileName)
if multi and mode == DEFAULT_FILE_WRITE_MODE:
deleteFile(fileName)
mode = DEFAULT_FILE_APPEND_MODE
GM.Globals[stdtype][GM.REDIRECT_FD] = openFile(fileName, mode)
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = GM.Globals[stdtype][GM.REDIRECT_FD] if not multi else StringIOobject()
if (stdtype == GM.STDOUT) and (GC.Values[GC.DEBUG_LEVEL] > 0):
GM.Globals[GM.SAVED_STDOUT] = sys.stdout
sys.stdout = GM.Globals[stdtype][GM.REDIRECT_MULTI_FD]
GM.Globals[stdtype][GM.REDIRECT_NAME] = fileName
GM.Globals[stdtype][GM.REDIRECT_MODE] = mode
GM.Globals[stdtype][GM.REDIRECT_MULTIPROCESS] = multi
GM.Globals[stdtype][GM.REDIRECT_QUEUE] = 'stdout' if stdtype == GM.STDOUT else 'stderr'
MULTIPROCESS_EXIT_COMP_PATTERN = re.compile(r'^rc([<>]=?|=|!=)(.+)$', re.IGNORECASE)
MULTIPROCESS_EXIT_RANGE_PATTERN = re.compile(r'^rcrange(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
def _setMultiprocessExit():
rcStr = getString(Cmd.OB_STRING)
mg = MULTIPROCESS_EXIT_COMP_PATTERN.match(rcStr)
if mg:
if not mg.group(2).isdigit():
usageErrorExit(f'{Msg.EXPECTED}: rc<Operator><Value>')
GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] = {'comp': mg.group(1), 'value': int(mg.group(2))}
return
mg = MULTIPROCESS_EXIT_RANGE_PATTERN.match(rcStr)
if mg:
if not mg.group(2).isdigit() or not mg.group(3).isdigit():
usageErrorExit(f'{Msg.EXPECTED}: rcrange<Operator><Value>/Value>')
GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] = {'range': mg.group(1), 'low': int(mg.group(2)), 'high': int(mg.group(3))}
return
usageErrorExit(f'{Msg.EXPECTED}: (rc<Operator><Value>)|(rcrange<Operator><Value>/Value>)')
if not GM.Globals[GM.PARSER]:
homePath = os.path.expanduser('~')
GM.Globals[GM.GAM_CFG_PATH] = os.environ.get(EV_GAMCFGDIR, None)
if GM.Globals[GM.GAM_CFG_PATH]:
GM.Globals[GM.GAM_CFG_PATH] = os.path.expanduser(GM.Globals[GM.GAM_CFG_PATH])
else:
GM.Globals[GM.GAM_CFG_PATH] = os.path.join(homePath, '.gam')
GC.Defaults[GC.CONFIG_DIR] = GM.Globals[GM.GAM_CFG_PATH]
GC.Defaults[GC.CACHE_DIR] = os.path.join(GM.Globals[GM.GAM_CFG_PATH], 'gamcache')
GC.Defaults[GC.DRIVE_DIR] = os.path.join(homePath, 'Downloads')
GM.Globals[GM.GAM_CFG_FILE] = os.path.join(GM.Globals[GM.GAM_CFG_PATH], FN_GAM_CFG)
if not os.path.isfile(GM.Globals[GM.GAM_CFG_FILE]):
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
if itemEntry[GC.VAR_TYPE] == GC.TYPE_DIRECTORY:
_getDefault(itemName, itemEntry, None)
oldGamPath = os.environ.get(EV_OLDGAMPATH, GC.Defaults[GC.CONFIG_DIR])
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
if itemEntry[GC.VAR_TYPE] != GC.TYPE_DIRECTORY:
_getDefault(itemName, itemEntry, oldGamPath)
GM.Globals[GM.PARSER] = configparser.RawConfigParser(defaults=collections.OrderedDict(sorted(list(GC.Defaults.items()), key=lambda t: t[0])))
_checkMakeDir(GC.CONFIG_DIR)
_checkMakeDir(GC.CACHE_DIR)
_checkMakeDir(GC.DRIVE_DIR)
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
if itemEntry[GC.VAR_TYPE] == GC.TYPE_FILE:
srcFile = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, itemName)))
_copyCfgFile(srcFile, GC.CONFIG_DIR, oldGamPath)
_writeGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE], Act.INITIALIZE)
else:
GM.Globals[GM.PARSER] = configparser.RawConfigParser(defaults=collections.OrderedDict(sorted(list(GC.Defaults.items()), key=lambda t: t[0])))
_readGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE])
status = {'errors': False}
inputFilterSectionName = outputFilterSectionName = None
GM.Globals[GM.GAM_CFG_SECTION] = os.environ.get(EV_GAMCFGSECTION, None)
if GM.Globals[GM.GAM_CFG_SECTION]:
sectionName = GM.Globals[GM.GAM_CFG_SECTION]
GM.Globals[GM.SECTION] = sectionName # Save section for inner gams
if not GM.Globals[GM.PARSER].has_section(sectionName):
usageErrorExit(formatKeyValueList('', [EV_GAMCFGSECTION, sectionName, Msg.NOT_FOUND], ''))
if checkArgumentPresent(Cmd.SELECT_CMD):
Cmd.Backup()
usageErrorExit(formatKeyValueList('', [EV_GAMCFGSECTION, sectionName, 'select', Msg.NOT_ALLOWED], ''))
else:
sectionName = _getCfgSection(configparser.DEFAULTSECT, GC.SECTION)
# select <SectionName> [save] [verify]
if checkArgumentPresent(Cmd.SELECT_CMD):
sectionName = _selectSection()
GM.Globals[GM.SECTION] = sectionName # Save section for inner gams
while Cmd.ArgumentsRemaining():
if checkArgumentPresent('save'):
GM.Globals[GM.PARSER].set(configparser.DEFAULTSECT, GC.SECTION, sectionName)
_writeGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE], Act.SAVE)
elif checkArgumentPresent('verify'):
_verifyValues(sectionName, inputFilterSectionName, outputFilterSectionName)
else:
break
GM.Globals[GM.GAM_CFG_SECTION_NAME] = sectionName
# showsections
if checkArgumentPresent(Cmd.SHOWSECTIONS_CMD):
_showSections()
# selectfilter|selectoutputfilter|selectinputfilter <SectionName>
while True:
filterCommand = getChoice([Cmd.SELECTFILTER_CMD, Cmd.SELECTOUTPUTFILTER_CMD, Cmd.SELECTINPUTFILTER_CMD], defaultChoice=None)
if filterCommand is None:
break
if filterCommand != Cmd.SELECTINPUTFILTER_CMD:
outputFilterSectionName = _selectSection()
else:
inputFilterSectionName = _selectSection()
# Handle todrive_nobrowser and todrive_noemail if not present
value = GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, GC.TODRIVE_NOBROWSER)
if value == '':
GM.Globals[GM.PARSER].set(configparser.DEFAULTSECT, GC.TODRIVE_NOBROWSER, str(_getCfgBoolean(configparser.DEFAULTSECT, GC.NO_BROWSER)).lower())
value = GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, GC.TODRIVE_NOEMAIL)
if value == '':
GM.Globals[GM.PARSER].set(configparser.DEFAULTSECT, GC.TODRIVE_NOEMAIL, str(not _getCfgBoolean(configparser.DEFAULTSECT, GC.NO_BROWSER)).lower())
# Handle todrive_sheet_timestamp and todrive_sheet_timeformat if not present
for section in [sectionName, configparser.DEFAULTSECT]:
value = GM.Globals[GM.PARSER].get(section, GC.TODRIVE_SHEET_TIMESTAMP)
if value == 'copy':
GM.Globals[GM.PARSER].set(section, GC.TODRIVE_SHEET_TIMESTAMP, str(_getCfgBoolean(section, GC.TODRIVE_TIMESTAMP)).lower())
value = GM.Globals[GM.PARSER].get(section, GC.TODRIVE_SHEET_TIMEFORMAT)
if value == 'copy':
GM.Globals[GM.PARSER].set(section, GC.TODRIVE_SHEET_TIMEFORMAT, _getCfgString(section, GC.TODRIVE_TIMEFORMAT))
# Fix mistyped keyword cmdlog_max__backups
for section in [configparser.DEFAULTSECT, sectionName]:
if GM.Globals[GM.PARSER].has_option(section, GC.CMDLOG_MAX__BACKUPS):
GM.Globals[GM.PARSER].set(section, GC.CMDLOG_MAX_BACKUPS, GM.Globals[GM.PARSER].get(section, GC.CMDLOG_MAX__BACKUPS))
GM.Globals[GM.PARSER].remove_option(section, GC.CMDLOG_MAX__BACKUPS)
# config (<VariableName> [=] <Value>)* [save] [verify]
if checkArgumentPresent(Cmd.CONFIG_CMD):
while Cmd.ArgumentsRemaining():
if checkArgumentPresent('save'):
_writeGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE], Act.SAVE)
elif checkArgumentPresent('verify'):
_verifyValues(sectionName, inputFilterSectionName, outputFilterSectionName)
else:
itemName = getChoice(GC.VAR_INFO, defaultChoice=None)
if itemName is None:
break
itemEntry = GC.VAR_INFO[itemName]
checkArgumentPresent('=')
varType = itemEntry[GC.VAR_TYPE]
if varType == GC.TYPE_BOOLEAN:
value = TRUE if getBoolean(None) else FALSE
elif varType == GC.TYPE_CHARACTER:
value = getCharacter()
elif varType == GC.TYPE_CHOICE:
value = getChoice(itemEntry[GC.VAR_CHOICES])
elif varType == GC.TYPE_INTEGER:
minVal, maxVal = itemEntry[GC.VAR_LIMITS]
value = str(getInteger(minVal=minVal, maxVal=maxVal))
elif varType == GC.TYPE_FLOAT:
minVal, maxVal = itemEntry[GC.VAR_LIMITS]
value = str(getFloat(minVal=minVal, maxVal=maxVal))
elif varType == GC.TYPE_LOCALE:
value = getLanguageCode(LOCALE_CODES_MAP)
elif varType == GC.TYPE_PASSWORD:
minLen, maxLen = itemEntry[GC.VAR_LIMITS]
value = getString(Cmd.OB_STRING, checkBlank=True, minLen=minLen, maxLen=maxLen)
if value and value.startswith("b'") and value.endswith("'"):
value = bytes(value[2:-1], UTF8)
elif varType == GC.TYPE_TIMEZONE:
value = getString(Cmd.OB_STRING, checkBlank=True)
else: # GC.TYPE_STRING, GC.TYPE_STRINGLIST
minLen, maxLen = itemEntry.get(GC.VAR_LIMITS, (0, None))
value = _quoteStringIfLeadingTrailingBlanks(getString(Cmd.OB_STRING, minLen=minLen, maxLen=maxLen))
GM.Globals[GM.PARSER].set(sectionName, itemName, value)
prevExtraArgsTxt = GC.Values.get(GC.EXTRA_ARGS, None)
prevOauth2serviceJson = GC.Values.get(GC.OAUTH2SERVICE_JSON, None)
# Assign global variables, directories, timezone first as other variables depend on them
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
varType = itemEntry[GC.VAR_TYPE]
if varType == GC.TYPE_DIRECTORY:
GC.Values[itemName] = _getCfgDirectory(sectionName, itemName)
elif varType == GC.TYPE_TIMEZONE:
GC.Values[itemName] = _getCfgTimezone(sectionName, itemName)
GM.Globals[GM.DATETIME_NOW] = datetime.datetime.now(GC.Values[GC.TIMEZONE])
# Everything else except row filters
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
varType = itemEntry[GC.VAR_TYPE]
if varType == GC.TYPE_BOOLEAN:
GC.Values[itemName] = _getCfgBoolean(sectionName, itemName)
elif varType == GC.TYPE_CHARACTER:
GC.Values[itemName] = _getCfgCharacter(sectionName, itemName)
elif varType == GC.TYPE_CHOICE:
GC.Values[itemName] = _getCfgChoice(sectionName, itemName)
elif varType in [GC.TYPE_INTEGER, GC.TYPE_FLOAT]:
GC.Values[itemName] = _getCfgNumber(sectionName, itemName)
elif varType == GC.TYPE_HEADERFILTER:
GC.Values[itemName] = _getCfgHeaderFilter(sectionName, itemName)
elif varType == GC.TYPE_LOCALE:
GC.Values[itemName] = _getCfgLocale(sectionName, itemName)
elif varType == GC.TYPE_PASSWORD:
GC.Values[itemName] = _getCfgPassword(sectionName, itemName)
elif varType == GC.TYPE_STRING:
GC.Values[itemName] = _getCfgString(sectionName, itemName)
elif varType in {GC.TYPE_STRINGLIST, GC.TYPE_HEADERFORCE, GC.TYPE_HEADERORDER}:
GC.Values[itemName] = _getCfgStringList(sectionName, itemName)
elif varType == GC.TYPE_FILE:
GC.Values[itemName] = _getCfgFile(sectionName, itemName)
# Row filters
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
varType = itemEntry[GC.VAR_TYPE]
if varType == GC.TYPE_ROWFILTER:
GC.Values[itemName] = _getCfgRowFilter(sectionName, itemName)
# Process selectfilter|selectoutputfilter|selectinputfilter
if inputFilterSectionName:
GC.Values[GC.CSV_INPUT_ROW_FILTER] = _getCfgRowFilter(inputFilterSectionName, GC.CSV_INPUT_ROW_FILTER)
GC.Values[GC.CSV_INPUT_ROW_FILTER_MODE] = _getCfgChoice(inputFilterSectionName, GC.CSV_INPUT_ROW_FILTER_MODE)
GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER] = _getCfgRowFilter(inputFilterSectionName, GC.CSV_INPUT_ROW_DROP_FILTER)
GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER_MODE] = _getCfgChoice(inputFilterSectionName, GC.CSV_INPUT_ROW_DROP_FILTER_MODE)
GC.Values[GC.CSV_INPUT_ROW_LIMIT] = _getCfgNumber(inputFilterSectionName, GC.CSV_INPUT_ROW_LIMIT)
if outputFilterSectionName:
GC.Values[GC.CSV_OUTPUT_HEADER_FORCE] = _getCfgStringList(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_FORCE)
if GC.Values[GC.CSV_OUTPUT_HEADER_FORCE]:
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = _getCfgHeaderFilterFromForce(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_FORCE)
else:
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = _getCfgHeaderFilter(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_FILTER)
GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER] = _getCfgHeaderFilter(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_DROP_FILTER)
GC.Values[GC.CSV_OUTPUT_HEADER_ORDER] = _getCfgStringList(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_ORDER)
GC.Values[GC.CSV_OUTPUT_ROW_FILTER] = _getCfgRowFilter(outputFilterSectionName, GC.CSV_OUTPUT_ROW_FILTER)
GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE] = _getCfgChoice(outputFilterSectionName, GC.CSV_OUTPUT_ROW_FILTER_MODE)
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER] = _getCfgRowFilter(outputFilterSectionName, GC.CSV_OUTPUT_ROW_DROP_FILTER)
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = _getCfgChoice(outputFilterSectionName, GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE)
GC.Values[GC.CSV_OUTPUT_ROW_LIMIT] = _getCfgNumber(outputFilterSectionName, GC.CSV_OUTPUT_ROW_LIMIT)
GC.Values[GC.CSV_OUTPUT_SORT_HEADERS] = _getCfgStringList(outputFilterSectionName, GC.CSV_OUTPUT_SORT_HEADERS)
elif GC.Values[GC.CSV_OUTPUT_HEADER_FORCE]:
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = _getCfgHeaderFilterFromForce(sectionName, GC.CSV_OUTPUT_HEADER_FORCE)
if status['errors']:
sys.exit(CONFIG_ERROR_RC)
# Global values cleanup
GC.Values[GC.DOMAIN] = GC.Values[GC.DOMAIN].lower()
if not GC.Values[GC.SMTP_FQDN]:
GC.Values[GC.SMTP_FQDN] = None
# Inherit debug_level, output_dateformat, output_timeformat if not locally defined
if GM.Globals[GM.PID] != 0:
if GC.Values[GC.DEBUG_LEVEL] == 0:
GC.Values[GC.DEBUG_LEVEL] = GM.Globals[GM.DEBUG_LEVEL]
if not GC.Values[GC.OUTPUT_DATEFORMAT]:
GC.Values[GC.OUTPUT_DATEFORMAT] = GM.Globals[GM.OUTPUT_DATEFORMAT]
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
GC.Values[GC.OUTPUT_TIMEFORMAT] = GM.Globals[GM.OUTPUT_TIMEFORMAT]
# Define lockfile: oauth2.txt.lock
GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
# Override httplib2 settings
httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL]
# Reset global variables if required
if prevExtraArgsTxt != GC.Values[GC.EXTRA_ARGS]:
GM.Globals[GM.EXTRA_ARGS_LIST] = [('prettyPrint', GC.Values[GC.DEBUG_LEVEL] > 0)]
if GC.Values[GC.EXTRA_ARGS]:
ea_config = configparser.ConfigParser()
ea_config.optionxform = str
ea_config.read(GC.Values[GC.EXTRA_ARGS])
GM.Globals[GM.EXTRA_ARGS_LIST].extend(ea_config.items('extra-args'))
if prevOauth2serviceJson != GC.Values[GC.OAUTH2SERVICE_JSON]:
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = {}
GM.Globals[GM.OAUTH2SERVICE_CLIENT_ID] = None
Cmd.SetEncoding(GM.Globals[GM.SYS_ENCODING])
# multiprocessexit (rc<Operator><Number>)|(rcrange=<Number>/<Number>)|(rcrange!=<Number>/<Number>)
if checkArgumentPresent(Cmd.MULTIPROCESSEXIT_CMD):
_setMultiprocessExit()
# redirect csv <FileName> [multiprocess] [append] [noheader] [charset <CharSet>]
# [columndelimiter <Character>] [quotechar <Character>]] [noescapechar [<Boolean>]]
# [sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Boolean>]]
# [todrive <ToDriveAttribute>*]
# redirect stdout <FileName> [multiprocess] [append]
# redirect stdout null
# redirect stderr <FileName> [multiprocess] [append]
# redirect stderr stdout
# redirect stderr null
while checkArgumentPresent(Cmd.REDIRECT_CMD):
myarg = getChoice(['csv', 'stdout', 'stderr'])
filename = re.sub(r'{{Section}}', sectionName, getString(Cmd.OB_FILE_NAME, checkBlank=True))
if myarg == 'csv':
multi = checkArgumentPresent('multiprocess')
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE
writeHeader = not checkArgumentPresent('noheader')
encoding = getCharSet()
if checkArgumentPresent('columndelimiter'):
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER] = getCharacter()
if checkArgumentPresent('quotechar'):
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR] = getCharacter()
if checkArgumentPresent('noescapechar'):
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR] = getBoolean()
if checkArgumentPresent('sortheaders'):
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = GC.Values[GC.CSV_OUTPUT_SORT_HEADERS] = getString(Cmd.OB_STRING_LIST, minLen=0).replace(',', ' ').split()
if checkArgumentPresent('timestampcolumn'):
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN] = getString(Cmd.OB_STRING, minLen=0)
if checkArgumentPresent('transpose'):
GM.Globals[GM.CSV_OUTPUT_TRANSPOSE] = getBoolean()
_setCSVFile(filename, mode, encoding, writeHeader, multi)
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF] = CSVPrintFile()
if checkArgumentPresent('todrive'):
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].GetTodriveParameters()
GM.Globals[GM.CSV_TODRIVE] = GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].todrive.copy()
elif myarg == 'stdout':
if filename.lower() == 'null':
multi = checkArgumentPresent('multiprocess')
_setSTDFile(GM.STDOUT, 'null', DEFAULT_FILE_WRITE_MODE, multi)
else:
multi = checkArgumentPresent('multiprocess')
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE
_setSTDFile(GM.STDOUT, filename, mode, multi)
else: # myarg == 'stderr'
if filename.lower() == 'null':
multi = checkArgumentPresent('multiprocess')
_setSTDFile(GM.STDERR, 'null', DEFAULT_FILE_WRITE_MODE, multi)
elif filename.lower() != 'stdout':
multi = checkArgumentPresent('multiprocess')
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE
_setSTDFile(GM.STDERR, filename, mode, multi)
else:
multi = checkArgumentPresent('multiprocess')
if not GM.Globals[GM.STDOUT]:
_setSTDFile(GM.STDOUT, '-', DEFAULT_FILE_WRITE_MODE, multi)
GM.Globals[GM.STDERR] = GM.Globals[GM.STDOUT].copy()
GM.Globals[GM.STDERR][GM.REDIRECT_NAME] = 'stdout'
if not GM.Globals[GM.STDOUT]:
_setSTDFile(GM.STDOUT, '-', DEFAULT_FILE_WRITE_MODE, False)
if not GM.Globals[GM.STDERR]:
_setSTDFile(GM.STDERR, '-', DEFAULT_FILE_WRITE_MODE, False)
# If both csv and stdout are redirected to - with same multiprocess setting and csv doesn't have any todrive parameters, collapse csv onto stdout
if (GM.Globals[GM.PID] == 0 and GM.Globals[GM.CSVFILE] and
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] == '-' and GM.Globals[GM.STDOUT][GM.REDIRECT_NAME] == '-' and
GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] == GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS] and
GM.Globals[GM.CSVFILE].get(GM.REDIRECT_QUEUE_CSVPF) and not GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].todrive):
_setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET],
GM.Globals[GM.CSVFILE].get(GM.REDIRECT_WRITE_HEADER, True), GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS])
elif not GM.Globals[GM.CSVFILE]:
_setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET], True, False)
initAPICallsRateCheck()
# Main process
# Clear input row filters/limit from parser, children can define but shouldn't inherit global value
# Clear output header/row filters/limit from parser, children can define or they will inherit global value if not defined
if GM.Globals[GM.PID] == 0:
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
varType = itemEntry[GC.VAR_TYPE]
if varType in {GC.TYPE_HEADERFILTER, GC.TYPE_HEADERFORCE, GC.TYPE_HEADERORDER, GC.TYPE_ROWFILTER}:
GM.Globals[GM.PARSER].set(sectionName, itemName, '')
elif (varType == GC.TYPE_INTEGER) and itemName in {GC.CSV_INPUT_ROW_LIMIT, GC.CSV_OUTPUT_ROW_LIMIT}:
GM.Globals[GM.PARSER].set(sectionName, itemName, '0')
# Child process
# Inherit main process output header/row filters/limit, print defaults if not locally defined
else:
if not GC.Values[GC.CSV_OUTPUT_HEADER_FILTER]:
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = GM.Globals[GM.CSV_OUTPUT_HEADER_FILTER][:]
if not GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER]:
GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER] = GM.Globals[GM.CSV_OUTPUT_HEADER_DROP_FILTER][:]
if not GC.Values[GC.CSV_OUTPUT_HEADER_FORCE]:
GC.Values[GC.CSV_OUTPUT_HEADER_FORCE] = GM.Globals[GM.CSV_OUTPUT_HEADER_FORCE][:]
if not GC.Values[GC.CSV_OUTPUT_HEADER_ORDER]:
GC.Values[GC.CSV_OUTPUT_HEADER_ORDER] = GM.Globals[GM.CSV_OUTPUT_HEADER_ORDER][:]
if not GC.Values[GC.CSV_OUTPUT_ROW_FILTER]:
GC.Values[GC.CSV_OUTPUT_ROW_FILTER] = GM.Globals[GM.CSV_OUTPUT_ROW_FILTER][:]
GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE] = GM.Globals[GM.CSV_OUTPUT_ROW_FILTER_MODE]
if not GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER]:
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER] = GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER][:]
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER_MODE]
if not GC.Values[GC.CSV_OUTPUT_ROW_LIMIT]:
GC.Values[GC.CSV_OUTPUT_ROW_LIMIT] = GM.Globals[GM.CSV_OUTPUT_ROW_LIMIT]
if not GC.Values[GC.PRINT_AGU_DOMAINS]:
GC.Values[GC.PRINT_AGU_DOMAINS] = GM.Globals[GM.PRINT_AGU_DOMAINS]
if not GC.Values[GC.PRINT_CROS_OUS]:
GC.Values[GC.PRINT_CROS_OUS] = GM.Globals[GM.PRINT_CROS_OUS]
if not GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]:
GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN] = GM.Globals[GM.PRINT_CROS_OUS_AND_CHILDREN]
GC.Values[GC.SHOW_GETTINGS] = GM.Globals[GM.SHOW_GETTINGS]
GC.Values[GC.SHOW_GETTINGS_GOT_NL] = GM.Globals[GM.SHOW_GETTINGS_GOT_NL]
# customer_id, domain and admin_email must be set when enable_dasa = true
if GC.Values[GC.ENABLE_DASA]:
errors = 0
for itemName in [GC.CUSTOMER_ID, GC.DOMAIN, GC.ADMIN_EMAIL]:
if not GC.Values[itemName] or (itemName == GC.CUSTOMER_ID and GC.Values[itemName] == GC.MY_CUSTOMER):
stderrErrorMsg(formatKeyValueList('',
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.SECTION), sectionName,
itemName, GC.Values[itemName] or '""',
GC.ENABLE_DASA, GC.Values[GC.ENABLE_DASA],
Msg.NOT_COMPATIBLE],
'\n'))
errors += 1
if errors:
sys.exit(USAGE_ERROR_RC)
# If no select/options commands were executed or some were and there are more arguments on the command line,
# warn if the json files are missing and return True
if (Cmd.Location() == 1) or (Cmd.ArgumentsRemaining()):
_chkCfgDirectories(sectionName)
_chkCfgFiles(sectionName)
if status['errors']:
sys.exit(CONFIG_ERROR_RC)
if GC.Values[GC.NO_CACHE]:
GM.Globals[GM.CACHE_DIR] = None
GM.Globals[GM.CACHE_DISCOVERY_ONLY] = False
else:
GM.Globals[GM.CACHE_DIR] = GC.Values[GC.CACHE_DIR]
GM.Globals[GM.CACHE_DISCOVERY_ONLY] = GC.Values[GC.CACHE_DISCOVERY_ONLY]
# Set environment variables so GData API can find cacerts.pem
os.environ['REQUESTS_CA_BUNDLE'] = GC.Values[GC.CACERTS_PEM]
os.environ['DEFAULT_CA_BUNDLE_PATH'] = GC.Values[GC.CACERTS_PEM]
os.environ['HTTPLIB2_CA_CERTS'] = GC.Values[GC.CACERTS_PEM]
os.environ['SSL_CERT_FILE'] = GC.Values[GC.CACERTS_PEM]
httplib2.CA_CERTS = GC.Values[GC.CACERTS_PEM]
# Needs to be set so oauthlib doesn't puke when Google changes our scopes
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = 'true'
# Set up command logging at top level only
if (GM.Globals[GM.PID] == 0) and GC.Values[GC.CMDLOG]:
openGAMCommandLog(GM.Globals, 'mainlog')
return True
# We're done, nothing else to do
return False
def handleServerError(e):
errMsg = str(e)
if 'setting tls' not in errMsg:
systemErrorExit(NETWORK_ERROR_RC, errMsg)
stderrErrorMsg(errMsg)
writeStderr(Msg.DISABLE_TLS_MIN_MAX)
systemErrorExit(NETWORK_ERROR_RC, None)
def getHttpObj(cache=None, timeout=None, override_min_tls=None, override_max_tls=None):
tls_minimum_version = override_min_tls if override_min_tls else GC.Values[GC.TLS_MIN_VERSION] if GC.Values[GC.TLS_MIN_VERSION] else None
tls_maximum_version = override_max_tls if override_max_tls else GC.Values[GC.TLS_MAX_VERSION] if GC.Values[GC.TLS_MAX_VERSION] else None
httpObj = httplib2.Http(cache=cache,
timeout=timeout,
ca_certs=GC.Values[GC.CACERTS_PEM],
disable_ssl_certificate_validation=GC.Values[GC.NO_VERIFY_SSL],
tls_maximum_version=tls_maximum_version,
tls_minimum_version=tls_minimum_version)
httpObj.redirect_codes = set(httpObj.redirect_codes) - {308}
return httpObj
def _force_user_agent(user_agent):
"""Creates a decorator which can force a user agent in HTTP headers."""
def decorator(request_method):
"""Wraps a request method to insert a user-agent in HTTP headers."""
def wrapped_request_method(*args, **kwargs):
"""Modifies HTTP headers to include a specified user-agent."""
if kwargs.get('headers') is not None:
if kwargs['headers'].get('user-agent'):
if user_agent not in kwargs['headers']['user-agent']:
# Save the existing user-agent header and tack on our own.
kwargs['headers']['user-agent'] = f'{user_agent} {kwargs["headers"]["user-agent"]}'
else:
kwargs['headers']['user-agent'] = user_agent
else:
kwargs['headers'] = {'user-agent': user_agent}
return request_method(*args, **kwargs)
return wrapped_request_method
return decorator
class transportAgentRequest(google_auth_httplib2.Request):
"""A Request which forces a user agent."""
@_force_user_agent(GAM_USER_AGENT)
def __call__(self, *args, **kwargs): #pylint: disable=arguments-differ
"""Inserts the GAM user-agent header in requests."""
return super().__call__(*args, **kwargs)
class transportAuthorizedHttp(google_auth_httplib2.AuthorizedHttp):
"""An AuthorizedHttp which forces a user agent during requests."""
@_force_user_agent(GAM_USER_AGENT)
def request(self, *args, **kwargs): #pylint: disable=arguments-differ
"""Inserts the GAM user-agent header in requests."""
return super().request(*args, **kwargs)
def transportCreateRequest(httpObj=None):
"""Creates a uniform Request object with a default http, if not provided.
Args:
httpObj: Optional httplib2.Http compatible object to be used with the request.
If not provided, a default HTTP will be used.
Returns:
Request: A google_auth_httplib2.Request compatible Request.
"""
if not httpObj:
httpObj = getHttpObj()
return transportAgentRequest(httpObj)
def doGAMCheckForUpdates(forceCheck):
def _gamLatestVersionNotAvailable():
if forceCheck:
systemErrorExit(NETWORK_ERROR_RC, Msg.GAM_LATEST_VERSION_NOT_AVAILABLE)
try:
_, c = getHttpObj(timeout=10).request(GAM_LATEST_RELEASE, 'GET', headers={'Accept': 'application/vnd.github.v3.text+json'})
try:
release_data = json.loads(c)
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
_gamLatestVersionNotAvailable()
return
if not isinstance(release_data, dict) or 'tag_name' not in release_data:
_gamLatestVersionNotAvailable()
return
current_version = __version__
latest_version = release_data['tag_name']
if latest_version[0].lower() == 'v':
latest_version = latest_version[1:]
printKeyValueList(['Version Check', None])
Ind.Increment()
printKeyValueList(['Current', current_version])
printKeyValueList([' Latest', latest_version])
Ind.Decrement()
if forceCheck < 0:
setSysExitRC(1 if latest_version > current_version else 0)
return
except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError,
google.auth.exceptions.TransportError,
RuntimeError, ConnectionError, OSError) as e:
if forceCheck:
handleServerError(e)
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
class signjwtJWTCredentials(google.auth.jwt.Credentials):
''' Class used for DASA '''
def _make_jwt(self):
now = datetime.datetime.utcnow()
lifetime = datetime.timedelta(seconds=self._token_lifetime)
expiry = now + lifetime
payload = {
"iat": google.auth._helpers.datetime_to_secs(now),
"exp": google.auth._helpers.datetime_to_secs(expiry),
"iss": self._issuer,
"sub": self._subject,
}
if self._audience:
payload["aud"] = self._audience
payload.update(self._additional_claims)
jwt = self._signer.sign(payload)
return jwt, expiry
# Some Workforce Identity Federation endpoints such as GitHub Actions
# only allow TLS 1.2 as of April 2023.
def getTLSv1_2Request():
httpc = getHttpObj(override_min_tls='TLSv1_2')
return transportCreateRequest(httpc)
class signjwtCredentials(google.oauth2.service_account.Credentials):
''' Class used for DwD '''
def _make_authorization_grant_assertion(self):
now = datetime.datetime.utcnow()
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
expiry = now + lifetime
payload = {
"iat": google.auth._helpers.datetime_to_secs(now),
"exp": google.auth._helpers.datetime_to_secs(expiry),
"iss": self._service_account_email,
"aud": API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
"scope": google.auth._helpers.scopes_to_string(self._scopes or ()),
}
payload.update(self._additional_claims)
# The subject can be a user email for domain-wide delegation.
if self._subject:
payload.setdefault("sub", self._subject)
token = self._signer(payload)
return token
class signjwtSignJwt(google.auth.crypt.Signer):
''' Signer class for SignJWT '''
def __init__(self, service_account_info):
self.service_account_email = service_account_info['client_email']
self.name = f'projects/-/serviceAccounts/{self.service_account_email}'
self._key_id = None
@property # type: ignore
def key_id(self):
return self._key_id
def sign(self, message):
''' Call IAM Credentials SignJWT API to get our signed JWT '''
try:
credentials, _ = google.auth.default(scopes=[API.IAM_SCOPE],
request=getTLSv1_2Request())
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(override_min_tls='TLSv1_2'))
iamc = getService(API.IAM_CREDENTIALS, httpObj)
response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt',
name=self.name, body={'payload': json.dumps(message)})
signed_jwt = response.get('signedJwt')
return signed_jwt
def handleOAuthTokenError(e, softErrors, displayError=False, i=0, count=0):
errMsg = str(e).replace('.', '')
if ((errMsg in API.OAUTH2_TOKEN_ERRORS) or
errMsg.startswith('Invalid response') or
errMsg.startswith('invalid_request: Invalid impersonation &quot;sub&quot; field')):
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
ClientAPIAccessDeniedExit()
if softErrors:
entityActionFailedWarning([Ent.USER, GM.Globals[GM.CURRENT_SVCACCT_USER], Ent.USER, None], errMsg, i, count)
return None
systemErrorExit(SERVICE_NOT_APPLICABLE_RC, Msg.SERVICE_NOT_APPLICABLE_THIS_ADDRESS.format(GM.Globals[GM.CURRENT_SVCACCT_USER]))
if errMsg in API.OAUTH2_UNAUTHORIZED_ERRORS:
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
ClientAPIAccessDeniedExit()
if softErrors:
if displayError:
apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]))
userServiceNotEnabledWarning(GM.Globals[GM.CURRENT_SVCACCT_USER], apiOrScopes, i, count)
return None
SvcAcctAPIAccessDeniedExit()
if errMsg in API.REFRESH_PERM_ERRORS:
if softErrors:
return None
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
expiredRevokedOauth2TxtExit()
stderrErrorMsg(f'Authentication Token Error - {errMsg}')
APIAccessDeniedExit()
def getOauth2TxtCredentials(exitOnError=True, api=None, noDASA=False, refreshOnly=False, noScopes=False):
if not noDASA and GC.Values[GC.ENABLE_DASA]:
jsonData = readFile(GC.Values[GC.OAUTH2SERVICE_JSON], continueOnError=True, displayError=False)
if jsonData:
try:
if api in API.APIS_NEEDING_ACCESS_TOKEN:
return (False, getSvcAcctCredentials(API.APIS_NEEDING_ACCESS_TOKEN[api], userEmail=None, forceOauth=True))
jsonDict = json.loads(jsonData)
api, _, _ = API.getVersion(api)
audience = f'https://{api}.googleapis.com/'
key_type = jsonDict.get('key_type', 'default')
if key_type == 'default':
return (True, JWTCredentials.from_service_account_info(jsonDict, audience=audience))
if key_type == 'yubikey':
yksigner = yubikey.YubiKey(jsonDict)
return (True, JWTCredentials._from_signer_and_info(yksigner, jsonDict, audience=audience))
if key_type == 'signjwt':
sjsigner = signjwtSignJwt(jsonDict)
return (True, signjwtJWTCredentials._from_signer_and_info(sjsigner, jsonDict, audience=audience))
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidOauth2serviceJsonExit(str(e))
invalidOauth2serviceJsonExit(Msg.NO_DATA)
jsonData = readFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True, displayError=False)
if jsonData:
try:
jsonDict = json.loads(jsonData)
if noScopes:
jsonDict['scopes'] = []
if 'client_id' in jsonDict:
if not refreshOnly:
if set(jsonDict.get('scopes', API.REQUIRED_SCOPES)) == API.REQUIRED_SCOPES_SET:
if exitOnError:
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_ALLOWED)
return (False, None)
else:
GM.Globals[GM.CREDENTIALS_SCOPES] = set(jsonDict.pop('scopes', API.REQUIRED_SCOPES))
token_expiry = jsonDict.get('token_expiry', REFRESH_EXPIRY)
if GC.Values[GC.TRUNCATE_CLIENT_ID]:
# chop off .apps.googleusercontent.com suffix as it's not needed and we need to keep things short for the Auth URL.
jsonDict['client_id'] = re.sub(r'\.apps\.googleusercontent\.com$', '', jsonDict['client_id'])
creds = google.oauth2.credentials.Credentials.from_authorized_user_info(jsonDict)
if 'id_token_jwt' not in jsonDict:
creds.token = jsonDict['token']
creds._id_token = jsonDict['id_token']
GM.Globals[GM.DECODED_ID_TOKEN] = jsonDict['decoded_id_token']
else:
creds.token = jsonDict['access_token']
creds._id_token = jsonDict['id_token_jwt']
GM.Globals[GM.DECODED_ID_TOKEN] = jsonDict['id_token']
creds.expiry = datetime.datetime.strptime(token_expiry, YYYYMMDDTHHMMSSZ_FORMAT)
return (not noScopes, creds)
if jsonDict and exitOnError:
invalidOauth2TxtExit(Msg.INVALID)
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
if exitOnError:
invalidOauth2TxtExit(str(e))
if exitOnError:
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_ALLOWED)
return (False, None)
def _getValueFromOAuth(field, credentials=None):
if not GM.Globals[GM.DECODED_ID_TOKEN]:
request = transportCreateRequest()
if credentials is None:
credentials = getClientCredentials(refreshOnly=True)
elif credentials.expired:
credentials.refresh(request)
try:
GM.Globals[GM.DECODED_ID_TOKEN] = google.oauth2.id_token.verify_oauth2_token(credentials.id_token, request,
clock_skew_in_seconds=GC.Values[GC.CLOCK_SKEW_IN_SECONDS])
except ValueError as e:
if 'Token used too early' in str(e):
stderrErrorMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
systemErrorExit(SYSTEM_ERROR_RC, str(e))
return GM.Globals[GM.DECODED_ID_TOKEN].get(field, UNKNOWN)
def _getAdminEmail():
if GC.Values[GC.ADMIN_EMAIL]:
return GC.Values[GC.ADMIN_EMAIL]
return _getValueFromOAuth('email')
def writeClientCredentials(creds, filename):
creds_data = {
'client_id': creds.client_id,
'client_secret': creds.client_secret,
'id_token': creds.id_token,
'refresh_token': creds.refresh_token,
'scopes': sorted(creds.scopes or GM.Globals[GM.CREDENTIALS_SCOPES]),
'token': creds.token,
'token_expiry': creds.expiry.strftime(YYYYMMDDTHHMMSSZ_FORMAT),
'token_uri': creds.token_uri,
}
expected_iss = ['https://accounts.google.com', 'accounts.google.com']
if _getValueFromOAuth('iss', creds) not in expected_iss:
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, f'Wrong OAuth 2.0 credentials issuer. Got {_getValueFromOAuth("iss", creds)} expected one of {", ".join(expected_iss)}')
request = transportCreateRequest()
try:
creds_data['decoded_id_token'] = google.oauth2.id_token.verify_oauth2_token(creds.id_token, request,
clock_skew_in_seconds=GC.Values[GC.CLOCK_SKEW_IN_SECONDS])
except ValueError as e:
if 'Token used too early' in str(e):
stderrErrorMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
systemErrorExit(SYSTEM_ERROR_RC, str(e))
GM.Globals[GM.DECODED_ID_TOKEN] = creds_data['decoded_id_token']
if filename != '-':
writeFile(filename, json.dumps(creds_data, indent=2, sort_keys=True)+'\n')
else:
writeStdout(json.dumps(creds_data, ensure_ascii=False, sort_keys=True, indent=2)+'\n')
URL_SHORTENER_ENDPOINT = 'https://gam-shortn.appspot.com/create'
def shortenURL(long_url):
if GC.Values[GC.NO_SHORT_URLS]:
return long_url
httpObj = getHttpObj(timeout=10)
try:
payload = json.dumps({'long_url': long_url})
resp, content = httpObj.request(URL_SHORTENER_ENDPOINT, 'POST',
payload,
headers={'Content-Type': 'application/json',
'User-Agent': GAM_USER_AGENT})
except:
return long_url
if resp.status != 200:
return long_url
try:
if isinstance(content, bytes):
content = content.decode()
return json.loads(content).get('short_url', long_url)
except:
return long_url
def runSqliteQuery(db_file, query):
conn = sqlite3.connect(db_file)
curr = conn.cursor()
curr.execute(query)
return curr.fetchone()[0]
def refreshCredentialsWithReauth(credentials):
def gcloudError():
writeStderr(f'Failed to run gcloud as {admin_email}. Please make sure it\'s setup')
e = Msg.REAUTHENTICATION_IS_NEEDED
handleOAuthTokenError(e, False)
writeStderr(Msg.CALLING_GCLOUD_FOR_REAUTH)
if 'termios' in sys.modules:
old_settings = termios.tcgetattr(sys.stdin)
admin_email = _getAdminEmail()
# First makes sure gcloud has a valid access token and thus
# should also have a valid RAPT token
try:
devnull = open(os.devnull, 'w', encoding=UTF8)
subprocess.run(['gcloud',
'auth',
'print-identity-token',
'--no-user-output-enabled'],
stderr=devnull,
check=False)
devnull.close()
# now determine gcloud's config path and token file
gcloud_path_result = subprocess.run(['gcloud',
'info',
'--format=value(config.paths.global_config_dir)'],
capture_output=True, check=False)
except KeyboardInterrupt as e:
# avoids loss of terminal echo on *nix
if 'termios' in sys.modules:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
printBlankLine()
raise KeyboardInterrupt from e
token_path = gcloud_path_result.stdout.decode().strip()
if not token_path:
gcloudError()
token_file = f'{token_path}/access_tokens.db'
try:
credentials._rapt_token = runSqliteQuery(token_file,
f'SELECT rapt_token FROM access_tokens WHERE account_id = "{admin_email}"')
except TypeError:
gcloudError()
if not credentials._rapt_token:
systemErrorExit(SYSTEM_ERROR_RC,
'Failed to retrieve reauth token from gcloud. You may need to wait until gcloud is also prompted for reauth.')
def getClientCredentials(forceRefresh=False, forceWrite=False, filename=None, api=None, noDASA=False, refreshOnly=False, noScopes=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 = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
with lock:
writeCreds, credentials = getOauth2TxtCredentials(api=api, noDASA=noDASA, refreshOnly=refreshOnly, noScopes=noScopes)
if not credentials:
invalidOauth2TxtExit('')
if credentials.expired or forceRefresh:
triesLimit = 3
for n in range(1, triesLimit+1):
try:
credentials.refresh(transportCreateRequest())
if writeCreds or forceWrite:
writeClientCredentials(credentials, filename or GC.Values[GC.OAUTH2_TXT])
break
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != triesLimit:
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple):
e = e.args[0]
if 'Reauthentication is needed' in str(e):
if GC.Values[GC.ENABLE_GCLOUD_REAUTH]:
refreshCredentialsWithReauth(credentials)
continue
e = Msg.REAUTHENTICATION_IS_NEEDED
handleOAuthTokenError(e, False)
return credentials
def waitOnFailure(n, triesLimit, error_code, error_message):
delta = min(2 ** n, 60)+float(random.randint(1, 1000))/1000
if n > 3:
writeStderr(f'Temporary error: {error_code} - {error_message}, Backing off: {int(delta)} seconds, Retry: {n}/{triesLimit}\n')
flushStderr()
time.sleep(delta)
if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]:
incrAPICallsRetryData(error_message, delta)
def clearServiceCache(service):
if hasattr(service._http, 'http') and hasattr(service._http.http, 'cache'):
if service._http.http.cache is None:
return False
service._http.http.cache = None
return True
if hasattr(service._http, 'cache'):
if service._http.cache is None:
return False
service._http.cache = None
return True
return False
DISCOVERY_URIS = [googleapiclient.discovery.V1_DISCOVERY_URI, googleapiclient.discovery.V2_DISCOVERY_URI]
# Used for API.CLOUDRESOURCEMANAGER, API.SERVICEUSAGE, API.IAM
def getAPIService(api, httpObj):
api, version, v2discovery = API.getVersion(api)
return googleapiclient.discovery.build(api, version, http=httpObj, cache_discovery=False,
discoveryServiceUrl=DISCOVERY_URIS[v2discovery], static_discovery=False)
def getService(api, httpObj):
### Drive v3beta
# mapDriveURL = api == API.DRIVE3 and GC.Values[GC.DRIVE_V3_BETA]
hasLocalJSON = API.hasLocalJSON(api)
api, version, v2discovery = API.getVersion(api)
if api in GM.Globals[GM.CURRENT_API_SERVICES] and version in GM.Globals[GM.CURRENT_API_SERVICES][api]:
service = googleapiclient.discovery.build_from_document(GM.Globals[GM.CURRENT_API_SERVICES][api][version], http=httpObj)
if GM.Globals[GM.CACHE_DISCOVERY_ONLY]:
clearServiceCache(service)
return service
if not hasLocalJSON:
triesLimit = 3
for n in range(1, triesLimit+1):
try:
service = googleapiclient.discovery.build(api, version, http=httpObj, cache_discovery=False,
discoveryServiceUrl=DISCOVERY_URIS[v2discovery], static_discovery=False)
GM.Globals[GM.CURRENT_API_SERVICES].setdefault(api, {})
GM.Globals[GM.CURRENT_API_SERVICES][api][version] = service._rootDesc.copy()
### Drive v3beta
# if mapDriveURL:
# setattr(service, '_baseUrl', getattr(service, '_baseUrl').replace('/v3/', '/v3beta/'))
if GM.Globals[GM.CACHE_DISCOVERY_ONLY]:
clearServiceCache(service)
return service
except googleapiclient.errors.UnknownApiNameOrVersion as e:
systemErrorExit(GOOGLE_API_ERROR_RC, Msg.UNKNOWN_API_OR_VERSION.format(str(e), __author__))
except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e:
if n != triesLimit:
waitOnFailure(n, triesLimit, INVALID_JSON_RC, str(e))
continue
systemErrorExit(INVALID_JSON_RC, str(e))
except (http_client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e:
errMsg = f'Connection error: {str(e) or repr(e)}'
if n != triesLimit:
waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
continue
systemErrorExit(SOCKET_ERROR_RC, errMsg)
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != triesLimit:
httpObj.connections = {}
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue
handleServerError(e)
disc_file, discovery = readDiscoveryFile(f'{api}-{version}')
try:
service = googleapiclient.discovery.build_from_document(discovery, http=httpObj)
GM.Globals[GM.CURRENT_API_SERVICES].setdefault(api, {})
GM.Globals[GM.CURRENT_API_SERVICES][api][version] = service._rootDesc.copy()
if GM.Globals[GM.CACHE_DISCOVERY_ONLY]:
clearServiceCache(service)
return service
except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e:
invalidDiscoveryJsonExit(disc_file, str(e))
except IOError as e:
systemErrorExit(FILE_ERROR_RC, str(e))
def defaultSvcAcctScopes():
scopesList = API.getSvcAcctScopesList(GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY], False)
saScopes = {}
for scope in scopesList:
saScopes.setdefault(scope['api'], [])
saScopes[scope['api']].append(scope['scope'])
saScopes[API.DRIVEACTIVITY].append(API.DRIVE_SCOPE)
saScopes[API.DRIVE2] = saScopes[API.DRIVE3]
saScopes[API.DRIVETD] = saScopes[API.DRIVE3]
saScopes[API.SHEETSTD] = saScopes[API.SHEETS]
return saScopes
def _getSvcAcctData():
if not GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
jsonData = readFile(GC.Values[GC.OAUTH2SERVICE_JSON], continueOnError=True, displayError=True)
if not jsonData:
invalidOauth2serviceJsonExit(Msg.NO_DATA)
try:
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = json.loads(jsonData)
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidOauth2serviceJsonExit(str(e))
if not GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED)
requiredFields = ['client_email', 'client_id', 'project_id', 'token_uri']
key_type = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if key_type == 'default':
requiredFields.extend(['private_key', 'private_key_id'])
missingFields = []
for field in requiredFields:
if field not in GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
missingFields.append(field)
if missingFields:
invalidOauth2serviceJsonExit(Msg.MISSING_FIELDS.format(','.join(missingFields)))
# Some old oauth2service.json files have: 'https://accounts.google.com/o/oauth2/auth' which no longer works
if GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['token_uri'] == 'https://accounts.google.com/o/oauth2/auth':
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['token_uri'] = API.GOOGLE_OAUTH2_TOKEN_ENDPOINT
if API.OAUTH2SA_SCOPES not in GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
GM.Globals[GM.SVCACCT_SCOPES_DEFINED] = False
GM.Globals[GM.SVCACCT_SCOPES] = defaultSvcAcctScopes()
else:
GM.Globals[GM.SVCACCT_SCOPES_DEFINED] = True
GM.Globals[GM.SVCACCT_SCOPES] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].pop(API.OAUTH2SA_SCOPES)
def getSvcAcctCredentials(scopesOrAPI, userEmail, softErrors=False, forceOauth=False):
_getSvcAcctData()
if isinstance(scopesOrAPI, str):
GM.Globals[GM.CURRENT_SVCACCT_API] = scopesOrAPI
if scopesOrAPI not in API.JWT_APIS:
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = GM.Globals[GM.SVCACCT_SCOPES].get(scopesOrAPI, [])
else:
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = API.JWT_APIS[scopesOrAPI]
if scopesOrAPI != API.CHAT_EVENTS and not GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]:
if softErrors:
return None
SvcAcctAPIAccessDeniedExit()
if scopesOrAPI in {API.PEOPLE, API.PEOPLE_DIRECTORY, API.PEOPLE_OTHERCONTACTS}:
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES].append(API.USERINFO_PROFILE_SCOPE)
if scopesOrAPI in {API.PEOPLE_OTHERCONTACTS}:
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES].append(API.PEOPLE_SCOPE)
elif scopesOrAPI == API.CHAT_EVENTS:
for chatAPI in [API.CHAT_SPACES, API.CHAT_MEMBERSHIPS, API.CHAT_MESSAGES]:
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES].extend(GM.Globals[GM.SVCACCT_SCOPES].get(chatAPI, []))
else:
GM.Globals[GM.CURRENT_SVCACCT_API] = ''
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = scopesOrAPI
key_type = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if not GM.Globals[GM.CURRENT_SVCACCT_API] or scopesOrAPI not in API.JWT_APIS or forceOauth:
try:
if key_type == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
elif key_type == 'yubikey':
yksigner = yubikey.YubiKey(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
elif key_type == 'signjwt':
sjsigner = signjwtSignJwt(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
credentials = signjwtCredentials._from_signer_and_info(sjsigner.sign,
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
except (ValueError, IndexError, KeyError) as e:
if softErrors:
return None
invalidOauth2serviceJsonExit(str(e))
credentials = credentials.with_scopes(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES])
else:
audience = f'https://{scopesOrAPI}.googleapis.com/'
try:
if key_type == 'default':
credentials = JWTCredentials.from_service_account_info(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA],
audience=audience)
elif key_type == 'yubikey':
yksigner = yubikey.YubiKey(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
credentials = JWTCredentials._from_signer_and_info(yksigner,
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA],
audience=audience)
elif key_type == 'signjwt':
sjsigner = signjwtSignJwt(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
credentials = signjwtJWTCredentials._from_signer_and_info(sjsigner,
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA],
audience=audience)
credentials.project_id = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id']
except (ValueError, IndexError, KeyError) as e:
if softErrors:
return None
invalidOauth2serviceJsonExit(str(e))
GM.Globals[GM.CURRENT_SVCACCT_USER] = userEmail
if userEmail:
credentials = credentials.with_subject(userEmail)
GM.Globals[GM.ADMIN] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email']
GM.Globals[GM.OAUTH2SERVICE_CLIENT_ID] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id']
return credentials
def getGDataOAuthToken(gdataObj, credentials=None):
if not credentials:
credentials = getClientCredentials(refreshOnly=True)
try:
credentials.refresh(transportCreateRequest())
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple):
e = e.args[0]
handleOAuthTokenError(e, False)
gdataObj.additional_headers['Authorization'] = f'Bearer {credentials.token}'
if not GC.Values[GC.DOMAIN]:
GC.Values[GC.DOMAIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('hd', 'UNKNOWN').lower()
if not GC.Values[GC.CUSTOMER_ID]:
GC.Values[GC.CUSTOMER_ID] = GC.MY_CUSTOMER
GM.Globals[GM.ADMIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('email', 'UNKNOWN').lower()
GM.Globals[GM.OAUTH2_CLIENT_ID] = credentials.client_id
gdataObj.domain = GC.Values[GC.DOMAIN]
gdataObj.source = GAM_USER_AGENT
return True
def checkGDataError(e, service):
error = e.args
reason = error[0].get('reason', '')
body = error[0].get('body', '').decode(UTF8)
# First check for errors that need special handling
if reason in ['Token invalid - Invalid token: Stateless token expired', 'Token invalid - Invalid token: Token not found', 'gone']:
keep_domain = service.domain
getGDataOAuthToken(service)
service.domain = keep_domain
return (GDATA.TOKEN_EXPIRED, reason)
error_code = getattr(e, 'error_code', 600)
if GC.Values[GC.DEBUG_LEVEL] > 0:
writeStdout(f'{ERROR_PREFIX} {error_code}: {reason}, {body}\n')
if error_code == 600:
if (body.startswith('Quota exceeded for the current request') or
body.startswith('Quota exceeded for quota metric') or
body.startswith('Request rate higher than configured')):
return (GDATA.QUOTA_EXCEEDED, body)
if (body.startswith('Photo delete failed') or
body.startswith('Upload photo failed') or
body.startswith('Photo query failed')):
return (GDATA.NOT_FOUND, body)
if body.startswith(GDATA.API_DEPRECATED_MSG):
return (GDATA.API_DEPRECATED, body)
if reason == 'Too Many Requests':
return (GDATA.QUOTA_EXCEEDED, reason)
if reason == 'Bad Gateway':
return (GDATA.BAD_GATEWAY, reason)
if reason == 'Gateway Timeout':
return (GDATA.GATEWAY_TIMEOUT, reason)
if reason == 'Service Unavailable':
return (GDATA.SERVICE_UNAVAILABLE, reason)
if reason == 'Service <jotspot> disabled by G Suite admin.':
return (GDATA.FORBIDDEN, reason)
if reason == 'Internal Server Error':
return (GDATA.INTERNAL_SERVER_ERROR, reason)
if reason == 'Token invalid - Invalid token: Token disabled, revoked, or expired.':
return (GDATA.TOKEN_INVALID, 'Token disabled, revoked, or expired. Please delete and re-create oauth.txt')
if reason == 'Token invalid - AuthSub token has wrong scope':
return (GDATA.INSUFFICIENT_PERMISSIONS, reason)
if reason.startswith('Only administrators can request entries belonging to'):
return (GDATA.INSUFFICIENT_PERMISSIONS, reason)
if reason == 'You are not authorized to access this API':
return (GDATA.INSUFFICIENT_PERMISSIONS, reason)
if reason == 'Invalid domain.':
return (GDATA.INVALID_DOMAIN, reason)
if reason.startswith('You are not authorized to perform operations on the domain'):
return (GDATA.INVALID_DOMAIN, reason)
if reason == 'Bad Request':
if 'already exists' in body:
return (GDATA.ENTITY_EXISTS, Msg.DUPLICATE)
return (GDATA.BAD_REQUEST, body)
if reason == 'Forbidden':
return (GDATA.FORBIDDEN, body)
if reason == 'Not Found':
return (GDATA.NOT_FOUND, Msg.DOES_NOT_EXIST)
if reason == 'Not Implemented':
return (GDATA.NOT_IMPLEMENTED, body)
if reason == 'Precondition Failed':
return (GDATA.PRECONDITION_FAILED, reason)
elif error_code == 602:
if body.startswith(GDATA.API_DEPRECATED_MSG):
return (GDATA.API_DEPRECATED, body)
if reason == 'Bad Request':
return (GDATA.BAD_REQUEST, body)
elif error_code == 610:
if reason == 'Service <jotspot> disabled by G Suite admin.':
return (GDATA.FORBIDDEN, reason)
# We got a "normal" error, define the mapping below
error_code_map = {
1000: reason,
1001: reason,
1002: 'Unauthorized and forbidden',
1100: 'User deleted recently',
1200: 'Domain user limit exceeded',
1201: 'Domain alias limit exceeded',
1202: 'Domain suspended',
1203: 'Domain feature unavailable',
1300: f'Entity {getattr(e, "invalidInput", "<unknown>")} exists',
1301: f'Entity {getattr(e, "invalidInput", "<unknown>")} Does Not Exist',
1302: 'Entity Name Is Reserved',
1303: f'Entity {getattr(e, "invalidInput", "<unknown>")} name not valid',
1306: f'{getattr(e, "invalidInput", "<unknown>")} has members. Cannot delete.',
1317: f'Invalid input {getattr(e, "invalidInput", "<unknown>")}, reason {getattr(e, "reason", "<unknown>")}',
1400: 'Invalid Given Name',
1401: 'Invalid Family Name',
1402: 'Invalid Password',
1403: 'Invalid Username',
1404: 'Invalid Hash Function Name',
1405: 'Invalid Hash Digest Length',
1406: 'Invalid Email Address',
1407: 'Invalid Query Parameter Value',
1408: 'Invalid SSO Signing Key',
1409: 'Invalid Encryption Public Key',
1410: 'Feature Unavailable For User',
1411: 'Invalid Encryption Public Key Format',
1500: 'Too Many Recipients On Email List',
1501: 'Too Many Aliases For User',
1502: 'Too Many Delegates For User',
1601: 'Duplicate Destinations',
1602: 'Too Many Destinations',
1603: 'Invalid Route Address',
1700: 'Group Cannot Contain Cycle',
1800: 'Group Cannot Contain Cycle',
1801: f'Invalid value {getattr(e, "invalidInput", "<unknown>")}',
}
return (error_code, error_code_map.get(error_code, f'Unknown Error: {str(e)}'))
def callGData(service, function,
bailOnInternalServerError=False, softErrors=False,
throwErrors=None, retryErrors=None, triesLimit=0,
**kwargs):
if throwErrors is None:
throwErrors = []
if retryErrors is None:
retryErrors = []
if triesLimit == 0:
triesLimit = GC.Values[GC.API_CALLS_TRIES_LIMIT]
allRetryErrors = GDATA.NON_TERMINATING_ERRORS+retryErrors
method = getattr(service, function)
if GC.Values[GC.API_CALLS_RATE_CHECK]:
checkAPICallsRate()
for n in range(1, triesLimit+1):
try:
return method(**kwargs)
except (gdata.service.RequestError, gdata.apps.service.AppsForYourDomainException) as e:
error_code, error_message = checkGDataError(e, service)
if (n != triesLimit) and (error_code in allRetryErrors):
if (error_code == GDATA.INTERNAL_SERVER_ERROR and
bailOnInternalServerError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
raise GDATA.ERROR_CODE_EXCEPTION_MAP[error_code](error_message)
waitOnFailure(n, triesLimit, error_code, error_message)
continue
if error_code in throwErrors:
if error_code in GDATA.ERROR_CODE_EXCEPTION_MAP:
raise GDATA.ERROR_CODE_EXCEPTION_MAP[error_code](error_message)
raise
if softErrors:
stderrErrorMsg(f'{error_code} - {error_message}{["", ": Giving up."][n > 1]}')
return None
if error_code == GDATA.INSUFFICIENT_PERMISSIONS:
APIAccessDeniedExit()
systemErrorExit(GOOGLE_API_ERROR_RC, f'{error_code} - {error_message}')
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != triesLimit:
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple):
e = e.args[0]
handleOAuthTokenError(e, GDATA.SERVICE_NOT_APPLICABLE in throwErrors)
raise GDATA.ERROR_CODE_EXCEPTION_MAP[GDATA.SERVICE_NOT_APPLICABLE](str(e))
except (http_client.ResponseNotReady, OSError) as e:
errMsg = f'Connection error: {str(e) or repr(e)}'
if n != triesLimit:
waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
continue
if softErrors:
writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n')
return None
systemErrorExit(SOCKET_ERROR_RC, errMsg)
def writeGotMessage(msg):
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
writeStderr(msg)
else:
writeStderr('\r')
msgLen = len(msg)
if msgLen < GM.Globals[GM.LAST_GOT_MSG_LEN]:
writeStderr(msg+' '*(GM.Globals[GM.LAST_GOT_MSG_LEN]-msgLen))
else:
writeStderr(msg)
GM.Globals[GM.LAST_GOT_MSG_LEN] = msgLen
flushStderr()
def callGDataPages(service, function,
pageMessage=None,
softErrors=False, throwErrors=None, retryErrors=None,
uri=None,
**kwargs):
if throwErrors is None:
throwErrors = []
if retryErrors is None:
retryErrors = []
nextLink = None
allResults = []
totalItems = 0
while True:
this_page = callGData(service, function,
softErrors=softErrors, throwErrors=throwErrors, retryErrors=retryErrors,
uri=uri,
**kwargs)
if this_page:
nextLink = this_page.GetNextLink()
pageItems = len(this_page.entry)
if pageItems == 0:
nextLink = None
totalItems += pageItems
allResults.extend(this_page.entry)
else:
nextLink = None
pageItems = 0
if pageMessage:
show_message = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
writeGotMessage(show_message.format(Ent.ChooseGetting(totalItems)))
if nextLink is None:
if pageMessage and (pageMessage[-1] != '\n'):
writeStderr('\r\n')
flushStderr()
return allResults
uri = nextLink.href
if 'url_params' in kwargs:
kwargs['url_params'].pop('start-index', None)
def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True):
def makeErrorDict(code, reason, message):
return {'error': {'code': code, 'errors': [{'reason': reason, 'message': message}]}}
try:
error = json.loads(e.content.decode(UTF8))
if GC.Values[GC.DEBUG_LEVEL] > 0:
writeStdout(f'{ERROR_PREFIX} JSON: {str(error)}\n')
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
eContent = e.content.decode(UTF8) if isinstance(e.content, bytes) else e.content
lContent = eContent.lower()
if GC.Values[GC.DEBUG_LEVEL] > 0:
writeStdout(f'{ERROR_PREFIX} HTTP: {str(eContent)}\n')
if eContent[0:15] != '<!DOCTYPE html>':
if (e.resp['status'] == '403') and (lContent.startswith('request rate higher than configured')):
return (e.resp['status'], GAPI.QUOTA_EXCEEDED, eContent)
if (e.resp['status'] == '429') and (lContent.startswith('quota exceeded for quota metric')):
return (e.resp['status'], GAPI.QUOTA_EXCEEDED, eContent)
if (e.resp['status'] == '502') and ('bad gateway' in lContent):
return (e.resp['status'], GAPI.BAD_GATEWAY, eContent)
if (e.resp['status'] == '503') and (lContent.startswith('quota exceeded for the current request')):
return (e.resp['status'], GAPI.QUOTA_EXCEEDED, eContent)
if (e.resp['status'] == '504') and ('gateway timeout' in lContent):
return (e.resp['status'], GAPI.GATEWAY_TIMEOUT, eContent)
else:
tg = HTML_TITLE_PATTERN.match(lContent)
lContent = tg.group(1) if tg else 'bad request'
if (e.resp['status'] == '403') and ('invalid domain.' in lContent):
error = makeErrorDict(403, GAPI.NOT_FOUND, 'Domain not found')
elif (e.resp['status'] == '403') and ('domain cannot use apis.' in lContent):
error = makeErrorDict(403, GAPI.DOMAIN_CANNOT_USE_APIS, 'Domain cannot use apis')
elif (e.resp['status'] == '400') and ('invalidssosigningkey' in lContent):
error = makeErrorDict(400, GAPI.INVALID, 'InvalidSsoSigningKey')
elif (e.resp['status'] == '400') and ('unknownerror' in lContent):
error = makeErrorDict(400, GAPI.INVALID, 'UnknownError')
elif (e.resp['status'] == '400') and ('featureunavailableforuser' in lContent):
error = makeErrorDict(400, GAPI.SERVICE_NOT_AVAILABLE, 'Feature Unavailable For User')
elif (e.resp['status'] == '400') and ('entitydoesnotexist' in lContent):
error = makeErrorDict(400, GAPI.NOT_FOUND, 'Entity Does Not Exist')
elif (e.resp['status'] == '400') and ('entitynamenotvalid' in lContent):
error = makeErrorDict(400, GAPI.INVALID_INPUT, 'Entity Name Not Valid')
elif (e.resp['status'] == '400') and ('failed to parse Content-Range header' in lContent):
error = makeErrorDict(400, GAPI.BAD_REQUEST, 'Failed to parse Content-Range header')
elif (e.resp['status'] == '400') and ('request contains an invalid argument' in lContent):
error = makeErrorDict(400, GAPI.INVALID_ARGUMENT, 'Request contains an invalid argument')
elif (e.resp['status'] == '404') and ('not found' in lContent):
error = makeErrorDict(404, GAPI.NOT_FOUND, lContent)
elif (e.resp['status'] == '404') and ('bad request' in lContent):
error = makeErrorDict(404, GAPI.BAD_REQUEST, lContent)
elif retryOnHttpError:
return (-1, None, eContent)
elif softErrors:
stderrErrorMsg(eContent)
return (0, None, None)
else:
systemErrorExit(HTTP_ERROR_RC, eContent)
if 'error' in error:
http_status = error['error']['code']
reason = ''
if 'errors' in error['error'] and 'message' in error['error']['errors'][0]:
message = error['error']['errors'][0]['message']
if 'reason' in error['error']['errors'][0]:
reason = error['error']['errors'][0]['reason']
elif 'errors' in error['error'] and 'Unknown Error' in error['error']['message'] and 'reason' in error['error']['errors'][0]:
message = error['error']['errors'][0]['reason']
else:
message = error['error']['message']
status = error['error'].get('status', '')
lmessage = message.lower() if message is not None else ''
if http_status == 500:
if not lmessage or status == 'UNKNOWN':
if not lmessage:
message = Msg.UNKNOWN
error = makeErrorDict(http_status, GAPI.UNKNOWN_ERROR, message)
elif 'backend error' in lmessage:
error = makeErrorDict(http_status, GAPI.BACKEND_ERROR, message)
elif 'internal error encountered' in lmessage:
error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message)
elif 'role assignment exists: roleassignment' in lmessage:
error = makeErrorDict(http_status, GAPI.DUPLICATE, message)
elif 'role assignment exists: roleid' in lmessage:
error = makeErrorDict(http_status, GAPI.DUPLICATE, message)
elif 'operation not supported' in lmessage:
error = makeErrorDict(http_status, GAPI.OPERATION_NOT_SUPPORTED, message)
elif 'failed status in update settings response' in lmessage:
error = makeErrorDict(http_status, GAPI.INVALID_INPUT, message)
elif 'cannot delete a field in use.resource.fields' in lmessage:
error = makeErrorDict(http_status, GAPI.FIELD_IN_USE, message)
elif status == 'INTERNAL':
error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message)
elif http_status == 502:
if 'bad gateway' in lmessage:
error = makeErrorDict(http_status, GAPI.BAD_GATEWAY, message)
elif http_status == 503:
if message.startswith('quota exceeded for the current request'):
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
elif status == 'UNAVAILABLE' or 'the service is currently unavailable' in lmessage:
error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message)
elif http_status == 504:
if 'gateway timeout' in lmessage:
error = makeErrorDict(http_status, GAPI.GATEWAY_TIMEOUT, message)
elif http_status == 400:
if '@attachmentnotvisible' in lmessage:
error = makeErrorDict(http_status, GAPI.BAD_REQUEST, message)
elif status == 'INVALID_ARGUMENT':
error = makeErrorDict(http_status, GAPI.INVALID_ARGUMENT, message)
elif status == 'FAILED_PRECONDITION' or 'precondition check failed' in lmessage:
error = makeErrorDict(http_status, GAPI.FAILED_PRECONDITION, message)
elif 'does not match' in lmessage or 'invalid' in lmessage:
error = makeErrorDict(http_status, GAPI.INVALID, message)
elif http_status == 401:
if 'active session is invalid' in lmessage and reason == 'authError':
# message += ' Drive SDK API access disabled'
# message = Msg.SERVICE_NOT_ENABLED.format('Drive')
error = makeErrorDict(http_status, GAPI.AUTH_ERROR, message)
elif status == 'PERMISSION_DENIED':
error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message)
elif status == 'UNAUTHENTICATED':
error = makeErrorDict(http_status, GAPI.AUTH_ERROR, message)
elif http_status == 403:
if 'quota exceeded for quota metric' in lmessage:
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
elif 'the authenticated user cannot access this service' in lmessage:
error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message)
elif status == 'PERMISSION_DENIED' or 'the caller does not have permission' in lmessage or 'permission iam.serviceaccountkeys' in lmessage:
error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message)
elif http_status == 404:
if status == 'NOT_FOUND' or 'requested entity was not found' in lmessage or 'does not exist' in lmessage:
error = makeErrorDict(http_status, GAPI.NOT_FOUND, message)
elif http_status == 409:
if status == 'ALREADY_EXISTS' or 'requested entity already exists' in lmessage:
error = makeErrorDict(http_status, GAPI.ALREADY_EXISTS, message)
elif status == 'ABORTED' or 'the operation was aborted' in lmessage:
error = makeErrorDict(http_status, GAPI.ABORTED, message)
elif http_status == 412:
if 'insufficient archived user licenses' in lmessage:
error = makeErrorDict(http_status, GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES, message)
elif http_status == 413:
if 'request too large' in lmessage:
error = makeErrorDict(http_status, GAPI.UPLOAD_TOO_LARGE, message)
elif http_status == 429:
if status == 'RESOURCE_EXHAUSTED' or 'quota exceeded' in lmessage or 'insufficient quota' in lmessage:
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
else:
if 'error_description' in error:
if error['error_description'] == 'Invalid Value':
message = error['error_description']
http_status = 400
error = makeErrorDict(http_status, GAPI.INVALID, message)
else:
systemErrorExit(GOOGLE_API_ERROR_RC, str(error))
else:
systemErrorExit(GOOGLE_API_ERROR_RC, str(error))
try:
reason = error['error']['errors'][0]['reason']
for messageItem in GAPI.REASON_MESSAGE_MAP.get(reason, []):
if messageItem[0] in message:
if reason in [GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND] and mapNotFound:
message = Msg.DOES_NOT_EXIST
reason = messageItem[1]
break
if reason == GAPI.INVALID_SHARING_REQUEST:
loc = message.find('User message: ')
if loc != -1:
message = message[loc+15:]
else:
loc = message.find('User message: ""')
if loc != -1:
message = message[:loc+14]+f'"{reason}"'
except KeyError:
reason = f'{http_status}'
return (http_status, reason, message)
def callGAPI(service, function,
bailOnInternalError=False, bailOnTransientError=False, bailOnInvalidError=False,
softErrors=False, mapNotFound=True,
throwReasons=None, retryReasons=None, triesLimit=0,
**kwargs):
if throwReasons is None:
throwReasons = []
if retryReasons is None:
retryReasons = []
if triesLimit == 0:
triesLimit = GC.Values[GC.API_CALLS_TRIES_LIMIT]
allRetryReasons = GAPI.DEFAULT_RETRY_REASONS+retryReasons
method = getattr(service, function)
svcparms = dict(list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST])
if GC.Values[GC.API_CALLS_RATE_CHECK]:
checkAPICallsRate()
for n in range(1, triesLimit+1):
try:
return method(**svcparms).execute()
except googleapiclient.errors.HttpError as e:
http_status, reason, message = checkGAPIError(e, softErrors=softErrors, retryOnHttpError=n < 3, mapNotFound=mapNotFound)
if http_status == -1:
# The error detail indicated that we should retry this request
# We'll refresh credentials and make another pass
try:
# service._http.credentials.refresh(getHttpObj())
service._http.credentials.refresh(transportCreateRequest())
except TypeError:
systemErrorExit(HTTP_ERROR_RC, message)
continue
if http_status == 0:
return None
if (n != triesLimit) and ((reason in allRetryReasons) or
(GC.Values[GC.RETRY_API_SERVICE_NOT_AVAILABLE] and (reason == GAPI.SERVICE_NOT_AVAILABLE))):
if (reason in [GAPI.INTERNAL_ERROR, GAPI.BACKEND_ERROR] and
bailOnInternalError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
if (reason in [GAPI.INVALID] and
bailOnInvalidError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
waitOnFailure(n, triesLimit, reason, message)
if reason == GAPI.TRANSIENT_ERROR and bailOnTransientError:
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
continue
if reason in throwReasons:
if reason in GAPI.REASON_EXCEPTION_MAP:
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
raise e
if softErrors:
stderrErrorMsg(f'{http_status}: {reason} - {message}{["", ": Giving up."][n > 1]}')
return None
if reason == GAPI.INSUFFICIENT_PERMISSIONS:
APIAccessDeniedExit()
systemErrorExit(HTTP_ERROR_RC, formatHTTPError(http_status, reason, message))
except googleapiclient.errors.MediaUploadSizeError as e:
raise e
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != triesLimit:
service._http.connections = {}
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple):
e = e.args[0]
handleOAuthTokenError(e, GAPI.SERVICE_NOT_AVAILABLE in throwReasons)
raise GAPI.REASON_EXCEPTION_MAP[GAPI.SERVICE_NOT_AVAILABLE](str(e))
except (http_client.ResponseNotReady, OSError) as e:
errMsg = f'Connection error: {str(e) or repr(e)}'
if n != triesLimit:
waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
continue
if softErrors:
writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n')
return None
systemErrorExit(SOCKET_ERROR_RC, errMsg)
except ValueError as e:
if clearServiceCache(service):
continue
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
except TypeError as e:
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
def _showGAPIpagesResult(results, pageItems, totalItems, pageMessage, messageAttribute, entityType):
showMessage = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
if pageItems:
if messageAttribute:
firstItem = results[0] if pageItems > 0 else {}
lastItem = results[-1] if pageItems > 1 else firstItem
if isinstance(messageAttribute, str):
firstItem = str(firstItem.get(messageAttribute, ''))
lastItem = str(lastItem.get(messageAttribute, ''))
else:
for attr in messageAttribute:
firstItem = firstItem.get(attr, {})
lastItem = lastItem.get(attr, {})
firstItem = str(firstItem)
lastItem = str(lastItem)
showMessage = showMessage.replace(FIRST_ITEM_MARKER, firstItem)
showMessage = showMessage.replace(LAST_ITEM_MARKER, lastItem)
else:
showMessage = showMessage.replace(FIRST_ITEM_MARKER, '')
showMessage = showMessage.replace(LAST_ITEM_MARKER, '')
writeGotMessage(showMessage.replace('{0}', str(Ent.Choose(entityType, totalItems))))
def _processGAPIpagesResult(results, items, allResults, totalItems, pageMessage, messageAttribute, entityType):
if results:
pageToken = results.get('nextPageToken')
if items in results:
pageItems = len(results[items])
totalItems += pageItems
if allResults is not None:
allResults.extend(results[items])
else:
results = {items: []}
pageItems = 0
else:
pageToken = None
results = {items: []}
pageItems = 0
if pageMessage:
_showGAPIpagesResult(results[items], pageItems, totalItems, pageMessage, messageAttribute, entityType)
return (pageToken, totalItems)
def _finalizeGAPIpagesResult(pageMessage):
if pageMessage and (pageMessage[-1] != '\n'):
writeStderr('\r\n')
flushStderr()
def callGAPIpages(service, function, items,
pageMessage=None, messageAttribute=None, maxItems=0, noFinalize=False,
throwReasons=None, retryReasons=None,
pageArgsInBody=False,
**kwargs):
if throwReasons is None:
throwReasons = []
if retryReasons is None:
retryReasons = []
allResults = []
totalItems = 0
maxArg = ''
if maxItems:
maxResults = kwargs.get('maxResults', 0)
if maxResults:
maxArg = 'maxResults'
else:
maxResults = kwargs.get('pageSize', 0)
if maxResults:
maxArg = 'pageSize'
if pageArgsInBody:
kwargs.setdefault('body', {})
entityType = Ent.Getting() if pageMessage else None
while True:
if maxArg and maxItems-totalItems < maxResults:
kwargs[maxArg] = maxItems-totalItems
results = callGAPI(service, function,
throwReasons=throwReasons, retryReasons=retryReasons,
**kwargs)
pageToken, totalItems = _processGAPIpagesResult(results, items, allResults, totalItems, pageMessage, messageAttribute, entityType)
if not pageToken or (maxItems and totalItems >= maxItems):
if not noFinalize:
_finalizeGAPIpagesResult(pageMessage)
return allResults
if pageArgsInBody:
kwargs['body']['pageToken'] = pageToken
else:
kwargs['pageToken'] = pageToken
def yieldGAPIpages(service, function, items,
pageMessage=None, messageAttribute=None, maxItems=0, noFinalize=False,
throwReasons=None, retryReasons=None,
pageArgsInBody=False,
**kwargs):
if throwReasons is None:
throwReasons = []
if retryReasons is None:
retryReasons = []
totalItems = 0
maxArg = ''
if maxItems:
maxResults = kwargs.get('maxResults', 0)
if maxResults:
maxArg = 'maxResults'
else:
maxResults = kwargs.get('pageSize', 0)
if maxResults:
maxArg = 'pageSize'
if pageArgsInBody:
kwargs.setdefault('body', {})
entityType = Ent.Getting() if pageMessage else None
while True:
if maxArg and maxItems-totalItems < maxResults:
kwargs[maxArg] = maxItems-totalItems
results = callGAPI(service, function,
throwReasons=throwReasons, retryReasons=retryReasons,
**kwargs)
if results:
pageToken = results.get('nextPageToken')
if items in results:
pageItems = len(results[items])
totalItems += pageItems
else:
results = {items: []}
pageItems = 0
else:
pageToken = None
results = {items: []}
pageItems = 0
if pageMessage:
_showGAPIpagesResult(results[items], pageItems, totalItems, pageMessage, messageAttribute, entityType)
yield results[items]
if not pageToken or (maxItems and totalItems >= maxItems):
if not noFinalize:
_finalizeGAPIpagesResult(pageMessage)
return
if pageArgsInBody:
kwargs['body']['pageToken'] = pageToken
else:
kwargs['pageToken'] = pageToken
def callGAPIitems(service, function, items,
throwReasons=None, retryReasons=None,
**kwargs):
if throwReasons is None:
throwReasons = []
if retryReasons is None:
retryReasons = []
results = callGAPI(service, function,
throwReasons=throwReasons, retryReasons=retryReasons,
**kwargs)
if results:
return results.get(items, [])
return []
def readDiscoveryFile(api_version):
disc_filename = f'{api_version}.json'
disc_file = os.path.join(GM.Globals[GM.GAM_PATH], disc_filename)
if hasattr(sys, '_MEIPASS'):
json_string = readFile(os.path.join(sys._MEIPASS, disc_filename), continueOnError=True, displayError=True) #pylint: disable=no-member
elif os.path.isfile(disc_file):
json_string = readFile(disc_file, continueOnError=True, displayError=True)
else:
json_string = None
if not json_string:
invalidDiscoveryJsonExit(disc_file, Msg.NO_DATA)
try:
discovery = json.loads(json_string)
return (disc_file, discovery)
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidDiscoveryJsonExit(disc_file, str(e))
def buildGAPIObject(api, credentials=None):
if credentials is None:
credentials = getClientCredentials(api=api, refreshOnly=True)
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(cache=GM.Globals[GM.CACHE_DIR]))
service = getService(api, httpObj)
if not GC.Values[GC.ENABLE_DASA]:
try:
API_Scopes = set(list(service._rootDesc['auth']['oauth2']['scopes']))
except KeyError:
API_Scopes = set(API.VAULT_SCOPES) if api == API.VAULT else set()
GM.Globals[GM.CURRENT_CLIENT_API] = api
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = API_Scopes.intersection(GM.Globals[GM.CREDENTIALS_SCOPES])
if api not in API.SCOPELESS_APIS and not GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]:
systemErrorExit(NO_SCOPES_FOR_API_RC, Msg.NO_SCOPES_FOR_API.format(API.getAPIName(api)))
if not GC.Values[GC.DOMAIN]:
GC.Values[GC.DOMAIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('hd', 'UNKNOWN').lower()
if not GC.Values[GC.CUSTOMER_ID]:
GC.Values[GC.CUSTOMER_ID] = GC.MY_CUSTOMER
GM.Globals[GM.ADMIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('email', 'UNKNOWN').lower()
GM.Globals[GM.OAUTH2_CLIENT_ID] = credentials.client_id
return service
def getSaUser(user):
currentClientAPI = GM.Globals[GM.CURRENT_CLIENT_API]
currentClientAPIScopes = GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
userEmail = convertUIDtoEmailAddress(user) if user else None
GM.Globals[GM.CURRENT_CLIENT_API] = currentClientAPI
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = currentClientAPIScopes
return userEmail
def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True):
userEmail = getSaUser(user)
httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR])
service = getService(api, httpObj)
if api == API.MEET_BETA:
api = API.MEET
credentials = getSvcAcctCredentials(api, userEmail)
request = transportCreateRequest(httpObj)
triesLimit = 3
for n in range(1, triesLimit+1):
try:
credentials.refresh(request)
service._http = transportAuthorizedHttp(credentials, http=httpObj)
return (userEmail, service)
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
if n != triesLimit:
httpObj.connections = {}
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple):
e = e.args[0]
if n < triesLimit:
if isinstance(e, str):
eContent = e
else:
eContent = e.content.decode(UTF8) if isinstance(e.content, bytes) else e.content
if eContent[0:15] == '<!DOCTYPE html>':
if GC.Values[GC.DEBUG_LEVEL] > 0:
writeStdout(f'{ERROR_PREFIX} HTTP: {str(eContent)}\n')
lContent = eContent.lower()
tg = HTML_TITLE_PATTERN.match(lContent)
lContent = tg.group(1) if tg else ''
if lContent.startswith('Error 502 (Server Error)'):
time.sleep(30)
continue
handleOAuthTokenError(e, True, displayError, i, count)
return (userEmail, None)
def buildGAPIObjectNoAuthentication(api):
httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR])
service = getService(api, httpObj)
return service
def initGDataObject(gdataObj, api):
GM.Globals[GM.CURRENT_CLIENT_API] = api
credentials = getClientCredentials(noDASA=True, refreshOnly=True)
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = API.getClientScopesSet(api).intersection(GM.Globals[GM.CREDENTIALS_SCOPES])
if not GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]:
systemErrorExit(NO_SCOPES_FOR_API_RC, Msg.NO_SCOPES_FOR_API.format(API.getAPIName(api)))
getGDataOAuthToken(gdataObj, credentials)
if GC.Values[GC.DEBUG_LEVEL] > 0:
gdataObj.debug = True
return gdataObj
def getGDataUserCredentials(api, user, i, count):
userEmail = getSaUser(user)
credentials = getSvcAcctCredentials(api, userEmail)
request = transportCreateRequest()
try:
credentials.refresh(request)
return (userEmail, credentials)
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
if isinstance(e.args, tuple):
e = e.args[0]
handleOAuthTokenError(e, True, True, i, count)
return (userEmail, None)
def getContactsObject(contactFeed):
contactsObject = initGDataObject(gdata.apps.contacts.service.ContactsService(contactFeed=contactFeed),
API.CONTACTS)
return (GC.Values[GC.DOMAIN], contactsObject)
def getContactsQuery(**kwargs):
if GC.Values[GC.NO_VERIFY_SSL]:
ssl._create_default_https_context = ssl._create_unverified_context
return gdata.apps.contacts.service.ContactsQuery(**kwargs)
def getEmailAuditObject():
return initGDataObject(gdata.apps.audit.service.AuditService(), API.EMAIL_AUDIT)
def getUserEmailFromID(uid, cd):
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=uid, fields='primaryEmail')
return result.get('primaryEmail')
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
return None
def getGroupEmailFromID(uid, cd):
try:
result = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
groupKey=uid, fields='email')
return result.get('email')
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
return None
def getServiceAccountEmailFromID(account_id, sal=None):
if sal is None:
sal = buildGAPIObject(API.SERVICEACCOUNTLOOKUP)
try:
certs = callGAPI(sal.serviceaccounts(), 'lookup',
throwReasons = [GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_ARGUMENT],
account=account_id)
except (GAPI.badRequest, GAPI.notFound, GAPI.resourceNotFound, GAPI.invalidArgument):
return None
sa_cn_rx = r'CN=(.+)\.(.+)\.iam\.gservice.*'
sa_emails = []
for _, raw_cert in certs.items():
cert = x509.load_pem_x509_certificate(raw_cert.encode(), default_backend())
# suppress crytography warning due to long service account email
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message='.*Attribute\'s length.*')
mg = re.match(sa_cn_rx, cert.issuer.rfc4514_string())
if mg:
sa_email = f'{mg.group(1)}@{mg.group(2)}.iam.gserviceaccount.com'
if sa_email not in sa_emails:
sa_emails.append(sa_email)
return GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER].join(sa_emails)
# Convert UID to email address and type
def convertUIDtoEmailAddressWithType(emailAddressOrUID, cd=None, sal=None, emailTypes=None,
checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True):
if emailTypes is None:
emailTypes = ['user']
elif not isinstance(emailTypes, list):
emailTypes = [emailTypes] if emailTypes != 'any' else ['user', 'group']
if checkForCustomerId and (emailAddressOrUID == GC.Values[GC.CUSTOMER_ID]):
return (emailAddressOrUID, 'email')
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI)
if ciGroupsAPI and emailAddressOrUID.startswith('groups/'):
return emailAddressOrUID
if normalizedEmailAddressOrUID.find('@') > 0 and aliasAllowed:
return (normalizedEmailAddressOrUID, 'email')
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
if 'user' in emailTypes and 'group' in emailTypes:
# Google User IDs *TEND* to be integers while groups tend to have letters
# thus we can optimize which check we try first. We'll still check
# both since there is no guarantee this will always be true.
if normalizedEmailAddressOrUID.isdigit():
uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd)
if uid:
return (uid, 'user')
uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd)
if uid:
return (uid, 'group')
else:
uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd)
if uid:
return (uid, 'group')
uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd)
if uid:
return (uid, 'user')
elif 'user' in emailTypes:
uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd)
if uid:
return (uid, 'user')
elif 'group' in emailTypes:
uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd)
if uid:
return (uid, 'group')
if 'resource' in emailTypes:
try:
result = callGAPI(cd.resources().calendars(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
calendarResourceId=normalizedEmailAddressOrUID,
customer=GC.Values[GC.CUSTOMER_ID], fields='resourceEmail')
if 'resourceEmail' in result:
return (result['resourceEmail'].lower(), 'resource')
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
pass
if 'serviceaccount' in emailTypes:
uid = getServiceAccountEmailFromID(normalizedEmailAddressOrUID, sal)
if uid:
return (uid, 'serviceaccount')
return (normalizedEmailAddressOrUID, 'unknown')
NON_EMAIL_MEMBER_PREFIXES = (
"cbcm-browser.",
"chrome-os-device.",
)
# Convert UID to email address
def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, emailTypes=None,
checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True):
if ciGroupsAPI:
if emailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES):
return emailAddressOrUID
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI)
if normalizedEmailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES):
return normalizedEmailAddressOrUID
email, _ = convertUIDtoEmailAddressWithType(emailAddressOrUID, cd, emailTypes,
checkForCustomerId, ciGroupsAPI, aliasAllowed)
return email
# Convert email address to User/Group UID; called immediately after getting email address from command line
def convertEmailAddressToUID(emailAddressOrUID, cd=None, emailType='user', savedLocation=None):
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID)
if normalizedEmailAddressOrUID.find('@') == -1:
return normalizedEmailAddressOrUID
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
if emailType != 'group':
try:
return callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=normalizedEmailAddressOrUID, fields='id')['id']
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
if emailType == 'user':
if savedLocation is not None:
Cmd.SetLocation(savedLocation)
entityDoesNotExistExit(Ent.USER, normalizedEmailAddressOrUID, errMsg=getPhraseDNEorSNA(normalizedEmailAddressOrUID))
try:
return callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=normalizedEmailAddressOrUID, fields='id')['id']
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError):
if savedLocation is not None:
Cmd.SetLocation(savedLocation)
entityDoesNotExistExit([Ent.USER, Ent.GROUP][emailType == 'group'], normalizedEmailAddressOrUID, errMsg=getPhraseDNEorSNA(normalizedEmailAddressOrUID))
# Convert User UID from API call to email address
def convertUserIDtoEmail(uid, cd=None):
primaryEmail = GM.Globals[GM.MAP_USER_ID_TO_NAME].get(uid)
if not primaryEmail:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
try:
primaryEmail = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=uid, fields='primaryEmail')['primaryEmail']
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
primaryEmail = f'uid:{uid}'
GM.Globals[GM.MAP_USER_ID_TO_NAME][uid] = primaryEmail
return primaryEmail
# Convert UID to split email address
# Return (foo@bar.com, foo, bar.com)
def splitEmailAddressOrUID(emailAddressOrUID):
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID)
atLoc = normalizedEmailAddressOrUID.find('@')
if atLoc > 0:
return (normalizedEmailAddressOrUID, normalizedEmailAddressOrUID[:atLoc], normalizedEmailAddressOrUID[atLoc+1:])
try:
cd = buildGAPIObject(API.DIRECTORY)
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=normalizedEmailAddressOrUID, fields='primaryEmail')
if 'primaryEmail' in result:
normalizedEmailAddressOrUID = result['primaryEmail'].lower()
atLoc = normalizedEmailAddressOrUID.find('@')
return (normalizedEmailAddressOrUID, normalizedEmailAddressOrUID[:atLoc], normalizedEmailAddressOrUID[atLoc+1:])
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
pass
return (normalizedEmailAddressOrUID, normalizedEmailAddressOrUID, GC.Values[GC.DOMAIN])
# Convert Org Unit Id to Org Unit Path
def convertOrgUnitIDtoPath(cd, orgUnitId):
if orgUnitId.lower().startswith('orgunits/'):
orgUnitId = f'id:{orgUnitId[9:]}'
orgUnitPath = GM.Globals[GM.MAP_ORGUNIT_ID_TO_NAME].get(orgUnitId)
if not orgUnitPath:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
try:
orgUnitPath = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitId, fields='orgUnitPath')['orgUnitPath']
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
orgUnitPath = orgUnitId
GM.Globals[GM.MAP_ORGUNIT_ID_TO_NAME][orgUnitId] = orgUnitPath
return orgUnitPath
def shlexSplitList(entity, dataDelimiter=' ,'):
lexer = shlex.shlex(entity, posix=True)
lexer.whitespace = dataDelimiter
lexer.whitespace_split = True
try:
return list(lexer)
except ValueError as e:
Cmd.Backup()
usageErrorExit(str(e))
def shlexSplitListStatus(entity, dataDelimiter=' ,'):
lexer = shlex.shlex(entity, posix=True)
lexer.whitespace = dataDelimiter
lexer.whitespace_split = True
try:
return (True, list(lexer))
except ValueError as e:
return (False, str(e))
def getQueries(myarg):
if myarg in {'query', 'filter'}:
return [getString(Cmd.OB_QUERY)]
return shlexSplitList(getString(Cmd.OB_QUERY_LIST))
def convertEntityToList(entity, shlexSplit=False, nonListEntityType=False):
if not entity:
return []
if isinstance(entity, (list, set, dict)):
return list(entity)
if nonListEntityType:
return [entity.strip()]
if not shlexSplit:
return entity.replace(',', ' ').split()
return shlexSplitList(entity)
GROUP_ROLES_MAP = {
'owner': Ent.ROLE_OWNER,
'owners': Ent.ROLE_OWNER,
'manager': Ent.ROLE_MANAGER,
'managers': Ent.ROLE_MANAGER,
'member': Ent.ROLE_MEMBER,
'members': Ent.ROLE_MEMBER,
}
ALL_GROUP_ROLES = {Ent.ROLE_MANAGER, Ent.ROLE_MEMBER, Ent.ROLE_OWNER}
def _getRoleVerification(memberRoles, fields):
if memberRoles and memberRoles.find(Ent.ROLE_MEMBER) != -1:
return (set(memberRoles.split(',')), None, fields if fields.find('role') != -1 else fields[:-1]+',role)')
return (set(), memberRoles, fields)
def _getCIRoleVerification(memberRoles):
if memberRoles:
return set(memberRoles.split(','))
return set()
def _checkMemberStatusIsSuspendedIsArchived(memberStatus, isSuspended, isArchived):
if isSuspended is None and isArchived is None:
return True
if isSuspended is not None and isArchived is not None:
if isSuspended == isArchived:
if not isSuspended:
return memberStatus not in {'SUSPENDED', 'ARCHIVED'}
return memberStatus in {'SUSPENDED', 'ARCHIVED'}
if isSuspended:
return memberStatus == 'SUSPENDED'
return memberStatus == 'ARCHIVED'
if isSuspended is not None:
if (not isSuspended and memberStatus != 'SUSPENDED') or (isSuspended and memberStatus == 'SUSPENDED'):
return True
if isArchived is not None:
if (not isArchived and memberStatus != 'ARCHIVED') or (isArchived and memberStatus == 'ARCHIVED'):
return True
return False
def _checkMemberIsSuspendedIsArchived(member, isSuspended, isArchived):
return _checkMemberStatusIsSuspendedIsArchived(member.get('status', 'UNKNOWN'), isSuspended, isArchived)
def _checkMemberRole(member, validRoles):
return not validRoles or member.get('role', Ent.ROLE_MEMBER) in validRoles
def _checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived):
return _checkMemberRole(member, validRoles) and _checkMemberIsSuspendedIsArchived(member, isSuspended, isArchived)
def _checkMemberCategory(member, memberDisplayOptions):
member_email = member.get('email', member.get('id', ''))
if member_email.find('@') > 0:
_, domain = member_email.lower().split('@', 1)
category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external'
else:
category = 'internal'
if memberDisplayOptions[category]:
member['category'] = category
return True
return False
def _checkCIMemberCategory(member, memberDisplayOptions):
member_email = member.get('preferredMemberKey', {}).get('id', '')
if member_email.find('@') > 0:
_, domain = member_email.lower().split('@', 1)
category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external'
else:
category = 'internal'
if memberDisplayOptions[category]:
member['category'] = category
return True
return False
def getCIGroupMemberRoleFixType(member):
''' fixes missing type and returns the highest role of member '''
if 'type' not in member:
if member['preferredMemberKey']['id'] == GC.Values[GC.CUSTOMER_ID]:
member['type'] = Ent.TYPE_CUSTOMER
else:
member['type'] = Ent.TYPE_OTHER
roles = {}
memberRoles = member.get('roles', [{'name': Ent.MEMBER}])
for role in memberRoles:
roles[role['name']] = role
for a_role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER]:
if a_role in roles:
member['role'] = a_role
if 'expiryDetail' in roles[a_role]:
member['expireTime'] = roles[a_role]['expiryDetail']['expireTime']
return
member['role'] = memberRoles[0]['name']
def getCIGroupTransitiveMemberRoleFixType(groupName, tmember):
''' map transitive member to normal member '''
tid = tmember['preferredMemberKey'][0].get('id', GC.Values[GC.CUSTOMER_ID]) if tmember['preferredMemberKey'] else ''
ttype, tname = tmember['member'].split('/')
member = {'name': f'{groupName}/membershipd/{tname}', 'preferredMemberKey': {'id': tid}}
if 'type' not in tmember:
if tid == GC.Values[GC.CUSTOMER_ID]:
member['type'] = Ent.TYPE_CUSTOMER
elif ttype == 'users':
member['type'] = Ent.TYPE_USER if not tid.endswith('.iam.gserviceaccount.com') else Ent.TYPE_SERVICE_ACCOUNT
elif ttype == 'groups':
member['type'] = Ent.TYPE_GROUP
elif tid.startswith('cbcm-browser.'):
member['type'] = Ent.TYPE_CBCM_BROWSER
else:
member['type'] = Ent.TYPE_OTHER
else:
member['type'] = tmember['type']
if 'roles' in tmember:
memberRoles = []
for trole in tmember['roles']:
if 'role' in trole:
trole['name'] = trole.pop('role')
if trole['name'] == 'ADMIN':
trole['name'] = Ent.ROLE_MANAGER
memberRoles.append(trole)
else:
memberRoles = [{'name': Ent.MEMBER}]
roles = {}
for role in memberRoles:
roles[role['name']] = role
for a_role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER]:
if a_role in roles:
member['role'] = a_role
if 'expiryDetail' in roles[a_role]:
member['expireTime'] = roles[a_role]['expiryDetail']['expireTime']
break
else:
member['role'] = memberRoles[0]['name']
return member
def convertGroupCloudIDToEmail(ci, group, i=0, count=0):
if not group.startswith('groups/'):
group = normalizeEmailAddressOrUID(group, ciGroupsAPI=True)
if not group.startswith('groups/'):
return (ci, None, group)
if not ci:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
try:
ciGroup = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=group, fields='groupKey(id)')
return (ci, None, ciGroup['groupKey']['id'])
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
action = Act.Get()
Act.Set(Act.LOOKUP)
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group, Ent.GROUP, None], str(e), i, count)
Act.Set(action)
return (ci, None, None)
def convertGroupEmailToCloudID(ci, group, i=0, count=0):
group = normalizeEmailAddressOrUID(group, ciGroupsAPI=True)
if not group.startswith('groups/') and group.find('@') == -1:
group = 'groups/'+group
if group.startswith('groups/'):
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, group, i, count)
return (ci, group, groupEmail)
if not ci:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
try:
ciGroup = callGAPI(ci.groups(), 'lookup',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
groupKey_id=group, fields='name')
return (ci, ciGroup['name'], group)
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
action = Act.Get()
Act.Set(Act.LOOKUP)
entityActionFailedWarning([Ent.GROUP, group, Ent.CLOUD_IDENTITY_GROUP, None], str(e), i, count)
Act.Set(action)
return (ci, None, None)
CIGROUP_DISCUSSION_FORUM_LABEL = 'cloudidentity.googleapis.com/groups.discussion_forum'
CIGROUP_DYNAMIC_LABEL = 'cloudidentity.googleapis.com/groups.dynamic'
CIGROUP_SECURITY_LABEL = 'cloudidentity.googleapis.com/groups.security'
CIGROUP_LOCKED_LABEL = 'cloudidentity.googleapis.com/groups.locked'
def getCIGroupMembershipGraph(ci, member):
if not ci:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
parent = 'groups/-'
try:
result = callGAPI(ci.groups().memberships(), 'getMembershipGraph',
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=parent,
query=f"member_key_id == '{member}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels")
return (ci, result.get('response', {}))
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
action = Act.Get()
Act.Set(Act.LOOKUP)
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, parent], str(e))
Act.Set(action)
return (ci, None)
def checkGroupExists(cd, ci, ciGroupsAPI, group, i=0, count=0):
group = normalizeEmailAddressOrUID(group, ciGroupsAPI=ciGroupsAPI)
if not ciGroupsAPI:
if not group.startswith('groups/'):
try:
result = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=group, fields='email')
return (ci, None, result['email'])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError):
entityUnknownWarning(Ent.GROUP, group, i, count)
return (ci, None, None)
else:
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, group, i, count)
return (ci, None, groupEmail)
else:
if not group.startswith('groups/') and group.find('@') == -1:
group = 'groups/'+group
if group.startswith('groups/'):
try:
result = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=group, fields='name,groupKey(id)')
return (ci, result['name'], result['groupKey']['id'])
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, group, i, count)
return (ci, None, None)
else:
return convertGroupEmailToCloudID(ci, group, i, count)
# Turn the entity into a list of Users/CrOS devices
def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isArchived=None,
groupMemberType=Ent.TYPE_USER, noListConversion=False, recursive=False, noCLArgs=False):
def _incrEntityDoesNotExist(entityType):
entityError['entityType'] = entityType
entityError[ENTITY_ERROR_DNE] += 1
def _showInvalidEntity(entityType, entityName):
entityError['entityType'] = entityType
entityError[ENTITY_ERROR_INVALID] += 1
printErrorMessage(INVALID_ENTITY_RC, formatKeyValueList('', [Ent.Singular(entityType), entityName, Msg.INVALID], ''))
def _addGroupUsersToUsers(group, domains, recursive, includeDerivedMembership):
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, group, entityType=Ent.GROUP)
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,type,status)')
try:
result = callGAPIpages(cd.members(), 'list', 'members',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
includeDerivedMembership=includeDerivedMembership,
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.GROUP, group)
_incrEntityDoesNotExist(Ent.GROUP)
return
for member in result:
if member['type'] == Ent.TYPE_USER:
email = member['email'].lower()
if email in entitySet:
continue
if _checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived):
if domains:
_, domain = splitEmailAddress(email)
if domain not in domains:
continue
entitySet.add(email)
entityList.append(email)
elif recursive and member['type'] == Ent.TYPE_GROUP:
_addGroupUsersToUsers(member['email'], domains, recursive, includeDerivedMembership)
def _addCIGroupUsersToUsers(groupName, groupEmail, recursive):
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, entityType=Ent.CLOUD_IDENTITY_GROUP)
validRoles = _getCIRoleVerification(memberRoles)
try:
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=groupName, view='FULL',
fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail)
_incrEntityDoesNotExist(Ent.CLOUD_IDENTITY_GROUP)
return
for member in result:
getCIGroupMemberRoleFixType(member)
if member['type'] == Ent.TYPE_USER:
email = member.get('preferredMemberKey', {}).get('id', '')
if (email and _checkMemberRole(member, validRoles) and email not in entitySet):
entitySet.add(email)
entityList.append(email)
elif recursive and member['type'] == Ent.TYPE_GROUP and _checkMemberRole(member, validRoles):
_, gname = member['name'].rsplit('/', 1)
_addCIGroupUsersToUsers(f'groups/{gname}', f'groups/{gname}', recursive)
GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE] = False
ENTITY_ERROR_DNE = 'doesNotExist'
ENTITY_ERROR_INVALID = 'invalid'
entityError = {'entityType': None, ENTITY_ERROR_DNE: 0, ENTITY_ERROR_INVALID: 0}
entityList = []
entitySet = set()
entityLocation = Cmd.Location()
if entityType in {Cmd.ENTITY_USER, Cmd.ENTITY_USERS}:
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] and not GC.Values[GC.DOMAIN]:
buildGAPIObject(API.DIRECTORY)
result = convertEntityToList(entity, nonListEntityType=entityType == Cmd.ENTITY_USER)
for user in result:
if validateEmailAddressOrUID(user):
if user not in entitySet:
entitySet.add(user)
entityList.append(user)
else:
_showInvalidEntity(Ent.USER, user)
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
return entityList
elif entityType in {Cmd.ENTITY_ALL_USERS, Cmd.ENTITY_ALL_USERS_NS, Cmd.ENTITY_ALL_USERS_NS_SUSP, Cmd.ENTITY_ALL_USERS_SUSP}:
cd = buildGAPIObject(API.DIRECTORY)
if entityType == Cmd.ENTITY_ALL_USERS and isSuspended is not None:
query = f'isSuspended={isSuspended}'
else:
query = Cmd.ALL_USERS_QUERY_MAP[entityType]
printGettingAllAccountEntities(Ent.USER)
try:
result = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=getPageMessage(),
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID],
query=query, orderBy='email', fields='nextPageToken,users(primaryEmail,archived)',
maxResults=GC.Values[GC.USER_MAX_RESULTS])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
entityList = [user['primaryEmail'] for user in result if isArchived is None or isArchived == user['archived']]
printGotAccountEntities(len(entityList))
elif entityType in {Cmd.ENTITY_DOMAINS, Cmd.ENTITY_DOMAINS_NS, Cmd.ENTITY_DOMAINS_SUSP}:
if entityType == Cmd.ENTITY_DOMAINS_NS:
query = 'isSuspended=False'
elif entityType == Cmd.ENTITY_DOMAINS_SUSP:
query = 'isSuspended=True'
elif isSuspended is not None:
query = f'isSuspended={isSuspended}'
else:
query = None
cd = buildGAPIObject(API.DIRECTORY)
domains = convertEntityToList(entity)
for domain in domains:
printGettingAllEntityItemsForWhom(Ent.USER, domain, entityType=Ent.DOMAIN)
try:
result = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN],
domain=domain,
query=query, orderBy='email', fields='nextPageToken,users(primaryEmail,archived)',
maxResults=GC.Values[GC.USER_MAX_RESULTS])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.DOMAIN, domain)
_incrEntityDoesNotExist(Ent.DOMAIN)
continue
entityList = [user['primaryEmail'] for user in result if isArchived is None or isArchived == user['archived']]
printGotAccountEntities(len(entityList))
elif entityType in {Cmd.ENTITY_GROUP, Cmd.ENTITY_GROUPS,
Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUPS_NS,
Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUPS_SUSP,
Cmd.ENTITY_GROUP_INDE, Cmd.ENTITY_GROUPS_INDE}:
if entityType in {Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUPS_NS}:
isSuspended = False
elif entityType in {Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUPS_SUSP}:
isSuspended = True
includeDerivedMembership = entityType in {Cmd.ENTITY_GROUP_INDE, Cmd.ENTITY_GROUPS_INDE}
cd = buildGAPIObject(API.DIRECTORY)
groups = convertEntityToList(entity, nonListEntityType=entityType in {Cmd.ENTITY_GROUP, Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUP_INDE})
for group in groups:
if validateEmailAddressOrUID(group, checkPeople=False):
group = normalizeEmailAddressOrUID(group)
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, group, entityType=Ent.GROUP)
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,id,type,status)')
try:
result = callGAPIpages(cd.members(), 'list', 'members',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
includeDerivedMembership=includeDerivedMembership,
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.GROUP, group)
_incrEntityDoesNotExist(Ent.GROUP)
continue
for member in result:
email = member['email'].lower() if member['type'] != Ent.TYPE_CUSTOMER else member['id']
if ((groupMemberType in ('ALL', member['type'])) and
(not includeDerivedMembership or (member['type'] == Ent.TYPE_USER)) and
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived) and
email not in entitySet):
entitySet.add(email)
entityList.append(email)
else:
_showInvalidEntity(Ent.GROUP, group)
elif entityType in {Cmd.ENTITY_GROUP_USERS, Cmd.ENTITY_GROUP_USERS_NS, Cmd.ENTITY_GROUP_USERS_SUSP, Cmd.ENTITY_GROUP_USERS_SELECT}:
if entityType == Cmd.ENTITY_GROUP_USERS_NS:
isSuspended = False
elif entityType == Cmd.ENTITY_GROUP_USERS_SUSP:
isSuspended = True
cd = buildGAPIObject(API.DIRECTORY)
groups = convertEntityToList(entity)
includeDerivedMembership = False
domains = []
rolesSet = set()
if not noCLArgs:
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[myarg])
elif myarg == 'primarydomain':
domains.append(GC.Values[GC.DOMAIN])
elif myarg == 'domains':
domains.extend(getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY))
elif myarg == 'recursive':
recursive = True
includeDerivedMembership = False
elif myarg == 'includederivedmembership':
includeDerivedMembership = True
recursive = False
elif entityType == Cmd.ENTITY_GROUP_USERS_SELECT and myarg in SUSPENDED_ARGUMENTS:
isSuspended = _getIsSuspended(myarg)
elif entityType == Cmd.ENTITY_GROUP_USERS_SELECT and myarg in ARCHIVED_ARGUMENTS:
isArchived = _getIsArchived(myarg)
elif myarg == 'end':
break
else:
Cmd.Backup()
missingArgumentExit('end')
if rolesSet:
memberRoles = ','.join(sorted(rolesSet))
for group in groups:
if validateEmailAddressOrUID(group, checkPeople=False):
_addGroupUsersToUsers(normalizeEmailAddressOrUID(group), domains, recursive, includeDerivedMembership)
else:
_showInvalidEntity(Ent.GROUP, group)
elif entityType in {Cmd.ENTITY_CIGROUP, Cmd.ENTITY_CIGROUPS}:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
groups = convertEntityToList(entity, nonListEntityType=entityType in {Cmd.ENTITY_CIGROUP})
for group in groups:
if validateEmailAddressOrUID(group, checkPeople=False, ciGroupsAPI=True):
_, name, groupEmail = convertGroupEmailToCloudID(ci, group)
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, entityType=Ent.CLOUD_IDENTITY_GROUP)
validRoles = _getCIRoleVerification(memberRoles)
try:
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=name, view='FULL',
fields='nextPageToken,memberships(preferredMemberKey(id),roles(name),type)',
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail)
_incrEntityDoesNotExist(Ent.CLOUD_IDENTITY_GROUP)
continue
for member in result:
getCIGroupMemberRoleFixType(member)
email = member.get('preferredMemberKey', {}).get('id', '')
if (email and (groupMemberType in ('ALL', member['type'])) and
_checkMemberRole(member, validRoles) and email not in entitySet):
entitySet.add(email)
entityList.append(email)
else:
_showInvalidEntity(Ent.CLOUD_IDENTITY_GROUP, groupEmail)
elif entityType in {Cmd.ENTITY_CIGROUP_USERS}:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
groups = convertEntityToList(entity)
rolesSet = set()
if not noCLArgs:
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[myarg])
elif myarg == 'recursive':
recursive = True
elif myarg == 'end':
break
else:
Cmd.Backup()
missingArgumentExit('end')
if rolesSet:
memberRoles = ','.join(sorted(rolesSet))
for group in groups:
_, name, groupEmail = convertGroupEmailToCloudID(ci, group)
if name:
_addCIGroupUsersToUsers(name, groupEmail, recursive)
else:
_showInvalidEntity(Ent.GROUP, group)
elif entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS, Cmd.ENTITY_OU_AND_CHILDREN, Cmd.ENTITY_OUS_AND_CHILDREN,
Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP}:
if entityType in {Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS}:
isSuspended = False
elif entityType in {Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP}:
isSuspended = True
cd = buildGAPIObject(API.DIRECTORY)
ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN,
Cmd.ENTITY_OU_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS,
Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP})
directlyInOU = entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS, Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP}
qualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if directlyInOU else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
fields = 'nextPageToken,users(primaryEmail,orgUnitPath,archived)' if directlyInOU else 'nextPageToken,users(primaryEmail,archived)'
prevLen = 0
for ou in ous:
ou = makeOrgUnitPathAbsolute(ou)
if ou.startswith('id:'):
try:
ou = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID],
orgUnitPath=ou, fields='orgUnitPath')['orgUnitPath']
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest,
GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
continue
ouLower = ou.lower()
printGettingAllEntityItemsForWhom(Ent.USER, ou, qualifier=Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)),
entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
usersInOU = 0
try:
feed = yieldGAPIpages(cd.users(), 'list', 'users',
pageMessage=pageMessage, messageAttribute='primaryEmail',
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(ou, isSuspended), orderBy='email',
fields=fields, maxResults=GC.Values[GC.USER_MAX_RESULTS])
for users in feed:
if directlyInOU:
for user in users:
if ouLower == user.get('orgUnitPath', '').lower() and (isArchived is None or isArchived == user['archived']):
usersInOU += 1
entityList.append(user['primaryEmail'])
elif isArchived is None:
entityList.extend([user['primaryEmail'] for user in users])
usersInOU += len(users)
else:
for user in users:
if isArchived == user['archived']:
usersInOU += 1
entityList.append(user['primaryEmail'])
setGettingAllEntityItemsForWhom(Ent.USER, ou, qualifier=qualifier)
printGotEntityItemsForWhom(usersInOU)
except (GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest,
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
elif entityType in {Cmd.ENTITY_QUERY, Cmd.ENTITY_QUERIES}:
cd = buildGAPIObject(API.DIRECTORY)
queries = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType == Cmd.ENTITY_QUERY)
prevLen = 0
for query in queries:
printGettingAllAccountEntities(Ent.USER, query)
try:
result = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], query=query, orderBy='email',
fields='nextPageToken,users(primaryEmail,suspended,archived)',
maxResults=GC.Values[GC.USER_MAX_RESULTS])
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput):
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
for user in result:
email = user['primaryEmail']
if ((isSuspended is None or isSuspended == user['suspended']) and
(isArchived is None or isArchived == user['archived']) and
email not in entitySet):
entitySet.add(email)
entityList.append(email)
totalLen = len(entityList)
printGotAccountEntities(totalLen-prevLen)
prevLen = totalLen
elif entityType == Cmd.ENTITY_LICENSES:
skusList = []
for item in entity.split(','):
productId, sku = SKU.getProductAndSKU(item)
if not productId:
_incrEntityDoesNotExist(Ent.SKU)
elif (productId, sku) not in skusList:
skusList.append((productId, sku))
if skusList:
entityList = doPrintLicenses(returnFields=['userId'], skus=skusList)
elif entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_TEACHERS, Cmd.ENTITY_STUDENTS}:
croom = buildGAPIObject(API.CLASSROOM)
if not noListConversion:
courseIdList = convertEntityToList(entity)
else:
courseIdList = [entity]
_, _, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
for courseId, courseInfo in coursesInfo.items():
try:
if entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_TEACHERS}:
printGettingAllEntityItemsForWhom(Ent.TEACHER, removeCourseIdScope(courseId), entityType=Ent.COURSE)
result = callGAPIpages(courseInfo['croom'].courses().teachers(), 'list', 'teachers',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, fields='nextPageToken,teachers/profile/emailAddress',
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for teacher in result:
email = teacher['profile'].get('emailAddress', None)
if email and (email not in entitySet):
entitySet.add(email)
entityList.append(email)
if entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_STUDENTS}:
printGettingAllEntityItemsForWhom(Ent.STUDENT, removeCourseIdScope(courseId), entityType=Ent.COURSE)
result = callGAPIpages(courseInfo['croom'].courses().students(), 'list', 'students',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, fields='nextPageToken,students/profile/emailAddress',
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for student in result:
email = student['profile'].get('emailAddress', None)
if email and (email not in entitySet):
entitySet.add(email)
entityList.append(email)
except GAPI.notFound:
entityDoesNotExistWarning(Ent.COURSE, removeCourseIdScope(courseId))
_incrEntityDoesNotExist(Ent.COURSE)
except GAPI.serviceNotAvailable as e:
entityActionNotPerformedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e))
GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE] = True
break
except (GAPI.forbidden, GAPI.badRequest):
ClientAPIAccessDeniedExit()
elif entityType == Cmd.ENTITY_CROS:
buildGAPIObject(API.DIRECTORY)
result = convertEntityToList(entity)
for deviceId in result:
if deviceId not in entitySet:
entitySet.add(deviceId)
entityList.append(deviceId)
elif entityType == Cmd.ENTITY_ALL_CROS:
cd = buildGAPIObject(API.DIRECTORY)
printGettingAllAccountEntities(Ent.CROS_DEVICE)
try:
result = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID],
fields='nextPageToken,chromeosdevices(deviceId)',
maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
except (GAPI.invalidInput, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
entityList = [device['deviceId'] for device in result]
elif entityType in {Cmd.ENTITY_CROS_QUERY, Cmd.ENTITY_CROS_QUERIES, Cmd.ENTITY_CROS_SN}:
cd = buildGAPIObject(API.DIRECTORY)
queries = convertEntityToList(entity, shlexSplit=entityType == Cmd.ENTITY_CROS_QUERIES,
nonListEntityType=entityType == Cmd.ENTITY_CROS_QUERY)
if entityType == Cmd.ENTITY_CROS_SN:
queries = [f'id:{query}' for query in queries]
prevLen = 0
for query in queries:
printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
try:
result = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], query=query,
fields='nextPageToken,chromeosdevices(deviceId)',
maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
except GAPI.invalidInput:
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
for device in result:
deviceId = device['deviceId']
if deviceId not in entitySet:
entitySet.add(deviceId)
entityList.append(deviceId)
totalLen = len(entityList)
printGotAccountEntities(totalLen-prevLen)
prevLen = totalLen
elif entityType in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
cd = buildGAPIObject(API.DIRECTORY)
ous = convertEntityToList(entity, shlexSplit=True,
nonListEntityType=entityType in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN,
Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY,
Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES})
numOus = len(ous)
includeChildOrgunits = entityType in {Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}
allQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Choose(Ent.ORGANIZATIONAL_UNIT, numOus)) if not includeChildOrgunits else Msg.IN_THE.format(Ent.Choose(Ent.ORGANIZATIONAL_UNIT, numOus))
if entityType in {Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY}:
queries = getQueries('query')
elif entityType in {Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
queries = getQueries('queries')
else:
queries = [None]
for ou in ous:
ou = makeOrgUnitPathAbsolute(ou)
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if not includeChildOrgunits else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
for query in queries:
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, ou,
query=query, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
try:
result = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], query=query,
orgUnitPath=ou, includeChildOrgunits=includeChildOrgunits,
fields='nextPageToken,chromeosdevices(deviceId)', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
except GAPI.invalidInput:
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
continue
if query is None:
entityList.extend([device['deviceId'] for device in result])
else:
for device in result:
deviceId = device['deviceId']
if deviceId not in entitySet:
entitySet.add(deviceId)
entityList.append(deviceId)
Ent.SetGettingQualifier(Ent.CROS_DEVICE, allQualifier)
Ent.SetGettingForWhom(','.join(ous))
printGotEntityItemsForWhom(len(entityList))
elif entityType == Cmd.ENTITY_BROWSER:
result = convertEntityToList(entity)
for deviceId in result:
if deviceId not in entitySet:
entitySet.add(deviceId)
entityList.append(deviceId)
elif entityType in {Cmd.ENTITY_BROWSER_OU, Cmd.ENTITY_BROWSER_OUS}:
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType == Cmd.ENTITY_BROWSER_OU)
numOus = len(ous)
allQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Choose(Ent.ORGANIZATIONAL_UNIT, numOus))
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
for ou in ous:
ou = makeOrgUnitPathAbsolute(ou)
printGettingAllEntityItemsForWhom(Ent.CHROME_BROWSER, ou, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
try:
result = callGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customerId, orgUnitPath=ou, projection='BASIC',
orderBy='id', sortOrder='ASCENDING', fields='nextPageToken,browsers(deviceId)')
except (GAPI.badRequest, GAPI.invalidOrgunit, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(None, Ent.ORGANIZATIONAL_UNIT, ou)
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
continue
entityList.extend([browser['deviceId'] for browser in result])
Ent.SetGettingQualifier(Ent.CHROME_BROWSER, allQualifier)
Ent.SetGettingForWhom(','.join(ous))
printGotEntityItemsForWhom(len(entityList))
elif entityType in {Cmd.ENTITY_BROWSER_QUERY, Cmd.ENTITY_BROWSER_QUERIES}:
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
queries = convertEntityToList(entity, shlexSplit=entityType == Cmd.ENTITY_BROWSER_QUERIES,
nonListEntityType=entityType == Cmd.ENTITY_BROWSER_QUERY)
prevLen = 0
for query in queries:
printGettingAllAccountEntities(Ent.CHROME_BROWSER, query)
try:
result = callGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customerId, query=query, projection='BASIC',
orderBy='id', sortOrder='ASCENDING', fields='nextPageToken,browsers(deviceId)')
except GAPI.invalidInput:
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
accessErrorExitNonDirectory(API.CBCM, str(e))
for device in result:
deviceId = device['deviceId']
if deviceId not in entitySet:
entitySet.add(deviceId)
entityList.append(deviceId)
totalLen = len(entityList)
printGotAccountEntities(totalLen-prevLen)
prevLen = totalLen
else:
systemErrorExit(UNKNOWN_ERROR_RC, 'getItemsToModify coding error')
for errorType in [ENTITY_ERROR_DNE, ENTITY_ERROR_INVALID]:
if entityError[errorType] > 0:
Cmd.SetLocation(entityLocation-1)
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
count = entityError[errorType]
if errorType == ENTITY_ERROR_DNE:
stderrErrorMsg(Msg.BAD_ENTITIES_IN_SOURCE.format(count, Ent.Choose(entityError['entityType'], count),
Msg.DO_NOT_EXIST if count != 1 else Msg.DOES_NOT_EXIST))
sys.exit(ENTITY_DOES_NOT_EXIST_RC)
else:
stderrErrorMsg(Msg.BAD_ENTITIES_IN_SOURCE.format(count, Msg.INVALID, Ent.Choose(entityError['entityType'], count)))
sys.exit(INVALID_ENTITY_RC)
return entityList
def splitEntityList(entity, dataDelimiter):
if not entity:
return []
if not dataDelimiter:
return [entity]
return entity.split(dataDelimiter)
def splitEntityListShlex(entity, dataDelimiter):
if not entity:
return (True, [])
if not dataDelimiter:
return (True, [entity])
return shlexSplitListStatus(entity, dataDelimiter)
def fileDataErrorExit(filename, row, itemName, value, errMessage):
if itemName:
systemErrorExit(DATA_ERROR_RC,
formatKeyValueList('',
[Ent.Singular(Ent.FILE), filename,
Ent.Singular(Ent.ROW), row,
Ent.Singular(Ent.ITEM), itemName,
Ent.Singular(Ent.VALUE), value,
errMessage],
''))
else:
systemErrorExit(DATA_ERROR_RC,
formatKeyValueList('',
[Ent.Singular(Ent.FILE), filename,
Ent.Singular(Ent.ROW), row,
Ent.Singular(Ent.VALUE), value,
errMessage],
''))
# <FileSelector>
def getEntitiesFromFile(shlexSplit, returnSet=False):
filename = getString(Cmd.OB_FILE_NAME)
filenameLower = filename.lower()
if filenameLower not in {'gcsv', 'gdoc', 'gcscsv', 'gcsdoc'}:
encoding = getCharSet()
f = openFile(filename, encoding=encoding, stripUTFBOM=True)
elif filenameLower in {'gcsv', 'gdoc'}:
f = getGDocData(filenameLower)
getCharSet()
else: #filenameLower in {'gcscsv', 'gcsdoc'}:
f = getStorageFileData(filenameLower)
getCharSet()
dataDelimiter = getDelimiter()
entitySet = set()
entityList = []
i = 0
for row in f:
i += 1
if shlexSplit:
splitStatus, itemList = splitEntityListShlex(row.strip(), dataDelimiter)
if not splitStatus:
fileDataErrorExit(filename, i, None, row.strip(), f'{Msg.INVALID_LIST}: {itemList}')
else:
itemList = splitEntityList(row.strip(), dataDelimiter)
for item in itemList:
item = item.strip()
if item and (item not in entitySet):
entitySet.add(item)
entityList.append(item)
closeFile(f)
return entityList if not returnSet else entitySet
# <CSVFileSelector>
def getEntitiesFromCSVFile(shlexSplit, returnSet=False):
fileFieldName = getString(Cmd.OB_FILE_NAME_FIELD_NAME)
if platform.system() == 'Windows' and not fileFieldName.startswith('-:'):
drive, fileFieldName = os.path.splitdrive(fileFieldName)
else:
drive = ''
if fileFieldName.find(':') == -1:
Cmd.Backup()
invalidArgumentExit(Cmd.OB_FILE_NAME_FIELD_NAME)
fileFieldNameList = fileFieldName.split(':')
filename = drive+fileFieldNameList[0]
f, csvFile, fieldnames = openCSVFileReader(filename)
for fieldName in fileFieldNameList[1:]:
if fieldName not in fieldnames:
csvFieldErrorExit(fieldName, fieldnames, backupArg=True, checkForCharset=True)
matchFields, skipFields = getMatchSkipFields(fieldnames)
dataDelimiter = getDelimiter()
entitySet = set()
entityList = []
i = 1
for row in csvFile:
i += 1
if checkMatchSkipFields(row, None, matchFields, skipFields):
for fieldName in fileFieldNameList[1:]:
if shlexSplit:
splitStatus, itemList = splitEntityListShlex(row[fieldName].strip(), dataDelimiter)
if not splitStatus:
fileDataErrorExit(filename, i, fieldName, row[fieldName].strip(), f'{Msg.INVALID_LIST}: {itemList}')
else:
itemList = splitEntityList(row[fieldName].strip(), dataDelimiter)
for item in itemList:
item = item.strip()
if item and (item not in entitySet):
entitySet.add(item)
entityList.append(item)
closeFile(f)
return entityList if not returnSet else entitySet
# <CSVFileSelector>
# keyfield <FieldName> [keypattern <RESearchPattern>] [keyvalue <RESubstitution>] [delimiter <Character>]
# subkeyfield <FieldName> [keypattern <RESearchPattern>] [keyvalue <RESubstitution>] [delimiter <Character>]
# (matchfield|skipfield <FieldName> <RESearchPattern>)*
# [datafield <FieldName>(:<FieldName>)* [delimiter <Character>]]
def getEntitiesFromCSVbyField():
def getKeyFieldInfo(keyword, required, globalKeyField):
if not checkArgumentPresent(keyword, required=required):
GM.Globals[globalKeyField] = None
return (None, None, None, None)
keyField = GM.Globals[globalKeyField] = getString(Cmd.OB_FIELD_NAME)
if keyField not in fieldnames:
csvFieldErrorExit(keyField, fieldnames, backupArg=True)
if checkArgumentPresent('keypattern'):
keyPattern = getREPattern()
else:
keyPattern = None
if checkArgumentPresent('keyvalue'):
keyValue = getString(Cmd.OB_STRING)
else:
keyValue = keyField
keyDelimiter = getDelimiter()
return (keyField, keyPattern, keyValue, keyDelimiter)
def getKeyList(row, keyField, keyPattern, keyValue, keyDelimiter, matchFields, skipFields):
item = row[keyField].strip()
if not item:
return []
if not checkMatchSkipFields(row, None, matchFields, skipFields):
return []
if keyPattern:
keyList = [keyPattern.sub(keyValue, keyItem.strip()) for keyItem in splitEntityList(item, keyDelimiter)]
else:
keyList = [re.sub(keyField, keyItem.strip(), keyValue) for keyItem in splitEntityList(item, keyDelimiter)]
return [key for key in keyList if key]
filename = getString(Cmd.OB_FILE_NAME)
f, csvFile, fieldnames = openCSVFileReader(filename)
mainKeyField, mainKeyPattern, mainKeyValue, mainKeyDelimiter = getKeyFieldInfo('keyfield', True, GM.CSV_KEY_FIELD)
subKeyField, subKeyPattern, subKeyValue, subKeyDelimiter = getKeyFieldInfo('subkeyfield', False, GM.CSV_SUBKEY_FIELD)
matchFields, skipFields = getMatchSkipFields(fieldnames)
if checkArgumentPresent('datafield'):
if GM.Globals[GM.CSV_DATA_DICT]:
csvDataAlreadySavedErrorExit()
GM.Globals[GM.CSV_DATA_FIELD] = getString(Cmd.OB_FIELD_NAME, checkBlank=True)
dataFields = GM.Globals[GM.CSV_DATA_FIELD].split(':')
for dataField in dataFields:
if dataField not in fieldnames:
csvFieldErrorExit(dataField, fieldnames, backupArg=True)
dataDelimiter = getDelimiter()
else:
GM.Globals[GM.CSV_DATA_FIELD] = None
dataFields = []
dataDelimiter = None
entitySet = set()
entityList = []
csvDataKeys = {}
GM.Globals[GM.CSV_DATA_DICT] = {}
if not subKeyField:
for row in csvFile:
mainKeyList = getKeyList(row, mainKeyField, mainKeyPattern, mainKeyValue, mainKeyDelimiter, matchFields, skipFields)
if not mainKeyList:
continue
for mainKey in mainKeyList:
if mainKey not in entitySet:
entitySet.add(mainKey)
entityList.append(mainKey)
if GM.Globals[GM.CSV_DATA_FIELD]:
csvDataKeys[mainKey] = set()
GM.Globals[GM.CSV_DATA_DICT][mainKey] = []
for dataField in dataFields:
if dataField in row:
dataList = splitEntityList(row[dataField].strip(), dataDelimiter)
for dataValue in dataList:
dataValue = dataValue.strip()
if not dataValue:
continue
for mainKey in mainKeyList:
if dataValue not in csvDataKeys[mainKey]:
csvDataKeys[mainKey].add(dataValue)
GM.Globals[GM.CSV_DATA_DICT][mainKey].append(dataValue)
else:
csvSubKeys = {}
for row in csvFile:
mainKeyList = getKeyList(row, mainKeyField, mainKeyPattern, mainKeyValue, mainKeyDelimiter, matchFields, skipFields)
if not mainKeyList:
continue
for mainKey in mainKeyList:
if mainKey not in entitySet:
entitySet.add(mainKey)
entityList.append(mainKey)
csvSubKeys[mainKey] = set()
csvDataKeys[mainKey] = {}
GM.Globals[GM.CSV_DATA_DICT][mainKey] = {}
subKeyList = getKeyList(row, subKeyField, subKeyPattern, subKeyValue, subKeyDelimiter, {}, {})
if not subKeyList:
continue
for mainKey in mainKeyList:
for subKey in subKeyList:
if subKey not in csvSubKeys[mainKey]:
csvSubKeys[mainKey].add(subKey)
if GM.Globals[GM.CSV_DATA_FIELD]:
csvDataKeys[mainKey][subKey] = set()
GM.Globals[GM.CSV_DATA_DICT][mainKey][subKey] = []
for dataField in dataFields:
if dataField in row:
dataList = splitEntityList(row[dataField].strip(), dataDelimiter)
for dataValue in dataList:
dataValue = dataValue.strip()
if not dataValue:
continue
for mainKey in mainKeyList:
for subKey in subKeyList:
if dataValue not in csvDataKeys[mainKey][subKey]:
csvDataKeys[mainKey][subKey].add(dataValue)
GM.Globals[GM.CSV_DATA_DICT][mainKey][subKey].append(dataValue)
closeFile(f)
return entityList
# Typically used to map courseparticipants to students or teachers
def mapEntityType(entityType, typeMap):
if (typeMap is not None) and (entityType in typeMap):
return typeMap[entityType]
return entityType
def getEntityArgument(entityList):
if entityList is None:
return (0, 0, entityList)
if isinstance(entityList, dict):
clLoc = Cmd.Location()
Cmd.SetLocation(GM.Globals[GM.ENTITY_CL_DELAY_START])
entityList = getItemsToModify(**entityList)
Cmd.SetLocation(clLoc)
return (0, len(entityList), entityList)
def getEntityToModify(defaultEntityType=None, browserAllowed=False, crosAllowed=False, userAllowed=True,
typeMap=None, isSuspended=None, isArchived=None, groupMemberType=Ent.TYPE_USER, delayGet=False):
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
crosAllowed = False
selectorChoices = Cmd.SERVICE_ACCOUNT_ONLY_ENTITY_SELECTORS[:]
else:
selectorChoices = Cmd.BASE_ENTITY_SELECTORS[:]
if userAllowed:
selectorChoices += Cmd.USER_ENTITY_SELECTORS+Cmd.USER_CSVDATA_ENTITY_SELECTORS
if crosAllowed:
selectorChoices += Cmd.CROS_ENTITY_SELECTORS+Cmd.CROS_CSVDATA_ENTITY_SELECTORS
if browserAllowed:
selectorChoices = Cmd.BROWSER_ENTITY_SELECTORS
entitySelector = getChoice(selectorChoices, defaultChoice=None)
if entitySelector:
choices = []
if entitySelector == Cmd.ENTITY_SELECTOR_ALL:
if userAllowed:
choices += Cmd.USER_ENTITY_SELECTOR_ALL_SUBTYPES
if crosAllowed:
choices += Cmd.CROS_ENTITY_SELECTOR_ALL_SUBTYPES
entityType = Cmd.ENTITY_SELECTOR_ALL_SUBTYPES_MAP[getChoice(choices)]
if not delayGet:
return (Cmd.ENTITY_USERS if entityType != Cmd.ENTITY_ALL_CROS else Cmd.ENTITY_CROS,
getItemsToModify(entityType, None))
GM.Globals[GM.ENTITY_CL_DELAY_START] = Cmd.Location()
buildGAPIObject(API.DIRECTORY)
return (Cmd.ENTITY_USERS if entityType != Cmd.ENTITY_ALL_CROS else Cmd.ENTITY_CROS,
{'entityType': entityType, 'entity': None})
if userAllowed:
if entitySelector == Cmd.ENTITY_SELECTOR_FILE:
return (Cmd.ENTITY_USERS, getItemsToModify(Cmd.ENTITY_USERS, getEntitiesFromFile(False)))
if entitySelector in [Cmd.ENTITY_SELECTOR_CSV, Cmd.ENTITY_SELECTOR_CSVFILE]:
return (Cmd.ENTITY_USERS, getItemsToModify(Cmd.ENTITY_USERS, getEntitiesFromCSVFile(False)))
if crosAllowed:
if entitySelector == Cmd.ENTITY_SELECTOR_CROSFILE:
return (Cmd.ENTITY_CROS, getEntitiesFromFile(False))
if entitySelector in [Cmd.ENTITY_SELECTOR_CROSCSV, Cmd.ENTITY_SELECTOR_CROSCSVFILE]:
return (Cmd.ENTITY_CROS, getEntitiesFromCSVFile(False))
if entitySelector == Cmd.ENTITY_SELECTOR_CROSFILE_SN:
return (Cmd.ENTITY_CROS, getItemsToModify(Cmd.ENTITY_CROS_SN, getEntitiesFromFile(False)))
if entitySelector in [Cmd.ENTITY_SELECTOR_CROSCSV_SN, Cmd.ENTITY_SELECTOR_CROSCSVFILE_SN]:
return (Cmd.ENTITY_CROS, getItemsToModify(Cmd.ENTITY_CROS_SN, getEntitiesFromCSVFile(False)))
if browserAllowed:
if entitySelector == Cmd.ENTITY_SELECTOR_FILE:
return (Cmd.ENTITY_BROWSER, getEntitiesFromFile(False))
if entitySelector in [Cmd.ENTITY_SELECTOR_CSV, Cmd.ENTITY_SELECTOR_CSVFILE]:
return (Cmd.ENTITY_BROWSER, getEntitiesFromCSVFile(False))
if entitySelector == Cmd.ENTITY_SELECTOR_DATAFILE:
if userAllowed:
choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
if crosAllowed:
choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
entityType = mapEntityType(getChoice(choices), typeMap)
return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
getItemsToModify(entityType, getEntitiesFromFile(shlexSplit=entityType in [Cmd.ENTITY_OUS, Cmd.ENTITY_OUS_AND_CHILDREN,
Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP,
Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN])))
if entitySelector == Cmd.ENTITY_SELECTOR_CSVDATAFILE:
if userAllowed:
choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
if crosAllowed:
choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
entityType = mapEntityType(getChoice(choices), typeMap)
return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
getItemsToModify(entityType, getEntitiesFromCSVFile(shlexSplit=entityType in [Cmd.ENTITY_OUS, Cmd.ENTITY_OUS_AND_CHILDREN,
Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP,
Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN])))
if entitySelector == Cmd.ENTITY_SELECTOR_CSVKMD:
if userAllowed:
choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
if crosAllowed:
choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
entityType = mapEntityType(getChoice(choices, choiceAliases=Cmd.ENTITY_ALIAS_MAP), typeMap)
return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
getItemsToModify(entityType, getEntitiesFromCSVbyField()))
if entitySelector in [Cmd.ENTITY_SELECTOR_CSVDATA, Cmd.ENTITY_SELECTOR_CROSCSVDATA]:
checkDataField()
return (Cmd.ENTITY_USERS if entitySelector == Cmd.ENTITY_SELECTOR_CSVDATA else Cmd.ENTITY_CROS,
GM.Globals[GM.CSV_DATA_DICT])
entityChoices = []
if userAllowed:
entityChoices += Cmd.USER_ENTITIES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USER, Cmd.ENTITY_USERS]
if crosAllowed:
entityChoices += Cmd.CROS_ENTITIES
if browserAllowed:
entityChoices += Cmd.BROWSER_ENTITIES
entityType = mapEntityType(getChoice(entityChoices, choiceAliases=Cmd.ENTITY_ALIAS_MAP, defaultChoice=defaultEntityType), typeMap)
if not entityType:
invalidChoiceExit(Cmd.Current(), selectorChoices+entityChoices, False)
if entityType not in Cmd.CROS_ENTITIES+Cmd.BROWSER_ENTITIES:
entityClass = Cmd.ENTITY_USERS
if entityType == Cmd.ENTITY_OAUTHUSER:
return (entityClass, [_getAdminEmail()])
entityItem = getString(Cmd.OB_USER_ENTITY, minLen=0)
elif entityType in Cmd.CROS_ENTITIES:
entityClass = Cmd.ENTITY_CROS
entityItem = getString(Cmd.OB_CROS_ENTITY, minLen=0)
else:
entityClass = Cmd.ENTITY_BROWSER
entityItem = getString(Cmd.OB_BROWSER_ENTITY, minLen=0)
if not delayGet:
if entityClass == Cmd.ENTITY_USERS:
return (entityClass, getItemsToModify(entityType, entityItem,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType))
return (entityClass, getItemsToModify(entityType, entityItem))
GM.Globals[GM.ENTITY_CL_DELAY_START] = Cmd.Location()
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
buildGAPIObject(API.DIRECTORY)
if entityClass == Cmd.ENTITY_USERS:
if entityType in [Cmd.ENTITY_GROUP_USERS,
Cmd.ENTITY_GROUP_USERS_NS, Cmd.ENTITY_GROUP_USERS_SUSP,
Cmd.ENTITY_GROUP_USERS_SELECT,
Cmd.ENTITY_CIGROUP_USERS]:
# Skip over sub-arguments
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in GROUP_ROLES_MAP or myarg in {'primarydomain', 'recursive', 'includederivedmembership'}:
pass
elif myarg == 'domains':
Cmd.Advance()
elif ((entityType == Cmd.ENTITY_GROUP_USERS_SELECT) and
(myarg in SUSPENDED_ARGUMENTS) or (myarg in ARCHIVED_ARGUMENTS)):
if myarg in {'issuspended', 'isarchived'}:
if Cmd.PeekArgumentPresent(TRUE_VALUES) or Cmd.PeekArgumentPresent(FALSE_VALUES):
Cmd.Advance()
elif myarg == 'end':
break
else:
Cmd.Backup()
missingArgumentExit('end')
return (entityClass,
{'entityType': entityType, 'entity': entityItem, 'isSuspended': isSuspended, 'isArchived': isArchived,
'groupMemberType': groupMemberType})
if entityClass == Cmd.ENTITY_CROS:
if entityType in {Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
Cmd.Advance()
return (entityClass,
{'entityType': entityType, 'entity': entityItem})
def getEntitySelector():
return getChoice(Cmd.ENTITY_LIST_SELECTORS, defaultChoice=None)
def getEntitySelection(entitySelector, shlexSplit):
if entitySelector in [Cmd.ENTITY_SELECTOR_FILE]:
return getEntitiesFromFile(shlexSplit)
if entitySelector in [Cmd.ENTITY_SELECTOR_CSV, Cmd.ENTITY_SELECTOR_CSVFILE]:
return getEntitiesFromCSVFile(shlexSplit)
if entitySelector == Cmd.ENTITY_SELECTOR_CSVKMD:
return getEntitiesFromCSVbyField()
if entitySelector in [Cmd.ENTITY_SELECTOR_CSVSUBKEY]:
checkSubkeyField()
return GM.Globals[GM.CSV_DATA_DICT]
if entitySelector in [Cmd.ENTITY_SELECTOR_CSVDATA]:
checkDataField()
return GM.Globals[GM.CSV_DATA_DICT]
return []
def getEntityList(item, shlexSplit=False):
entitySelector = getEntitySelector()
if entitySelector:
return getEntitySelection(entitySelector, shlexSplit)
return convertEntityToList(getString(item, minLen=0), shlexSplit=shlexSplit)
def getNormalizedEmailAddressEntity(shlexSplit=False, noUid=True, noLower=False):
return [normalizeEmailAddressOrUID(emailAddress, noUid=noUid, noLower=noLower) for emailAddress in getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY, shlexSplit)]
def getUserObjectEntity(clObject, itemType, shlexSplit=False):
entity = {'item': itemType, 'list': getEntityList(clObject, shlexSplit), 'dict': None}
if isinstance(entity['list'], dict):
entity['dict'] = entity['list']
return entity
def _validateUserGetObjectList(user, i, count, entity, api=API.GMAIL, showAction=True):
if entity['dict']:
entityList = entity['dict'][user]
else:
entityList = entity['list']
user, svc = buildGAPIServiceObject(api, user, i, count)
if not svc:
return (user, None, [], 0)
jcount = len(entityList)
if showAction:
entityPerformActionNumItems([Ent.USER, user], jcount, entity['item'], i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (user, svc, entityList, jcount)
def _validateUserGetMessageIds(user, i, count, entity):
if entity:
if entity['dict']:
entityList = entity['dict'][user]
else:
entityList = entity['list']
else:
entityList = []
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
return (user, None, None)
return (user, gmail, entityList)
def checkUserExists(cd, user, entityType=Ent.USER, i=0, count=0):
user = normalizeEmailAddressOrUID(user)
try:
return callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=user, fields='primaryEmail')['primaryEmail']
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
entityUnknownWarning(entityType, user, i, count)
return None
def checkUserSuspended(cd, user, entityType=Ent.USER, i=0, count=0):
user = normalizeEmailAddressOrUID(user)
try:
return callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=user, fields='suspended')['suspended']
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
entityUnknownWarning(entityType, user, i, count)
return None
# Add attachements to an email message
def _addAttachmentsToMessage(message, attachments):
for attachment in attachments:
try:
attachFilename = attachment[0]
attachContentType, attachEncoding = mimetypes.guess_type(attachFilename)
if attachContentType is None or attachEncoding is not None:
attachContentType = 'application/octet-stream'
main_type, sub_type = attachContentType.split('/', 1)
if main_type == 'text':
msg = MIMEText(readFile(attachFilename, 'r', attachment[1]), _subtype=sub_type, _charset=UTF8)
elif main_type == 'image':
msg = MIMEImage(readFile(attachFilename, 'rb'), _subtype=sub_type)
elif main_type == 'audio':
msg = MIMEAudio(readFile(attachFilename, 'rb'), _subtype=sub_type)
elif main_type == 'application':
msg = MIMEApplication(readFile(attachFilename, 'rb'), _subtype=sub_type)
else:
msg = MIMEBase(main_type, sub_type)
msg.set_payload(readFile(attachFilename, 'rb'))
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(attachFilename))
message.attach(msg)
except (IOError, UnicodeDecodeError) as e:
usageErrorExit(f'{attachFilename}: {str(e)}')
# Add embedded images to an email message
def _addEmbeddedImagesToMessage(message, embeddedImages):
for embeddedImage in embeddedImages:
try:
imageFilename = embeddedImage[0]
imageContentType, imageEncoding = mimetypes.guess_type(imageFilename)
if imageContentType is None or imageEncoding is not None:
imageContentType = 'application/octet-stream'
main_type, sub_type = imageContentType.split('/', 1)
if main_type == 'image':
msg = MIMEImage(readFile(imageFilename, 'rb'), _subtype=sub_type)
else:
msg = MIMEBase(main_type, sub_type)
msg.set_payload(readFile(imageFilename, 'rb'))
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(imageFilename))
msg.add_header('Content-ID', f'<{embeddedImage[1]}>')
message.attach(msg)
except (IOError, UnicodeDecodeError) as e:
usageErrorExit(f'{imageFilename}: {str(e)}')
NAME_EMAIL_ADDRESS_PATTERN = re.compile(r'^.*<(.+)>$')
# Send an email
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
html=False, charset=UTF8, attachments=None, embeddedImages=None,
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None):
def checkResult(entityType, recipients):
if not recipients:
return
toSent = set(recipients.split(','))
toFailed = {}
for addr, err in iter(result.items()):
if addr in toSent:
toSent.remove(addr)
toFailed[addr] = f'{err[0]}: {err[1]}'
if toSent:
entityActionPerformed([entityType, ','.join(toSent), Ent.MESSAGE, msgSubject], i, count)
for addr, errMsg in iter(toFailed.items()):
entityActionFailedWarning([entityType, addr, Ent.MESSAGE, msgSubject], errMsg, i, count)
def cleanAddr(emailAddr):
match = NAME_EMAIL_ADDRESS_PATTERN.match(emailAddr)
if match:
return match.group(1)
return emailAddr
if msgFrom is None:
msgFrom = _getAdminEmail()
# Force ASCII for RFC compliance
# xmlcharref seems to work to display at least
# some unicode in HTML body and is ignored in
# plain text body.
# msgBody = msgBody.encode('ascii', 'xmlcharrefreplace').decode(UTF8)
if not attachments and not embeddedImages:
message = MIMEText(msgBody, ['plain', 'html'][html], charset)
else:
message = MIMEMultipart()
msg = MIMEText(msgBody, ['plain', 'html'][html], charset)
message.attach(msg)
if attachments:
_addAttachmentsToMessage(message, attachments)
if embeddedImages:
_addEmbeddedImagesToMessage(message, embeddedImages)
message['Subject'] = msgSubject
message['From'] = msgFrom
if msgReplyTo is not None:
message['Reply-To'] = msgReplyTo
if ccRecipients:
message['Cc'] = ccRecipients.lower()
if bccRecipients:
message['Bcc'] = bccRecipients.lower()
if msgHeaders:
for header, value in iter(msgHeaders.items()):
if header not in {'Subject', 'From', 'To', 'Reply-To', 'Cc', 'Bcc'}:
message[header] = value
if mailBox is None:
mailBox = msgFrom
mailBoxAddr = normalizeEmailAddressOrUID(cleanAddr(mailBox), noUid=True, noLower=True)
action = Act.Get()
Act.Set(Act.SENDEMAIL)
if not GC.Values[GC.SMTP_HOST]:
if not clientAccess:
userId, gmail = buildGAPIServiceObject(API.GMAIL, mailBoxAddr)
if not gmail:
return
else:
userId = mailBoxAddr
gmail = buildGAPIObject(API.GMAIL)
message['To'] = msgTo if msgTo else userId
try:
result = callGAPI(gmail.users().messages(), 'send',
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
userId=userId, body={'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}, fields='id')
entityActionPerformedMessage([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], f"{result['id']}", i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
else:
message['To'] = msgTo if msgTo else mailBoxAddr
server = None
try:
server = smtplib.SMTP(GC.Values[GC.SMTP_HOST], 587, GC.Values[GC.SMTP_FQDN])
if GC.Values[GC.DEBUG_LEVEL] > 0:
server.set_debuglevel(1)
server.starttls(context=ssl.create_default_context(cafile=GC.Values[GC.CACERTS_PEM]))
if GC.Values[GC.SMTP_USERNAME] and GC.Values[GC.SMTP_PASSWORD]:
if isinstance(GC.Values[GC.SMTP_PASSWORD], bytes):
server.login(GC.Values[GC.SMTP_USERNAME], base64.b64decode(GC.Values[GC.SMTP_PASSWORD]).decode(UTF8))
else:
server.login(GC.Values[GC.SMTP_USERNAME], GC.Values[GC.SMTP_PASSWORD])
result = server.send_message(message)
checkResult(Ent.RECIPIENT, message['To'])
checkResult(Ent.RECIPIENT_CC, ccRecipients)
checkResult(Ent.RECIPIENT_BCC, bccRecipients)
except smtplib.SMTPException as e:
entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
if server:
try:
server.quit()
except Exception:
pass
Act.Set(action)
def addFieldToFieldsList(fieldName, fieldsChoiceMap, fieldsList):
fields = fieldsChoiceMap[fieldName.lower()]
if isinstance(fields, list):
fieldsList.extend(fields)
else:
fieldsList.append(fields)
def _getFieldsList():
return getString(Cmd.OB_FIELD_NAME_LIST).lower().replace('_', '').replace(',', ' ').split()
def _getRawFields(requiredField=None):
rawFields = getString(Cmd.OB_FIELDS)
if requiredField is None or requiredField in rawFields:
return rawFields
return f'{requiredField},{rawFields}'
def CheckInputRowFilterHeaders(titlesList, rowFilter, rowDropFilter):
status = True
for filterVal in rowFilter:
columns = [t for t in titlesList if filterVal[0].match(t)]
if not columns:
stderrErrorMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_INPUT_COLUMNS.format(GC.CSV_INPUT_ROW_FILTER, filterVal[0].pattern))
status = False
for filterVal in rowDropFilter:
columns = [t for t in titlesList if filterVal[0].match(t)]
if not columns:
stderrErrorMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_INPUT_COLUMNS.format(GC.CSV_INPUT_ROW_DROP_FILTER, filterVal[0].pattern))
status = False
if not status:
sys.exit(USAGE_ERROR_RC)
def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter, rowDropFilterModeAll):
def rowRegexFilterMatch(filterPattern):
if anyMatch:
for column in columns:
if filterPattern.search(str(row.get(column, ''))):
return True
return False
for column in columns:
if not filterPattern.search(str(row.get(column, ''))):
return False
return True
def rowNotRegexFilterMatch(filterPattern):
if anyMatch:
for column in columns:
if filterPattern.search(str(row.get(column, ''))):
return False
return True
for column in columns:
if not filterPattern.search(str(row.get(column, ''))):
return True
return False
def stripTimeFromDateTime(rowDate):
if YYYYMMDD_PATTERN.match(rowDate):
try:
rowTime = datetime.datetime.strptime(rowDate, YYYYMMDD_FORMAT)
except ValueError:
return None
else:
try:
rowTime, _ = iso8601.parse_date(rowDate)
except (iso8601.ParseError, OverflowError):
return None
return ISOformatTimeStamp(datetime.datetime(rowTime.year, rowTime.month, rowTime.day, tzinfo=iso8601.UTC))
def rowDateTimeFilterMatch(dateMode, op, filterDate):
def checkMatch(rowDate):
if not rowDate or not isinstance(rowDate, str):
return False
if rowDate == GC.Values[GC.NEVER_TIME]:
rowDate = NEVER_TIME
if dateMode:
rowDate = stripTimeFromDateTime(rowDate)
if not rowDate:
return False
if op == '<':
return rowDate < filterDate
if op == '<=':
return rowDate <= filterDate
if op == '>':
return rowDate > filterDate
if op == '>=':
return rowDate >= filterDate
if op == '!=':
return rowDate != filterDate
return rowDate == filterDate
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def rowDateTimeRangeFilterMatch(dateMode, op, filterDateL, filterDateR):
def checkMatch(rowDate):
if not rowDate or not isinstance(rowDate, str):
return False
if rowDate == GC.Values[GC.NEVER_TIME]:
rowDate = NEVER_TIME
if dateMode:
rowDate = stripTimeFromDateTime(rowDate)
if not rowDate:
return False
if op == '!=':
return not filterDateL <= rowDate <= filterDateR
return filterDateL <= rowDate <= filterDateR
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def getHourMinuteFromDateTime(rowDate):
if YYYYMMDD_PATTERN.match(rowDate):
return None
try:
rowTime, _ = iso8601.parse_date(rowDate)
except (iso8601.ParseError, OverflowError):
return None
return f'{rowTime.hour:02d}:{rowTime.minute:02d}'
def rowTimeOfDayRangeFilterMatch(op, startHourMinute, endHourMinute):
def checkMatch(rowDate):
if not rowDate or not isinstance(rowDate, str) or rowDate == GC.Values[GC.NEVER_TIME]:
return False
rowHourMinute = getHourMinuteFromDateTime(rowDate)
if not rowHourMinute:
return False
if op == '!=':
return not startHourMinute <= rowHourMinute <= endHourMinute
return startHourMinute <= rowHourMinute <= endHourMinute
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def rowCountFilterMatch(op, filterCount):
def checkMatch(rowCount):
if isinstance(rowCount, str):
##### Blank = 0
if not rowCount:
rowCount = '0'
elif not rowCount.isdigit():
return False
rowCount = int(rowCount)
elif not isinstance(rowCount, int):
return False
if op == '<':
return rowCount < filterCount
if op == '<=':
return rowCount <= filterCount
if op == '>':
return rowCount > filterCount
if op == '>=':
return rowCount >= filterCount
if op == '!=':
return rowCount != filterCount
return rowCount == filterCount
if anyMatch:
for column in columns:
if checkMatch(row.get(column, 0)):
return True
return False
for column in columns:
if not checkMatch(row.get(column, 0)):
return False
return True
def rowCountRangeFilterMatch(op, filterCountL, filterCountR):
def checkMatch(rowCount):
if isinstance(rowCount, str):
if not rowCount.isdigit():
return False
rowCount = int(rowCount)
elif not isinstance(rowCount, int):
return False
if op == '!=':
return not filterCountL <= rowCount <= filterCountR
return filterCountL <= rowCount <= filterCountR
if anyMatch:
for column in columns:
if checkMatch(row.get(column, 0)):
return True
return False
for column in columns:
if not checkMatch(row.get(column, 0)):
return False
return True
def rowLengthFilterMatch(op, filterLength):
def checkMatch(rowString):
if not isinstance(rowString, str):
return False
rowLength = len(rowString)
if op == '<':
return rowLength < filterLength
if op == '<=':
return rowLength <= filterLength
if op == '>':
return rowLength > filterLength
if op == '>=':
return rowLength >= filterLength
if op == '!=':
return rowLength != filterLength
return rowLength == filterLength
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def rowLengthRangeFilterMatch(op, filterLengthL, filterLengthR):
def checkMatch(rowString):
if not isinstance(rowString, str):
return False
rowLength = len(rowString)
if op == '!=':
return not filterLengthL <= rowLength <= filterLengthR
return filterLengthL <= rowLength <= filterLengthR
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def rowBooleanFilterMatch(filterBoolean):
def checkMatch(rowBoolean):
if isinstance(rowBoolean, bool):
return rowBoolean == filterBoolean
if isinstance(rowBoolean, str):
if rowBoolean.lower() in TRUE_FALSE:
return rowBoolean.capitalize() == str(filterBoolean)
##### Blank = False
if not rowBoolean:
return not filterBoolean
return False
if anyMatch:
for column in columns:
if checkMatch(row.get(column, False)):
return True
return False
for column in columns:
if not checkMatch(row.get(column, False)):
return False
return True
def rowDataFilterMatch(filterData):
if anyMatch:
for column in columns:
if str(row.get(column, '')) in filterData:
return True
return False
for column in columns:
if not str(row.get(column, '')) in filterData:
return False
return True
def rowNotDataFilterMatch(filterData):
if anyMatch:
for column in columns:
if str(row.get(column, '')) in filterData:
return False
return True
for column in columns:
if not str(row.get(column, '')) in filterData:
return True
return False
def rowTextFilterMatch(op, filterText):
def checkMatch(rowText):
if not isinstance(rowText, str):
rowText = str(rowText)
if op == '<':
return rowText < filterText
if op == '<=':
return rowText <= filterText
if op == '>':
return rowText > filterText
if op == '>=':
return rowText >= filterText
if op == '!=':
return rowText != filterText
return rowText == filterText
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def rowTextRangeFilterMatch(op, filterTextL, filterTextR):
def checkMatch(rowText):
if not isinstance(rowText, str):
rowText = str(rowText)
if op == '!=':
return not filterTextL <= rowText <= filterTextR
return filterTextL <= rowText <= filterTextR
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def filterMatch(filterVal):
if filterVal[2] == 'regex':
if rowRegexFilterMatch(filterVal[3]):
return True
elif filterVal[2] == 'notregex':
if rowNotRegexFilterMatch(filterVal[3]):
return True
elif filterVal[2] in {'date', 'time'}:
if rowDateTimeFilterMatch(filterVal[2] == 'date', filterVal[3], filterVal[4]):
return True
elif filterVal[2] in {'daterange', 'timerange'}:
if rowDateTimeRangeFilterMatch(filterVal[2] == 'date', filterVal[3], filterVal[4], filterVal[5]):
return True
elif filterVal[2] == 'timeofdayrange':
if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True
elif filterVal[2] == 'count':
if rowCountFilterMatch(filterVal[3], filterVal[4]):
return True
elif filterVal[2] == 'countrange':
if rowCountRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True
elif filterVal[2] == 'length':
if rowLengthFilterMatch(filterVal[3], filterVal[4]):
return True
elif filterVal[2] == 'lengthrange':
if rowLengthRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True
elif filterVal[2] == 'boolean':
if rowBooleanFilterMatch(filterVal[3]):
return True
elif filterVal[2] == 'data':
if rowDataFilterMatch(filterVal[3]):
return True
elif filterVal[2] == 'notdata':
if rowNotDataFilterMatch(filterVal[3]):
return True
elif filterVal[2] == 'text':
if rowTextFilterMatch(filterVal[3], filterVal[4]):
return True
elif filterVal[2] == 'textrange':
if rowTextRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True
return False
if rowFilter:
anyMatches = False
for filterVal in rowFilter:
columns = [t for t in titlesList if filterVal[0].match(t)]
if not columns:
columns = [None]
anyMatch = filterVal[1]
if filterMatch(filterVal):
if not rowFilterModeAll: # Any - any match selects
anyMatches = True
break
else:
if rowFilterModeAll: # All - any match failure doesn't select
return False
if not rowFilterModeAll and not anyMatches: # Any - no matches doesn't select
return False
if rowDropFilter:
allMatches = True
for filterVal in rowDropFilter:
columns = [t for t in titlesList if filterVal[0].match(t)]
if not columns:
columns = [None]
anyMatch = filterVal[1]
if filterMatch(filterVal):
if not rowDropFilterModeAll: # Any - any match drops
return False
else:
if rowDropFilterModeAll: # All - any match failure doesn't drop
allMatches = False
break
if rowDropFilterModeAll and allMatches: # All - all matches drops
return False
return True
# myarg is command line argument
# fieldChoiceMap maps myarg to API field names
#FIELD_CHOICE_MAP = {
# 'foo': 'foo',
# 'foobar': 'fooBar',
# }
# fieldsList is the list of API fields
def getFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField=None, fieldsArg='fields', onlyFieldsArg=False):
def addInitialField():
if isinstance(initialField, list):
fieldsList.extend(initialField)
else:
fieldsList.append(initialField)
def addMappedFields(mappedFields):
if isinstance(mappedFields, list):
fieldsList.extend(mappedFields)
else:
fieldsList.append(mappedFields)
if not onlyFieldsArg and myarg in fieldsChoiceMap:
if not fieldsList and initialField is not None:
addInitialField()
addMappedFields(fieldsChoiceMap[myarg])
elif myarg == fieldsArg:
if not fieldsList and initialField is not None:
addInitialField()
for field in _getFieldsList():
if field in fieldsChoiceMap:
addMappedFields(fieldsChoiceMap[field])
else:
invalidChoiceExit(field, fieldsChoiceMap, True)
else:
return False
return True
def getFieldsFromFieldsList(fieldsList):
if fieldsList:
return ','.join(set(fieldsList)).replace('.', '/')
return None
def getItemFieldsFromFieldsList(item, fieldsList, returnItemIfNoneList=False):
if fieldsList:
return f'nextPageToken,{item}({",".join(set(fieldsList))})'.replace('.', '/')
if not returnItemIfNoneList:
return None
return f'nextPageToken,{item}'
class CSVPrintFile():
def __init__(self, titles=None, sortTitles=None, indexedTitles=None):
self.rows = []
self.rowCount = 0
self.outputTranspose = GM.Globals[GM.CSV_OUTPUT_TRANSPOSE]
self.todrive = GM.Globals[GM.CSV_TODRIVE]
self.titlesSet = set()
self.titlesList = []
self.JSONtitlesSet = set()
self.JSONtitlesList = []
self.sortHeaders = []
self.SetHeaderForce(GC.Values[GC.CSV_OUTPUT_HEADER_FORCE])
if not self.headerForce and titles is not None:
self.SetTitles(titles)
self.SetJSONTitles(titles)
self.SetHeaderOrder(GC.Values[GC.CSV_OUTPUT_HEADER_ORDER])
if GM.Globals.get(GM.CSV_OUTPUT_COLUMN_DELIMITER) is None:
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = GC.Values.get(GC.CSV_OUTPUT_COLUMN_DELIMITER, ',')
self.SetColumnDelimiter(GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER])
if GM.Globals.get(GM.CSV_OUTPUT_QUOTE_CHAR) is None:
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = GC.Values.get(GC.CSV_OUTPUT_QUOTE_CHAR, '"')
if GM.Globals.get(GM.CSV_OUTPUT_NO_ESCAPE_CHAR) is None:
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = GC.Values.get(GC.CSV_OUTPUT_NO_ESCAPE_CHAR, False)
self.SetNoEscapeChar(GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR])
self.SetQuoteChar(GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR])
# if GM.Globals.get(GM.CSV_OUTPUT_SORT_HEADERS) is None:
if not GM.Globals.get(GM.CSV_OUTPUT_SORT_HEADERS):
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = GC.Values.get(GC.CSV_OUTPUT_SORT_HEADERS, [])
self.SetSortHeaders(GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS])
# if GM.Globals.get(GM.CSV_OUTPUT_TIMESTAMP_COLUMN) is None:
if not GM.Globals.get(GM.CSV_OUTPUT_TIMESTAMP_COLUMN):
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = GC.Values.get(GC.CSV_OUTPUT_TIMESTAMP_COLUMN, '')
self.SetTimestampColumn(GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN])
self.SetFormatJSON(False)
self.SetMapDrive3Titles(False)
self.SetNodataFields(False, None, None, None, False)
self.SetFixPaths(False)
self.SetShowPermissionsLast(False)
self.sortTitlesSet = set()
self.sortTitlesList = []
if sortTitles is not None:
if not isinstance(sortTitles, str) or sortTitles != 'sortall':
self.SetSortTitles(sortTitles)
else:
self.SetSortAllTitles()
self.SetIndexedTitles(indexedTitles if indexedTitles is not None else [])
self.SetHeaderFilter(GC.Values[GC.CSV_OUTPUT_HEADER_FILTER])
self.SetHeaderDropFilter(GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER])
self.SetRowFilter(GC.Values[GC.CSV_OUTPUT_ROW_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
self.SetRowDropFilter(GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE])
self.SetRowLimit(GC.Values[GC.CSV_OUTPUT_ROW_LIMIT])
self.SetZeroBlankMimeTypeCounts(False)
def AddTitle(self, title):
self.titlesSet.add(title)
self.titlesList.append(title)
def AddTitles(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title not in self.titlesSet:
self.AddTitle(title)
def SetTitles(self, titles):
self.titlesSet = set()
self.titlesList = []
self.AddTitles(titles)
def RemoveTitles(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title in self.titlesSet:
self.titlesSet.remove(title)
self.titlesList.remove(title)
def MoveTitlesToEnd(self, titles):
self.RemoveTitles(titles)
self.AddTitles(titles)
def MapTitles(self, ov, nv):
if ov in self.titlesSet:
self.titlesSet.remove(ov)
self.titlesSet.add(nv)
for i, v in enumerate(self.titlesList):
if v == ov:
self.titlesList[i] = nv
break
def AddSortTitle(self, title):
self.sortTitlesSet.add(title)
self.sortTitlesList.append(title)
def AddSortTitles(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title not in self.sortTitlesSet:
self.AddSortTitle(title)
def SetSortTitles(self, titles):
self.sortTitlesSet = set()
self.sortTitlesList = []
self.AddSortTitles(titles)
def SetSortAllTitles(self):
self.sortTitlesList = self.titlesList[:]
self.sortTitlesSet = set(self.sortTitlesList)
def SetMapDrive3Titles(self, mapDrive3Titles):
self.mapDrive3Titles = mapDrive3Titles
def MapDrive3TitlesToDrive2(self):
_mapDrive3TitlesToDrive2(self.titlesList, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
self.titlesSet = set(self.titlesList)
def AddJSONTitle(self, title):
self.JSONtitlesSet.add(title)
self.JSONtitlesList.append(title)
def AddJSONTitles(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title not in self.JSONtitlesSet:
self.AddJSONTitle(title)
def RemoveJSONTitles(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title in self.JSONtitlesSet:
self.JSONtitlesSet.remove(title)
self.JSONtitlesList.remove(title)
def MoveJSONTitlesToEnd(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title in self.JSONtitlesList:
self.JSONtitlesList.remove(title)
self.JSONtitlesList.append(title)
def SetJSONTitles(self, titles):
self.JSONtitlesSet = set()
self.JSONtitlesList = []
self.AddJSONTitles(titles)
# fieldName is command line argument
# fieldNameMap maps fieldName to API field names; CSV file header will be API field name
#ARGUMENT_TO_PROPERTY_MAP = {
# 'admincreated': 'adminCreated',
# 'aliases': ['aliases', 'nonEditableAliases'],
# }
# fieldsList is the list of API fields
def AddField(self, fieldName, fieldNameMap, fieldsList):
fields = fieldNameMap[fieldName.lower()]
if isinstance(fields, list):
for field in fields:
if field not in fieldsList:
fieldsList.append(field)
self.AddTitles(field.replace('.', GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]))
elif fields not in fieldsList:
fieldsList.append(fields)
self.AddTitles(fields.replace('.', GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]))
def addInitialField(self, initialField, fieldsChoiceMap, fieldsList):
if isinstance(initialField, list):
for field in initialField:
self.AddField(field, fieldsChoiceMap, fieldsList)
else:
self.AddField(initialField, fieldsChoiceMap, fieldsList)
def GetFieldsListTitles(self, fieldName, fieldsChoiceMap, fieldsList, initialField=None):
if fieldName in fieldsChoiceMap:
if not fieldsList and initialField is not None:
self.addInitialField(initialField, fieldsChoiceMap, fieldsList)
self.AddField(fieldName, fieldsChoiceMap, fieldsList)
elif fieldName == 'fields':
if not fieldsList and initialField is not None:
self.addInitialField(initialField, fieldsChoiceMap, fieldsList)
for field in _getFieldsList():
if field in fieldsChoiceMap:
self.AddField(field, fieldsChoiceMap, fieldsList)
else:
invalidChoiceExit(field, fieldsChoiceMap, True)
else:
return False
return True
TDSHEET_ENTITY_MAP = {'tdsheet': 'sheetEntity', 'tdbackupsheet': 'backupSheetEntity', 'tdcopysheet': 'copySheetEntity'}
TDSHARE_ACL_ROLES_MAP = {
'commenter': 'commenter',
'contributor': 'writer',
'editor': 'writer',
'read': 'reader',
'reader': 'reader',
'viewer': 'reader',
'writer': 'writer',
}
def GetTodriveParameters(self):
def invalidTodriveFileIdExit(entityValueList, message, location):
Cmd.SetLocation(location-1)
usageErrorExit(formatKeyValueList('', Ent.FormatEntityValueList([Ent.DRIVE_FILE_ID, self.todrive['fileId']]+entityValueList)+[message], ''))
def invalidTodriveParentExit(entityType, message):
Cmd.SetLocation(tdparentLocation-1)
if not localParent:
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType),
formatKeyValueList('',
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.ITEM), GC.TODRIVE_PARENT,
Ent.Singular(Ent.VALUE), self.todrive['parent'],
message],
'')))
else:
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType), message))
def invalidTodriveUserExit(entityType, message):
Cmd.SetLocation(tduserLocation-1)
if not localUser:
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType),
formatKeyValueList('',
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
Ent.Singular(Ent.ITEM), GC.TODRIVE_USER,
Ent.Singular(Ent.VALUE), self.todrive['user'],
message],
'')))
else:
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType), message))
def getDriveObject():
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
_, drive = buildGAPIServiceObject(API.DRIVETD, self.todrive['user'])
if not drive:
invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
else:
drive = buildGAPIObject(API.DRIVE3)
return drive
CELL_WRAP_MAP = {'clip': 'CLIP', 'overflow': 'OVERFLOW_CELL', 'overflowcell': 'OVERFLOW_CELL', 'wrap': 'WRAP'}
CELL_NUMBER_FORMAT_MAP = {'text': 'TEXT', 'number': 'NUMBER'}
localUser = localParent = False
tdfileidLocation = tdparentLocation = tdaddsheetLocation = tdupdatesheetLocation = tduserLocation = Cmd.Location()
tdsheetLocation = {}
for sheetEntity in iter(self.TDSHEET_ENTITY_MAP.values()):
tdsheetLocation[sheetEntity] = Cmd.Location()
self.todrive = {'user': GC.Values[GC.TODRIVE_USER], 'title': None, 'description': None,
'sheetEntity': None, 'addsheet': False, 'updatesheet': False, 'sheettitle': None,
'cellwrap': None, 'cellnumberformat': None, 'clearfilter': GC.Values[GC.TODRIVE_CLEARFILTER],
'backupSheetEntity': None, 'copySheetEntity': None,
'locale': GC.Values[GC.TODRIVE_LOCALE], 'timeZone': GC.Values[GC.TODRIVE_TIMEZONE],
'timestamp': GC.Values[GC.TODRIVE_TIMESTAMP], 'timeformat': GC.Values[GC.TODRIVE_TIMEFORMAT],
'noescapechar': GC.Values[GC.TODRIVE_NO_ESCAPE_CHAR],
'daysoffset': None, 'hoursoffset': None,
'sheettimestamp': GC.Values[GC.TODRIVE_SHEET_TIMESTAMP], 'sheettimeformat': GC.Values[GC.TODRIVE_SHEET_TIMEFORMAT],
'sheetdaysoffset': None, 'sheethoursoffset': None,
'fileId': None, 'parentId': None, 'parent': GC.Values[GC.TODRIVE_PARENT], 'retaintitle': False,
'localcopy': GC.Values[GC.TODRIVE_LOCALCOPY], 'uploadnodata': GC.Values[GC.TODRIVE_UPLOAD_NODATA],
'nobrowser': GC.Values[GC.TODRIVE_NOBROWSER], 'noemail': GC.Values[GC.TODRIVE_NOEMAIL], 'returnidonly': False,
'alert': [], 'share': [], 'notify': False, 'subject': None, 'from': None}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'tduser':
self.todrive['user'] = getString(Cmd.OB_EMAIL_ADDRESS)
tduserLocation = Cmd.Location()
localUser = True
elif myarg == 'tdtitle':
self.todrive['title'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'tddescription':
self.todrive['description'] = getString(Cmd.OB_STRING)
elif myarg in self.TDSHEET_ENTITY_MAP:
sheetEntity = self.TDSHEET_ENTITY_MAP[myarg]
tdsheetLocation[sheetEntity] = Cmd.Location()
self.todrive[sheetEntity] = getSheetEntity(True)
elif myarg == 'tdaddsheet':
tdaddsheetLocation = Cmd.Location()
self.todrive['addsheet'] = getBoolean()
if self.todrive['addsheet']:
self.todrive['updatesheet'] = False
elif myarg == 'tdupdatesheet':
tdupdatesheetLocation = Cmd.Location()
self.todrive['updatesheet'] = getBoolean()
if self.todrive['updatesheet']:
self.todrive['addsheet'] = False
elif myarg == 'tdcellwrap':
self.todrive['cellwrap'] = getChoice(CELL_WRAP_MAP, mapChoice=True)
elif myarg == 'tdcellnumberformat':
self.todrive['cellnumberformat'] = getChoice(CELL_NUMBER_FORMAT_MAP, mapChoice=True)
elif myarg == 'tdclearfilter':
self.todrive['clearfilter'] = getBoolean()
elif myarg == 'tdlocale':
self.todrive['locale'] = getLanguageCode(LOCALE_CODES_MAP)
elif myarg == 'tdtimezone':
self.todrive['timeZone'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'tdtimestamp':
self.todrive['timestamp'] = getBoolean()
elif myarg == 'tdtimeformat':
self.todrive['timeformat'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'tdsheettitle':
self.todrive['sheettitle'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'tdsheettimestamp':
self.todrive['sheettimestamp'] = getBoolean()
elif myarg == 'tdsheettimeformat':
self.todrive['sheettimeformat'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'tddaysoffset':
self.todrive['daysoffset'] = getInteger(minVal=0)
elif myarg == 'tdhoursoffset':
self.todrive['hoursoffset'] = getInteger(minVal=0)
elif myarg == 'tdsheetdaysoffset':
self.todrive['sheetdaysoffset'] = getInteger(minVal=0)
elif myarg == 'tdsheethoursoffset':
self.todrive['sheethoursoffset'] = getInteger(minVal=0)
elif myarg == 'tdfileid':
self.todrive['fileId'] = getString(Cmd.OB_DRIVE_FILE_ID)
tdfileidLocation = Cmd.Location()
elif myarg == 'tdretaintitle':
self.todrive['retaintitle'] = getBoolean()
elif myarg == 'tdparent':
self.todrive['parent'] = escapeDriveFileName(getString(Cmd.OB_DRIVE_FOLDER_NAME, minLen=0))
tdparentLocation = Cmd.Location()
localParent = True
elif myarg == 'tdlocalcopy':
self.todrive['localcopy'] = getBoolean()
elif myarg == 'tduploadnodata':
self.todrive['uploadnodata'] = getBoolean()
elif myarg == 'tdnobrowser':
self.todrive['nobrowser'] = getBoolean()
elif myarg == 'tdnoemail':
self.todrive['noemail'] = getBoolean()
elif myarg == 'tdreturnidonly':
self.todrive['returnidonly'] = getBoolean()
elif myarg == 'tdnoescapechar':
self.todrive['noescapechar'] = getBoolean()
elif myarg == 'tdalert':
self.todrive['alert'].append({'emailAddress': normalizeEmailAddressOrUID(getString(Cmd.OB_EMAIL_ADDRESS))})
elif myarg == 'tdshare':
self.todrive['share'].append({'emailAddress': normalizeEmailAddressOrUID(getString(Cmd.OB_EMAIL_ADDRESS)),
'type': 'user',
'role': getChoice(self.TDSHARE_ACL_ROLES_MAP, mapChoice=True)})
elif myarg == 'tdnotify':
self.todrive['notify'] = getBoolean()
elif myarg == 'tdsubject':
self.todrive['subject'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'tdfrom':
self.todrive['from'] = getString(Cmd.OB_EMAIL_ADDRESS)
else:
Cmd.Backup()
break
if self.todrive['addsheet']:
if not self.todrive['fileId']:
Cmd.SetLocation(tdaddsheetLocation-1)
missingArgumentExit('tdfileid')
if self.todrive['sheetEntity'] and self.todrive['sheetEntity']['sheetId']:
Cmd.SetLocation(tdsheetLocation[sheetEntity]-1)
invalidArgumentExit('tdsheet <String>')
if self.todrive['updatesheet'] and (not self.todrive['fileId'] or not self.todrive['sheetEntity']):
Cmd.SetLocation(tdupdatesheetLocation-1)
missingArgumentExit('tdfileid and tdsheet')
if self.todrive['sheetEntity'] and self.todrive['sheetEntity']['sheetId'] and (not self.todrive['fileId'] or not self.todrive['updatesheet']):
Cmd.SetLocation(tdsheetLocation['sheetEntity']-1)
missingArgumentExit('tdfileid and tdupdatesheet')
if not self.todrive['user'] or GC.Values[GC.TODRIVE_CLIENTACCESS]:
self.todrive['user'] = _getAdminEmail()
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] and not GC.Values[GC.TODRIVE_CLIENTACCESS]:
user = checkUserExists(buildGAPIObject(API.DIRECTORY), self.todrive['user'])
if not user:
invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
self.todrive['user'] = user
else:
self.todrive['user'] = normalizeEmailAddressOrUID(self.todrive['user'])
if self.todrive['fileId']:
drive = getDriveObject()
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=self.todrive['fileId'], fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
if result['mimeType'] == MIMETYPE_GA_FOLDER:
invalidTodriveFileIdExit([], Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FILE)), tdfileidLocation)
if not result['capabilities']['canEdit']:
invalidTodriveFileIdExit([], Msg.NOT_WRITABLE, tdfileidLocation)
if self.todrive['sheetEntity']:
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
invalidTodriveFileIdExit([], f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}', tdfileidLocation)
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
_, sheet = buildGAPIServiceObject(API.SHEETSTD, self.todrive['user'])
if sheet is None:
invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
else:
sheet = buildGAPIObject(API.SHEETS)
try:
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=self.todrive['fileId'],
fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
for sheetEntity in iter(self.TDSHEET_ENTITY_MAP.values()):
if self.todrive[sheetEntity]:
sheetId = getSheetIdFromSheetEntity(spreadsheet, self.todrive[sheetEntity])
if sheetId is None:
if not self.todrive['addsheet'] and ((sheetEntity != 'sheetEntity') or (self.todrive[sheetEntity]['sheetType'] == Ent.SHEET_ID)):
invalidTodriveFileIdExit([self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']], Msg.NOT_FOUND, tdsheetLocation[sheetEntity])
else:
if self.todrive['addsheet']:
invalidTodriveFileIdExit([self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']], Msg.ALREADY_EXISTS, tdsheetLocation[sheetEntity])
if protectedSheetId(spreadsheet, sheetId):
invalidTodriveFileIdExit([self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']], Msg.NOT_WRITABLE, tdsheetLocation[sheetEntity])
self.todrive[sheetEntity]['sheetId'] = sheetId
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
invalidTodriveFileIdExit([], str(e), tdfileidLocation)
except GAPI.fileNotFound:
invalidTodriveFileIdExit([], Msg.NOT_FOUND, tdfileidLocation)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
invalidTodriveUserExit(Ent.USER, str(e))
elif not self.todrive['parent'] or self.todrive['parent'] == ROOT:
self.todrive['parentId'] = ROOT
else:
drive = getDriveObject()
if self.todrive['parent'].startswith('id:'):
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND, GAPI.INVALID],
fileId=self.todrive['parent'][3:], fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
except GAPI.fileNotFound:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, Msg.NOT_FOUND)
except GAPI.invalid as e:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, str(e))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
invalidTodriveUserExit(Ent.USER, str(e))
if result['mimeType'] != MIMETYPE_GA_FOLDER:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER)))
if not result['capabilities']['canEdit']:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, Msg.NOT_WRITABLE)
self.todrive['parentId'] = result['id']
else:
try:
results = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=f"name = '{self.todrive['parent']}'",
fields='nextPageToken,files(id,mimeType,capabilities(canEdit))',
pageSize=1, supportsAllDrives=True)
except GAPI.invalidQuery:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_FOUND)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
invalidTodriveUserExit(Ent.USER, str(e))
if not results:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_FOUND)
if results[0]['mimeType'] != MIMETYPE_GA_FOLDER:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER)))
if not results[0]['capabilities']['canEdit']:
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_WRITABLE)
self.todrive['parentId'] = results[0]['id']
def SortTitles(self):
if not self.sortTitlesList:
return
restoreTitles = []
for title in self.sortTitlesList:
if title in self.titlesSet:
self.titlesList.remove(title)
restoreTitles.append(title)
self.titlesList.sort()
for title in restoreTitles[::-1]:
self.titlesList.insert(0, title)
def RemoveIndexedTitles(self, titles):
for title in titles if isinstance(titles, list) else [titles]:
if title in self.indexedTitles:
self.indexedTitles.remove(title)
def SetIndexedTitles(self, indexedTitles):
self.indexedTitles = indexedTitles
def SortIndexedTitles(self, titlesList):
for field in self.indexedTitles:
fieldDotN = re.compile(fr'({field}){GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}(\d+)(.*)')
indexes = []
subtitles = []
for i, v in enumerate(titlesList):
mg = fieldDotN.match(v)
if mg:
indexes.append(i)
subtitles.append(mg.groups(''))
for i, ii in enumerate(indexes):
titlesList[ii] = [f'{subtitle[0]}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subtitle[1]}{subtitle[2]}' for subtitle in sorted(subtitles, key=lambda k: (int(k[1]), k[2]))][i]
@staticmethod
def FixPathsTitles(titlesList):
# Put paths before path.0
try:
index = titlesList.index(f'path{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0')
titlesList.remove('paths')
titlesList.insert(index, 'paths')
except ValueError:
pass
def FixNodataTitles(self):
if self.mapNodataFields:
titles = []
addPermissionsTitle = not self.oneItemPerRow
for field in self.nodataFields:
if field.find('(') != -1:
field, subFields = field.split('(', 1)
if field in self.driveListFields:
if field != 'permissions':
titles.append(field)
elif addPermissionsTitle:
titles.append(field)
addPermissionsTitle = False
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}' for subField in subFields[:-1].split(',') if subField])
else:
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}' for subField in subFields[:-1].split(',') if subField])
elif field.find('.') != -1:
field, subField = field.split('.', 1)
if field in self.driveListFields:
if field != 'permissions':
titles.append(field)
elif addPermissionsTitle:
titles.append(field)
addPermissionsTitle = False
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
else:
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
elif field.lower() in self.driveSubfieldsChoiceMap:
if field in self.driveListFields:
if field != 'permissions':
titles.append(field)
elif addPermissionsTitle:
titles.append(field)
addPermissionsTitle = False
for subField in iter(self.driveSubfieldsChoiceMap[field.lower()].values()):
if not isinstance(subField, list):
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
else:
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subSubField}' for subSubField in subField])
else:
for subField in iter(self.driveSubfieldsChoiceMap[field.lower()].values()):
if not isinstance(subField, list):
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
else:
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subSubField}' for subSubField in subField])
else:
titles.append(field)
if self.oneItemPerRow:
for i, title in enumerate(titles):
if title.startswith('permissions.0'):
titles[i] = title.replace('permissions.0', 'permission')
if not self.formatJSON:
self.SetTitles(titles)
self.SetSortTitles(['Owner', 'id', 'name', 'title'])
self.SortTitles()
else:
self.SetJSONTitles(titles)
else:
self.SetTitles(self.nodataFields)
self.SetJSONTitles(self.nodataFields)
def MovePermsToEnd(self):
# Put permissions at end of titles
try:
last = len(self.titlesList)
start = end = self.titlesList.index('permissions')
while end < last and self.titlesList[end].startswith('permissions'):
end += 1
self.titlesList = self.titlesList[:start]+self.titlesList[end:]+self.titlesList[start:end]
except ValueError:
pass
def SetColumnDelimiter(self, columnDelimiter):
self.columnDelimiter = columnDelimiter
def SetNoEscapeChar(self, noEscapeChar):
self.noEscapeChar = noEscapeChar
def SetQuoteChar(self, quoteChar):
self.quoteChar = quoteChar
def SetTimestampColumn(self, timestampColumn):
self.timestampColumn = timestampColumn
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
self.todaysTime = ISOformatTimeStamp(todaysTime())
else:
self.todaysTime = todaysTime().strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
def SetSortHeaders(self, sortHeaders):
self.sortHeaders = sortHeaders
def SetFormatJSON(self, formatJSON):
self.formatJSON = formatJSON
def SetNodataFields(self, mapNodataFields, nodataFields, driveListFields, driveSubfieldsChoiceMap, oneItemPerRow):
self.mapNodataFields = mapNodataFields
self.nodataFields = nodataFields
self.driveListFields = driveListFields
self.driveSubfieldsChoiceMap = driveSubfieldsChoiceMap
self.oneItemPerRow = oneItemPerRow
def SetFixPaths(self, fixPaths):
self.fixPaths = fixPaths
def SetShowPermissionsLast(self, showPermissionsLast):
self.showPermissionsLast = showPermissionsLast
def FixCourseAliasesTitles(self):
# Put Aliases.* after Aliases
try:
aliasesIndex = self.sortTitlesList.index('Aliases')
index = self.titlesList.index('Aliases.0')
tempSortTitlesList = self.sortTitlesList[:]
self.SetSortTitles(tempSortTitlesList[:aliasesIndex+1])
while self.titlesList[index].startswith('Aliases.'):
self.AddSortTitle(self.titlesList[index])
index += 1
self.AddSortTitles(tempSortTitlesList[aliasesIndex+1:])
except ValueError:
pass
def RearrangeCourseTitles(self, ttitles, stitles):
# Put teachers and students after courseMaterialSets if present, otherwise at end
for title in ttitles['list']:
if title in self.titlesList:
self.titlesList.remove(title)
for title in stitles['list']:
if title in self.titlesList:
self.titlesList.remove(title)
try:
cmsIndex = self.titlesList.index('courseMaterialSets')
self.titlesList = self.titlesList[:cmsIndex]+ttitles['list']+stitles['list']+self.titlesList[cmsIndex:]
except ValueError:
self.titlesList.extend(ttitles['list'])
self.titlesList.extend(stitles['list'])
def SortRows(self, title, reverse):
if title in self.titlesSet:
self.rows.sort(key=lambda k: k[title], reverse=reverse)
def SortRowsTwoTitles(self, title1, title2, reverse):
if title1 in self.titlesSet and title2 in self.titlesSet:
self.rows.sort(key=lambda k: (k[title1], k[title2]), reverse=reverse)
def SetRowFilter(self, rowFilter, rowFilterMode):
self.rowFilter = rowFilter
self.rowFilterMode = rowFilterMode
def SetRowDropFilter(self, rowDropFilter, rowDropFilterMode):
self.rowDropFilter = rowDropFilter
self.rowDropFilterMode = rowDropFilterMode
def SetRowLimit(self, rowLimit):
self.rowLimit = rowLimit
def AppendRow(self, row):
if self.timestampColumn:
row[self.timestampColumn] = self.todaysTime
if not self.rowLimit or self.rowCount < self.rowLimit:
self.rowCount +=1
self.rows.append(row)
def WriteRowNoFilter(self, row):
self.AppendRow(row)
def WriteRow(self, row):
if RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode):
self.AppendRow(row)
def WriteRowTitles(self, row):
for title in row:
if title not in self.titlesSet:
self.AddTitle(title)
if RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode):
self.AppendRow(row)
def WriteRowTitlesNoFilter(self, row):
for title in row:
if title not in self.titlesSet:
self.AddTitle(title)
self.AppendRow(row)
def WriteRowTitlesJSONNoFilter(self, row):
for title in row:
if title not in self.JSONtitlesSet:
self.AddJSONTitle(title)
self.AppendRow(row)
def CheckRowTitles(self, row):
if not self.rowFilter and not self.rowDropFilter:
return True
for title in row:
if title not in self.titlesSet:
self.AddTitle(title)
return RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode)
def UpdateMimeTypeCounts(self, row, mimeTypeInfo, sizeField):
saveList = self.titlesList[:]
saveSet = set(self.titlesSet)
for title in row:
if title not in self.titlesSet:
self.AddTitle(title)
if RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode):
mimeTypeInfo.setdefault(row['mimeType'], {'count': 0, 'size': 0})
mimeTypeInfo[row['mimeType']]['count'] += 1
mimeTypeInfo[row['mimeType']]['size'] += int(row.get(sizeField, '0'))
self.titlesList = saveList[:]
self.titlesSet = set(saveSet)
def SetZeroBlankMimeTypeCounts(self, zeroBlankMimeTypeCounts):
self.zeroBlankMimeTypeCounts = zeroBlankMimeTypeCounts
def ZeroBlankMimeTypeCounts(self):
for row in self.rows:
for title in self.titlesList:
if title not in self.sortTitlesSet and title not in row:
row[title] = 0
def CheckOutputRowFilterHeaders(self):
for filterVal in self.rowFilter:
columns = [t for t in self.titlesList if filterVal[0].match(t)]
if not columns:
stderrWarningMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_OUTPUT_COLUMNS.format(GC.CSV_OUTPUT_ROW_FILTER, filterVal[0].pattern))
for filterVal in self.rowDropFilter:
columns = [t for t in self.titlesList if filterVal[0].match(t)]
if not columns:
stderrWarningMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_OUTPUT_COLUMNS.format(GC.CSV_OUTPUT_ROW_DROP_FILTER, filterVal[0].pattern))
def SetHeaderFilter(self, headerFilter):
self.headerFilter = headerFilter
def SetHeaderDropFilter(self, headerDropFilter):
self.headerDropFilter = headerDropFilter
def SetHeaderForce(self, headerForce):
self.headerForce = headerForce
self.SetTitles(headerForce)
self.SetJSONTitles(headerForce)
def SetHeaderOrder(self, headerOrder):
self.headerOrder = headerOrder
def orderHeaders(self, titlesList):
for title in self.headerOrder:
if title in titlesList:
titlesList.remove(title)
return self.headerOrder+titlesList
@staticmethod
def HeaderFilterMatch(filters, title):
for filterStr in filters:
if filterStr.match(title):
return True
return False
def FilterHeaders(self):
if self.headerDropFilter:
self.titlesList = [t for t in self.titlesList if not self.HeaderFilterMatch(self.headerDropFilter, t)]
if self.headerFilter:
self.titlesList = [t for t in self.titlesList if self.HeaderFilterMatch(self.headerFilter, t)]
self.titlesSet = set(self.titlesList)
if not self.titlesSet:
systemErrorExit(USAGE_ERROR_RC, Msg.NO_COLUMNS_SELECTED_WITH_CSV_OUTPUT_HEADER_FILTER.format(GC.CSV_OUTPUT_HEADER_FILTER, GC.CSV_OUTPUT_HEADER_DROP_FILTER))
def FilterJSONHeaders(self):
if self.headerDropFilter:
self.JSONtitlesList = [t for t in self.JSONtitlesList if not self.HeaderFilterMatch(self.headerDropFilter, t)]
if self.headerFilter:
self.JSONtitlesList = [t for t in self.JSONtitlesList if self.HeaderFilterMatch(self.headerFilter, t)]
self.JSONtitlesSet = set(self.JSONtitlesList)
if not self.JSONtitlesSet:
systemErrorExit(USAGE_ERROR_RC, Msg.NO_COLUMNS_SELECTED_WITH_CSV_OUTPUT_HEADER_FILTER.format(GC.CSV_OUTPUT_HEADER_FILTER, GC.CSV_OUTPUT_HEADER_DROP_FILTER))
def writeCSVfile(self, list_type):
def todriveCSVErrorExit(entityValueList, errMsg):
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg],
currentCountNL(0, 0)))
@staticmethod
def itemgetter(*items):
if len(items) == 1:
item = items[0]
def g(obj):
return obj.get(item, '')
else:
def g(obj):
return tuple(obj.get(item, '') for item in items)
return g
def writeCSVData(writer):
try:
if GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER]:
writer.writerow(dict((item, item) for item in writer.fieldnames))
if not self.sortHeaders:
writer.writerows(self.rows)
else:
for row in sorted(self.rows, key=itemgetter(*self.sortHeaders)):
writer.writerow(row)
return True
except IOError as e:
stderrErrorMsg(e)
return False
def setDialect(lineterminator, noEscapeChar):
writerDialect = {
'delimiter': self.columnDelimiter,
'doublequote': True,
'escapechar': '\\' if not noEscapeChar else None,
'lineterminator': lineterminator,
'quotechar': self.quoteChar,
'quoting': csv.QUOTE_MINIMAL,
'skipinitialspace': False,
'strict': False}
return writerDialect
def normalizeSortHeaders():
if self.sortHeaders:
writerKeyMap = {}
for k in titlesList:
writerKeyMap[k.lower()] = k
self.sortHeaders = [writerKeyMap[k.lower()] for k in self.sortHeaders if k.lower() in writerKeyMap]
def writeCSVToStdout():
csvFile = StringIOobject()
writerDialect = setDialect('\n', self.noEscapeChar)
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
if writeCSVData(writer):
try:
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].write(csvFile.getvalue())
except IOError as e:
stderrErrorMsg(fdErrorMessage(GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD], 'stdout', e))
setSysExitRC(FILE_ERROR_RC)
closeFile(csvFile)
def writeCSVToFile():
csvFile = openFile(GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME], GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE], newline='',
encoding=GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING], errors='backslashreplace',
continueOnError=True)
if csvFile:
writerDialect = setDialect(str(GC.Values[GC.CSV_OUTPUT_LINE_TERMINATOR]), self.noEscapeChar)
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
writeCSVData(writer)
closeFile(csvFile)
def writeCSVToDrive():
numRows = len(self.rows)
numColumns = len(titlesList)
if numRows == 0 and not self.todrive['uploadnodata']:
printKeyValueList([Msg.NO_CSV_DATA_TO_UPLOAD])
setSysExitRC(NO_CSV_DATA_TO_UPLOAD_RC)
return
if self.todrive['addsheet'] or self.todrive['updatesheet']:
csvFile = TemporaryFile(mode='w+', encoding=UTF8)
else:
csvFile = StringIOobject()
writerDialect = setDialect('\n', self.todrive['noescapechar'])
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
if writeCSVData(writer):
if ((self.todrive['title'] is None) or
(not self.todrive['title'] and not self.todrive['timestamp'])):
title = f'{GC.Values[GC.DOMAIN]} - {list_type}'
else:
title = self.todrive['title']
if ((self.todrive['sheettitle'] is None) or
(not self.todrive['sheettitle'] and not self.todrive['sheettimestamp'])):
if ((self.todrive['sheetEntity'] is None) or
(not self.todrive['sheetEntity']['sheetTitle'])):
sheetTitle = title
else:
sheetTitle = self.todrive['sheetEntity']['sheetTitle']
else:
sheetTitle = self.todrive['sheettitle']
tdbasetime = tdtime = datetime.datetime.now(GC.Values[GC.TIMEZONE])
if self.todrive['daysoffset'] is not None or self.todrive['hoursoffset'] is not None:
tdtime = tdbasetime+relativedelta(days=-self.todrive['daysoffset'] if self.todrive['daysoffset'] is not None else 0,
hours=-self.todrive['hoursoffset'] if self.todrive['hoursoffset'] is not None else 0)
if self.todrive['timestamp']:
if title:
title += ' - '
if not self.todrive['timeformat']:
title += ISOformatTimeStamp(tdtime)
else:
title += tdtime.strftime(self.todrive['timeformat'])
if self.todrive['sheettimestamp']:
if self.todrive['sheetdaysoffset'] is not None or self.todrive['sheethoursoffset'] is not None:
tdtime = tdbasetime+relativedelta(days=-self.todrive['sheetdaysoffset'] if self.todrive['sheetdaysoffset'] is not None else 0,
hours=-self.todrive['sheethoursoffset'] if self.todrive['sheethoursoffset'] is not None else 0)
if sheetTitle:
sheetTitle += ' - '
if not self.todrive['sheettimeformat']:
sheetTitle += ISOformatTimeStamp(tdtime)
else:
sheetTitle += tdtime.strftime(self.todrive['sheettimeformat'])
action = Act.Get()
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
user, drive = buildGAPIServiceObject(API.DRIVETD, self.todrive['user'])
if not drive:
closeFile(csvFile)
return
else:
user = self.todrive['user']
drive = buildGAPIObject(API.DRIVE3)
importSize = csvFile.tell()
# Add/Update sheet
try:
if self.todrive['addsheet'] or self.todrive['updatesheet']:
Act.Set(Act.CREATE if self.todrive['addsheet'] else Act.UPDATE)
result = callGAPI(drive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='maxImportSizes')
if numRows*numColumns > MAX_GOOGLE_SHEET_CELLS or importSize > int(result['maxImportSizes'][MIMETYPE_GA_SPREADSHEET]):
todriveCSVErrorExit([Ent.USER, user], Msg.RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET)
fields = ','.join(['id', 'mimeType', 'webViewLink', 'name', 'capabilities(canEdit)'])
body = {'description': self.todrive['description']}
if body['description'] is None:
body['description'] = Cmd.QuotedArgumentList(Cmd.AllArguments())
if not self.todrive['retaintitle']:
body['name'] = title
result = callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR],
fileId=self.todrive['fileId'], body=body, fields=fields, supportsAllDrives=True)
entityValueList = [Ent.USER, user, Ent.DRIVE_FILE_ID, self.todrive['fileId']]
if not result['capabilities']['canEdit']:
todriveCSVErrorExit(entityValueList, Msg.NOT_WRITABLE)
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
todriveCSVErrorExit(entityValueList, f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}')
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
_, sheet = buildGAPIServiceObject(API.SHEETSTD, user)
if sheet is None:
return
else:
sheet = buildGAPIObject(API.SHEETS)
csvFile.seek(0)
spreadsheet = None
if self.todrive['updatesheet']:
for sheetEntity in iter(self.TDSHEET_ENTITY_MAP.values()):
if self.todrive[sheetEntity]:
entityValueList = [Ent.USER, user, Ent.SPREADSHEET, title, self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']]
if spreadsheet is None:
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=self.todrive['fileId'],
fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
sheetId = getSheetIdFromSheetEntity(spreadsheet, self.todrive[sheetEntity])
if sheetId is None:
if ((sheetEntity != 'sheetEntity') or (self.todrive[sheetEntity]['sheetType'] == Ent.SHEET_ID)):
todriveCSVErrorExit(entityValueList, Msg.NOT_FOUND)
self.todrive['addsheet'] = True
else:
if protectedSheetId(spreadsheet, sheetId):
todriveCSVErrorExit(entityValueList, Msg.NOT_WRITABLE)
self.todrive[sheetEntity]['sheetId'] = sheetId
if self.todrive['addsheet']:
body = {'requests': [{'addSheet': {'properties': {'title': sheetTitle, 'sheetType': 'GRID'}}}]}
try:
addresult = callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=self.todrive['fileId'], body=body)
self.todrive['sheetEntity'] = {'sheetId': addresult['replies'][0]['addSheet']['properties']['sheetId']}
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
todriveCSVErrorExit(entityValueList, str(e))
body = {'requests': []}
if not self.todrive['addsheet']:
if self.todrive['backupSheetEntity']:
body['requests'].append({'copyPaste': {'source': {'sheetId': self.todrive['sheetEntity']['sheetId']},
'destination': {'sheetId': self.todrive['backupSheetEntity']['sheetId']}, 'pasteType': 'PASTE_NORMAL'}})
if self.todrive['clearfilter']:
body['requests'].append({'clearBasicFilter': {'sheetId': self.todrive['sheetEntity']['sheetId']}})
if self.todrive['sheettitle']:
body['requests'].append({'updateSheetProperties':
{'properties': {'sheetId': self.todrive['sheetEntity']['sheetId'], 'title': sheetTitle}, 'fields': 'title'}})
body['requests'].append({'updateCells': {'range': {'sheetId': self.todrive['sheetEntity']['sheetId']}, 'fields': '*'}})
if self.todrive['cellwrap']:
body['requests'].append({'repeatCell': {'range': {'sheetId': self.todrive['sheetEntity']['sheetId']},
'fields': 'userEnteredFormat.wrapStrategy',
'cell': {'userEnteredFormat': {'wrapStrategy': self.todrive['cellwrap']}}}})
if self.todrive['cellnumberformat']:
body['requests'].append({'repeatCell': {'range': {'sheetId': self.todrive['sheetEntity']['sheetId']},
'fields': 'userEnteredFormat.numberFormat',
'cell': {'userEnteredFormat': {'numberFormat': {'type': self.todrive['cellnumberformat']}}}}})
body['requests'].append({'pasteData': {'coordinate': {'sheetId': self.todrive['sheetEntity']['sheetId'], 'rowIndex': '0', 'columnIndex': '0'},
'data': csvFile.read(), 'type': 'PASTE_NORMAL', 'delimiter': self.columnDelimiter}})
if self.todrive['copySheetEntity']:
body['requests'].append({'copyPaste': {'source': {'sheetId': self.todrive['sheetEntity']['sheetId']},
'destination': {'sheetId': self.todrive['copySheetEntity']['sheetId']}, 'pasteType': 'PASTE_NORMAL'}})
try:
callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=self.todrive['fileId'], body=body)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
todriveCSVErrorExit(entityValueList, str(e))
closeFile(csvFile)
# Create/update file
else:
if GC.Values[GC.TODRIVE_CONVERSION]:
result = callGAPI(drive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='maxImportSizes')
if numRows*len(titlesList) > MAX_GOOGLE_SHEET_CELLS or importSize > int(result['maxImportSizes'][MIMETYPE_GA_SPREADSHEET]):
printKeyValueList([WARNING, Msg.RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET])
mimeType = 'text/csv'
else:
mimeType = MIMETYPE_GA_SPREADSHEET
else:
mimeType = 'text/csv'
fields = ','.join(['id', 'mimeType', 'webViewLink'])
body = {'description': self.todrive['description'], 'mimeType': mimeType}
if body['description'] is None:
body['description'] = Cmd.QuotedArgumentList(Cmd.AllArguments())
if not self.todrive['fileId'] or not self.todrive['retaintitle']:
body['name'] = title
try:
if not self.todrive['fileId']:
Act.Set(Act.CREATE)
body['parents'] = [self.todrive['parentId']]
result = callGAPI(drive.files(), 'create',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
body=body,
media_body=googleapiclient.http.MediaIoBaseUpload(io.BytesIO(csvFile.getvalue().encode()), mimetype='text/csv', resumable=True),
fields=fields, supportsAllDrives=True)
else:
Act.Set(Act.UPDATE)
result = callGAPI(drive.files(), 'update',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR],
fileId=self.todrive['fileId'],
body=body,
media_body=googleapiclient.http.MediaIoBaseUpload(io.BytesIO(csvFile.getvalue().encode()), mimetype='text/csv', resumable=True),
fields=fields, supportsAllDrives=True)
spreadsheetId = result['id']
except GAPI.internalError as e:
entityActionFailedWarning([Ent.DRIVE_FILE, body['name']], Msg.UPLOAD_CSV_FILE_INTERNAL_ERROR.format(str(e), str(numRows)))
closeFile(csvFile)
return
closeFile(csvFile)
if not self.todrive['fileId'] and self.todrive['share']:
Act.Set(Act.SHARE)
for share in self.todrive['share']:
if share['emailAddress'] != user:
try:
callGAPI(drive.permissions(), 'create',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS,
fileId=spreadsheetId, sendNotificationEmail=False, body=share, fields='', supportsAllDrives=True)
entityActionPerformed([Ent.USER, user, Ent.SPREADSHEET, title,
Ent.TARGET_USER, share['emailAddress'], Ent.ROLE, share['role']])
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
GAPI.ownerOnTeamDriveItemNotSupported,
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility,
GAPI.invalidSharingRequest, GAPI.fileNeverWritable, GAPI.abusiveContentRestriction) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, title,
Ent.TARGET_USER, share['emailAddress'], Ent.ROLE, share['role']],
str(e))
if ((result['mimeType'] == MIMETYPE_GA_SPREADSHEET) and
(self.todrive['sheetEntity'] or self.todrive['locale'] or self.todrive['timeZone'] or
self.todrive['sheettitle'] or self.todrive['cellwrap'] or self.todrive['cellnumberformat'])):
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
_, sheet = buildGAPIServiceObject(API.SHEETSTD, user)
if sheet is None:
return
else:
sheet = buildGAPIObject(API.SHEETS)
try:
body = {'requests': []}
if self.todrive['sheetEntity'] or self.todrive['sheettitle'] or self.todrive['cellwrap']:
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, fields='sheets/properties')
spreadsheet['sheets'][0]['properties']['title'] = sheetTitle
body['requests'].append({'updateSheetProperties':
{'properties': spreadsheet['sheets'][0]['properties'], 'fields': 'title'}})
if self.todrive['cellwrap']:
body['requests'].append({'repeatCell': {'range': {'sheetId': spreadsheet['sheets'][0]['properties']['sheetId']},
'fields': 'userEnteredFormat.wrapStrategy',
'cell': {'userEnteredFormat': {'wrapStrategy': self.todrive['cellwrap']}}}})
if self.todrive['locale']:
body['requests'].append({'updateSpreadsheetProperties':
{'properties': {'locale': self.todrive['locale']}, 'fields': 'locale'}})
if self.todrive['timeZone']:
body['requests'].append({'updateSpreadsheetProperties':
{'properties': {'timeZone': self.todrive['timeZone']}, 'fields': 'timeZone'}})
if body['requests']:
callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, body=body)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
todriveCSVErrorExit([Ent.USER, user, Ent.SPREADSHEET, title], str(e))
Act.Set(action)
file_url = result['webViewLink']
msg_txt = f'{Msg.DATA_UPLOADED_TO_DRIVE_FILE}:\n{file_url}'
if not self.todrive['returnidonly']:
printKeyValueList([msg_txt])
else:
if self.todrive['fileId']:
writeStdout(f'{self.todrive["fileId"]}\n')
else:
writeStdout(f'{spreadsheetId}\n')
if not self.todrive['subject']:
subject = title
else:
subject = self.todrive['subject'].replace('#file#', title).replace('#sheet#', sheetTitle)
if not self.todrive['noemail']:
send_email(subject, msg_txt, user, clientAccess=GC.Values[GC.TODRIVE_CLIENTACCESS], msgFrom=self.todrive['from'])
if self.todrive['notify']:
for recipient in self.todrive['share']+self.todrive['alert']:
if recipient['emailAddress'] != user:
send_email(subject, msg_txt, recipient['emailAddress'], clientAccess=GC.Values[GC.TODRIVE_CLIENTACCESS], msgFrom=self.todrive['from'])
if not self.todrive['nobrowser']:
webbrowser.open(file_url)
except (GAPI.forbidden, GAPI.insufficientPermissions):
printWarningMessage(INSUFFICIENT_PERMISSIONS_RC, Msg.INSUFFICIENT_PERMISSIONS_TO_PERFORM_TASK)
except (GAPI.fileNotFound, GAPI.unknownError, GAPI.internalError, GAPI.storageQuotaExceeded) as e:
if not self.todrive['fileId']:
entityActionFailedWarning([Ent.DRIVE_FOLDER, self.todrive['parentId']], str(e))
else:
entityActionFailedWarning([Ent.DRIVE_FILE, self.todrive['fileId']], str(e))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), 0, 0)
else:
closeFile(csvFile)
if GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] is not None:
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_NAME, list_type))
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_TODRIVE, self.todrive))
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_CSVPF,
(self.titlesList, self.sortTitlesList, self.indexedTitles,
self.formatJSON, self.JSONtitlesList,
self.columnDelimiter, self.noEscapeChar, self.quoteChar,
self.sortHeaders, self.timestampColumn,
self.mapDrive3Titles,
self.fixPaths,
self.mapNodataFields,
self.nodataFields,
self.driveListFields,
self.driveSubfieldsChoiceMap,
self.oneItemPerRow,
self.showPermissionsLast,
self.zeroBlankMimeTypeCounts)))
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_DATA, self.rows))
return
if self.zeroBlankMimeTypeCounts:
self.ZeroBlankMimeTypeCounts()
if self.rowFilter or self.rowDropFilter:
self.CheckOutputRowFilterHeaders()
if self.headerFilter or self.headerDropFilter:
if not self.formatJSON:
self.FilterHeaders()
else:
self.FilterJSONHeaders()
extrasaction = 'ignore'
else:
extrasaction = 'raise'
if not self.formatJSON:
if not self.headerForce:
self.SortTitles()
self.SortIndexedTitles(self.titlesList)
if self.fixPaths:
self.FixPathsTitles(self.titlesList)
if self.showPermissionsLast:
self.MovePermsToEnd()
if not self.rows and self.nodataFields is not None:
self.FixNodataTitles()
if self.mapDrive3Titles:
self. MapDrive3TitlesToDrive2()
else:
self.titlesList = self.headerForce
if self.timestampColumn:
self.AddTitle(self.timestampColumn)
if self.headerOrder:
self.titlesList = self.orderHeaders(self.titlesList)
titlesList = self.titlesList
else:
if not self.headerForce:
if self.fixPaths:
self.FixPathsTitles(self.JSONtitlesList)
if not self.rows and self.nodataFields is not None:
self.FixNodataTitles()
else:
self.JSONtitlesList = self.headerForce
if self.timestampColumn:
for i, v in enumerate(self.JSONtitlesList):
if v.startswith('JSON'):
self.JSONtitlesList.insert(i, self.timestampColumn)
self.JSONtitlesSet.add(self.timestampColumn)
break
else:
self.AddJSONTitle(self.timestampColumn)
if self.headerOrder:
self.JSONtitlesList = self.orderHeaders(self.JSONtitlesList)
titlesList = self.JSONtitlesList
normalizeSortHeaders()
if self.outputTranspose:
newRows = []
pivotKey = titlesList[0]
newTitlesList = [pivotKey]
newTitlesSet = set(newTitlesList)
for title in titlesList[1:]:
newRow = {pivotKey: title}
for row in self.rows:
pivotValue = row[pivotKey]
if pivotValue not in newTitlesSet:
newTitlesSet.add(pivotValue)
newTitlesList.append(pivotValue)
newRow[pivotValue] = row.get(title)
newRows.append(newRow)
titlesList = newTitlesList
self.rows = newRows
if (not self.todrive) or self.todrive['localcopy']:
if GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] == '-':
if GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]:
writeCSVToStdout()
else:
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = GM.Globals[GM.STDOUT][GM.REDIRECT_NAME]
writeCSVToFile()
else:
writeCSVToFile()
if self.todrive:
writeCSVToDrive()
if GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE] == DEFAULT_FILE_APPEND_MODE:
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = False
def writeEntityNoHeaderCSVFile(entityType, entityList):
csvPF = CSVPrintFile(entityType)
_, _, entityList = getEntityArgument(entityList)
if entityType == Ent.USER:
for entity in entityList:
csvPF.WriteRowNoFilter({entityType: normalizeEmailAddressOrUID(entity)})
else:
for entity in entityList:
csvPF.WriteRowNoFilter({entityType: entity})
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = False
csvPF.writeCSVfile(Ent.Plural(entityType))
def getTodriveOnly(csvPF):
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
DEFAULT_SKIP_OBJECTS = {'kind', 'etag', 'etags', '@type'}
# Clean a JSON object
def cleanJSON(topStructure, listLimit=None, skipObjects=None, timeObjects=None):
def _clean(structure, key, subSkipObjects):
if not isinstance(structure, (dict, list)):
if key not in timeObjects:
if isinstance(structure, str) and GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]:
return escapeCRsNLs(structure)
return structure
if isinstance(structure, str) and not structure.isdigit():
return formatLocalTime(structure)
return formatLocalTimestamp(structure)
if isinstance(structure, list):
listLen = len(structure)
listLen = min(listLen, listLimit or listLen)
return [_clean(v, '', DEFAULT_SKIP_OBJECTS) for v in structure[0:listLen]]
return {k: _clean(v, k, DEFAULT_SKIP_OBJECTS) for k, v in sorted(iter(structure.items())) if k not in subSkipObjects}
timeObjects = timeObjects or set()
return _clean(topStructure, '', DEFAULT_SKIP_OBJECTS.union(skipObjects or set()))
# Flatten a JSON object
def flattenJSON(topStructure, flattened=None,
listLimit=None, skipObjects=None, timeObjects=None, noLenObjects=None, simpleLists=None, delimiter=None):
def _flatten(structure, key, path):
if not isinstance(structure, (dict, list)):
if key not in timeObjects:
if isinstance(structure, str):
if GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] and (structure.find('\n') >= 0 or structure.find('\r') >= 0):
flattened[path] = escapeCRsNLs(structure)
else:
flattened[path] = structure
else:
flattened[path] = structure
else:
if isinstance(structure, str) and not structure.isdigit():
flattened[path] = formatLocalTime(structure)
else:
flattened[path] = formatLocalTimestamp(structure)
elif isinstance(structure, list):
listLen = len(structure)
listLen = min(listLen, listLimit or listLen)
if key in simpleLists:
flattened[path] = delimiter.join(structure[:listLen])
else:
if key not in noLenObjects:
flattened[path] = listLen
for i in range(listLen):
_flatten(structure[i], '', f'{path}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}')
else:
if structure:
for k, v in sorted(iter(structure.items())):
if k not in DEFAULT_SKIP_OBJECTS:
_flatten(v, k, f'{path}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}')
else:
flattened[path] = ''
flattened = flattened or {}
allSkipObjects = DEFAULT_SKIP_OBJECTS.union(skipObjects or set())
timeObjects = timeObjects or set()
noLenObjects = noLenObjects or set()
simpleLists = simpleLists or set()
for k, v in sorted(iter(topStructure.items())):
if k not in allSkipObjects:
_flatten(v, k, k)
return flattened
# Show a json object
def showJSON(showName, showValue, skipObjects=None, timeObjects=None,
simpleLists=None, dictObjectsKey=None, sortDictKeys=True):
def _show(objectName, objectValue, subObjectKey, level, subSkipObjects):
if objectName in subSkipObjects:
return
if objectName is not None:
printJSONKey(objectName)
subObjectKey = dictObjectsKey.get(objectName)
if isinstance(objectValue, list):
if objectName in simpleLists:
printJSONValue(' '.join(objectValue))
return
if len(objectValue) == 1 and isinstance(objectValue[0], (str, bool, float, int)):
if objectName is not None:
printJSONValue(objectValue[0])
else:
printKeyValueList([objectValue[0]])
return
if objectName is not None:
printBlankLine()
Ind.Increment()
for subValue in objectValue:
if isinstance(subValue, (str, bool, float, int)):
printKeyValueList([subValue])
else:
_show(None, subValue, subObjectKey, level+1, DEFAULT_SKIP_OBJECTS)
if objectName is not None:
Ind.Decrement()
elif isinstance(objectValue, dict):
indentAfterFirst = unindentAfterLast = False
if objectName is not None:
printBlankLine()
Ind.Increment()
elif level > 0:
indentAfterFirst = unindentAfterLast = True
subObjects = sorted(objectValue) if sortDictKeys else objectValue.keys()
if subObjectKey and (subObjectKey in subObjects):
subObjects.remove(subObjectKey)
subObjects.insert(0, subObjectKey)
subObjectKey = None
for subObject in subObjects:
if subObject not in subSkipObjects:
_show(subObject, objectValue[subObject], subObjectKey, level+1, DEFAULT_SKIP_OBJECTS)
if indentAfterFirst:
Ind.Increment()
indentAfterFirst = False
if objectName is not None or ((not indentAfterFirst) and unindentAfterLast):
Ind.Decrement()
else:
if objectName not in timeObjects:
if isinstance(objectValue, str) and (objectValue.find('\n') >= 0 or objectValue.find('\r') >= 0):
if GC.Values[GC.SHOW_CONVERT_CR_NL]:
printJSONValue(escapeCRsNLs(objectValue))
else:
printBlankLine()
Ind.Increment()
printKeyValueList([Ind.MultiLineText(objectValue)])
Ind.Decrement()
else:
printJSONValue(objectValue if objectValue is not None else '')
else:
if isinstance(objectValue, str) and not objectValue.isdigit():
printJSONValue(formatLocalTime(objectValue))
else:
printJSONValue(formatLocalTimestamp(objectValue))
timeObjects = timeObjects or set()
simpleLists = simpleLists or set()
dictObjectsKey = dictObjectsKey or {}
_show(showName, showValue, None, 0, DEFAULT_SKIP_OBJECTS.union(skipObjects or set()))
class FormatJSONQuoteChar():
def __init__(self, csvPF=None, formatJSONOnly=False):
self.SetCsvPF(csvPF)
self.SetFormatJSON(False)
self.SetQuoteChar(GM.Globals.get(GM.CSV_OUTPUT_QUOTE_CHAR, GC.Values.get(GC.CSV_OUTPUT_QUOTE_CHAR, '"')))
if not formatJSONOnly:
return
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'formatjson':
self.SetFormatJSON(True)
return
unknownArgumentExit()
def SetCsvPF(self, csvPF):
self.csvPF = csvPF
def SetFormatJSON(self, formatJSON):
self.formatJSON = formatJSON
if self.csvPF:
self.csvPF.SetFormatJSON(formatJSON)
def GetFormatJSON(self, myarg):
if myarg == 'formatjson':
self.SetFormatJSON(True)
return
unknownArgumentExit()
def SetQuoteChar(self, quoteChar):
self.quoteChar = quoteChar
if self.csvPF:
self.csvPF.SetQuoteChar(quoteChar)
def GetQuoteChar(self, myarg):
if self.csvPF and myarg == 'quotechar':
self.SetQuoteChar(getCharacter())
return
unknownArgumentExit()
def GetFormatJSONQuoteChar(self, myarg, addTitle=False, noExit=False):
if myarg == 'formatjson':
self.SetFormatJSON(True)
if self.csvPF and addTitle:
self.csvPF.AddJSONTitles('JSON')
return True
if self.csvPF and myarg == 'quotechar':
self.SetQuoteChar(getCharacter())
return True
if noExit:
return False
unknownArgumentExit()
# Batch processing request_id fields
RI_ENTITY = 0
RI_I = 1
RI_COUNT = 2
RI_J = 3
RI_JCOUNT = 4
RI_ITEM = 5
RI_ROLE = 6
RI_OPTION = 7
def batchRequestID(entityName, i, count, j, jcount, item, role=None, option=None):
if role is None and option is None:
return f'{entityName}\n{i}\n{count}\n{j}\n{jcount}\n{item}'
return f'{entityName}\n{i}\n{count}\n{j}\n{jcount}\n{item}\n{role}\n{option}'
TIME_OFFSET_UNITS = [('day', SECONDS_PER_DAY), ('hour', SECONDS_PER_HOUR), ('minute', SECONDS_PER_MINUTE), ('second', 1)]
def getLocalGoogleTimeOffset(testLocation=GOOGLE_TIMECHECK_LOCATION):
# If local time is well off, it breaks https because the server certificate will be seen as too old or new and thus invalid; http doesn't have that issue.
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds), retry with https as it should be OK
httpObj = getHttpObj()
for prot in ['http', 'https']:
try:
headerData = httpObj.request(f'{prot}://'+testLocation, 'HEAD')
googleUTC = datetime.datetime.strptime(headerData[0]['date'], '%a, %d %b %Y %H:%M:%S %Z').replace(tzinfo=iso8601.UTC)
except (httplib2.HttpLib2Error, RuntimeError) as e:
handleServerError(e)
except httplib2.socks.HTTPError as e:
# If user has specified an HTTPS proxy, the http request will probably fail as httplib2
# turns a GET into a CONNECT which is not valid for an http address
if prot == 'http':
continue
handleServerError(e)
except (ValueError, KeyError):
if prot == 'http':
continue
systemErrorExit(NETWORK_ERROR_RC, Msg.INVALID_HTTP_HEADER.format(str(headerData)))
offset = remainder = int(abs((datetime.datetime.now(iso8601.UTC)-googleUTC).total_seconds()))
if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
continue
timeoff = []
for tou in TIME_OFFSET_UNITS:
uval, remainder = divmod(remainder, tou[1])
if uval:
timeoff.append(f'{uval} {tou[0]}{"s" if uval != 1 else ""}')
if not timeoff:
timeoff.append(Msg.LESS_THAN_1_SECOND)
nicetime = ', '.join(timeoff)
return (offset, nicetime)
def _getServerTLSUsed(location):
url = 'https://'+location
_, netloc, _, _, _, _ = urlparse(url)
conn = 'https:'+netloc
httpObj = getHttpObj()
triesLimit = 5
for n in range(1, triesLimit+1):
try:
httpObj.request(url, headers={'user-agent': GAM_USER_AGENT})
cipher_name, tls_ver, _ = httpObj.connections[conn].sock.cipher()
return tls_ver, cipher_name
except (httplib2.HttpLib2Error, RuntimeError) as e:
if n != triesLimit:
httpObj.connections = {}
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
continue
handleServerError(e)
MACOS_CODENAMES = {
10: {
6: 'Snow Leopard',
7: 'Lion',
8: 'Mountain Lion',
9: 'Mavericks',
10: 'Yosemite',
11: 'El Capitan',
12: 'Sierra',
13: 'High Sierra',
14: 'Mojave',
15: 'Catalina',
16: 'Big Sur'
},
11: 'Big Sur',
12: 'Monterey',
13: 'Ventura',
14: 'Sonoma',
15: 'Sequoia',
}
def getOSPlatform():
myos = platform.system()
if myos == 'Linux':
pltfrm = ' '.join(distro.linux_distribution(full_distribution_name=False)).title()
elif myos == 'Windows':
pltfrm = ' '.join(platform.win32_ver())
elif myos == 'Darwin':
myos = 'MacOS'
mac_ver = platform.mac_ver()[0]
major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
if major_ver == 10:
codename = MACOS_CODENAMES[major_ver].get(minor_ver, '')
else:
codename = MACOS_CODENAMES.get(major_ver, '')
pltfrm = ' '.join([codename, mac_ver])
else:
pltfrm = platform.platform()
return f'{myos} {pltfrm}'
# gam checkconnection
def doCheckConnection():
def check_host(host):
nonlocal try_count, okay, not_okay, success_count
try_count += 1
dns_err = None
ip = 'unknown'
try:
ip = socket.getaddrinfo(host, None)[0][-1][0] # works with ipv6
except socket.gaierror as e:
dns_err = f'{not_okay}\n DNS failure: {str(e)}\n'
except Exception as e:
dns_err = f'{not_okay}\n Unknown DNS failure: {str(e)}\n'
check_line = f'Checking {host} ({ip}) ({try_count})...'
writeStdout(f'{check_line:<100}')
flushStdout()
if dns_err:
writeStdout(dns_err)
return
gen_firewall = 'You probably have security software or a firewall on your machine or network that is preventing GAM from making Internet connections. Check your network configuration or try running GAM on a hotspot or home network to see if the problem exists only on your organization\'s network.'
try:
if host.startswith('http'):
url = host
else:
url = f'https://{host}:443/'
httpObj.request(url, 'HEAD', headers=headers)
success_count += 1
writeStdout(f'{okay}\n')
except ConnectionRefusedError:
writeStdout(f'{not_okay}\n Connection refused. {gen_firewall}\n')
except ConnectionResetError:
writeStdout(f'{not_okay}\n Connection reset by peer. {gen_firewall}\n')
except httplib2.error.ServerNotFoundError:
writeStdout(f'{not_okay}\n Failed to find server. Your DNS is probably misconfigured.\n')
except ssl.SSLError as e:
if e.reason == 'SSLV3_ALERT_HANDSHAKE_FAILURE':
writeStdout(f'{not_okay}\n GAM expects to connect with TLS 1.3 or newer and that failed. If your firewall / proxy server is not compatible with TLS 1.3 then you can tell GAM to allow TLS 1.2 by setting tls_min_version = TLSv1.2 in gam.cfg.\n')
elif e.reason == 'CERTIFICATE_VERIFY_FAILED':
writeStdout(f'{not_okay}\n Certificate verification failed. If you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.\n')
elif e.strerror and e.strerror.startswith('TLS/SSL connection has been closed\n'):
writeStdout(f'{not_okay}\n TLS connection was closed. {gen_firewall}\n')
else:
writeStdout(f'{not_okay}\n {str(e)}\n')
except TimeoutError:
writeStdout(f'{not_okay}\n Timed out trying to connect to host\n')
except Exception as e:
writeStdout(f'{not_okay}\n {str(e)}\n')
try_count = 0
httpObj = getHttpObj(timeout=30)
httpObj.follow_redirects = False
headers = {'user-agent': GAM_USER_AGENT}
okay = createGreenText('OK')
not_okay = createRedText('ERROR')
success_count = 0
initial_hosts = ['api.github.com',
'raw.githubusercontent.com',
'accounts.google.com',
'oauth2.googleapis.com',
'www.googleapis.com']
for host in initial_hosts:
check_host(host)
api_hosts = ['apps-apis.google.com',
'www.google.com']
for host in api_hosts:
check_host(host)
# For v2 discovery APIs, GAM gets discovery file from <api>.googleapis.com so
# add those domains.
disc_hosts = []
for api, config in API._INFO.items():
if config.get('v2discovery') and not config.get('localdiscovery'):
if mapped_api := config.get('mappedAPI'):
api = mapped_api
host = f'{api}.googleapis.com'
if host not in disc_hosts:
disc_hosts.append(host)
for host in disc_hosts:
check_host(host)
checked_hosts = initial_hosts + api_hosts + disc_hosts
# now we need to "build" each API and check it's base URL host
# if we haven't already. This may not be any hosts at all but
# to ensure we are checking all hosts GAM may use we should
# keep this.
for api in API._INFO:
if api in [API.CONTACTS, API.EMAIL_AUDIT]:
continue
svc = getService(api, httpObj)
base_url = svc._rootDesc.get('baseUrl')
parsed_base_url = urlparse(base_url)
base_host = parsed_base_url.netloc
if base_host not in checked_hosts:
print(f'checking {base_host} for {api}')
check_host(base_host)
checked_hosts.append(base_host)
if success_count == try_count:
writeStdout(createGreenText('All hosts passed!\n'))
else:
systemErrorExit(3, createYellowText('Some hosts failed to connect! Please follow the recommendations for those hosts to correct any issues and try again.'))
# gam comment
def doComment():
writeStdout(Cmd.QuotedArgumentList(Cmd.Remaining())+'\n')
# gam version [check|checkrc|simple|extended] [timeoffset] [nooffseterror] [location <HostName>]
def doVersion(checkForArgs=True):
forceCheck = 0
extended = noOffsetError = timeOffset = simple = False
testLocation = GOOGLE_TIMECHECK_LOCATION
if checkForArgs:
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'check':
forceCheck = 1
elif myarg == 'checkrc':
forceCheck = -1
elif myarg == 'simple':
simple = True
elif myarg == 'extended':
extended = timeOffset = True
elif myarg == 'timeoffset':
timeOffset = True
elif myarg == 'nooffseterror':
noOffsetError = True
elif myarg == 'location':
testLocation = getString(Cmd.OB_HOST_NAME)
else:
unknownArgumentExit()
if simple:
writeStdout(__version__)
return
writeStdout((f'{GAM} {__version__} - {GAM_URL} - {GM.Globals[GM.GAM_TYPE]}\n'
f'{__author__}\n'
f'Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {struct.calcsize("P")*8}-bit {sys.version_info[3]}\n'
f'{getOSPlatform()} {platform.machine()}\n'
f'Path: {GM.Globals[GM.GAM_PATH]}\n'
f'{Ent.Singular(Ent.CONFIG_FILE)}: {GM.Globals[GM.GAM_CFG_FILE]}, {Ent.Singular(Ent.SECTION)}: {GM.Globals[GM.GAM_CFG_SECTION_NAME]}, '
f'{GC.CUSTOMER_ID}: {GC.Values[GC.CUSTOMER_ID]}, {GC.DOMAIN}: {GC.Values[GC.DOMAIN]}\n'
f'Time: {ISOformatTimeStamp(todaysTime())}\n'
))
if sys.platform.startswith('win') and str(struct.calcsize('P')*8).find('32') != -1 and platform.machine().find('64') != -1:
printKeyValueList([Msg.UPDATE_GAM_TO_64BIT])
if timeOffset:
offsetSeconds, offsetFormatted = getLocalGoogleTimeOffset(testLocation)
printKeyValueList([Msg.YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE.format(testLocation, offsetFormatted)])
if offsetSeconds > MAX_LOCAL_GOOGLE_TIME_OFFSET:
if not noOffsetError:
systemErrorExit(NETWORK_ERROR_RC, Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
stderrWarningMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
if forceCheck:
doGAMCheckForUpdates(forceCheck)
if extended:
printKeyValueList([ssl.OPENSSL_VERSION])
tls_ver, cipher_name = _getServerTLSUsed(testLocation)
for lib in glverlibs.GAM_VER_LIBS:
try:
writeStdout(f'{lib} {lib_version(lib)}\n')
except:
pass
printKeyValueList([f'{testLocation} connects using {tls_ver} {cipher_name}'])
# gam help
def doUsage():
printBlankLine()
doVersion(checkForArgs=False)
writeStdout(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
writeStdout(Msg.HELP_WIKI.format(GAM_WIKI))
class NullHandler(logging.Handler):
def emit(self, record):
pass
def initializeLogging():
nh = NullHandler()
logging.getLogger().addHandler(nh)
def saveNonPickleableValues():
savedValues = {GM.STDOUT: {}, GM.STDERR: {}, GM.SAVED_STDOUT: None,
GM.CMDLOG_HANDLER: None, GM.CMDLOG_LOGGER: None}
savedValues[GM.SAVED_STDOUT] = GM.Globals[GM.SAVED_STDOUT]
GM.Globals[GM.SAVED_STDOUT] = None
savedValues[GM.STDOUT][GM.REDIRECT_FD] = GM.Globals[GM.STDOUT].get(GM.REDIRECT_FD, None)
GM.Globals[GM.STDOUT].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDERR][GM.REDIRECT_FD] = GM.Globals[GM.STDERR].get(GM.REDIRECT_FD, None)
GM.Globals[GM.STDERR].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, None)
GM.Globals[GM.STDOUT].pop(GM.REDIRECT_MULTI_FD, None)
savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, None)
GM.Globals[GM.STDERR].pop(GM.REDIRECT_MULTI_FD, None)
savedValues[GM.CMDLOG_HANDLER] = GM.Globals[GM.CMDLOG_HANDLER]
GM.Globals[GM.CMDLOG_HANDLER] = None
savedValues[GM.CMDLOG_LOGGER] = GM.Globals[GM.CMDLOG_LOGGER]
GM.Globals[GM.CMDLOG_LOGGER] = None
return savedValues
def restoreNonPickleableValues(savedValues):
GM.Globals[GM.SAVED_STDOUT] = savedValues[GM.SAVED_STDOUT]
GM.Globals[GM.STDOUT][GM.REDIRECT_FD] = savedValues[GM.STDOUT][GM.REDIRECT_FD]
GM.Globals[GM.STDERR][GM.REDIRECT_FD] = savedValues[GM.STDERR][GM.REDIRECT_FD]
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD]
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD]
GM.Globals[GM.CMDLOG_HANDLER] = savedValues[GM.CMDLOG_HANDLER]
GM.Globals[GM.CMDLOG_LOGGER] = savedValues[GM.CMDLOG_LOGGER]
def CSVFileQueueHandler(mpQueue, mpQueueStdout, mpQueueStderr, csvPF, datetimeNow, tzinfo, output_timeformat):
global Cmd
def reopenSTDFile(stdtype):
if GM.Globals[stdtype][GM.REDIRECT_NAME] == 'null':
GM.Globals[stdtype][GM.REDIRECT_FD] = open(os.devnull, GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=UTF8)
elif GM.Globals[stdtype][GM.REDIRECT_NAME] == '-':
GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup([sys.stderr.fileno(), sys.stdout.fileno()][stdtype == GM.STDOUT]),
GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=GM.Globals[GM.SYS_ENCODING])
elif stdtype == GM.STDERR and GM.Globals[stdtype][GM.REDIRECT_NAME] == 'stdout':
GM.Globals[stdtype][GM.REDIRECT_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_FD]
else:
GM.Globals[stdtype][GM.REDIRECT_FD] = openFile(GM.Globals[stdtype][GM.REDIRECT_NAME], GM.Globals[stdtype][GM.REDIRECT_MODE])
if stdtype == GM.STDERR and GM.Globals[stdtype][GM.REDIRECT_NAME] == 'stdout':
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
else:
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = GM.Globals[stdtype][GM.REDIRECT_FD] if not GM.Globals[stdtype][GM.REDIRECT_MULTIPROCESS] else StringIOobject()
GM.Globals[GM.DATETIME_NOW] = datetimeNow
GC.Values[GC.TIMEZONE] = tzinfo
GC.Values[GC.OUTPUT_TIMEFORMAT] = output_timeformat
# if sys.platform.startswith('win'):
# signal.signal(signal.SIGINT, signal.SIG_IGN)
if multiprocessing.get_start_method() == 'spawn':
signal.signal(signal.SIGINT, signal.SIG_IGN)
Cmd = glclargs.GamCLArgs()
else:
csvPF.SetColumnDelimiter(GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER])
csvPF.SetNoEscapeChar(GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR])
csvPF.SetQuoteChar(GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR])
csvPF.SetSortHeaders(GC.Values[GC.CSV_OUTPUT_SORT_HEADERS])
csvPF.SetTimestampColumn(GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN])
csvPF.SetHeaderFilter(GC.Values[GC.CSV_OUTPUT_HEADER_FILTER])
csvPF.SetHeaderDropFilter(GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER])
csvPF.SetRowFilter(GC.Values[GC.CSV_OUTPUT_ROW_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
csvPF.SetRowDropFilter(GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE])
csvPF.SetRowLimit(GC.Values[GC.CSV_OUTPUT_ROW_LIMIT])
list_type = 'CSV'
while True:
dataType, dataItem = mpQueue.get()
if dataType == GM.REDIRECT_QUEUE_NAME:
list_type = dataItem
elif dataType == GM.REDIRECT_QUEUE_TODRIVE:
csvPF.todrive = dataItem
elif dataType == GM.REDIRECT_QUEUE_CSVPF:
csvPF.AddTitles(dataItem[0])
csvPF.SetSortTitles(dataItem[1])
csvPF.SetIndexedTitles(dataItem[2])
csvPF.SetFormatJSON(dataItem[3])
csvPF.AddJSONTitles(dataItem[4])
csvPF.SetColumnDelimiter(dataItem[5])
csvPF.SetNoEscapeChar(dataItem[6])
csvPF.SetQuoteChar(dataItem[7])
csvPF.SetSortHeaders(dataItem[8])
csvPF.SetTimestampColumn(dataItem[9])
csvPF.SetMapDrive3Titles(dataItem[10])
csvPF.SetFixPaths(dataItem[11])
csvPF.SetNodataFields(dataItem[12], dataItem[13], dataItem[14], dataItem[15], dataItem[16])
csvPF.SetShowPermissionsLast(dataItem[17])
csvPF.SetZeroBlankMimeTypeCounts(dataItem[18])
elif dataType == GM.REDIRECT_QUEUE_DATA:
csvPF.rows.extend(dataItem)
elif dataType == GM.REDIRECT_QUEUE_ARGS:
Cmd.InitializeArguments(dataItem)
elif dataType == GM.REDIRECT_QUEUE_GLOBALS:
GM.Globals = dataItem
if multiprocessing.get_start_method() == 'spawn':
reopenSTDFile(GM.STDOUT)
reopenSTDFile(GM.STDERR)
elif dataType == GM.REDIRECT_QUEUE_VALUES:
GC.Values = dataItem
csvPF.SetColumnDelimiter(GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER])
csvPF.SetNoEscapeChar(GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR])
csvPF.SetQuoteChar(GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR])
csvPF.SetSortHeaders(GC.Values[GC.CSV_OUTPUT_SORT_HEADERS])
csvPF.SetTimestampColumn(GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN])
csvPF.SetHeaderFilter(GC.Values[GC.CSV_OUTPUT_HEADER_FILTER])
csvPF.SetHeaderDropFilter(GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER])
csvPF.SetRowFilter(GC.Values[GC.CSV_OUTPUT_ROW_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
csvPF.SetRowDropFilter(GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE])
csvPF.SetRowLimit(GC.Values[GC.CSV_OUTPUT_ROW_LIMIT])
else: #GM.REDIRECT_QUEUE_EOF
break
csvPF.writeCSVfile(list_type)
if mpQueueStdout:
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()))
else:
flushStdout()
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()))
else:
flushStderr()
def initializeCSVFileQueueHandler(mpManager, mpQueueStdout, mpQueueStderr):
mpQueue = mpManager.Queue()
mpQueueHandler = multiprocessing.Process(target=CSVFileQueueHandler,
args=(mpQueue, mpQueueStdout, mpQueueStderr,
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF],
GM.Globals[GM.DATETIME_NOW], GC.Values[GC.TIMEZONE],
GC.Values[GC.OUTPUT_TIMEFORMAT]))
mpQueueHandler.start()
return (mpQueue, mpQueueHandler)
def terminateCSVFileQueueHandler(mpQueue, mpQueueHandler):
GM.Globals[GM.PARSER] = None
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None
if multiprocessing.get_start_method() == 'spawn':
mpQueue.put((GM.REDIRECT_QUEUE_ARGS, Cmd.AllArguments()))
savedValues = saveNonPickleableValues()
mpQueue.put((GM.REDIRECT_QUEUE_GLOBALS, GM.Globals))
restoreNonPickleableValues(savedValues)
mpQueue.put((GM.REDIRECT_QUEUE_VALUES, GC.Values))
mpQueue.put((GM.REDIRECT_QUEUE_EOF, None))
mpQueueHandler.join()
def StdQueueHandler(mpQueue, stdtype, gmGlobals, gcValues):
PROCESS_MSG = '{0}: {1:6d}, {2:>5s}: {3}, RC: {4:3d}, Cmd: {5}\n'
def _writeData(data):
fd.write(data)
def _writePidData(pid, data):
try:
if pid != 0 and GC.Values[GC.SHOW_MULTIPROCESS_INFO]:
_writeData(PROCESS_MSG.format(pidData[pid]['queue'], pid, 'Start', pidData[pid]['start'], 0, pidData[pid]['cmd']))
if data[1] is not None:
_writeData(data[1])
if GC.Values[GC.SHOW_MULTIPROCESS_INFO]:
_writeData(PROCESS_MSG.format(pidData[pid]['queue'], pid, 'End', currentISOformatTimeStamp(), data[0], pidData[pid]['cmd']))
fd.flush()
except IOError as e:
systemErrorExit(FILE_ERROR_RC, fdErrorMessage(fd, GM.Globals[stdtype][GM.REDIRECT_NAME], e))
# if sys.platform.startswith('win'):
# signal.signal(signal.SIGINT, signal.SIG_IGN)
if multiprocessing.get_start_method() == 'spawn':
signal.signal(signal.SIGINT, signal.SIG_IGN)
GM.Globals = gmGlobals.copy()
GC.Values = gcValues.copy()
pid0DataItem = [KEYBOARD_INTERRUPT_RC, None]
pidData = {}
if multiprocessing.get_start_method() == 'spawn':
if GM.Globals[stdtype][GM.REDIRECT_NAME] == 'null':
fd = open(os.devnull, GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=UTF8)
elif GM.Globals[stdtype][GM.REDIRECT_NAME] == '-':
fd = os.fdopen(os.dup([sys.stderr.fileno(), sys.stdout.fileno()][GM.Globals[stdtype][GM.REDIRECT_QUEUE] == 'stdout']),
GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=GM.Globals[GM.SYS_ENCODING])
elif GM.Globals[stdtype][GM.REDIRECT_NAME] == 'stdout' and GM.Globals[stdtype][GM.REDIRECT_QUEUE] == 'stderr':
fd = os.fdopen(os.dup(sys.stdout.fileno()), GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=GM.Globals[GM.SYS_ENCODING])
else:
fd = openFile(GM.Globals[stdtype][GM.REDIRECT_NAME], GM.Globals[stdtype][GM.REDIRECT_MODE])
else:
fd = GM.Globals[stdtype][GM.REDIRECT_FD]
while True:
try:
pid, dataType, dataItem = mpQueue.get()
except (EOFError, ValueError):
break
if dataType == GM.REDIRECT_QUEUE_START:
pidData[pid] = {'queue': GM.Globals[stdtype][GM.REDIRECT_QUEUE],
'start': currentISOformatTimeStamp(),
'cmd': Cmd.QuotedArgumentList(dataItem)}
if pid == 0 and GC.Values[GC.SHOW_MULTIPROCESS_INFO]:
fd.write(PROCESS_MSG.format(pidData[pid]['queue'], pid, 'Start', pidData[pid]['start'], 0, pidData[pid]['cmd']))
elif dataType == GM.REDIRECT_QUEUE_DATA:
_writeData(dataItem)
elif dataType == GM.REDIRECT_QUEUE_END:
if pid != 0:
_writePidData(pid, dataItem)
del pidData[pid]
else:
pid0DataItem = dataItem
else: #GM.REDIRECT_QUEUE_EOF
break
for pid in pidData:
if pid != 0:
_writePidData(pid, [KEYBOARD_INTERRUPT_RC, None])
_writePidData(0, pid0DataItem)
if fd not in [sys.stdout, sys.stderr]:
try:
fd.flush()
fd.close()
except IOError:
pass
GM.Globals[stdtype][GM.REDIRECT_FD] = None
def initializeStdQueueHandler(mpManager, stdtype, gmGlobals, gcValues):
mpQueue = mpManager.Queue()
mpQueueHandler = multiprocessing.Process(target=StdQueueHandler, args=(mpQueue, stdtype, gmGlobals, gcValues))
mpQueueHandler.start()
return (mpQueue, mpQueueHandler)
def batchWriteStderr(data):
try:
sys.stderr.write(data)
sys.stderr.flush()
except IOError as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage('stderr', e))
def writeStdQueueHandler(mpQueue, item):
while True:
try:
mpQueue.put(item)
return
except Exception as e:
time.sleep(1)
batchWriteStderr(f'{currentISOformatTimeStamp()},{item[0]}/{GM.Globals[GM.NUM_BATCH_ITEMS]},Error,{str(e)}\n')
def terminateStdQueueHandler(mpQueue, mpQueueHandler):
mpQueue.put((0, GM.REDIRECT_QUEUE_EOF, None))
mpQueueHandler.join()
def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, mpQueueStderr,
debugLevel, todrive, printAguDomains,
printCrosOUs, printCrosOUsAndChildren,
output_dateformat, output_timeformat,
csvColumnDelimiter, csvNoEscapeChar, csvQuoteChar,
csvSortHeaders, csvTimestampColumn,
csvHeaderFilter, csvHeaderDropFilter,
csvHeaderForce, csvHeaderOrder,
csvRowFilter, csvRowFilterMode, csvRowDropFilter, csvRowDropFilterMode,
csvRowLimit,
showGettings, showGettingsGotNL,
args):
global mplock
with mplock:
initializeLogging()
# if sys.platform.startswith('win'):
if multiprocessing.get_start_method() == 'spawn':
signal.signal(signal.SIGINT, signal.SIG_IGN)
GM.Globals[GM.API_CALLS_RETRY_DATA] = {}
GM.Globals[GM.CMDLOG_LOGGER] = None
GM.Globals[GM.CSVFILE] = {}
GM.Globals[GM.CSV_DATA_DICT] = {}
GM.Globals[GM.CSV_KEY_FIELD] = None
GM.Globals[GM.CSV_SUBKEY_FIELD] = None
GM.Globals[GM.CSV_DATA_FIELD] = None
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = csvColumnDelimiter
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = csvNoEscapeChar
GM.Globals[GM.CSV_OUTPUT_HEADER_DROP_FILTER] = csvHeaderDropFilter[:]
GM.Globals[GM.CSV_OUTPUT_HEADER_FILTER] = csvHeaderFilter[:]
GM.Globals[GM.CSV_OUTPUT_HEADER_FORCE] = csvHeaderForce[:]
GM.Globals[GM.CSV_OUTPUT_HEADER_ORDER] = csvHeaderOrder[:]
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = csvQuoteChar
GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER] = csvRowDropFilter[:]
GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = csvRowDropFilterMode
GM.Globals[GM.CSV_OUTPUT_ROW_FILTER] = csvRowFilter[:]
GM.Globals[GM.CSV_OUTPUT_ROW_FILTER_MODE] = csvRowFilterMode
GM.Globals[GM.CSV_OUTPUT_ROW_LIMIT] = csvRowLimit
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = csvSortHeaders[:]
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = csvTimestampColumn
GM.Globals[GM.CSV_TODRIVE] = todrive.copy()
GM.Globals[GM.DEBUG_LEVEL] = debugLevel
GM.Globals[GM.OUTPUT_DATEFORMAT] = output_dateformat
GM.Globals[GM.OUTPUT_TIMEFORMAT] = output_timeformat
GM.Globals[GM.NUM_BATCH_ITEMS] = numItems
GM.Globals[GM.PID] = pid
GM.Globals[GM.PRINT_AGU_DOMAINS] = printAguDomains[:]
GM.Globals[GM.PRINT_CROS_OUS] = printCrosOUs[:]
GM.Globals[GM.PRINT_CROS_OUS_AND_CHILDREN] = printCrosOUsAndChildren[:]
GM.Globals[GM.SAVED_STDOUT] = None
GM.Globals[GM.SHOW_GETTINGS] = showGettings
GM.Globals[GM.SHOW_GETTINGS_GOT_NL] = showGettingsGotNL
GM.Globals[GM.SYSEXITRC] = 0
GM.Globals[GM.PARSER] = None
if mpQueueCSVFile:
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = mpQueueCSVFile
if mpQueueStdout:
GM.Globals[GM.STDOUT] = {GM.REDIRECT_NAME: '', GM.REDIRECT_FD: None, GM.REDIRECT_MULTI_FD: StringIOobject()}
if debugLevel:
sys.stdout = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
# mpQueueStdout.put((pid, GM.REDIRECT_QUEUE_START, args))
writeStdQueueHandler(mpQueueStdout,(pid, GM.REDIRECT_QUEUE_START, args))
else:
GM.Globals[GM.STDOUT] = {}
if mpQueueStderr:
if mpQueueStderr is not mpQueueStdout:
GM.Globals[GM.STDERR] = {GM.REDIRECT_NAME: '', GM.REDIRECT_FD: None, GM.REDIRECT_MULTI_FD: StringIOobject()}
# mpQueueStderr.put((pid, GM.REDIRECT_QUEUE_START, args))
writeStdQueueHandler(mpQueueStderr, (pid, GM.REDIRECT_QUEUE_START, args))
else:
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
else:
GM.Globals[GM.STDERR] = {}
sysRC = ProcessGAMCommand(args)
with mplock:
if mpQueueStdout:
# mpQueueStdout.put((pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()]))
writeStdQueueHandler(mpQueueStdout, (pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()]))
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].close()
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = None
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
# mpQueueStderr.put((pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()]))
writeStdQueueHandler(mpQueueStderr, (pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()]))
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].close()
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = None
return (pid, sysRC, logCmd)
ERROR_PLURAL_SINGULAR = [Msg.ERRORS, Msg.ERROR]
PROCESS_PLURAL_SINGULAR = [Msg.PROCESSES, Msg.PROCESS]
THREAD_PLURAL_SINGULAR = [Msg.THREADS, Msg.THREAD]
def checkChildProcessRC(rc):
# Comparison
if 'comp' in GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]:
op = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['comp']
value = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['value']
if op == '<':
return rc < value
if op == '<=':
return rc <= value
if op == '>':
return rc > value
if op == '>=':
return rc >= value
if op == '!=':
return rc != value
return rc == value
# Range
op = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['range']
low = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['low']
high = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['high']
if op == '!=':
return not low <= rc <= high
return low <= rc <= high
def initGamWorker(l):
global mplock
mplock = l
def MultiprocessGAMCommands(items, showCmds):
def poolCallback(result):
poolProcessResults[0] -= 1
if showCmds:
batchWriteStderr(f'{currentISOformatTimeStamp()},{result[0]}/{numItems},End,{result[1]},{result[2]}\n')
if GM.Globals[GM.CMDLOG_LOGGER]:
GM.Globals[GM.CMDLOG_LOGGER].info(f'{currentISOformatTimeStamp()},{result[1]},{result[2]}')
if GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] is not None and checkChildProcessRC(result[1]):
GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING] = True
def signal_handler(*_):
nonlocal controlC
controlC = True
def handleControlC(source):
nonlocal controlC
batchWriteStderr(f'Control-C (Multiprocess-{source})\n')
setSysExitRC(KEYBOARD_INTERRUPT_RC)
batchWriteStderr(Msg.BATCH_CSV_TERMINATE_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, poolProcessResults[0],
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1]))
pool.terminate()
controlC = False
if not items:
return
GM.Globals[GM.NUM_BATCH_ITEMS] = numItems = len(items)
numPoolProcesses = min(numItems, GC.Values[GC.NUM_THREADS])
if GC.Values[GC.MULTIPROCESS_POOL_LIMIT] == -1:
parallelPoolProcesses = -1
elif GC.Values[GC.MULTIPROCESS_POOL_LIMIT] == 0:
parallelPoolProcesses = numPoolProcesses
else:
parallelPoolProcesses = min(numItems, GC.Values[GC.MULTIPROCESS_POOL_LIMIT])
# origSigintHandler = signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN)
mpManager = multiprocessing.Manager()
l = mpManager.Lock()
try:
if multiprocessing.get_start_method() == 'spawn':
pool = mpManager.Pool(processes=numPoolProcesses, initializer=initGamWorker, initargs=(l,), maxtasksperchild=200)
else:
pool = multiprocessing.Pool(processes=numPoolProcesses, initializer=initGamWorker, initargs=(l,), maxtasksperchild=200)
except IOError as e:
systemErrorExit(FILE_ERROR_RC, e)
except AssertionError as e:
Cmd.SetLocation(0)
usageErrorExit(str(e))
if multiprocessing.get_start_method() == 'spawn':
savedValues = saveNonPickleableValues()
if GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS]:
mpQueueStdout, mpQueueHandlerStdout = initializeStdQueueHandler(mpManager, GM.STDOUT, GM.Globals, GC.Values)
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_START, Cmd.AllArguments()))
else:
mpQueueStdout = None
if GM.Globals[GM.STDERR][GM.REDIRECT_MULTIPROCESS]:
if GM.Globals[GM.STDERR][GM.REDIRECT_NAME] != 'stdout':
mpQueueStderr, mpQueueHandlerStderr = initializeStdQueueHandler(mpManager, GM.STDERR, GM.Globals, GC.Values)
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_START, Cmd.AllArguments()))
else:
mpQueueStderr = mpQueueStdout
else:
mpQueueStderr = None
if multiprocessing.get_start_method() == 'spawn':
restoreNonPickleableValues(savedValues)
if mpQueueStdout:
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()))
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].truncate(0)
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()))
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].truncate(0)
if GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS]:
mpQueueCSVFile, mpQueueHandlerCSVFile = initializeCSVFileQueueHandler(mpManager, mpQueueStdout, mpQueueStderr)
else:
mpQueueCSVFile = None
# signal.signal(signal.SIGINT, origSigintHandler)
controlC = False
signal.signal(signal.SIGINT, signal_handler)
batchWriteStderr(Msg.USING_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, numPoolProcesses,
PROCESS_PLURAL_SINGULAR[numPoolProcesses == 1]))
try:
pid = 0
poolProcessResults = {pid: 0}
for item in items:
if GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING]:
break
if controlC:
break
if item[0] == Cmd.COMMIT_BATCH_CMD:
batchWriteStderr(Msg.COMMIT_BATCH_WAIT_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, poolProcessResults[0],
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1]))
while poolProcessResults[0] > 0:
time.sleep(1)
completedProcesses = []
for p, result in iter(poolProcessResults.items()):
if p != 0 and result.ready():
poolCallback(result.get())
completedProcesses.append(p)
for p in completedProcesses:
del poolProcessResults[p]
batchWriteStderr(Msg.COMMIT_BATCH_COMPLETE.format(currentISOformatTimeStamp(), numItems, Msg.PROCESSES))
if len(item) > 1:
readStdin(f'{currentISOformatTimeStamp()},0/{numItems},{Cmd.QuotedArgumentList(item[1:])}')
continue
if item[0] == Cmd.PRINT_CMD:
batchWriteStderr(Cmd.QuotedArgumentList(item[1:])+'\n')
continue
if item[0] == Cmd.SLEEP_CMD:
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},Sleepiing {item[1]} seconds\n')
time.sleep(int(item[1]))
continue
pid += 1
if not showCmds and ((pid % 100 == 0) or (pid == numItems)):
batchWriteStderr(Msg.PROCESSING_ITEM_N_OF_M.format(currentISOformatTimeStamp(), pid, numItems))
if showCmds or GM.Globals[GM.CMDLOG_LOGGER]:
logCmd = Cmd.QuotedArgumentList(item)
if showCmds:
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Start,0,{logCmd}\n')
else:
logCmd = ''
poolProcessResults[pid] = pool.apply_async(ProcessGAMCommandMulti,
[pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, mpQueueStderr,
GC.Values[GC.DEBUG_LEVEL], GM.Globals[GM.CSV_TODRIVE],
GC.Values[GC.PRINT_AGU_DOMAINS],
GC.Values[GC.PRINT_CROS_OUS], GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN],
GC.Values[GC.OUTPUT_DATEFORMAT], GC.Values[GC.OUTPUT_TIMEFORMAT],
GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER],
GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR],
GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR],
GC.Values[GC.CSV_OUTPUT_SORT_HEADERS],
GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN],
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER],
GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER],
GC.Values[GC.CSV_OUTPUT_HEADER_FORCE],
GC.Values[GC.CSV_OUTPUT_HEADER_ORDER],
GC.Values[GC.CSV_OUTPUT_ROW_FILTER],
GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE],
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER],
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE],
GC.Values[GC.CSV_OUTPUT_ROW_LIMIT],
GC.Values[GC.SHOW_GETTINGS], GC.Values[GC.SHOW_GETTINGS_GOT_NL],
item])
poolProcessResults[0] += 1
if parallelPoolProcesses > 0:
while poolProcessResults[0] == parallelPoolProcesses:
completedProcesses = []
for p, result in iter(poolProcessResults.items()):
if p != 0 and result.ready():
poolCallback(result.get())
completedProcesses.append(p)
if completedProcesses:
for p in completedProcesses:
del poolProcessResults[p]
break
time.sleep(1)
processWaitStart = time.time()
if not controlC:
if GC.Values[GC.PROCESS_WAIT_LIMIT] > 0:
waitRemaining = GC.Values[GC.PROCESS_WAIT_LIMIT]
else:
waitRemaining = 'unlimited'
while poolProcessResults[0] > 0:
batchWriteStderr(Msg.BATCH_CSV_WAIT_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, poolProcessResults[0],
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1],
Msg.BATCH_CSV_WAIT_LIMIT.format(waitRemaining)))
completedProcesses = []
for p, result in iter(poolProcessResults.items()):
if p != 0 and result.ready():
poolCallback(result.get())
completedProcesses.append(p)
for p in completedProcesses:
del poolProcessResults[p]
if poolProcessResults[0] > 0:
if controlC:
handleControlC('SIG')
break
time.sleep(5)
if GC.Values[GC.PROCESS_WAIT_LIMIT] > 0:
delta = int(time.time()-processWaitStart)
if delta >= GC.Values[GC.PROCESS_WAIT_LIMIT]:
batchWriteStderr(Msg.BATCH_CSV_TERMINATE_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, poolProcessResults[0],
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1]))
pool.terminate()
break
waitRemaining = GC.Values[GC.PROCESS_WAIT_LIMIT] - delta
pool.close()
else:
handleControlC('SIG')
except KeyboardInterrupt:
handleControlC('KBI')
pool.join()
batchWriteStderr(Msg.BATCH_CSV_PROCESSING_COMPLETE.format(currentISOformatTimeStamp(), numItems))
if mpQueueCSVFile:
terminateCSVFileQueueHandler(mpQueueCSVFile, mpQueueHandlerCSVFile)
if mpQueueStdout:
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_END, [GM.Globals[GM.SYSEXITRC], GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()]))
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].close()
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = None
terminateStdQueueHandler(mpQueueStdout, mpQueueHandlerStdout)
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_END, [GM.Globals[GM.SYSEXITRC], GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()]))
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].close()
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = None
terminateStdQueueHandler(mpQueueStderr, mpQueueHandlerStderr)
def threadBatchWorker(showCmds=False, numItems=0):
while True:
pid, item, logCmd = GM.Globals[GM.TBATCH_QUEUE].get()
try:
sysRC = subprocess.call(item, stdout=GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout),
stderr=GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr))
if showCmds:
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},End,{sysRC},{logCmd}\n')
if GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] is not None and checkChildProcessRC(sysRC):
GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING] = True
except Exception as e:
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Error,{str(e)},{logCmd}\n')
GM.Globals[GM.TBATCH_QUEUE].task_done()
BATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD]
TBATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.EXECUTE_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD]
def ThreadBatchGAMCommands(items, showCmds):
if not items:
return
pythonCmd = [sys.executable]
if not getattr(sys, 'frozen', False): # we're not frozen
pythonCmd.append(os.path.realpath(Cmd.Argument(0)))
GM.Globals[GM.NUM_BATCH_ITEMS] = numItems = len(items)
numWorkerThreads = min(numItems, GC.Values[GC.NUM_TBATCH_THREADS])
# GM.Globals[GM.TBATCH_QUEUE].put() gets blocked when trying to create more items than there are workers
GM.Globals[GM.TBATCH_QUEUE] = queue.Queue(maxsize=numWorkerThreads)
batchWriteStderr(Msg.USING_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, numWorkerThreads,
THREAD_PLURAL_SINGULAR[numWorkerThreads == 1]))
for _ in range(numWorkerThreads):
t = threading.Thread(target=threadBatchWorker, kwargs={'showCmds': showCmds, 'numItems': numItems})
t.daemon = True
t.start()
pid = 0
numThreadsInUse = 0
for item in items:
if GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING]:
break
if item[0] == Cmd.COMMIT_BATCH_CMD:
batchWriteStderr(Msg.COMMIT_BATCH_WAIT_N_PROCESSES.format(currentISOformatTimeStamp(),
numItems, numThreadsInUse,
THREAD_PLURAL_SINGULAR[numThreadsInUse == 1]))
GM.Globals[GM.TBATCH_QUEUE].join()
batchWriteStderr(Msg.COMMIT_BATCH_COMPLETE.format(currentISOformatTimeStamp(), numItems, Msg.THREADS))
numThreadsInUse = 0
if len(item) > 1:
readStdin(f'{currentISOformatTimeStamp()},0/{numItems},{Cmd.QuotedArgumentList(item[1:])}')
continue
if item[0] == Cmd.PRINT_CMD:
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},{Cmd.QuotedArgumentList(item[1:])}\n')
continue
if item[0] == Cmd.SLEEP_CMD:
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},Sleeping {item[1]} seconds\n')
time.sleep(int(item[1]))
continue
pid += 1
if not showCmds and ((pid % 100 == 0) or (pid == numItems)):
batchWriteStderr(Msg.PROCESSING_ITEM_N_OF_M.format(currentISOformatTimeStamp(), pid, numItems))
if showCmds:
logCmd = Cmd.QuotedArgumentList(item)
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Start,{Cmd.QuotedArgumentList(item)}\n')
else:
logCmd = ''
if item[0] == Cmd.GAM_CMD:
GM.Globals[GM.TBATCH_QUEUE].put((pid, pythonCmd+item[1:], logCmd))
else:
GM.Globals[GM.TBATCH_QUEUE].put((pid, item[1:], logCmd))
numThreadsInUse += 1
GM.Globals[GM.TBATCH_QUEUE].join()
if showCmds:
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},Complete\n')
def _getShowCommands():
if checkArgumentPresent('showcmds'):
return getBoolean()
return GC.Values[GC.SHOW_COMMANDS]
def _getSkipRows():
if checkArgumentPresent('skiprows'):
return getInteger(minVal=0)
# return GC.Values[GC.CSV_INPUT_ROW_SKIP]
return 0
def _getMaxRows():
if checkArgumentPresent('maxrows'):
return getInteger(minVal=0)
return GC.Values[GC.CSV_INPUT_ROW_LIMIT]
# gam batch <BatchContent> [showcmds [<Boolean>]]
def doBatch(threadBatch=False):
filename = getString(Cmd.OB_FILE_NAME)
if (filename == '-') and (GC.Values[GC.DEBUG_LEVEL] > 0):
Cmd.Backup()
usageErrorExit(Msg.BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE.format(Cmd.BATCH_CMD))
filenameLower = filename.lower()
if filenameLower not in {'gdoc', 'gcsdoc'}:
encoding = getCharSet()
f = openFile(filename, encoding=encoding, stripUTFBOM=True)
elif filenameLower == 'gdoc':
f = getGDocData(filenameLower)
getCharSet()
else: #filenameLower == 'gcsdoc':
f = getStorageFileData(filenameLower)
getCharSet()
showCmds = _getShowCommands()
checkForExtraneousArguments()
validCommands = BATCH_COMMANDS if not threadBatch else TBATCH_COMMANDS
kwValues = {}
items = []
errors = 0
try:
for line in f:
if line.startswith('#'):
continue
if kwValues:
for kw, value in iter(kwValues.items()):
line = line.replace(f'%{kw}%', value)
try:
argv = shlex.split(line)
except ValueError as e:
writeStderr(f'Command: >>>{line.strip()}<<<\n')
writeStderr(f'{ERROR_PREFIX}{str(e)}\n')
errors += 1
continue
if argv:
cmd = argv[0].strip().lower()
if cmd == Cmd.SET_CMD:
if len(argv) == 3:
kwValues[argv[1]] = argv[2]
else:
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.SET_CMD} keyword value>)>\n')
errors += 1
continue
if cmd == Cmd.CLEAR_CMD:
if len(argv) == 2:
kwValues.pop(argv[1], None)
else:
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.CLEAR_CMD} keyword>)>\n')
errors += 1
continue
if cmd == Cmd.SLEEP_CMD:
if len(argv) != 2 or not argv[1].isdigit():
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.SLEEP_CMD} integer>)>\n')
errors += 1
continue
if (not cmd) or ((len(argv) == 1) and (cmd not in [Cmd.COMMIT_BATCH_CMD, Cmd.PRINT_CMD])):
continue
if cmd in validCommands:
items.append(argv)
else:
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{formatChoiceList(validCommands)}>\n')
errors += 1
except IOError as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
closeFile(f)
if errors == 0:
if not threadBatch:
MultiprocessGAMCommands(items, showCmds)
else:
ThreadBatchGAMCommands(items, showCmds)
else:
writeStderr(Msg.BATCH_NOT_PROCESSED_ERRORS.format(ERROR_PREFIX, filename, errors, ERROR_PLURAL_SINGULAR[errors == 1]))
setSysExitRC(USAGE_ERROR_RC)
# gam tbatch <BatchContent> [showcmds [<Boolean>]]
def doThreadBatch():
adjustRedirectedSTDFilesIfNotMultiprocessing()
doBatch(True)
def doAutoBatch(entityType, entityList, CL_command):
remaining = Cmd.Remaining()
items = []
initial_argv = [Cmd.GAM_CMD]
if GM.Globals[GM.SECTION] and not GM.Globals[GM.GAM_CFG_SECTION]:
initial_argv.extend([Cmd.SELECT_CMD, GM.Globals[GM.SECTION]])
for entity in entityList:
items.append(initial_argv+[entityType, entity, CL_command]+remaining)
MultiprocessGAMCommands(items, GC.Values[GC.SHOW_COMMANDS])
# Process command line arguments, find substitutions
# An argument containing instances of ~~xxx~!~pattern~!~replacement~~ has ~~...~~ replaced by re.sub(pattern, replacement, value of field xxx from the CSV file)
# For example, ~~primaryEmail~!~^(.+)@(.+)$~!~\1 AT \2~~ would replace foo@bar.com (from the primaryEmail column) with foo AT bar.com
# An argument containing instances of ~~xxx~~ has xxx replaced by the value of field xxx from the CSV file
# An argument containing exactly ~xxx is replaced by the value of field xxx from the CSV file
# Otherwise, the argument is preserved as is
SUB_PATTERN = re.compile(r'~~(.+?)~~')
RE_PATTERN = re.compile(r'~~(.+?)~!~(.+?)~!~(.+?)~~')
SUB_TYPE = 'sub'
RE_TYPE = 're'
# SubFields is a dictionary; the key is the argument number, the value is a list of tuples that mark
# the substition (type, fieldname, start, end). Type is 'sub' for simple substitution, 're' for regex substitution.
# Example: update user '~User' address type work unstructured '~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~' primary
# {2: [('sub', 'User', 0, 5)], 7: [('sub', 'Street', 0, 10), ('sub', 'City', 12, 20), ('sub', 'State', 22, 31), ('sub', 'ZIP', 32, 39)]}
def getSubFields(initial_argv, fieldNames):
subFields = {}
GAM_argv = initial_argv[:]
GAM_argvI = len(GAM_argv)
while Cmd.ArgumentsRemaining():
myarg = Cmd.Current()
if not myarg:
GAM_argv.append(myarg)
elif SUB_PATTERN.search(myarg):
pos = 0
subFields.setdefault(GAM_argvI, [])
while True:
submatch = SUB_PATTERN.search(myarg, pos)
if not submatch:
break
rematch = RE_PATTERN.match(submatch.group(0))
if not rematch:
fieldName = submatch.group(1)
if fieldName not in fieldNames:
csvFieldErrorExit(fieldName, fieldNames)
subFields[GAM_argvI].append((SUB_TYPE, fieldName, submatch.start(), submatch.end()))
else:
fieldName = rematch.group(1)
if fieldName not in fieldNames:
csvFieldErrorExit(fieldName, fieldNames)
try:
re.compile(rematch.group(2))
subFields[GAM_argvI].append((RE_TYPE, fieldName, submatch.start(), submatch.end(), rematch.group(2), rematch.group(3)))
except re.error as e:
usageErrorExit(f'{Cmd.OB_RE_PATTERN} {Msg.ERROR}: {e}')
pos = submatch.end()
GAM_argv.append(myarg)
elif myarg[0] == '~':
fieldName = myarg[1:]
if fieldName in fieldNames:
subFields[GAM_argvI] = [(SUB_TYPE, fieldName, 0, len(myarg))]
GAM_argv.append(myarg)
else:
csvFieldErrorExit(fieldName, fieldNames)
else:
GAM_argv.append(myarg)
GAM_argvI += 1
Cmd.Advance()
return(GAM_argv, subFields)
def processSubFields(GAM_argv, row, subFields):
argv = GAM_argv[:]
for GAM_argvI, fields in iter(subFields.items()):
oargv = argv[GAM_argvI][:]
argv[GAM_argvI] = ''
pos = 0
for field in fields:
argv[GAM_argvI] += oargv[pos:field[2]]
if field[0] == SUB_TYPE:
if row[field[1]]:
argv[GAM_argvI] += row[field[1]]
else:
if row[field[1]]:
argv[GAM_argvI] += re.sub(field[4], field[5], row[field[1]])
pos = field[3]
argv[GAM_argvI] += oargv[pos:]
return argv
# gam csv <CSVLoopContent> [warnifnodata]
# [columndelimiter <Character>] [quotechar <Character>] [fields <FieldNameList>]
# (matchfield|skipfield <FieldName> <RESearchPattern>)* [showcmds [<Boolean>]]
# [skiprows <Integer>] [maxrows <Integer>]
# gam <GAM argument list>
def doCSV(testMode=False):
filename = getString(Cmd.OB_FILE_NAME)
if (filename == '-') and (GC.Values[GC.DEBUG_LEVEL] > 0):
Cmd.Backup()
usageErrorExit(Msg.BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE.format(Cmd.CSV_CMD))
f, csvFile, fieldnames = openCSVFileReader(filename)
matchFields, skipFields = getMatchSkipFields(fieldnames)
showCmds = _getShowCommands()
skipRows = _getSkipRows()
maxRows = _getMaxRows()
checkArgumentPresent(Cmd.GAM_CMD, required=True)
if not Cmd.ArgumentsRemaining():
missingArgumentExit(Cmd.OB_GAM_ARGUMENT_LIST)
initial_argv = [Cmd.GAM_CMD]
if GM.Globals[GM.SECTION] and not GM.Globals[GM.GAM_CFG_SECTION] and not Cmd.PeekArgumentPresent(Cmd.SELECT_CMD):
initial_argv.extend([Cmd.SELECT_CMD, GM.Globals[GM.SECTION]])
GAM_argv, subFields = getSubFields(initial_argv, fieldnames)
if GC.Values[GC.CSV_INPUT_ROW_FILTER] or GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER]:
CheckInputRowFilterHeaders(fieldnames, GC.Values[GC.CSV_INPUT_ROW_FILTER], GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER])
items = []
i = 0
for row in csvFile:
if checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
i += 1
if skipRows:
if i <= skipRows:
continue
i = 1
skipRows = 0
items.append(processSubFields(GAM_argv, row, subFields))
if maxRows and i >= maxRows:
break
closeFile(f)
if not testMode:
MultiprocessGAMCommands(items, showCmds)
else:
numItems = min(len(items), 10)
writeStdout(Msg.CSV_FILE_HEADERS.format(filename))
Ind.Increment()
for field in fieldnames:
writeStdout(f'{Ind.Spaces()}{field}\n')
Ind.Decrement()
writeStdout(Msg.CSV_SAMPLE_COMMANDS.format(numItems, GAM))
Ind.Increment()
for i in range(numItems):
writeStdout(f'{Ind.Spaces()}{Cmd.QuotedArgumentList(items[i])}\n')
Ind.Decrement()
def doCSVTest():
doCSV(testMode=True)
# gam loop <CSVLoopContent> [warnifnodata]
# [columndelimiter <Character>] [quotechar <Character>] [fields <FieldNameList>]
# (matchfield|skipfield <FieldName> <RESearchPattern>)* [showcmds [<Boolean>]]
# [skiprows <Integer>] [maxrows <Integer>]
# gam <GAM argument list>
def doLoop(loopCmd):
filename = getString(Cmd.OB_FILE_NAME)
if (filename == '-') and (GC.Values[GC.DEBUG_LEVEL] > 0):
Cmd.Backup()
usageErrorExit(Msg.BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE.format(Cmd.LOOP_CMD))
f, csvFile, fieldnames = openCSVFileReader(filename)
matchFields, skipFields = getMatchSkipFields(fieldnames)
showCmds = _getShowCommands()
skipRows = _getSkipRows()
maxRows = _getMaxRows()
checkArgumentPresent(Cmd.GAM_CMD, required=True)
if not Cmd.ArgumentsRemaining():
missingArgumentExit(Cmd.OB_GAM_ARGUMENT_LIST)
if GC.Values[GC.CSV_INPUT_ROW_FILTER] or GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER]:
CheckInputRowFilterHeaders(fieldnames, GC.Values[GC.CSV_INPUT_ROW_FILTER], GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER])
choice = Cmd.Current().strip().lower()
if choice == Cmd.LOOP_CMD:
usageErrorExit(Msg.NESTED_LOOP_CMD_NOT_ALLOWED)
# gam loop ... gam redirect|select|config ... process gam.cfg on each iteration
# gam redirect|select|config ... loop ... gam redirect|select|config ... process gam.cfg on each iteration
# gam loop ... gam !redirect|select|config ... no further processing of gam.cfg
# gam redirect|select|config ... loop ... gam !redirect|select|config ... no further processing of gam.cfg
processGamCfg = choice in Cmd.GAM_META_COMMANDS
GAM_argv, subFields = getSubFields([Cmd.GAM_CMD], fieldnames)
multi = GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS]
if multi:
mpManager = multiprocessing.Manager()
mpQueue, mpQueueHandler = initializeCSVFileQueueHandler(mpManager, None, None)
else:
mpQueue = None
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = mpQueue
# Set up command logging at top level only
if GM.Globals[GM.CMDLOG_LOGGER]:
LoopGlobals = GM.Globals
else:
LoopGlobals = {GM.CMDLOG_LOGGER: None, GM.CMDLOG_HANDLER: None}
if (GM.Globals[GM.PID] > 0) and GC.Values[GC.CMDLOG]:
openGAMCommandLog(LoopGlobals, 'looplog')
if LoopGlobals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(LoopGlobals, loopCmd, '*')
if not showCmds:
i = 0
for row in csvFile:
if checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
i += 1
if skipRows:
if i <= skipRows:
continue
i = 1
skipRows = 0
item = processSubFields(GAM_argv, row, subFields)
logCmd = Cmd.QuotedArgumentList(item)
if i % 100 == 0:
batchWriteStderr(Msg.PROCESSING_ITEM_N.format(currentISOformatTimeStamp(), i))
sysRC = ProcessGAMCommand(item, processGamCfg=processGamCfg, inLoop=True)
if (GM.Globals[GM.PID] > 0) and LoopGlobals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(LoopGlobals, logCmd, sysRC)
if (sysRC > 0) and (GM.Globals[GM.SYSEXITRC] <= HARD_ERROR_RC):
break
if maxRows and i >= maxRows:
break
closeFile(f)
else:
items = []
i = 0
for row in csvFile:
if checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
i += 1
if skipRows:
if i <= skipRows:
continue
i = 1
skipRows = 0
items.append(processSubFields(GAM_argv, row, subFields))
if maxRows and i >= maxRows:
break
closeFile(f)
numItems = len(items)
pid = 0
for item in items:
pid += 1
logCmd = Cmd.QuotedArgumentList(item)
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Start,0,{logCmd}\n')
sysRC = ProcessGAMCommand(item, processGamCfg=processGamCfg, inLoop=True)
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},End,{sysRC},{logCmd}\n')
if (GM.Globals[GM.PID] > 0) and LoopGlobals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(LoopGlobals, logCmd, sysRC)
if (sysRC > 0) and (GM.Globals[GM.SYSEXITRC] <= HARD_ERROR_RC):
break
if (GM.Globals[GM.PID] > 0) and LoopGlobals[GM.CMDLOG_LOGGER]:
closeGAMCommandLog(LoopGlobals)
if multi:
terminateCSVFileQueueHandler(mpQueue, mpQueueHandler)
def _doList(entityList, entityType):
buildGAPIObject(API.DIRECTORY)
if GM.Globals[GM.CSV_DATA_DICT]:
keyField = GM.Globals[GM.CSV_KEY_FIELD]
dataField = GM.Globals[GM.CSV_DATA_FIELD]
else:
keyField = 'Entity'
dataField = 'Data'
csvPF = CSVPrintFile(keyField)
if checkArgumentPresent('todrive'):
csvPF.GetTodriveParameters()
if entityList is None:
entityList = getEntityList(Cmd.OB_ENTITY)
showData = checkArgumentPresent('data')
if showData:
if not entityType:
itemType, itemList = getEntityToModify(crosAllowed=True)
else:
itemType = None
itemList = getEntityList(Cmd.OB_ENTITY)
entityItemLists = itemList if isinstance(itemList, dict) else None
csvPF.AddTitle(dataField)
else:
entityItemLists = None
dataDelimiter = getDelimiter()
checkForExtraneousArguments()
_, _, entityList = getEntityArgument(entityList)
for entity in entityList:
entityEmail = normalizeEmailAddressOrUID(entity)
if showData:
if entityItemLists:
if entity not in entityItemLists:
csvPF.WriteRow({keyField: entityEmail})
continue
itemList = entityItemLists[entity]
if itemType == Cmd.ENTITY_USERS:
for i, item in enumerate(itemList):
itemList[i] = normalizeEmailAddressOrUID(item)
if dataDelimiter:
csvPF.WriteRow({keyField: entityEmail, dataField: dataDelimiter.join(itemList)})
else:
for item in itemList:
csvPF.WriteRow({keyField: entityEmail, dataField: item})
else:
csvPF.WriteRow({keyField: entityEmail})
csvPF.writeCSVfile('Entity')
# gam list [todrive <ToDriveAttribute>*] <EntityList> [data <CrOSTypeEntity>|<UserTypeEntity> [delimiter <Character>]]
def doListType():
_doList(None, None)
# gam <CrOSTypeEntity> list [todrive <ToDriveAttribute>*] [data <EntityList> [delimiter <Character>]]
def doListCrOS(entityList):
_doList(entityList, Cmd.ENTITY_CROS)
# gam <UserTypeEntity> list [todrive <ToDriveAttribute>*] [data <EntityList> [delimiter <Character>]]
def doListUser(entityList):
_doList(entityList, Cmd.ENTITY_USERS)
def _showCount(entityList, entityType):
buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
_, count, entityList = getEntityArgument(entityList)
actionPerformedNumItems(count, entityType)
# gam <CrOSTypeEntity> show count
def showCountCrOS(entityList):
_showCount(entityList, Ent.CHROME_DEVICE)
# gam <UserTypeEntity> show count
def showCountUser(entityList):
_showCount(entityList, Ent.USER)
VALIDEMAIL_PATTERN = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
def _getValidateLoginHint(login_hint, projectId=None):
while True:
if not login_hint:
if not projectId:
login_hint = readStdin(Msg.ENTER_GSUITE_ADMIN_EMAIL_ADDRESS).strip()
else:
login_hint = readStdin(Msg.ENTER_MANAGE_GCP_PROJECT_EMAIL_ADDRESS.format(projectId)).strip()
if login_hint.find('@') == -1 and GC.Values[GC.DOMAIN]:
login_hint = f'{login_hint}@{GC.Values[GC.DOMAIN]}'
if VALIDEMAIL_PATTERN.match(login_hint):
return login_hint
sys.stdout.write(f'{ERROR_PREFIX}Invalid email address: {login_hint}\n')
login_hint = None
def getOAuthClientIDAndSecret():
cs_data = readFile(GC.Values[GC.CLIENT_SECRETS_JSON], continueOnError=True, displayError=True)
if not cs_data:
invalidClientSecretsJsonExit(Msg.NO_DATA)
try:
cs_json = json.loads(cs_data)
if not cs_json:
systemErrorExit(CLIENT_SECRETS_JSON_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_CREATE_UPDATE_ALLOWED)
return (cs_json['installed']['client_id'], cs_json['installed']['client_secret'])
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidClientSecretsJsonExit(str(e))
def getScopesFromUser(scopesList, clientAccess, currentScopes=None):
OAUTH2_CMDS = ['s', 'u', 'e', 'c']
oauth2_menu = ''
numScopes = len(scopesList)
for a_scope in scopesList:
oauth2_menu += f"[%%s] %2d) {a_scope['name']}"
if a_scope['subscopes']:
oauth2_menu += f' (supports {" and ".join(a_scope["subscopes"])})'
oauth2_menu += '\n'
oauth2_menu += '''
Select an unselected scope [ ] by entering a number; yields [*]
For scopes that support readonly, enter a number and an 'r' to grant read-only access; yields [R]
For scopes that support action, enter a number and an 'a' to grant action-only access; yields [A]
Clear read-only access [R] or action-only access [A] from a scope by entering a number; yields [*]
Unselect a selected scope [*] by entering a number; yields [ ]
Select all default scopes by entering an 's'; yields [*] for default scopes, [ ] for others
Unselect all scopes by entering a 'u'; yields [ ] for all scopes
Exit without changes/authorization by entering an 'e'
Continue to authorization by entering a 'c'
'''
if clientAccess:
oauth2_menu += ''' Note, if all scopes are selected, Google will probably generate an authorization error
'''
menu = oauth2_menu % tuple(range(numScopes))
selectedScopes = ['*'] * numScopes
if currentScopes is None and clientAccess:
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
with lock:
_, credentials = getOauth2TxtCredentials(exitOnError=False)
if credentials and credentials.scopes is not None:
currentScopes = sorted(credentials.scopes)
if currentScopes is not None:
if clientAccess:
i = 0
for a_scope in scopesList:
selectedScopes[i] = ' '
possibleScope = a_scope['scope']
for currentScope in currentScopes:
if currentScope == possibleScope:
selectedScopes[i] = '*'
break
if 'readonly' in a_scope['subscopes']:
if currentScope == possibleScope+'.readonly':
selectedScopes[i] = 'R'
break
if 'action' in a_scope['subscopes']:
if currentScope == possibleScope+'.action':
selectedScopes[i] = 'A'
break
i += 1
else:
i = 0
for a_scope in scopesList:
selectedScopes[i] = ' '
api = a_scope['api']
possibleScope = a_scope['scope']
if api in currentScopes:
for scope in currentScopes[api]:
if scope == possibleScope:
selectedScopes[i] = '*'
break
if 'readonly' in a_scope['subscopes']:
if (scope == possibleScope+'.readonly') or (scope == a_scope.get('roscope')):
selectedScopes[i] = 'R'
break
i += 1
else:
i = 0
for a_scope in scopesList:
if a_scope.get('offByDefault'):
selectedScopes[i] = ' '
elif a_scope.get('roByDefault'):
selectedScopes[i] = 'R'
else:
selectedScopes[i] = '*'
i += 1
prompt = f'\nPlease enter 0-{numScopes-1}[a|r] or {"|".join(OAUTH2_CMDS)}: '
while True:
os.system(['clear', 'cls'][sys.platform.startswith('win')])
sys.stdout.write(menu % tuple(selectedScopes))
while True:
choice = readStdin(prompt)
if choice:
selection = choice.lower()
if selection.find('r') >= 0:
mode = 'R'
selection = selection.replace('r', '')
elif selection.find('a') >= 0:
mode = 'A'
selection = selection.replace('a', '')
else:
mode = ' '
if selection and selection.isdigit():
selection = int(selection)
if isinstance(selection, int) and selection < numScopes:
if mode == 'R':
if 'readonly' not in scopesList[selection]['subscopes']:
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support read-only mode!\n')
continue
elif mode == 'A':
if 'action' not in scopesList[selection]['subscopes']:
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support action-only mode!\n')
continue
elif selectedScopes[selection] != '*':
mode = '*'
else:
mode = ' '
selectedScopes[selection] = mode
break
if isinstance(selection, str) and selection in OAUTH2_CMDS:
if selection == 's':
i = 0
for a_scope in scopesList:
selectedScopes[i] = ' ' if a_scope.get('offByDefault', False) else '*'
i += 1
elif selection == 'u':
for i in range(numScopes):
selectedScopes[i] = ' '
elif selection == 'e':
return None
break
sys.stdout.write(f'{ERROR_PREFIX}Invalid input "{choice}"\n')
if selection == 'c':
break
return selectedScopes
def _localhost_to_ip():
'''returns IPv4 or IPv6 loopback address which localhost resolves to.
If localhost does not resolve to valid loopback IP address then returns
127.0.0.1'''
# TODO gethostbyname() will only ever return ipv4
# find a way to support IPv6 here and get preferred IP
# note that IPv6 may be broken on some systems also :-(
# for now IPv4 should do.
local_ip = socket.gethostbyname('localhost')
# local_ip = socket.getaddrinfo('localhost', None)[0][-1][0] # works with ipv6, makes wsgiref fail
if not ipaddress.ip_address(local_ip).is_loopback:
local_ip = '127.0.0.1'
return local_ip
def _waitForHttpClient(d):
wsgi_app = google_auth_oauthlib.flow._RedirectWSGIApp(Msg.AUTHENTICATION_FLOW_COMPLETE_CLOSE_BROWSER.format(GAM))
wsgiref.simple_server.WSGIServer.allow_reuse_address = False
# Convert hostname to IP since apparently binding to the IP
# reduces odds of firewall blocking us
local_ip = _localhost_to_ip()
for port in range(8080, 8099):
try:
local_server = wsgiref.simple_server.make_server(
local_ip,
port,
wsgi_app,
handler_class=wsgiref.simple_server.WSGIRequestHandler
)
break
except OSError:
pass
redirect_uri_format = "http://{}:{}/" if d['trailing_slash'] else "http://{}:{}"
# provide redirect_uri to main process so it can formulate auth_url
d['redirect_uri'] = redirect_uri_format.format(*local_server.server_address)
# wait until main process provides auth_url
# so we can open it in web browser.
while 'auth_url' not in d:
time.sleep(0.1)
if d['open_browser']:
webbrowser.open(d['auth_url'], new=1, autoraise=True)
try:
local_server.handle_request()
authorization_response = wsgi_app.last_request_uri.replace("http", "https")
d['code'] = authorization_response
except:
pass
local_server.server_close()
def _waitForUserInput(d):
sys.stdin = open(0, DEFAULT_FILE_READ_MODE, encoding=UTF8)
d['code'] = readStdin(Msg.ENTER_VERIFICATION_CODE_OR_URL)
class _GamOauthFlow(google_auth_oauthlib.flow.InstalledAppFlow):
def run_dual(self, **kwargs):
mgr = multiprocessing.Manager()
d = mgr.dict()
d['trailing_slash'] = True
d['open_browser'] = not GC.Values[GC.NO_BROWSER]
httpClientProcess = multiprocessing.Process(target=_waitForHttpClient, args=(d,))
userInputProcess = multiprocessing.Process(target=_waitForUserInput, args=(d,))
httpClientProcess.start()
# we need to wait until web server starts on avail port
# so we know redirect_uri to use
while 'redirect_uri' not in d:
time.sleep(0.1)
self.redirect_uri = d['redirect_uri']
d['auth_url'], _ = super().authorization_url(**kwargs)
d['auth_url'] = shortenURL(d['auth_url'])
print(Msg.OAUTH2_GO_TO_LINK_MESSAGE.format(url=d['auth_url']))
userInputProcess.start()
userInput = False
checkHttp = checkUser = True
alive = 2
while alive > 0:
time.sleep(0.1)
if checkHttp and not httpClientProcess.is_alive():
if 'code' in d:
if checkUser:
userInputProcess.terminate()
break
checkHttp = False
alive -= 1
if checkUser and not userInputProcess.is_alive():
userInput = True
if 'code' in d:
if checkHttp:
httpClientProcess.terminate()
break
checkUser = False
alive -= 1
if 'code' not in d:
systemErrorExit(SYSTEM_ERROR_RC, Msg.AUTHENTICATION_FLOW_FAILED)
while True:
code = d['code']
if code.startswith('http'):
parsed_url = urlparse(code)
parsed_params = parse_qs(parsed_url.query)
code = parsed_params.get('code', [None])[0]
try:
fetch_args = {'code': code}
if GC.Values[GC.CACERTS_PEM]:
fetch_args['verify'] = GC.Values[GC.CACERTS_PEM]
self.fetch_token(**fetch_args)
break
except Exception as e:
if not userInput:
systemErrorExit(INVALID_TOKEN_RC, str(e))
stderrErrorMsg(str(e))
_waitForUserInput(d)
print(Msg.AUTHENTICATION_FLOW_COMPLETE)
return self.credentials
class Credentials(google.oauth2.credentials.Credentials):
"""Google OAuth2.0 Credentials with GAM-specific properties and methods."""
def __init__(self,
token,
refresh_token=None,
id_token=None,
token_uri=None,
client_id=None,
client_secret=None,
scopes=None,
quota_project_id=None,
expiry=None,
id_token_data=None,
filename=None):
"""A thread-safe OAuth2.0 credentials object.
Credentials adds additional utility properties and methods to a
standard OAuth2.0 credentials object. When used to store credentials on
disk, it implements a file lock to avoid collision during writes.
Args:
token: Optional String, The OAuth 2.0 access token. Can be None if refresh
information is provided.
refresh_token: String, The OAuth 2.0 refresh token. If specified,
credentials can be refreshed.
id_token: String, The Open ID Connect ID Token.
token_uri: String, The OAuth 2.0 authorization server's token endpoint
URI. Must be specified for refresh, can be left as None if the token can
not be refreshed.
client_id: String, The OAuth 2.0 client ID. Must be specified for refresh,
can be left as None if the token can not be refreshed.
client_secret: String, The OAuth 2.0 client secret. Must be specified for
refresh, can be left as None if the token can not be refreshed.
scopes: Sequence[str], The scopes used to obtain authorization.
This parameter is used by :meth:`has_scopes`. OAuth 2.0 credentials can
not request additional scopes after authorization. The scopes must be
derivable from the refresh token if refresh information is provided
(e.g. The refresh token scopes are a superset of this or contain a
wild card scope like
'https://www.googleapis.com/auth/any-api').
quota_project_id: String, The project ID used for quota and billing. This
project may be different from the project used to create the
credentials.
expiry: datetime.datetime, The time at which the provided token will
expire.
id_token_data: Oauth2.0 ID Token data which was previously fetched for
this access token against the google.oauth2.id_token library.
filename: String, Path to a file that will be used to store the
credentials. If provided, a lock file of the same name and a ".lock"
extension will be created for concurrency controls. Note: New
credentials are not saved to disk until write() or refresh() are
called.
Raises:
TypeError: If id_token_data is not the required dict type.
"""
super().__init__(token=token,
refresh_token=refresh_token,
id_token=id_token,
token_uri=token_uri,
client_id=client_id,
client_secret=client_secret,
scopes=scopes,
quota_project_id=quota_project_id)
# Load data not restored by the super class
self.expiry = expiry
if id_token_data and not isinstance(id_token_data, dict):
raise TypeError(f'Expected type id_token_data dict but received {type(id_token_data)}')
self._id_token_data = id_token_data.copy() if id_token_data else None
# If a filename is provided, use a lock file to control concurrent access
# to the resource. If no filename is provided, use a thread lock that has
# the same interface as FileLock in order to simplify the implementation.
if filename:
# Convert relative paths into absolute
self._filename = os.path.abspath(filename)
else:
self._filename = None
# Use a property to prevent external mutation of the filename.
@property
def filename(self):
return self._filename
@classmethod
def from_authorized_user_info_gam(cls, info, filename=None):
"""Generates Credentials from JSON containing authorized user info.
Args:
info: Dict, authorized user info in Google format.
filename: String, the filename used to store these credentials on disk. If
no filename is provided, the credentials will not be saved to disk.
Raises:
ValueError: If missing fields are detected in the info.
"""
# We need all of these keys
keys_needed = {'client_id', 'client_secret'}
# We need 1 or more of these keys
keys_need_one_of = {'refresh_token', 'auth_token', 'token'}
missing = keys_needed.difference(info.keys())
has_one_of = set(info) & keys_need_one_of
if missing or not has_one_of:
raise ValueError(
'Authorized user info was not in the expected format, missing '
f'fields {", ".join(missing)} and one of {", ".join(keys_need_one_of)}.')
expiry = info.get('token_expiry')
if expiry:
# Convert the raw expiry to datetime
expiry = datetime.datetime.strptime(expiry, YYYYMMDDTHHMMSSZ_FORMAT)
id_token_data = info.get('decoded_id_token')
# Provide backwards compatibility with field names when loading from JSON.
# Some field names may be different, depending on when/how the credentials
# were pickled.
return cls(token=info.get('token', info.get('auth_token', '')),
refresh_token=info.get('refresh_token', ''),
id_token=info.get('id_token_jwt', info.get('id_token')),
token_uri=info.get('token_uri'),
client_id=info['client_id'],
client_secret=info['client_secret'],
scopes=info.get('scopes'),
quota_project_id=info.get('quota_project_id'),
expiry=expiry,
id_token_data=id_token_data,
filename=filename)
@classmethod
def from_google_oauth2_credentials(cls, credentials, filename=None):
"""Generates Credentials from a google.oauth2.Credentials object."""
info = json.loads(credentials.to_json())
# Add properties which are not exported with the native to_json() output.
info['id_token'] = credentials.id_token
if credentials.expiry:
info['token_expiry'] = credentials.expiry.strftime(YYYYMMDDTHHMMSSZ_FORMAT)
info['quota_project_id'] = credentials.quota_project_id
return cls.from_authorized_user_info_gam(info, filename=filename)
@classmethod
def from_client_secrets(cls,
client_id,
client_secret,
scopes,
access_type='offline',
login_hint=None,
filename=None,
open_browser=True):
"""Runs an OAuth Flow from client secrets to generate credentials.
Args:
client_id: String, The OAuth2.0 Client ID.
client_secret: String, The OAuth2.0 Client Secret.
scopes: Sequence[str], A list of scopes to include in the credentials.
access_type: String, 'offline' or 'online'. Indicates whether your
application can refresh access tokens when the user is not present at
the browser. Valid parameter values are online, which is the default
value, and offline. Set the value to offline if your application needs
to refresh access tokens when the user is not present at the browser.
This is the method of refreshing access tokens described later in this
document. This value instructs the Google authorization server to return
a refresh token and an access token the first time that your application
exchanges an authorization code for tokens.
login_hint: String, The email address that will be displayed on the Google
login page as a hint for the user to login to the correct account.
filename: String, the path to a file to use to save the credentials.
open_browser: Boolean: whether or not GAM should try to open the browser
automatically.
Returns:
Credentials
"""
client_config = {
'installed': {
'client_id': client_id,
'client_secret': client_secret,
'redirect_uris': ['http://localhost'],
'auth_uri': API.GOOGLE_OAUTH2_ENDPOINT,
'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
}
}
flow = _GamOauthFlow.from_client_config(client_config,
scopes,
autogenerate_code_verifier=True)
flow_kwargs = {'access_type': access_type,
'open_browser': open_browser}
if login_hint:
flow_kwargs['login_hint'] = login_hint
flow.run_dual(**flow_kwargs)
return cls.from_google_oauth2_credentials(flow.credentials, filename=filename)
def to_json(self, strip=None):
"""Creates a JSON representation of a Credentials.
Args:
strip: Sequence[str], Optional list of members to exclude from the
generated JSON.
Returns:
str: A JSON representation of this instance, suitable to pass to
from_json().
"""
expiry = self.expiry.strftime(YYYYMMDDTHHMMSSZ_FORMAT) if self.expiry else None
prep = {
'token': self.token,
'refresh_token': self.refresh_token,
'token_uri': self.token_uri,
'client_id': self.client_id,
'client_secret': self.client_secret,
'id_token': self.id_token,
# Google auth doesn't currently give us scopes back on refresh.
# 'scopes': sorted(self.scopes),
'token_expiry': expiry,
'decoded_id_token': self._id_token_data,
}
# Remove empty entries
prep = {k: v for k, v in prep.items() if v is not None}
# Remove entries that explicitly need to be removed
if strip is not None:
prep = {k: v for k, v in prep.items() if k not in strip}
return json.dumps(prep, indent=2, sort_keys=True)
def doOAuthRequest(currentScopes, login_hint, verifyScopes=False):
client_id, client_secret = getOAuthClientIDAndSecret()
scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS])
if not currentScopes or verifyScopes:
selectedScopes = getScopesFromUser(scopesList, True, currentScopes)
if selectedScopes is None:
return False
scopes = set(API.REQUIRED_SCOPES)
i = 0
for scope in scopesList:
if selectedScopes[i] == '*':
if scope['scope']:
scopes.add(scope['scope'])
elif selectedScopes[i] == 'R':
scopes.add(f'{scope["scope"]}.readonly')
elif selectedScopes[i] == 'A':
scopes.add(f'{scope["scope"]}.action')
i += 1
else:
scopes = set(currentScopes+API.REQUIRED_SCOPES)
if API.STORAGE_READWRITE_SCOPE in scopes:
scopes.discard(API.STORAGE_READONLY_SCOPE)
login_hint = _getValidateLoginHint(login_hint)
# Needs to be set so oauthlib doesn't puke when Google changes our scopes
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = 'true'
credentials = Credentials.from_client_secrets(
client_id,
client_secret,
scopes=list(scopes),
access_type='offline',
login_hint=login_hint,
open_browser=not GC.Values[GC.NO_BROWSER])
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
with lock:
writeClientCredentials(credentials, GC.Values[GC.OAUTH2_TXT])
entityActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
return True
# gam oauth|oauth2 create|request [<EmailAddress>]
# gam oauth|oauth2 create|request [admin <EmailAddress>] [scope|scopes <APIScopeURLList>]
def doOAuthCreate():
if not Cmd.PeekArgumentPresent(['admin', 'scope', 'scopes']):
login_hint = getEmailAddress(noUid=True, optional=True)
scopes = None
checkForExtraneousArguments()
else:
login_hint = None
scopes = []
scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS])
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'admin':
login_hint = getEmailAddress(noUid=True)
elif myarg in {'scope', 'scopes'}:
for uscope in getString(Cmd.OB_API_SCOPE_URL_LIST).lower().replace(',', ' ').split():
if uscope in {'openid', 'email', API.USERINFO_EMAIL_SCOPE, 'profile', API.USERINFO_PROFILE_SCOPE}:
continue
for scope in scopesList:
if ((uscope == scope['scope']) or
(uscope.endswith('.action') and 'action' in scope['subscopes']) or
(uscope.endswith('.readonly') and 'readonly' in scope['subscopes'])):
scopes.append(uscope)
break
else:
invalidChoiceExit(uscope, API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS]), True)
else:
unknownArgumentExit()
if len(scopes) == 0:
scopes = None
doOAuthRequest(scopes, login_hint)
def exitIfNoOauth2Txt():
if not os.path.isfile(GC.Values[GC.OAUTH2_TXT]):
entityActionNotPerformedWarning([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Msg.DOES_NOT_EXIST)
sys.exit(GM.Globals[GM.SYSEXITRC])
# gam oauth|oauth2 delete|revoke
def doOAuthDelete():
checkForExtraneousArguments()
exitIfNoOauth2Txt()
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK], timeout=10)
with lock:
_, credentials = getOauth2TxtCredentials(noScopes=True)
if not credentials:
return
entityType = Ent.OAUTH2_TXT_FILE
entityName = GC.Values[GC.OAUTH2_TXT]
sys.stdout.write(f'{Ent.Singular(entityType)}: {entityName}, will be Deleted in 3...')
sys.stdout.flush()
time.sleep(1)
sys.stdout.write('2...')
sys.stdout.flush()
time.sleep(1)
sys.stdout.write('1...')
sys.stdout.flush()
time.sleep(1)
sys.stdout.write('boom!\n')
sys.stdout.flush()
httpObj = getHttpObj()
params = {'token': credentials.refresh_token}
revoke_uri = f'https://accounts.google.com/o/oauth2/revoke?{urlencode(params)}'
httpObj.request(revoke_uri, 'GET')
deleteFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True)
entityActionPerformed([entityType, entityName])
# gam oauth|oauth2 info|verify [showsecret] [accesstoken <AccessToken> idtoken <IDToken>] [showdetails]
def doOAuthInfo():
credentials = access_token = id_token = None
showDetails = showSecret = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'accesstoken':
access_token = getString(Cmd.OB_ACCESS_TOKEN)
elif myarg == 'idtoken':
id_token = getString(Cmd.OB_ID_TOKEN)
elif myarg == 'showdetails':
showDetails = True
elif myarg == 'showsecret':
showSecret = True
else:
unknownArgumentExit()
exitIfNoOauth2Txt()
if not access_token and not id_token:
credentials = getClientCredentials(noScopes=True)
access_token = credentials.token
printEntity([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
oa2 = buildGAPIObject(API.OAUTH2)
try:
token_info = callGAPI(oa2, 'tokeninfo',
throwReasons=[GAPI.INVALID],
access_token=access_token, id_token=id_token)
except GAPI.invalid as e:
entityActionFailedExit([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], str(e))
if 'issued_to' in token_info:
printKeyValueList(['Client ID', token_info['issued_to']])
if credentials is not None and showSecret:
printKeyValueList(['Secret', credentials.client_secret])
if 'scope' in token_info:
scopes = token_info['scope'].split(' ')
printKeyValueList(['Scopes', len(scopes)])
Ind.Increment()
for scope in sorted(scopes):
printKeyValueList([scope])
Ind.Decrement()
if 'email' in token_info:
printKeyValueList(['Google Workspace Admin', f'{token_info["email"]}'])
if 'expires_in' in token_info:
printKeyValueList(['Expires', ISOformatTimeStamp((datetime.datetime.now()+datetime.timedelta(seconds=token_info['expires_in'])).replace(tzinfo=GC.Values[GC.TIMEZONE]))])
if showDetails:
for k, v in sorted(iter(token_info.items())):
if k not in ['email', 'expires_in', 'issued_to', 'scope']:
printKeyValueList([k, v])
printBlankLine()
# gam oauth|oauth2 update [<EmailAddress>]
# gam oauth|oauth2 update [admin <EmailAddress>]
def doOAuthUpdate():
if Cmd.PeekArgumentPresent(['admin']):
Cmd.Advance()
login_hint = getEmailAddress(noUid=True)
else:
login_hint = getEmailAddress(noUid=True, optional=True)
checkForExtraneousArguments()
exitIfNoOauth2Txt()
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
with lock:
jsonData = readFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True, displayError=False)
if not jsonData:
invalidOauth2TxtExit(Msg.NO_DATA)
try:
jsonDict = json.loads(jsonData)
if 'client_id' in jsonDict:
if 'scopes' in jsonDict:
currentScopes = jsonDict['scopes']
else:
currentScopes = API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS])
else:
currentScopes = []
except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidOauth2TxtExit(str(e))
if not doOAuthRequest(currentScopes, login_hint, verifyScopes=True):
entityActionNotPerformedWarning([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Msg.USER_CANCELLED)
sys.exit(GM.Globals[GM.SYSEXITRC])
# gam oauth|oauth2 refresh
def doOAuthRefresh():
checkForExtraneousArguments()
exitIfNoOauth2Txt()
getClientCredentials(forceRefresh=True, forceWrite=True, filename=GC.Values[GC.OAUTH2_TXT], refreshOnly=True)
entityActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
# gam oauth|oauth2 export [<FileName>]
def doOAuthExport():
if Cmd.ArgumentsRemaining():
filename = getString(Cmd.OB_FILE_NAME)
checkForExtraneousArguments()
else:
filename = GC.Values[GC.OAUTH2_TXT]
getClientCredentials(forceRefresh=True, forceWrite=True, filename=filename, refreshOnly=True)
if filename != '-':
entityModifierNewValueActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Act.MODIFIER_TO, filename)
def getCRMService(login_hint):
scopes = [API.CLOUD_PLATFORM_SCOPE]
client_id = GAM_PROJECT_CREATION_CLIENT_ID
client_secret = 'qM3dP8f_4qedwzWQE1VR4zzU'
credentials = Credentials.from_client_secrets(
client_id,
client_secret,
scopes=scopes,
access_type='online',
login_hint=login_hint,
open_browser=not GC.Values[GC.NO_BROWSER])
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj())
return (httpObj, getAPIService(API.CLOUDRESOURCEMANAGER, httpObj))
def enableGAMProjectAPIs(httpObj, projectId, login_hint, checkEnabled, i=0, count=0):
apis = API.PROJECT_APIS[:]
projectName = f'projects/{projectId}'
serveu = getAPIService(API.SERVICEUSAGE, httpObj)
status = True
if checkEnabled:
try:
services = callGAPIpages(serveu.services(), 'list', 'services',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
parent=projectName, filter='state:ENABLED',
fields='nextPageToken,services(name)')
Act.Set(Act.CHECK)
jcount = len(services)
entityPerformActionNumItems([Ent.PROJECT, projectId], jcount, Ent.API, i, count)
Ind.Increment()
j = 0
for service in sorted(services, key=lambda k: k['name']):
j += 1
if 'name' in service:
serviceName = service['name'].split('/')[-1]
if serviceName in apis:
printEntityKVList([Ent.API, serviceName], ['Already enabled'], j, jcount)
apis.remove(serviceName)
else:
printEntityKVList([Ent.API, serviceName], ['Already enabled (non-GAM which is fine)'], j, jcount)
Ind.Decrement()
except (GAPI.notFound, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PROJECT, projectId], str(e), i, count)
status = False
jcount = len(apis)
if status and jcount > 0:
Act.Set(Act.ENABLE)
entityPerformActionNumItems([Ent.PROJECT, projectId], jcount, Ent.API, i, count)
failed = 0
Ind.Increment()
j = 0
for api in apis:
j += 1
serviceName = f'projects/{projectId}/services/{api}'
while True:
try:
callGAPI(serveu.services(), 'enable',
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
retryReasons=[GAPI.INTERNAL_ERROR],
name=serviceName)
entityActionPerformed([Ent.API, api], j, jcount)
break
except GAPI.failedPrecondition as e:
entityActionFailedWarning([Ent.API, api], str(e), j, jcount)
readStdin(Msg.ACCEPT_CLOUD_TOS.format(login_hint))
except (GAPI.forbidden, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.API, api], str(e), j, jcount)
failed += 1
break
Ind.Decrement()
if not checkEnabled:
status = failed <= 2
else:
status = failed == 0
return status
# gam enable apis [auto|manual]
def doEnableAPIs():
automatic = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'auto':
automatic = True
elif myarg == 'manual':
automatic = False
else:
unknownArgumentExit()
request = getTLSv1_2Request()
try:
_, projectId = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError):
projectId = readStdin(Msg.WHAT_IS_YOUR_PROJECT_ID).strip()
while automatic is None:
a_or_m = readStdin(Msg.ENABLE_PROJECT_APIS_AUTOMATICALLY_OR_MANUALLY).strip().lower()
if a_or_m.startswith('a'):
automatic = True
break
if a_or_m.startswith('m'):
automatic = False
break
writeStdout(Msg.PLEASE_ENTER_A_OR_M)
if automatic:
login_hint = _getValidateLoginHint(None)
httpObj, _ = getCRMService(login_hint)
enableGAMProjectAPIs(httpObj, projectId, login_hint, True)
else:
apis = API.PROJECT_APIS[:]
chunk_size = 20
writeStdout('Using an account with project access, please use ALL of these URLs to enable 20 APIs at a time:\n\n')
for chunk in range(0, len(apis), chunk_size):
apiid = ",".join(apis[chunk:chunk+chunk_size])
url = f'https://console.cloud.google.com/apis/enableflow?apiid={apiid}&project={projectId}'
writeStdout(f' {url}\n\n')
def _waitForSvcAcctCompletion(i):
sleep_time = i*5
if i > 3:
sys.stdout.write(Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.SVCACCT), sleep_time))
time.sleep(sleep_time)
def _grantRotateRights(iam, projectId, service_account, account_type='serviceAccount'):
body = {'policy': {'bindings': [{'role': 'roles/iam.serviceAccountKeyAdmin',
'members': [f'{account_type}:{service_account}']}]}}
maxRetries = 10
kvList = [Ent.PROJECT, projectId, Ent.SVCACCT, service_account]
printEntityMessage(kvList, Msg.GRANTING_RIGHTS_TO_ROTATE_ITS_OWN_PRIVATE_KEY.format('Granting'))
for retry in range(1, maxRetries+1):
try:
callGAPI(iam.projects().serviceAccounts(), 'setIamPolicy',
throwReasons=[GAPI.INVALID_ARGUMENT],
resource=f'projects/{projectId}/serviceAccounts/{service_account}', body=body)
printEntityMessage(kvList, Msg.GRANTING_RIGHTS_TO_ROTATE_ITS_OWN_PRIVATE_KEY.format('Granted'))
return True
except GAPI.invalidArgument as e:
entityActionFailedWarning(kvList, str(e))
if 'does not exist' not in str(e) or retry == maxRetries:
return False
_waitForSvcAcctCompletion(retry)
except Exception as e:
entityActionFailedWarning(kvList, str(e))
return False
def _createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo, create_key=True):
iam = getAPIService(API.IAM, httpObj)
try:
service_account = callGAPI(iam.projects().serviceAccounts(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
name=f'projects/{projectInfo["projectId"]}',
body={'accountId': svcAcctInfo['name'],
'serviceAccount': {'displayName': svcAcctInfo['displayName'],
'description': svcAcctInfo['description']}})
entityActionPerformed([Ent.PROJECT, projectInfo['projectId'], Ent.SVCACCT, service_account['name'].rsplit('/', 1)[-1]])
except (GAPI.notFound, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PROJECT, projectInfo['projectId']], str(e))
return False
except GAPI.alreadyExists as e:
entityActionFailedWarning([Ent.PROJECT, projectInfo['projectId'], Ent.SVCACCT, svcAcctInfo['name']], str(e))
writeStderr(Msg.RERUN_THE_COMMAND_AND_SPECIFY_A_NEW_SANAME)
return False
GM.Globals[GM.SVCACCT_SCOPES_DEFINED] = False
if create_key and not doProcessSvcAcctKeys(mode='retainexisting', iam=iam,
projectId=service_account['projectId'],
clientEmail=service_account['email'],
clientId=service_account['uniqueId']):
return False
sa_email = service_account['name'].rsplit('/', 1)[-1]
return _grantRotateRights(iam, projectInfo['projectId'], sa_email)
def _createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key=True):
def _checkClientAndSecret(csHttpObj, client_id, client_secret):
post_data = {'client_id': client_id, 'client_secret': client_secret,
'code': 'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid',
'redirect_uri': 'http://127.0.0.1:8080', 'grant_type': 'authorization_code'}
_, content = csHttpObj.request(API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'POST', urlencode(post_data),
headers={'Content-type': 'application/x-www-form-urlencoded'})
try:
content = json.loads(content)
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
sys.stderr.write(f'{str(e)}: {content}')
return False
if not 'error' in content or not 'error_description' in content:
sys.stderr.write(f'Unknown error: {content}\n')
return False
if content['error'] == 'invalid_grant':
return True
if content['error_description'] == 'The OAuth client was not found.':
sys.stderr.write(Msg.IS_NOT_A_VALID_CLIENT_ID.format(client_id))
return False
if content['error_description'] == 'Unauthorized':
sys.stderr.write(Msg.IS_NOT_A_VALID_CLIENT_SECRET.format(client_secret))
return False
sys.stderr.write(f'Unknown error: {content}\n')
return False
if not enableGAMProjectAPIs(httpObj, projectInfo['projectId'], login_hint, False):
return
sys.stdout.write(Msg.SETTING_GAM_PROJECT_CONSENT_SCREEN_CREATING_CLIENT)
console_url = f'https://console.cloud.google.com/auth/clients?project={projectInfo["projectId"]}&authuser={login_hint}'
csHttpObj = getHttpObj()
while True:
sys.stdout.write(Msg.CREATE_CLIENT_INSTRUCTIONS.format(console_url, appInfo['applicationTitle'], appInfo['supportEmail']))
client_id = readStdin(Msg.ENTER_YOUR_CLIENT_ID).strip()
if not client_id:
client_id = readStdin('').strip()
client_secret = readStdin(Msg.ENTER_YOUR_CLIENT_SECRET).strip()
if not client_secret:
client_secret = readStdin('').strip()
client_valid = _checkClientAndSecret(csHttpObj, client_id, client_secret)
if client_valid:
break
sys.stdout.write('\n')
cs_data = f'''{{
"installed": {{
"auth_provider_x509_cert_url": "{API.GOOGLE_AUTH_PROVIDER_X509_CERT_URL}",
"auth_uri": "{API.GOOGLE_OAUTH2_ENDPOINT}",
"client_id": "{client_id}",
"client_secret": "{client_secret}",
"created_by": "{login_hint}",
"project_id": "{projectInfo['projectId']}",
"token_uri": "{API.GOOGLE_OAUTH2_TOKEN_ENDPOINT}"
}}
}}'''
writeFile(GC.Values[GC.CLIENT_SECRETS_JSON], cs_data, continueOnError=False)
sys.stdout.write(Msg.TRUST_GAM_CLIENT_ID.format(GAM, client_id))
readStdin('')
if not _createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo, create_key):
return
sys.stdout.write(Msg.YOUR_GAM_PROJECT_IS_CREATED_AND_READY_TO_USE)
def _getProjects(crm, pfilter, returnNF=False):
try:
projects = callGAPIpages(crm.projects(), 'search', 'projects',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
query=pfilter)
if projects:
return projects
if (not pfilter) or pfilter == GAM_PROJECT_FILTER:
return []
if pfilter.startswith('id:'):
projects = [callGAPI(crm.projects(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=f'projects/{pfilter[3:]}')]
if projects or not returnNF:
return projects
return []
except (GAPI.badRequest, GAPI.invalidArgument) as e:
entityActionFailedExit([Ent.PROJECT, pfilter], str(e))
except GAPI.permissionDenied:
if (not pfilter) or (not pfilter.startswith('id:')) or (not returnNF):
return []
return [{'projectId': pfilter[3:], 'state': 'NF'}]
def _checkProjectFound(project, i, count):
if project.get('state', '') != 'NF':
return True
entityActionFailedWarning([Ent.PROJECT, project['projectId']], Msg.DOES_NOT_EXIST, i, count)
return False
def convertGCPFolderNameToID(parent, crm):
folders = callGAPIpages(crm.folders(), 'search', 'folders',
query=f'displayName="{parent}"')
if not folders:
entityActionFailedExit([Ent.PROJECT_FOLDER, parent], Msg.NOT_FOUND)
jcount = len(folders)
if jcount > 1:
entityActionNotPerformedWarning([Ent.PROJECT_FOLDER, parent],
Msg.PLEASE_SELECT_ENTITY_TO_PROCESS.format(jcount, Ent.Plural(Ent.PROJECT_FOLDER), 'use in create', 'parent <String>'))
Ind.Increment()
j = 0
for folder in folders:
j += 1
printKeyValueListWithCount(['Name', folder['name'], 'ID', folder['displayName']], j, jcount)
Ind.Decrement()
systemErrorExit(MULTIPLE_PROJECT_FOLDERS_FOUND_RC, None)
return folders[0]['name']
PROJECTID_PATTERN = re.compile(r'^[a-z][a-z0-9-]{4,28}[a-z0-9]$')
PROJECTID_FORMAT_REQUIRED = '[a-z][a-z0-9-]{4,28}[a-z0-9]'
def _checkProjectId(projectId):
if not PROJECTID_PATTERN.match(projectId):
Cmd.Backup()
invalidArgumentExit(PROJECTID_FORMAT_REQUIRED)
PROJECTNAME_PATTERN = re.compile('^[a-zA-Z0-9 '+"'"+'"!-]{4,30}$')
PROJECTNAME_FORMAT_REQUIRED = '[a-zA-Z0-9 \'"!-]{4,30}'
def _checkProjectName(projectName):
if not PROJECTNAME_PATTERN.match(projectName):
Cmd.Backup()
invalidArgumentExit(PROJECTNAME_FORMAT_REQUIRED)
def _getSvcAcctInfo(myarg, svcAcctInfo):
if myarg == 'saname':
svcAcctInfo['name'] = getString(Cmd.OB_STRING, minLen=6, maxLen=30)
_checkProjectId(svcAcctInfo['name'])
elif myarg == 'sadisplayname':
svcAcctInfo['displayName'] = getString(Cmd.OB_STRING, maxLen=100)
elif myarg == 'sadescription':
svcAcctInfo['description'] = getString(Cmd.OB_STRING, maxLen=256)
else:
return False
return True
def _getAppInfo(myarg, appInfo):
if myarg == 'appname':
appInfo['applicationTitle'] = getString(Cmd.OB_STRING)
elif myarg == 'supportemail':
appInfo['supportEmail'] = getEmailAddress(noUid=True)
else:
return False
return True
def _generateProjectSvcAcctId(prefix):
return f'{prefix}-{"".join(random.choice(LOWERNUMERIC_CHARS) for _ in range(5))}'
def _getLoginHintProjectInfo(createCmd):
login_hint = None
create_key = True
appInfo = {'applicationTitle': '', 'supportEmail': ''}
projectInfo = {'projectId': '', 'parent': '', 'name': ''}
svcAcctInfo = {'name': '', 'displayName': '', 'description': ''}
if not Cmd.PeekArgumentPresent(['admin', 'appname', 'supportemail', 'project', 'parent',
'projectname', 'saname', 'sadisplayname', 'sadescription',
'algorithm', 'localkeysize', 'validityhours', 'yubikey', 'nokey']):
login_hint = getString(Cmd.OB_EMAIL_ADDRESS, optional=True)
if login_hint and login_hint.find('@') == -1:
Cmd.Backup()
login_hint = None
projectInfo['projectId'] = getString(Cmd.OB_STRING, optional=True, minLen=6, maxLen=30).strip()
if projectInfo['projectId']:
_checkProjectId(projectInfo['projectId'])
checkForExtraneousArguments()
else:
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'admin':
login_hint = getEmailAddress(noUid=True)
elif myarg == 'nokey':
create_key = False
elif myarg == 'project':
projectInfo['projectId'] = getString(Cmd.OB_STRING, minLen=6, maxLen=30)
_checkProjectId(projectInfo['projectId'])
elif createCmd and myarg == 'parent':
projectInfo['parent'] = getString(Cmd.OB_STRING)
elif myarg == 'projectname':
projectInfo['name'] = getString(Cmd.OB_STRING, minLen=4, maxLen=30)
_checkProjectName(projectInfo['name'])
elif _getSvcAcctInfo(myarg, svcAcctInfo):
pass
elif _getAppInfo(myarg, appInfo):
pass
elif myarg in {'algorithm', 'localkeysize', 'validityhours', 'yubikey'}:
Cmd.Backup()
break
else:
unknownArgumentExit()
if not projectInfo['projectId']:
if createCmd:
projectInfo['projectId'] = _generateProjectSvcAcctId('gam-project')
else:
projectInfo['projectId'] = readStdin(Msg.WHAT_IS_YOUR_PROJECT_ID).strip()
if not PROJECTID_PATTERN.match(projectInfo['projectId']):
systemErrorExit(USAGE_ERROR_RC, f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]} {Cmd.OB_PROJECT_ID}: {Msg.EXPECTED} <{PROJECTID_FORMAT_REQUIRED}>')
if not projectInfo['name']:
projectInfo['name'] = 'GAM Project' if not GC.Values[GC.USE_PROJECTID_AS_NAME] else projectInfo['projectId']
if not svcAcctInfo['name']:
svcAcctInfo['name'] = projectInfo['projectId']
if not svcAcctInfo['displayName']:
svcAcctInfo['displayName'] = projectInfo['name']
if not svcAcctInfo['description']:
svcAcctInfo['description'] = svcAcctInfo['displayName']
login_hint = _getValidateLoginHint(login_hint, projectInfo['projectId'])
if not appInfo['applicationTitle']:
appInfo['applicationTitle'] = 'GAM' if not GC.Values[GC.USE_PROJECTID_AS_NAME] else projectInfo['projectId']
if not appInfo['supportEmail']:
appInfo['supportEmail'] = login_hint
httpObj, crm = getCRMService(login_hint)
if projectInfo['parent'] and not projectInfo['parent'].startswith('organizations/') and not projectInfo['parent'].startswith('folders/'):
projectInfo['parent'] = convertGCPFolderNameToID(projectInfo['parent'], crm)
projects = _getProjects(crm, f'id:{projectInfo["projectId"]}')
if not createCmd:
if not projects:
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], Msg.DOES_NOT_EXIST)
if projects[0]['state'] != 'ACTIVE':
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], Msg.NOT_ACTIVE)
else:
if projects:
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], Msg.DUPLICATE)
return (crm, httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key)
def _getCurrentProjectId():
jsonData = readFile(GC.Values[GC.OAUTH2SERVICE_JSON], continueOnError=True, displayError=False)
if jsonData:
try:
return json.loads(jsonData)['project_id']
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
pass
jsonData = readFile(GC.Values[GC.CLIENT_SECRETS_JSON], continueOnError=True, displayError=True)
if not jsonData:
invalidClientSecretsJsonExit(Msg.NO_DATA)
try:
return json.loads(jsonData)['installed']['project_id']
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidClientSecretsJsonExit(str(e))
GAM_PROJECT_FILTER = 'id:gam-project-*'
PROJECTID_FILTER_REQUIRED = '<ProjectIDEntity>'
PROJECTS_CREATESVCACCT_OPTIONS = {'saname', 'sadisplayname', 'sadescription'}
PROJECTS_DELETESVCACCT_OPTIONS = {'saemail', 'saname', 'sauniqueid'}
PROJECTS_PRINTSHOW_OPTIONS = {'showsakeys', 'showiampolicies', 'onememberperrow', 'states', 'todrive', 'delimiter', 'formatjson', 'quotechar'}
def _getLoginHintProjects(createSvcAcctCmd=False, deleteSvcAcctCmd=False, printShowCmd=False, readOnly=False):
if checkArgumentPresent(['admin']):
login_hint = getEmailAddress(noUid=True)
else:
login_hint = getString(Cmd.OB_EMAIL_ADDRESS, optional=True)
if login_hint and login_hint.find('@') == -1:
Cmd.Backup()
login_hint = None
if readOnly and login_hint and login_hint != _getAdminEmail():
readOnly = False
projectIds = None
pfilter = getString(Cmd.OB_STRING, optional=True)
if not pfilter:
pfilter = 'current' if not printShowCmd else GAM_PROJECT_FILTER
elif printShowCmd and pfilter in PROJECTS_PRINTSHOW_OPTIONS:
pfilter = GAM_PROJECT_FILTER
Cmd.Backup()
elif createSvcAcctCmd and pfilter in PROJECTS_CREATESVCACCT_OPTIONS:
pfilter = 'current'
Cmd.Backup()
elif deleteSvcAcctCmd and pfilter in PROJECTS_DELETESVCACCT_OPTIONS:
pfilter = 'current'
Cmd.Backup()
elif printShowCmd and pfilter.lower() == 'all':
pfilter = None
elif pfilter.lower() == 'current':
pfilter = 'current'
elif pfilter.lower() == 'gam':
pfilter = GAM_PROJECT_FILTER
elif pfilter.lower() == 'filter':
pfilter = getString(Cmd.OB_STRING)
elif pfilter.lower() == 'select':
projectIds = getEntityList(Cmd.OB_PROJECT_ID_ENTITY, False)
projectId = None
elif PROJECTID_PATTERN.match(pfilter):
pfilter = f'id:{pfilter}'
elif pfilter.startswith('id:') and PROJECTID_PATTERN.match(pfilter[3:]):
pass
else:
Cmd.Backup()
invalidArgumentExit(['', 'all|'][printShowCmd]+PROJECTID_FILTER_REQUIRED)
if not printShowCmd and not createSvcAcctCmd and not deleteSvcAcctCmd:
checkForExtraneousArguments()
if projectIds is None:
if pfilter in {'current', 'id:current'}:
projectId = _getCurrentProjectId()
else:
projectId = f'filter {pfilter or "all"}'
login_hint = _getValidateLoginHint(login_hint, projectId)
crm = None
if readOnly:
_, crm = buildGAPIServiceObject(API.CLOUDRESOURCEMANAGER, None)
if crm:
httpObj = crm._http
if not crm:
httpObj, crm = getCRMService(login_hint)
if projectIds is None:
if pfilter in {'current', 'id:current'}:
if not printShowCmd:
projects = [{'projectId': projectId}]
else:
projects = _getProjects(crm, f'id:{projectId}', returnNF=True)
else:
projects = _getProjects(crm, pfilter, returnNF=printShowCmd)
else:
projects = []
for projectId in projectIds:
projects.extend(_getProjects(crm, f'id:{projectId}', returnNF=True))
return (crm, httpObj, login_hint, projects)
def _checkForExistingProjectFiles(projectFiles):
for a_file in projectFiles:
if os.path.exists(a_file):
systemErrorExit(JSON_ALREADY_EXISTS_RC, Msg.AUTHORIZATION_FILE_ALREADY_EXISTS.format(a_file, Act.ToPerform()))
def getGCPOrg(crm, login_hint, login_domain):
try:
getorg = callGAPI(crm.organizations(), 'search',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
query=f'domain:{login_domain}')
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.USER, login_hint, Ent.DOMAIN, login_domain], str(e))
try:
organization = getorg['organizations'][0]['name']
sys.stdout.write(Msg.YOUR_ORGANIZATION_NAME_IS.format(organization))
return organization
except (KeyError, IndexError):
systemErrorExit(3, Msg.YOU_HAVE_NO_RIGHTS_TO_CREATE_PROJECTS_AND_YOU_ARE_NOT_A_SUPER_ADMIN)
# gam create gcpfolder <String>
# gam create gcpfolder [admin <EmailAddress] folder <String>
def doCreateGCPFolder():
login_hint = None
if not Cmd.PeekArgumentPresent(['admin', 'folder']):
name = getString(Cmd.OB_STRING)
checkForExtraneousArguments()
else:
name = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'admin':
login_hint = getEmailAddress(noUid=True)
elif myarg == 'folder':
name = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
if not name:
missingChoiceExit('folder')
login_hint = _getValidateLoginHint(login_hint)
login_domain = getEmailAddressDomain(login_hint)
_, crm = getCRMService(login_hint)
organization = getGCPOrg(crm, login_hint, login_domain)
try:
result = callGAPI(crm.folders(), 'create',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
body={'parent': organization, 'displayName': name})
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.USER, login_hint, Ent.GCP_FOLDER, name], str(e))
entityActionPerformed([Ent.USER, login_hint, Ent.GCP_FOLDER, name, Ent.GCP_FOLDER_NAME, result['name']])
# gam create project [<EmailAddress>] [<ProjectID>]
# gam create project [admin <EmailAddress>] [project <ProjectID>]
# [appname <String>] [supportemail <EmailAddress>]
# [projectname <ProjectName>] [parent <String>]
# [saname <ServiceAccountName>] [sadisplayname <ServiceAccountDisplayName>] [sadescription <ServiceAccountDescription>]
# [(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)|
# nokey]
def doCreateProject():
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON], GC.Values[GC.CLIENT_SECRETS_JSON]])
sys.stdout.write(Msg.TRUST_GAM_CLIENT_ID.format(GAM_PROJECT_CREATION, GAM_PROJECT_CREATION_CLIENT_ID))
readStdin('')
crm, httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key = _getLoginHintProjectInfo(True)
login_domain = getEmailAddressDomain(login_hint)
body = {'projectId': projectInfo['projectId'], 'displayName': projectInfo['name']}
if projectInfo['parent']:
body['parent'] = projectInfo['parent']
while True:
create_again = False
sys.stdout.write(Msg.CREATING_PROJECT.format(body['displayName']))
try:
create_operation = callGAPI(crm.projects(), 'create',
throwReasons=[GAPI.BAD_REQUEST, GAPI.ALREADY_EXISTS,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
body=body)
except (GAPI.badRequest, GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], str(e))
operation_name = create_operation['name']
time.sleep(5) # Google recommends always waiting at least 5 seconds
for i in range(1, 10):
sys.stdout.write(Msg.CHECKING_PROJECT_CREATION_STATUS)
status = callGAPI(crm.operations(), 'get',
name=operation_name)
if 'error' in status:
if status['error'].get('message', '') == 'No permission to create project in organization':
sys.stdout.write(Msg.NO_RIGHTS_GOOGLE_CLOUD_ORGANIZATION)
organization = getGCPOrg(crm, login_hint, login_domain)
org_policy = callGAPI(crm.organizations(), 'getIamPolicy',
resource=organization)
if 'bindings' not in org_policy:
org_policy['bindings'] = []
sys.stdout.write(Msg.LOOKS_LIKE_NO_ONE_HAS_RIGHTS_TO_YOUR_GOOGLE_CLOUD_ORGANIZATION_ATTEMPTING_TO_GIVE_YOU_CREATE_RIGHTS)
else:
sys.stdout.write(Msg.THE_FOLLOWING_RIGHTS_SEEM_TO_EXIST)
for a_policy in org_policy['bindings']:
if 'role' in a_policy:
sys.stdout.write(f' Role: {a_policy["role"]}\n')
if 'members' in a_policy:
sys.stdout.write(' Members:\n')
for member in a_policy['members']:
sys.stdout.write(f' {member}\n')
my_role = 'roles/resourcemanager.projectCreator'
sys.stdout.write(Msg.GIVING_LOGIN_HINT_THE_CREATOR_ROLE.format(login_hint, my_role))
org_policy['bindings'].append({'role': my_role, 'members': [f'user:{login_hint}']})
callGAPI(crm.organizations(), 'setIamPolicy',
resource=organization, body={'policy': org_policy})
create_again = True
break
try:
if status['error']['details'][0]['violations'][0]['description'] == 'Callers must accept Terms of Service':
readStdin(Msg.ACCEPT_CLOUD_TOS.format(login_hint))
create_again = True
break
except (IndexError, KeyError):
pass
systemErrorExit(1, str(status)+'\n')
if status.get('done', False):
break
sleep_time = min(2 ** i, 60)
sys.stdout.write(Msg.PROJECT_STILL_BEING_CREATED_SLEEPING.format(sleep_time))
time.sleep(sleep_time)
if create_again:
continue
if not status.get('done', False):
systemErrorExit(1, Msg.FAILED_TO_CREATE_PROJECT.format(status))
elif 'error' in status:
systemErrorExit(2, status['error']+'\n')
break
# Try to set policy on project to allow Service Account Key Upload
# orgp = getAPIService(API.ORGPOLICY, httpObj)
# projectParent = f"projects/{projectInfo['projectId']}"
# policyName = f'{projectParent}/policies/iam.managed.disableServiceAccountKeyUpload'
# try:
# result = callGAPI(orgp.projects().policies(), 'get',
# throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
# name=policyName)
# if result['spec']['rules'][0]['enforce']:
# callGAPI(orgp.projects().policies(), 'patch',
# throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
# name=policyName, body={'spec': {'rules': [{'enforce': False}]}}, updateMask='policy.spec')
# except GAPI.notFound:
# callGAPI(orgp.projects().policies(), 'create',
# throwReasons=[GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
# parent=projectParent, body={'name': policyName, 'spec': {'rules': [{'enforce': False}]}})
# except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.permissionDenied):
# pass
# Create client_secrets.json and oauth2service.json
_createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key)
# gam use project [<EmailAddress>] [<ProjectID>]
# gam use project [admin <EmailAddress>] [project <ProjectID>]
# [appname <String>] [supportemail <EmailAddress>]
# [saname <ServiceAccountName>] [sadisplayname <ServiceAccountDisplayName>] [sadescription <ServiceAccountDescription>]
# [(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)]
def doUseProject():
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON], GC.Values[GC.CLIENT_SECRETS_JSON]])
_, httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key = _getLoginHintProjectInfo(False)
_createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key)
# gam update project [[admin] <EmailAddress>] [<ProjectIDEntity>]
def doUpdateProject():
_, httpObj, login_hint, projects = _getLoginHintProjects()
count = len(projects)
entityPerformActionNumItems([Ent.USER, login_hint], count, Ent.PROJECT)
Ind.Increment()
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
projectId = project['projectId']
Act.Set(Act.UPDATE)
if not enableGAMProjectAPIs(httpObj, projectId, login_hint, True, i, count):
continue
iam = getAPIService(API.IAM, httpObj)
_getSvcAcctData() # needed to read in GM.OAUTH2SERVICE_JSON_DATA
_grantRotateRights(iam, projectId, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email'])
Ind.Decrement()
# gam delete project [[admin] <EmailAddress>] [<ProjectIDEntity>]
def doDeleteProject():
crm, _, login_hint, projects = _getLoginHintProjects()
count = len(projects)
entityPerformActionNumItems([Ent.USER, login_hint], count, Ent.PROJECT)
Ind.Increment()
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
projectId = project['projectId']
try:
callGAPI(crm.projects(), 'delete',
throwReasons=[GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
name=project['name'])
entityActionPerformed([Ent.PROJECT, projectId])
except (GAPI.forbidden, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PROJECT, projectId], str(e))
Ind.Decrement()
PROJECT_TIMEOBJECTS = ['createTime']
PROJECT_STATE_CHOICE_MAP = {
'all': {'ACTIVE', 'DELETE_REQUESTED'},
'active': {'ACTIVE'},
'deleterequested': {'DELETE_REQUESTED'}
}
# gam print projects [[admin] <EmailAddress>] [all|<ProjectIDEntity>] [todrive <ToDriveAttribute>*]
# [states all|active|deleterequested] [showiampolicies 0|1|3 [onememberperrow]]
# [delimiter <Character>] [formatjson [quotechar <Character>]]
# gam show projects [[admin] <EmailAddress>] [all|<ProjectIDEntity>]
# [states all|active|deleterequested] [showiampolicies 0|1|3]
def doPrintShowProjects():
def _getProjectPolicies(crm, project, policyBody, i, count):
try:
policy = callGAPI(crm.projects(), 'getIamPolicy',
throwReasons=[GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
resource=project['name'], body=policyBody)
return policy
except (GAPI.forbidden, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PROJECT, project['projectId'], Ent.IAM_POLICY, None], str(e), i, count)
return {}
readOnly = not Cmd.ArgumentIsAhead('showiampolicies')
crm, _, login_hint, projects = _getLoginHintProjects(printShowCmd=True, readOnly=readOnly)
csvPF = CSVPrintFile(['User', 'projectId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
oneMemberPerRow = False
showIAMPolicies = -1
lifecycleStates = PROJECT_STATE_CHOICE_MAP['active']
policy = None
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'onememberperrow':
oneMemberPerRow = True
elif myarg == 'states':
lifecycleStates = getChoice(PROJECT_STATE_CHOICE_MAP, mapChoice=True)
elif myarg == 'showiampolicies':
showIAMPolicies = int(getChoice(['0', '1', '3']))
policyBody = {'options': {"requestedPolicyVersion": showIAMPolicies}}
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not csvPF:
count = len(projects)
entityPerformActionNumItems([Ent.USER, login_hint], count, Ent.PROJECT)
Ind.Increment()
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
if project['state'] not in lifecycleStates:
continue
projectId = project['projectId']
if showIAMPolicies >= 0:
policy = _getProjectPolicies(crm, project, policyBody, i, count)
printEntity([Ent.PROJECT, projectId], i, count)
Ind.Increment()
printKeyValueList(['name', project['name']])
printKeyValueList(['displayName', project['displayName']])
for field in ['createTime', 'updateTime', 'deleteTime']:
if field in project:
printKeyValueList([field, formatLocalTime(project[field])])
printKeyValueList(['state', project['state']])
jcount = len(project.get('labels', []))
if jcount > 0:
printKeyValueList(['labels', jcount])
Ind.Increment()
for k, v in iter(project['labels'].items()):
printKeyValueList([k, v])
Ind.Decrement()
if 'parent' in project:
printKeyValueList(['parent', project['parent']])
if policy:
printKeyValueList([Ent.Singular(Ent.IAM_POLICY), ''])
Ind.Increment()
bindings = policy.get('bindings', [])
jcount = len(bindings)
printKeyValueList(['version', policy['version']])
printKeyValueList(['bindings', jcount])
Ind.Increment()
j = 0
for binding in bindings:
j += 1
printKeyValueListWithCount(['role', binding['role']], j, jcount)
Ind.Increment()
for member in binding.get('members', []):
printKeyValueList(['member', member])
if 'condition' in binding:
printKeyValueList(['condition', ''])
Ind.Increment()
for k, v in iter(binding['condition'].items()):
printKeyValueList([k, v])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
else:
if not FJQC.formatJSON:
csvPF.AddTitles(['projectId', 'name', 'displayName', 'createTime', 'updateTime', 'deleteTime', 'state'])
csvPF.SetSortAllTitles()
count = len(projects)
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
if project['state'] not in lifecycleStates:
continue
projectId = project['projectId']
if showIAMPolicies >= 0:
policy = _getProjectPolicies(crm, project, policyBody, i, count)
if FJQC.formatJSON:
if policy is not None:
project['policy'] = policy
row = flattenJSON(project, flattened={'User': login_hint}, timeObjects=PROJECT_TIMEOBJECTS)
if csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'User': login_hint, 'projectId': projectId,
'JSON': json.dumps(cleanJSON(project),
ensure_ascii=False, sort_keys=True)})
continue
row = flattenJSON(project, flattened={'User': login_hint}, timeObjects=PROJECT_TIMEOBJECTS)
if not policy:
csvPF.WriteRowTitles(row)
continue
row[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}version'] = policy['version']
for binding in policy.get('bindings', []):
prow = row.copy()
prow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}role'] = binding['role']
if 'condition' in binding:
for k, v in iter(binding['condition'].items()):
prow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}condition{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
members = binding.get('members', [])
if not oneMemberPerRow:
prow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}members'] = delimiter.join(members)
csvPF.WriteRowTitles(prow)
else:
for member in members:
mrow = prow.copy()
mrow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}member'] = member
csvPF.WriteRowTitles(mrow)
csvPF.writeCSVfile('Projects')
# gam info currentprojectid
def doInfoCurrentProjectId():
checkForExtraneousArguments()
printEntity([Ent.PROJECT_ID, _getCurrentProjectId()])
# gam create svcacct [[admin] <EmailAddress>] [<ProjectIDEntity>]
# [saname <ServiceAccountName>] [sadisplayname <ServiceAccountDisplayName>] [sadescription <ServiceAccountDescription>]
# [(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)]
def doCreateSvcAcct():
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
_, httpObj, login_hint, projects = _getLoginHintProjects(createSvcAcctCmd=True)
svcAcctInfo = {'name': '', 'displayName': '', 'description': ''}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getSvcAcctInfo(myarg, svcAcctInfo):
pass
else:
unknownArgumentExit()
if not svcAcctInfo['name']:
svcAcctInfo['name'] = _generateProjectSvcAcctId('gam-svcacct')
if not svcAcctInfo['displayName']:
svcAcctInfo['displayName'] = svcAcctInfo['name']
if not svcAcctInfo['description']:
svcAcctInfo['description'] = svcAcctInfo['displayName']
count = len(projects)
entityPerformActionSubItemModifierNumItems([Ent.USER, login_hint], Ent.SVCACCT, Act.MODIFIER_TO, count, Ent.PROJECT)
Ind.Increment()
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
projectInfo = {'projectId': project['projectId']}
_createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo)
Ind.Decrement()
# gam delete svcacct [[admin] <EmailAddress>] [<ProjectIDEntity>]
# (saemail <ServiceAccountEmail>)|(saname <ServiceAccountName>)|(sauniqueid <ServiceAccountUniqueID>)
def doDeleteSvcAcct():
_, httpObj, login_hint, projects = _getLoginHintProjects(deleteSvcAcctCmd=True)
iam = getAPIService(API.IAM, httpObj)
clientEmail = clientId = clientName = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'saemail':
clientEmail = getEmailAddress(noUid=True)
clientName = clientId = None
elif myarg == 'saname':
clientName = getString(Cmd.OB_STRING, minLen=6, maxLen=30).strip()
_checkProjectId(clientName)
clientEmail = clientId = None
elif myarg == 'sauniqueid':
clientId = getInteger(minVal=0)
clientEmail = clientName = None
else:
unknownArgumentExit()
if not clientEmail and not clientId and not clientName:
missingArgumentExit('email|name|uniqueid')
count = len(projects)
entityPerformActionSubItemModifierNumItems([Ent.USER, login_hint], Ent.SVCACCT, Act.MODIFIER_FROM, count, Ent.PROJECT)
Ind.Increment()
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
projectId = project['projectId']
try:
if clientEmail:
saName = clientEmail
elif clientName:
saName = f'{clientName}@{projectId}.iam.gserviceaccount.com'
else: #clientId
saName = clientId
callGAPI(iam.projects().serviceAccounts(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST],
name=f'projects/{projectId}/serviceAccounts/{saName}')
entityActionPerformed([Ent.PROJECT, projectId, Ent.SVCACCT, saName], i, count)
except (GAPI.notFound, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, saName], str(e), i, count)
Ind.Decrement()
def _getSvcAcctKeyProjectClientFields():
return (GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('private_key_id', ''),
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id'],
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email'],
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'])
# gam <UserTypeEntity> check serviceaccount (scope|scopes <APIScopeURLList>)* [usecolor]
# gam <UserTypeEntity> update serviceaccount (scope|scopes <APIScopeURLList>)* [usecolor]
def checkServiceAccount(users):
def printMessage(message):
writeStdout(Ind.Spaces()+message+'\n')
def printPassFail(description, result):
writeStdout(Ind.Spaces()+f'{description:73} {result}'+'\n')
def authorizeScopes(message):
long_url = ('https://admin.google.com/ac/owl/domainwidedelegation'
f'?clientScopeToAdd={",".join(checkScopes)}'
f'&clientIdToAdd={service_account}&overwriteClientId=true')
if GC.Values[GC.DOMAIN]:
long_url += f'&dn={GC.Values[GC.DOMAIN]}'
long_url += f'&authuser={_getAdminEmail()}'
short_url = shortenURL(long_url)
printLine(message.format('', short_url))
credentials = getSvcAcctCredentials([API.USERINFO_EMAIL_SCOPE], None, forceOauth=True)
allScopes = API.getSvcAcctScopes(GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY], Act.Get() == Act.UPDATE)
checkScopesSet = set()
saScopes = {}
useColor = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'scope', 'scopes'}:
for scope in getString(Cmd.OB_API_SCOPE_URL_LIST).lower().replace(',', ' ').split():
api = API.getSvcAcctScopeAPI(scope)
if api is not None:
saScopes.setdefault(api, [])
saScopes[api].append(scope)
checkScopesSet.add(scope)
else:
invalidChoiceExit(scope, allScopes, True)
elif myarg == 'usecolor':
useColor = True
else:
unknownArgumentExit()
if useColor:
testPass = createGreenText('PASS')
testFail = createRedText('FAIL')
testWarn = createYellowText('WARN')
else:
testPass = 'PASS'
testFail = 'FAIL'
testWarn = 'WARN'
if Act.Get() == Act.CHECK:
if not checkScopesSet:
for scope in iter(GM.Globals[GM.SVCACCT_SCOPES].values()):
checkScopesSet.update(scope)
else:
if not checkScopesSet:
scopesList = API.getSvcAcctScopesList(GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY], True)
selectedScopes = getScopesFromUser(scopesList, False, GM.Globals[GM.SVCACCT_SCOPES])
if selectedScopes is None:
return False
i = 0
for scope in scopesList:
if selectedScopes[i] == '*':
saScopes.setdefault(scope['api'], [])
saScopes[scope['api']].append(scope['scope'])
checkScopesSet.add(scope['scope'])
elif selectedScopes[i] == 'R':
saScopes.setdefault(scope['api'], [])
if 'roscope' not in scope:
saScopes[scope['api']].append(f'{scope["scope"]}.readonly')
checkScopesSet.add(f'{scope["scope"]}.readonly')
else:
saScopes[scope['api']].append(scope['roscope'])
checkScopesSet.add(scope['roscope'])
i += 1
if API.DRIVEACTIVITY in saScopes and API.DRIVE3 in saScopes:
saScopes[API.DRIVEACTIVITY].append(API.DRIVE_SCOPE)
if API.DRIVE3 in saScopes:
saScopes[API.DRIVE2] = saScopes[API.DRIVE3]
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA][API.OAUTH2SA_SCOPES] = saScopes
writeFile(GC.Values[GC.OAUTH2SERVICE_JSON],
json.dumps(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA], ensure_ascii=False, sort_keys=True, indent=2),
continueOnError=False)
checkScopes = sorted(checkScopesSet)
jcount = len(checkScopes)
printMessage(Msg.SYSTEM_TIME_STATUS)
offsetSeconds, offsetFormatted = getLocalGoogleTimeOffset()
if offsetSeconds <= MAX_LOCAL_GOOGLE_TIME_OFFSET:
timeStatus = testPass
else:
timeStatus = testFail
Ind.Increment()
printPassFail(Msg.YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE.format(GOOGLE_TIMECHECK_LOCATION, offsetFormatted), timeStatus)
Ind.Decrement()
oa2 = buildGAPIObject(API.OAUTH2)
printMessage(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AUTHENTICATION)
# We are explicitly not doing DwD here, just confirming service account can auth
auth_error = ''
try:
request = transportCreateRequest()
credentials.refresh(request)
sa_token_info = callGAPI(oa2, 'tokeninfo', access_token=credentials.token)
if sa_token_info:
saTokenStatus = testPass
else:
saTokenStatus = testFail
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
handleServerError(e)
except google.auth.exceptions.RefreshError as e:
saTokenStatus = testFail
if isinstance(e.args, tuple):
e = e.args[0]
auth_error = ' - '+str(e)
Ind.Increment()
printPassFail(f'Authentication{auth_error}', saTokenStatus)
Ind.Decrement()
if saTokenStatus == testFail:
invalidOauth2serviceJsonExit(f'Authentication{auth_error}')
_getSvcAcctData() # needed to read in GM.OAUTH2SERVICE_JSON_DATA
if GM.Globals[GM.SVCACCT_SCOPES_DEFINED] and API.IAM not in GM.Globals[GM.SVCACCT_SCOPES]:
GM.Globals[GM.SVCACCT_SCOPES][API.IAM] = [API.CLOUD_PLATFORM_SCOPE]
key_type = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if key_type == 'default':
printMessage(Msg.SERVICE_ACCOUNT_CHECK_PRIVATE_KEY_AGE)
_, iam = buildGAPIServiceObject(API.IAM, None)
currentPrivateKeyId, projectId, _, clientId = _getSvcAcctKeyProjectClientFields()
name = f'projects/{projectId}/serviceAccounts/{clientId}/keys/{currentPrivateKeyId}'
Ind.Increment()
try:
key = callGAPI(iam.projects().serviceAccounts().keys(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
name=name, fields='validAfterTime')
key_created, _ = iso8601.parse_date(key['validAfterTime'])
key_age = todaysTime()-key_created
printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format(key_age.days), testWarn if key_age.days > 30 else testPass)
except GAPI.permissionDenied:
printMessage(Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format('UNKNOWN'), testWarn)
except (GAPI.badRequest, GAPI.invalid, GAPI.notFound) as e:
entityActionFailedWarning([Ent.PROJECT, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id'],
Ent.SVCACCT, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email']],
str(e))
printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format('UNKNOWN'), testWarn)
else:
printPassFail(Msg.SERVICE_ACCOUNT_SKIPPING_KEY_AGE_CHECK.format(key_type), testPass)
Ind.Decrement()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
allScopesPass = True
user = convertUIDtoEmailAddress(user)
printKeyValueListWithCount([Msg.DOMAIN_WIDE_DELEGATION_AUTHENTICATION, '',
Ent.Singular(Ent.USER), user,
Ent.Choose(Ent.SCOPE, jcount), jcount],
i, count)
Ind.Increment()
j = 0
for scope in checkScopes:
j += 1
# try with and without email scope
for scopes in [[scope, API.USERINFO_EMAIL_SCOPE], [scope]]:
try:
credentials = getSvcAcctCredentials(scopes, user)
credentials.refresh(request)
break
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
handleServerError(e)
except google.auth.exceptions.RefreshError:
continue
if credentials.token:
token_info = callGAPI(oa2, 'tokeninfo', access_token=credentials.token)
if scope in token_info.get('scope', '').split(' ') and user == token_info.get('email', user).lower():
scopeStatus = testPass
else:
scopeStatus = testFail
allScopesPass = False
else:
scopeStatus = testFail
allScopesPass = False
printPassFail(scope, f'{scopeStatus}{currentCount(j, jcount)}')
Ind.Decrement()
service_account = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id']
if allScopesPass:
if Act.Get() == Act.CHECK:
printLine(Msg.SCOPE_AUTHORIZATION_PASSED.format(service_account))
else:
authorizeScopes(Msg.SCOPE_AUTHORIZATION_UPDATE_PASSED)
else:
# Tack on email scope for more accurate checking
checkScopes.append(API.USERINFO_EMAIL_SCOPE)
setSysExitRC(SCOPES_NOT_AUTHORIZED_RC)
authorizeScopes(Msg.SCOPE_AUTHORIZATION_FAILED)
printBlankLine()
# gam check svcacct <UserTypeEntity> (scope|scopes <APIScopeURLList>)*
# gam update svcacct <UserTypeEntity> (scope|scopes <APIScopeURLList>)*
def doCheckUpdateSvcAcct():
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USER)
checkServiceAccount(entityList)
def _getSAKeys(iam, projectId, clientEmail, name, keyTypes):
try:
keys = callGAPIitems(iam.projects().serviceAccounts().keys(), 'list', 'keys',
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
name=name, fields='*', keyTypes=keyTypes)
return (True, keys)
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
return (False, None)
SVCACCT_KEY_TIME_OBJECTS = {'validAfterTime', 'validBeforeTime'}
def _showSAKeys(keys, count, currentPrivateKeyId):
Ind.Increment()
i = 0
for key in keys:
i += 1
keyName = key.pop('name').rsplit('/', 1)[-1]
printKeyValueListWithCount(['name', keyName], i, count)
Ind.Increment()
for k, v in sorted(iter(key.items())):
if k not in SVCACCT_KEY_TIME_OBJECTS:
printKeyValueList([k, v])
else:
printKeyValueList([k, formatLocalTime(v)])
if keyName == currentPrivateKeyId:
printKeyValueList(['usedToAuthenticateThisRequest', True])
Ind.Decrement()
Ind.Decrement()
SVCACCT_DISPLAY_FIELDS = ['displayName', 'description', 'oauth2ClientId', 'uniqueId', 'disabled']
SVCACCT_KEY_TYPE_CHOICE_MAP = {
'all': None,
'system': 'SYSTEM_MANAGED',
'systemmanaged': 'SYSTEM_MANAGED',
'user': 'USER_MANAGED',
'usermanaged': 'USER_MANAGED'
}
# gam print svcaccts [[admin] <EmailAddress>] [all|<ProjectIDEntity>]
# [showsakeys all|system|user]
# [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]
# gam show svcaccts [<EmailAddress>] [all|<ProjectIDEntity>]
# [showsakeys all|system|user]
def doPrintShowSvcAccts():
_, httpObj, login_hint, projects = _getLoginHintProjects(printShowCmd=True, readOnly=False)
csvPF = CSVPrintFile(['User', 'projectId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
iam = getAPIService(API.IAM, httpObj)
keyTypes = None
showSAKeys = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showsakeys':
keyTypes = getChoice(SVCACCT_KEY_TYPE_CHOICE_MAP, mapChoice=True)
showSAKeys = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
count = len(projects)
if not csvPF:
entityPerformActionSubItemModifierNumItems([Ent.USER, login_hint], Ent.SVCACCT, Act.MODIFIER_FOR, count, Ent.PROJECT)
else:
csvPF.AddTitles(['projectId']+SVCACCT_DISPLAY_FIELDS)
csvPF.SetSortAllTitles()
i = 0
for project in projects:
i += 1
if not _checkProjectFound(project, i, count):
continue
projectId = project['projectId']
if csvPF:
printGettingAllEntityItemsForWhom(Ent.SVCACCT, projectId, i, count)
if project['state'] != 'ACTIVE':
entityActionNotPerformedWarning([Ent.PROJECT, projectId], Msg.DELETED, i, count)
continue
try:
svcAccts = callGAPIpages(iam.projects().serviceAccounts(), 'list', 'accounts',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
name=f'projects/{projectId}')
jcount = len(svcAccts)
if not csvPF:
entityPerformActionNumItems([Ent.PROJECT, projectId], jcount, Ent.SVCACCT, i, count)
Ind.Increment()
j = 0
for svcAcct in svcAccts:
j += 1
printKeyValueListWithCount(['email', svcAcct['email']], j, jcount)
Ind.Increment()
for field in SVCACCT_DISPLAY_FIELDS:
if field in svcAcct:
printKeyValueList([field, svcAcct[field]])
if showSAKeys:
name = f"projects/{projectId}/serviceAccounts/{svcAcct['oauth2ClientId']}"
status, keys = _getSAKeys(iam, projectId, svcAcct['email'], name, keyTypes)
if status:
kcount = len(keys)
if kcount > 0:
printKeyValueList([Ent.Choose(Ent.SVCACCT_KEY, kcount), kcount])
_showSAKeys(keys, kcount, '')
Ind.Decrement()
Ind.Decrement()
else:
for svcAcct in svcAccts:
if showSAKeys:
name = f"projects/{projectId}/serviceAccounts/{svcAcct['oauth2ClientId']}"
status, keys = _getSAKeys(iam, projectId, svcAcct['email'], name, keyTypes)
if status:
svcAcct['keys'] = keys
row = flattenJSON(svcAcct, flattened={'User': login_hint}, timeObjects=SVCACCT_KEY_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'User': login_hint, 'projectId': projectId,
'JSON': json.dumps(cleanJSON(svcAcct, timeObjects=SVCACCT_KEY_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
except (GAPI.notFound, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PROJECT, projectId], str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Service Accounts')
def _generatePrivateKeyAndPublicCert(projectId, clientEmail, name, key_size, b64enc_pub=True, validityHours=0):
if projectId:
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.GENERATING_NEW_PRIVATE_KEY)
else:
writeStdout(Msg.GENERATING_NEW_PRIVATE_KEY+'\n')
private_key = rsa.generate_private_key(public_exponent=65537, key_size=key_size, backend=default_backend())
private_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()).decode()
if projectId:
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.EXTRACTING_PUBLIC_CERTIFICATE)
else:
writeStdout(Msg.EXTRACTING_PUBLIC_CERTIFICATE+'\n')
public_key = private_key.public_key()
builder = x509.CertificateBuilder()
# suppress cryptography warnings on service account email length
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message='.*Attribute\'s length.*')
builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,
name,
_validate=False)]))
builder = builder.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,
name,
_validate=False)]))
# Gooogle seems to enforce the not before date strictly. Set the not before
# date to be UTC two minutes ago which should cover any clock skew.
now = datetime.datetime.utcnow()
builder = builder.not_valid_before(now - datetime.timedelta(minutes=2))
# Google defaults to 12/31/9999 date for end time if there's no
# policy to restrict key age
if validityHours:
expires = now + datetime.timedelta(hours=validityHours) - datetime.timedelta(minutes=2)
builder = builder.not_valid_after(expires)
else:
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(public_key)
builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
builder = builder.add_extension(x509.KeyUsage(key_cert_sign=False,
crl_sign=False, digital_signature=True, content_commitment=False,
key_encipherment=False, data_encipherment=False, key_agreement=False,
encipher_only=False, decipher_only=False), critical=True)
builder = builder.add_extension(x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), critical=True)
certificate = builder.sign(private_key=private_key, algorithm=hashes.SHA256(), backend=default_backend())
public_cert_pem = certificate.public_bytes(serialization.Encoding.PEM).decode()
if projectId:
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.DONE_GENERATING_PRIVATE_KEY_AND_PUBLIC_CERTIFICATE)
else:
writeStdout(Msg.DONE_GENERATING_PRIVATE_KEY_AND_PUBLIC_CERTIFICATE+'\n')
if not b64enc_pub:
return (private_pem, public_cert_pem)
publicKeyData = base64.b64encode(public_cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
return (private_pem, publicKeyData)
def _formatOAuth2ServiceData(service_data):
quotedEmail = quote(service_data.get('client_email', ''))
service_data['auth_provider_x509_cert_url'] = API.GOOGLE_AUTH_PROVIDER_X509_CERT_URL
service_data['auth_uri'] = API.GOOGLE_OAUTH2_ENDPOINT
service_data['client_x509_cert_url'] = f'https://www.googleapis.com/robot/v1/metadata/x509/{quotedEmail}'
service_data['token_uri'] = API.GOOGLE_OAUTH2_TOKEN_ENDPOINT
service_data['type'] = 'service_account'
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = service_data.copy()
return json.dumps(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA], indent=2, sort_keys=True)
def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None, clientId=None):
def getSAKeyParms(body, new_data):
nonlocal local_key_size, validityHours
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'algorithm':
body['keyAlgorithm'] = getChoice(["key_alg_rsa_1024", "key_alg_rsa_2048"]).upper()
local_key_size = 0
elif myarg == 'localkeysize':
local_key_size = int(getChoice(['1024', '2048', '4096']))
elif myarg == 'validityhours':
validityHours = getInteger()
elif myarg == 'yubikey':
new_data['key_type'] = 'yubikey'
elif myarg == 'yubikeyslot':
new_data['yubikey_slot'] = getString(Cmd.OB_STRING).upper()
elif myarg == 'yubikeypin':
new_data['yubikey_pin'] = readStdin('Enter your YubiKey PIN: ')
elif myarg == 'yubikeyserialnumber':
new_data['yubikey_serial_number'] = getInteger()
else:
unknownArgumentExit()
local_key_size = 2048
validityHours = 0
body = {}
if mode is None:
mode = getChoice(['retainnone', 'retainexisting', 'replacecurrent'])
if iam is None or mode == 'upload':
if iam is None:
_, iam = buildGAPIServiceObject(API.IAM, None)
_getSvcAcctData()
currentPrivateKeyId, projectId, clientEmail, clientId = _getSvcAcctKeyProjectClientFields()
# dict() ensures we have a real copy, not pointer
new_data = dict(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
# assume default key type unless we are told otherwise
new_data['key_type'] = 'default'
getSAKeyParms(body, new_data)
else:
new_data = {
'client_email': clientEmail,
'project_id': projectId,
'client_id': clientId,
'key_type': 'default'
}
getSAKeyParms(body, new_data)
name = f'projects/{projectId}/serviceAccounts/{clientId}'
if mode != 'retainexisting':
try:
keys = callGAPIitems(iam.projects().serviceAccounts().keys(), 'list', 'keys',
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
name=name, keyTypes='USER_MANAGED')
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
return False
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
return False
if new_data.get('key_type') == 'yubikey':
# Use yubikey private key
new_data['yubikey_key_type'] = f'RSA{local_key_size}'
new_data.pop('private_key', None)
yk = yubikey.YubiKey(new_data)
if 'yubikey_serial_number' not in new_data:
new_data['yubikey_serial_number'] = yk.get_serial_number()
yk = yubikey.YubiKey(new_data)
if 'yubikey_slot' not in new_data:
new_data['yubikey_slot'] = 'AUTHENTICATION'
publicKeyData = yk.get_certificate()
elif local_key_size:
# Generate private key locally, store in file
new_data['private_key'], publicKeyData = _generatePrivateKeyAndPublicCert(projectId, clientEmail, name,
local_key_size, validityHours=validityHours)
new_data['key_type'] = 'default'
for key in list(new_data):
if key.startswith('yubikey_'):
new_data.pop(key, None)
if local_key_size:
Act.Set(Act.UPLOAD)
maxRetries = 10
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPLOADING_NEW_PUBLIC_CERTIFICATE_TO_GOOGLE)
for retry in range(1, maxRetries+1):
try:
result = callGAPI(iam.projects().serviceAccounts().keys(), 'upload',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, body={'publicKeyData': publicKeyData})
newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
break
except GAPI.notFound as e:
if retry == maxRetries:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
return False
_waitForSvcAcctCompletion(retry)
except GAPI.permissionDenied:
if retry == maxRetries:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
return False
_waitForSvcAcctCompletion(retry)
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
return False
except GAPI.failedPrecondition as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
if 'iam.disableServiceAccountKeyUpload' not in str(e) and 'iam.managed.disableServiceAccountKeyUpload' not in str(e):
return False
if retry == maxRetries or mode != 'upload':
sys.stdout.write(Msg.ENABLE_SERVICE_ACCOUNT_PRIVATE_KEY_UPLOAD.format(projectId))
new_data['private_key'] = ''
newPrivateKeyId = ''
break
_waitForSvcAcctCompletion(retry)
new_data['private_key_id'] = newPrivateKeyId
oauth2service_data = _formatOAuth2ServiceData(new_data)
else:
Act.Set(Act.CREATE)
maxRetries = 10
for retry in range(1, maxRetries+1):
try:
result = callGAPI(iam.projects().serviceAccounts().keys(), 'create',
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
name=name, body=body)
newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
break
except GAPI.permissionDenied:
if retry == maxRetries:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
return False
_waitForSvcAcctCompletion(retry)
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
return False
oauth2service_data = base64.b64decode(result['privateKeyData']).decode(UTF8)
if newPrivateKeyId != '':
entityActionPerformed([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail, Ent.SVCACCT_KEY, newPrivateKeyId])
if GM.Globals[GM.SVCACCT_SCOPES_DEFINED]:
try:
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = json.loads(oauth2service_data)
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
invalidOauth2serviceJsonExit(str(e))
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA][API.OAUTH2SA_SCOPES] = GM.Globals[GM.SVCACCT_SCOPES]
oauth2service_data = json.dumps(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA], ensure_ascii=False, sort_keys=True, indent=2)
writeFile(GC.Values[GC.OAUTH2SERVICE_JSON], oauth2service_data, continueOnError=False)
Act.Set(Act.UPDATE)
entityActionPerformed([Ent.OAUTH2SERVICE_JSON_FILE, GC.Values[GC.OAUTH2SERVICE_JSON],
Ent.SVCACCT_KEY, newPrivateKeyId])
if mode in {'retainexisting', 'upload'}:
return newPrivateKeyId != ''
Act.Set(Act.REVOKE)
count = len(keys) if mode == 'retainnone' else 1
entityPerformActionNumItems([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], count, Ent.SVCACCT_KEY)
Ind.Increment()
i = 0
for key in keys:
keyName = key['name'].rsplit('/', 1)[-1]
if mode == 'retainnone' or keyName == currentPrivateKeyId and keyName != newPrivateKeyId:
i += 1
maxRetries = 5
for retry in range(1, maxRetries+1):
try:
callGAPI(iam.projects().serviceAccounts().keys(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
name=key['name'])
entityActionPerformed([Ent.SVCACCT_KEY, keyName], i, count)
break
except GAPI.permissionDenied:
if retry == maxRetries:
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
break
_waitForSvcAcctCompletion(retry)
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], str(e), i, count)
break
if mode != 'retainnone':
break
Ind.Decrement()
return True
# gam create sakey|sakeys
# gam rotate sakey|sakeys retain_existing
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
def doCreateSvcAcctKeys():
doProcessSvcAcctKeys(mode='retainexisting')
# gam update sakey|sakeys
# gam rotate sakey|sakeys replace_current
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
def doUpdateSvcAcctKeys():
doProcessSvcAcctKeys(mode='replacecurrent')
# gam replace sakey|sakeys
# gam rotate sakey|sakeys retain_none
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
def doReplaceSvcAcctKeys():
doProcessSvcAcctKeys(mode='retainnone')
# gam upload sakey|sakeys [admin <EmailAddress>]
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
def doUploadSvcAcctKeys():
login_hint = getEmailAddress(noUid=True) if checkArgumentPresent(['admin']) else None
httpObj, _ = getCRMService(login_hint)
iam = getAPIService(API.IAM, httpObj)
if doProcessSvcAcctKeys(mode='upload', iam=iam):
sa_email = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email']
_grantRotateRights(iam, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id'], sa_email)
sys.stdout.write(Msg.YOUR_GAM_PROJECT_IS_CREATED_AND_READY_TO_USE)
# gam delete sakeys <ServiceAccountKeyList>
def doDeleteSvcAcctKeys():
_, iam = buildGAPIServiceObject(API.IAM, None)
doit = False
keyList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'doit':
doit = True
else:
Cmd.Backup()
keyList.extend(getString(Cmd.OB_SERVICE_ACCOUNT_KEY_LIST, minLen=0).strip().replace(',', ' ').split())
currentPrivateKeyId, projectId, clientEmail, clientId = _getSvcAcctKeyProjectClientFields()
name = f'projects/{projectId}/serviceAccounts/{clientId}'
try:
keys = callGAPIitems(iam.projects().serviceAccounts().keys(), 'list', 'keys',
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
name=name, keyTypes='USER_MANAGED')
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
return
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
return
Act.Set(Act.REVOKE)
count = len(keyList)
entityPerformActionNumItems([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], count, Ent.SVCACCT_KEY)
Ind.Increment()
i = 0
for dkeyName in keyList:
i += 1
for key in keys:
keyName = key['name'].rsplit('/', 1)[-1]
if keyName == dkeyName:
if keyName == currentPrivateKeyId and not doit:
entityActionNotPerformedWarning([Ent.SVCACCT_KEY, keyName],
Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION+Msg.ON_CURRENT_PRIVATE_KEY, i, count)
break
try:
callGAPI(iam.projects().serviceAccounts().keys(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
name=key['name'])
entityActionPerformed([Ent.SVCACCT_KEY, keyName], i, count)
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], str(e), i, count)
break
else:
entityActionNotPerformedWarning([Ent.SVCACCT_KEY, dkeyName], Msg.NOT_FOUND, i, count)
Ind.Decrement()
# gam show sakeys [all|system|user]
def doShowSvcAcctKeys():
_, iam = buildGAPIServiceObject(API.IAM, None)
keyTypes = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in SVCACCT_KEY_TYPE_CHOICE_MAP:
keyTypes = SVCACCT_KEY_TYPE_CHOICE_MAP[myarg]
else:
unknownArgumentExit()
currentPrivateKeyId, projectId, clientEmail, clientId = _getSvcAcctKeyProjectClientFields()
name = f'projects/{projectId}/serviceAccounts/{clientId}'
status, keys = _getSAKeys(iam, projectId, clientEmail, name, keyTypes)
if not status:
return
count = len(keys)
entityPerformActionNumItems([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], count, Ent.SVCACCT_KEY)
if count > 0:
_showSAKeys(keys, count, currentPrivateKeyId)
# gam create gcpserviceaccount|signjwtserviceaccount
def doCreateGCPServiceAccount():
checkForExtraneousArguments()
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
sa_info = {'key_type': 'signjwt', 'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'type': 'service_account'}
request = getTLSv1_2Request()
try:
credentials, sa_info['project_id'] = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
credentials.refresh(request)
sa_info['client_email'] = credentials.service_account_email
oa2 = buildGAPIObjectNoAuthentication(API.OAUTH2)
try:
token_info = callGAPI(oa2, 'tokeninfo',
throwReasons=[GAPI.INVALID],
access_token=credentials.token)
except GAPI.invalid as e:
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
sa_info['client_id'] = token_info['issued_to']
sa_output = json.dumps(sa_info, ensure_ascii=False, sort_keys=True, indent=2)
writeStdout(f'Writing SignJWT service account data:\n\n{sa_output}\n')
writeFile(GC.Values[GC.OAUTH2SERVICE_JSON], sa_output, continueOnError=False)
# Audit command utilities
def getAuditParameters(emailAddressRequired=True, requestIdRequired=True, destUserRequired=False):
auditObject = getEmailAuditObject()
emailAddress = getEmailAddress(noUid=True, optional=not emailAddressRequired)
parameters = {}
if emailAddress:
parameters['auditUser'] = emailAddress
parameters['auditUserName'], auditObject.domain = splitEmailAddress(emailAddress)
if requestIdRequired:
parameters['requestId'] = getString(Cmd.OB_REQUEST_ID)
if destUserRequired:
destEmailAddress = getEmailAddress(noUid=True)
parameters['auditDestUser'] = destEmailAddress
parameters['auditDestUserName'], destDomain = splitEmailAddress(destEmailAddress)
if auditObject.domain != destDomain:
Cmd.Backup()
invalidArgumentExit(f'{parameters["auditDestUserName"]}@{auditObject.domain}')
return (auditObject, parameters)
# Audit monitor command utilities
def _showMailboxMonitorRequestStatus(request, i=0, count=0):
printKeyValueListWithCount(['Destination', normalizeEmailAddressOrUID(request['destUserName'])], i, count)
Ind.Increment()
printKeyValueList(['Begin', request.get('beginDate', 'immediately')])
printKeyValueList(['End', request['endDate']])
printKeyValueList(['Monitor Incoming', request['outgoingEmailMonitorLevel']])
printKeyValueList(['Monitor Outgoing', request['incomingEmailMonitorLevel']])
printKeyValueList(['Monitor Chats', request.get('chatMonitorLevel', 'NONE')])
printKeyValueList(['Monitor Drafts', request.get('draftMonitorLevel', 'NONE')])
Ind.Decrement()
# gam audit monitor create <EmailAddress> <DestEmailAddress> [begin <DateTime>] [end <DateTime>] [incoming_headers] [outgoing_headers] [nochats] [nodrafts] [chat_headers] [draft_headers]
def doCreateMonitor():
auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=True)
#end_date defaults to 30 days in the future...
end_date = (GM.Globals[GM.DATETIME_NOW]+datetime.timedelta(days=30)).strftime(YYYYMMDD_HHMM_FORMAT)
begin_date = None
incoming_headers_only = outgoing_headers_only = drafts_headers_only = chats_headers_only = False
drafts = chats = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'begin':
begin_date = getYYYYMMDD_HHMM()
elif myarg == 'end':
end_date = getYYYYMMDD_HHMM()
elif myarg == 'incomingheaders':
incoming_headers_only = True
elif myarg == 'outgoingheaders':
outgoing_headers_only = True
elif myarg == 'nochats':
chats = False
elif myarg == 'nodrafts':
drafts = False
elif myarg == 'chatheaders':
chats_headers_only = True
elif myarg == 'draftheaders':
drafts_headers_only = True
else:
unknownArgumentExit()
try:
request = callGData(auditObject, 'createEmailMonitor',
throwErrors=[GDATA.INVALID_VALUE, GDATA.INVALID_INPUT, GDATA.DOES_NOT_EXIST, GDATA.INVALID_DOMAIN],
source_user=parameters['auditUserName'], destination_user=parameters['auditDestUserName'], end_date=end_date, begin_date=begin_date,
incoming_headers_only=incoming_headers_only, outgoing_headers_only=outgoing_headers_only,
drafts=drafts, drafts_headers_only=drafts_headers_only, chats=chats, chats_headers_only=chats_headers_only)
entityActionPerformed([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None])
Ind.Increment()
_showMailboxMonitorRequestStatus(request)
Ind.Decrement()
except (GDATA.invalidValue, GDATA.invalidInput) as e:
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
except (GDATA.doesNotExist, GDATA.invalidDomain) as e:
if str(e).find(parameters['auditUser']) != -1:
entityUnknownWarning(Ent.USER, parameters['auditUser'])
else:
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
# gam audit monitor delete <EmailAddress> <DestEmailAddress>
def doDeleteMonitor():
auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=True)
checkForExtraneousArguments()
try:
callGData(auditObject, 'deleteEmailMonitor',
throwErrors=[GDATA.INVALID_INPUT, GDATA.DOES_NOT_EXIST, GDATA.INVALID_DOMAIN],
source_user=parameters['auditUserName'], destination_user=parameters['auditDestUserName'])
entityActionPerformed([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, parameters['auditDestUser']])
except GDATA.invalidInput as e:
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
except (GDATA.doesNotExist, GDATA.invalidDomain) as e:
if str(e).find(parameters['auditUser']) != -1:
entityUnknownWarning(Ent.USER, parameters['auditUser'])
else:
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
# gam audit monitor list <EmailAddress>
def doShowMonitors():
auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=False)
checkForExtraneousArguments()
try:
results = callGData(auditObject, 'getEmailMonitors',
throwErrors=[GDATA.DOES_NOT_EXIST, GDATA.INVALID_DOMAIN],
user=parameters['auditUserName'])
jcount = len(results) if (results) else 0
entityPerformActionNumItems([Ent.USER, parameters['auditUser']], jcount, Ent.AUDIT_MONITOR_REQUEST)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
j = 0
for request in results:
j += 1
_showMailboxMonitorRequestStatus(request, j, jcount)
Ind.Decrement()
except (GDATA.doesNotExist, GDATA.invalidDomain):
entityUnknownWarning(Ent.USER, parameters['auditUser'])
# gam whatis <EmailItem> [noinfo] [noinvitablecheck]
def doWhatIs():
def _showPrimaryType(entityType, email):
printEntity([entityType, email])
def _showAliasType(entityType, email, primaryEntityType, primaryEmail):
printEntity([entityType, email, primaryEntityType, primaryEmail])
cd = buildGAPIObject(API.DIRECTORY)
email = getEmailAddress()
showInfo = invitableCheck = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'noinfo':
showInfo = False
elif myarg == 'noinvitablecheck':
invitableCheck = False
else:
unknownArgumentExit()
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=email, fields='id,primaryEmail')
if (result['primaryEmail'].lower() == email) or (result['id'] == email):
if showInfo:
infoUsers(entityList=[email])
else:
_showPrimaryType(Ent.USER, email)
setSysExitRC(ENTITY_IS_A_USER_RC)
else:
if showInfo:
infoAliases(entityList=[email])
else:
_showAliasType(Ent.USER_ALIAS, email, Ent.USER, result['primaryEmail'])
setSysExitRC(ENTITY_IS_A_USER_ALIAS_RC)
return
except (GAPI.userNotFound, GAPI.badRequest):
pass
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.backendError, GAPI.systemError):
entityUnknownWarning(Ent.EMAIL, email)
setSysExitRC(ENTITY_IS_UKNOWN_RC)
return
try:
result = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
groupKey=email, fields='id,email')
if (result['email'].lower() == email) or (result['id'] == email):
if showInfo:
infoGroups([email])
else:
_showPrimaryType(Ent.GROUP, email)
setSysExitRC(ENTITY_IS_A_GROUP_RC)
else:
if showInfo:
infoAliases(entityList=[email])
else:
_showAliasType(Ent.GROUP_ALIAS, email, Ent.GROUP, result['email'])
setSysExitRC(ENTITY_IS_A_GROUP_ALIAS_RC)
return
except (GAPI.groupNotFound, GAPI.forbidden):
pass
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.badRequest):
entityUnknownWarning(Ent.EMAIL, email)
setSysExitRC(ENTITY_IS_UKNOWN_RC)
return
if not invitableCheck or not getSvcAcctCredentials(API.CLOUDIDENTITY_USERINVITATIONS, _getAdminEmail(), softErrors=True):
isInvitableUser = False
else:
isInvitableUser, ci = _getIsInvitableUser(None, email)
if isInvitableUser:
if showInfo:
name, user, ci = _getCIUserInvitationsEntity(ci, email)
infoCIUserInvitations(name, user, ci, None)
else:
_showPrimaryType(Ent.USER_INVITATION, email)
setSysExitRC(ENTITY_IS_AN_UNMANAGED_ACCOUNT_RC)
else:
entityUnknownWarning(Ent.EMAIL, email)
setSysExitRC(ENTITY_IS_UKNOWN_RC)
def _adjustTryDate(errMsg, numDateChanges, limitDateChanges, prevTryDate):
match_date = re.match('Data for dates later than (.*) is not yet available. Please check back later', errMsg)
if match_date:
tryDate = match_date.group(1)
else:
match_date = re.match('Start date can not be later than (.*)', errMsg)
if match_date:
tryDate = match_date.group(1)
else:
match_date = re.match('End date greater than LastReportedDate.', errMsg)
if match_date:
tryDateTime = datetime.datetime.strptime(prevTryDate, YYYYMMDD_FORMAT)-datetime.timedelta(days=1)
tryDate = tryDateTime.strftime(YYYYMMDD_FORMAT)
if (not match_date) or (numDateChanges > limitDateChanges >= 0):
printWarningMessage(DATA_NOT_AVALIABLE_RC, errMsg)
return None
return tryDate
def _checkDataRequiredServices(result, tryDate, dataRequiredServices, parameterServices=None, checkUserEmail=False):
# -1: Data not available:
# 0: Backup to earlier date
# 1: Data available
oneDay = datetime.timedelta(days=1)
dataWarnings = result.get('warnings', [])
usageReports = result.get('usageReports', [])
# move to day before if we don't have at least one usageReport with parameters
if not usageReports or not usageReports[0].get('parameters', []):
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
for warning in dataWarnings:
if warning['code'] == 'PARTIAL_DATA_AVAILABLE':
for app in warning['data']:
if app['key'] == 'application' and app['value'] != 'docs' and app['value'] in dataRequiredServices:
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
elif warning['code'] == 'DATA_NOT_AVAILABLE':
for app in warning['data']:
if app['key'] == 'application' and app['value'] != 'docs' and app['value'] in dataRequiredServices:
return (-1, tryDate, None)
if parameterServices:
requiredServices = parameterServices.copy()
for item in usageReports[0].get('parameters', []):
if 'name' not in item:
continue
service, _ = item['name'].split(':', 1)
if service in requiredServices:
requiredServices.remove(service)
if not requiredServices:
break
else:
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
if checkUserEmail:
if 'entity' not in usageReports[0] or 'userEmail' not in usageReports[0]['entity']:
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
return (1, tryDate, usageReports)
CUSTOMER_REPORT_SERVICES = {
'accounts',
'app_maker',
'apps_scripts',
'calendar',
'chat',
'classroom',
'cros',
'device_management',
'docs',
'drive',
'gmail',
'gplus',
'meet',
'sites',
}
USER_REPORT_SERVICES = {
'accounts',
'chat',
'classroom',
'docs',
'drive',
'gmail',
'gplus',
}
CUSTOMER_USER_CHOICES = {'customer', 'user'}
# gam report usageparameters customer|user [todrive <ToDriveAttribute>*]
def doReportUsageParameters():
report = getChoice(CUSTOMER_USER_CHOICES)
csvPF = CSVPrintFile(['parameter'], 'sortall')
getTodriveOnly(csvPF)
rep = buildGAPIObject(API.REPORTS)
if report == 'customer':
service = rep.customerUsageReports()
dataRequiredServices = CUSTOMER_REPORT_SERVICES
kwargs = {}
else: # 'user'
service = rep.userUsageReport()
dataRequiredServices = USER_REPORT_SERVICES
kwargs = {'userKey': _getAdminEmail()}
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId == GC.MY_CUSTOMER:
customerId = None
tryDate = todaysDate().strftime(YYYYMMDD_FORMAT)
allParameters = set()
while True:
try:
result = callGAPI(service, 'get',
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST],
date=tryDate, customerId=customerId, fields='warnings,usageReports(parameters(name))', **kwargs)
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate, dataRequiredServices)
if fullData < 0:
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_USAGE_PARAMETERS_DATA_AVAILABLE)
return
if usageReports:
for parameter in usageReports[0]['parameters']:
name = parameter.get('name')
if name:
allParameters.add(name)
if fullData == 1:
break
except GAPI.badRequest:
printErrorMessage(BAD_REQUEST_RC, Msg.BAD_REQUEST)
return
except GAPI.invalid as e:
tryDate = _adjustTryDate(str(e), 0, -1, tryDate)
if not tryDate:
break
for parameter in sorted(allParameters):
csvPF.WriteRow({'parameter': parameter})
csvPF.writeCSVfile(f'{report.capitalize()} Report Usage Parameters')
def getUserOrgUnits(cd, orgUnit, orgUnitId):
try:
if orgUnit == orgUnitId:
orgUnit = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnit, fields='orgUnitPath')['orgUnitPath']
printGettingAllEntityItemsForWhom(Ent.USER, orgUnit, qualifier=Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)),
entityType=Ent.ORGANIZATIONAL_UNIT)
result = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnit, None), orderBy='email',
fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
userOrgUnits = {}
for user in result:
userOrgUnits[user['primaryEmail']] = user['orgUnitPath']
return userOrgUnits
except (GAPI.badRequest, GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnit)
# Convert report mb item to gb
def convertReportMBtoGB(name, item):
if item is not None:
item['intValue'] = f"{int(item['intValue'])/1024:.2f}"
return name.replace('_in_mb', '_in_gb')
REPORTS_PARAMETERS_SIMPLE_TYPES = ['intValue', 'boolValue', 'datetimeValue', 'stringValue']
# gam report usage user [todrive <ToDriveAttribute>*]
# [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
# [([start|startdate <Date>] [end|enddate <Date>])|(range <Date> <Date>)|
# thismonth|(previousmonths <Integer>)]
# [fields|parameters <String>)]
# [convertmbtogb]
# gam report usage customer [todrive <ToDriveAttribute>*]
# [([start|startdate <Date>] [end|enddate <Date>])|(range <Date> <Date>)|
# thismonth|(previousmonths <Integer>)]
# [fields|parameters <String>)]
# [convertmbtogb]
def doReportUsage():
def usageEntitySelectors():
selectorChoices = Cmd.USER_ENTITY_SELECTORS+Cmd.USER_CSVDATA_ENTITY_SELECTORS
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
selectorChoices += Cmd.SERVICE_ACCOUNT_ONLY_ENTITY_SELECTORS[:]+[Cmd.ENTITY_USER, Cmd.ENTITY_USERS]
else:
selectorChoices += Cmd.BASE_ENTITY_SELECTORS[:]+Cmd.USER_ENTITIES[:]
return selectorChoices
def validateYYYYMMDD(argstr):
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
if argstr == 'NOW':
argstr = 'TODAY'
deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
if deltaDate is None:
Cmd.Backup()
invalidArgumentExit(DELTA_DATE_FORMAT_REQUIRED)
return deltaDate
try:
argDate = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
return datetime.datetime(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE])
except ValueError:
Cmd.Backup()
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
report = getChoice(CUSTOMER_USER_CHOICES)
rep = buildGAPIObject(API.REPORTS)
titles = ['date']
if report == 'customer':
fullDataServices = CUSTOMER_REPORT_SERVICES
userReports = False
service = rep.customerUsageReports()
kwargs = [{}]
else: # 'user'
fullDataServices = USER_REPORT_SERVICES
userReports = True
service = rep.userUsageReport()
kwargs = [{'userKey': 'all'}]
titles.append('user')
csvPF = CSVPrintFile()
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId == GC.MY_CUSTOMER:
customerId = None
parameters = set()
convertMbToGb = select = showOrgUnit = False
userKey = 'all'
cd = orgUnit = orgUnitId = None
userOrgUnits = {}
startEndTime = StartEndTime('startdate', 'enddate', 'date')
skipDayNumbers = []
skipDates = set()
oneDay = datetime.timedelta(days=1)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'start', 'startdate', 'end', 'enddate', 'range', 'thismonth', 'previousmonths'}:
startEndTime.Get(myarg)
elif userReports and myarg in {'ou', 'org', 'orgunit'}:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
orgUnit, orgUnitId = getOrgUnitId(cd)
select = False
elif userReports and myarg == 'showorgunit':
showOrgUnit = True
elif myarg in {'fields', 'parameters'}:
for field in getString(Cmd.OB_STRING).replace(',', ' ').split():
if ':' in field:
repsvc, _ = field.split(':', 1)
if repsvc in fullDataServices:
parameters.add(field)
else:
invalidChoiceExit(repsvc, fullDataServices, True)
else:
Cmd.Backup()
invalidArgumentExit('service:parameter')
elif myarg == 'skipdates':
for skip in getString(Cmd.OB_STRING).upper().split(','):
if skip.find(':') == -1:
skipDates.add(validateYYYYMMDD(skip))
else:
skipStart, skipEnd = skip.split(':', 1)
skipStartDate = validateYYYYMMDD(skipStart)
skipEndDate = validateYYYYMMDD(skipEnd)
if skipEndDate < skipStartDate:
Cmd.Backup()
usageErrorExit(Msg.INVALID_DATE_TIME_RANGE.format(myarg, skipEnd, myarg, skipStart))
while skipStartDate <= skipEndDate:
skipDates.add(skipStartDate)
skipStartDate += oneDay
elif myarg == 'skipdaysofweek':
skipdaynames = getString(Cmd.OB_STRING).split(',')
dow = [d.lower() for d in calendarlib.day_abbr]
skipDayNumbers = [dow.index(d) for d in skipdaynames if d in dow]
elif userReports and myarg == 'user':
userKey = getString(Cmd.OB_EMAIL_ADDRESS)
orgUnit = orgUnitId = None
select = False
elif userReports and (myarg == 'select' or myarg in usageEntitySelectors()):
if myarg != 'select':
Cmd.Backup()
_, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
orgUnit = orgUnitId = None
select = True
elif myarg == 'convertmbtogb':
convertMbToGb = True
else:
unknownArgumentExit()
if startEndTime.endDateTime is None:
startEndTime.endDateTime = todaysDate()
if startEndTime.startDateTime is None:
startEndTime.startDateTime = startEndTime.endDateTime+datetime.timedelta(days=-30)
startDateTime = startEndTime.startDateTime
startDate = startDateTime.strftime(YYYYMMDD_FORMAT)
endDateTime = startEndTime.endDateTime
endDate = endDateTime.strftime(YYYYMMDD_FORMAT)
startUseDate = endUseDate = None
if not orgUnitId:
showOrgUnit = False
if userReports:
if select:
Ent.SetGetting(Ent.REPORT)
kwargs = [{'userKey': normalizeEmailAddressOrUID(user)} for user in users]
elif userKey == 'all':
if orgUnitId:
kwargs[0]['orgUnitID'] = orgUnitId
userOrgUnits = getUserOrgUnits(cd, orgUnit, orgUnitId)
forWhom = f'users in orgUnit {orgUnit}'
else:
forWhom = 'all users'
printGettingEntityItemForWhom(Ent.REPORT, forWhom)
else:
Ent.SetGetting(Ent.REPORT)
kwargs = [{'userKey': normalizeEmailAddressOrUID(userKey)}]
printGettingEntityItemForWhom(Ent.REPORT, kwargs[0]['userKey'])
if showOrgUnit:
titles.append('orgUnitPath')
else:
pageMessage = None
csvPF.SetTitles(titles)
csvPF.SetSortAllTitles()
parameters = ','.join(parameters) if parameters else None
while startDateTime <= endDateTime:
if startDateTime.weekday() in skipDayNumbers or startDateTime in skipDates:
startDateTime += oneDay
continue
useDate = startDateTime.strftime(YYYYMMDD_FORMAT)
startDateTime += oneDay
try:
for kwarg in kwargs:
if userReports:
if not select and userKey == 'all':
pageMessage = getPageMessageForWhom(forWhom, showDate=useDate)
else:
pageMessage = getPageMessageForWhom(kwarg['userKey'], showDate=useDate)
try:
usage = callGAPIpages(service, 'get', 'usageReports',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customerId=customerId, date=useDate,
parameters=parameters, **kwarg)
except GAPI.badRequest:
continue
for entity in usage:
row = {'date': useDate}
if userReports:
if 'userEmail' in entity['entity']:
row['user'] = entity['entity']['userEmail']
if showOrgUnit:
row['orgUnitPath'] = userOrgUnits.get(row['user'], UNKNOWN)
else:
row['user'] = UNKNOWN
for item in entity.get('parameters', []):
if 'name' not in item:
continue
name = item['name']
if name == 'cros:device_version_distribution':
versions = {}
for version in item['msgValue']:
versions[version['version_number']] = version['num_devices']
for k, v in sorted(iter(versions.items()), reverse=True):
title = f'cros:num_devices_chrome_{k}'
row[title] = v
else:
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
if ptype in item:
if ptype != 'datetimeValue':
if convertMbToGb and name.endswith('_in_mb'):
name = convertReportMBtoGB(name, item)
row[name] = item[ptype]
else:
row[name] = formatLocalTime(item[ptype])
break
else:
row[name] = ''
if not startUseDate:
startUseDate = useDate
endUseDate = useDate
csvPF.WriteRowTitles(row)
except GAPI.invalid as e:
stderrWarningMsg(str(e))
break
except GAPI.invalidInput as e:
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
except GAPI.forbidden as e:
accessErrorExit(None, str(e))
if startUseDate:
reportName = f'{report.capitalize()} Usage Report - {startUseDate}:{endUseDate}'
else:
reportName = f'{report.capitalize()} Usage Report - {startDate}:{endDate} - No Data'
csvPF.writeCSVfile(reportName)
NL_SPACES_PATTERN = re.compile(r'\n +')
DISABLED_REASON_TIME_PATTERN = re.compile(r'.*(\d{4}/\d{2}/\d{2}-\d{2}:\d{2}:\d{2})')
REPORT_CHOICE_MAP = {
'access': 'access_transparency',
'accesstransparency': 'access_transparency',
'admin': 'admin',
'calendar': 'calendar',
'calendars': 'calendar',
'chat': 'chat',
'chrome': 'chrome',
'contextawareaccess': 'context_aware_access',
'customer': 'customer',
'customers': 'customer',
'datastudio': 'data_studio',
'devices': 'mobile',
'doc': 'drive',
'docs': 'drive',
'domain': 'customer',
'drive': 'drive',
'enterprisegroups': 'groups_enterprise',
'gcp': 'gcp',
'gemini': 'gemini_for_workspace',
'geminiforworkspace': 'gemini_for_workspace',
'gplus': 'gplus',
'google+': 'gplus',
'group': 'groups',
'groups': 'groups',
'groupsenterprise': 'groups_enterprise',
'hangoutsmeet': 'meet',
'jamboard': 'jamboard',
'keep': 'keep',
'login': 'login',
'logins': 'login',
'lookerstudio': 'data_studio',
'meet': 'meet',
'mobile': 'mobile',
'oauthtoken': 'token',
'rules': 'rules',
'saml': 'saml',
'token': 'token',
'tokens': 'token',
'usage': 'usage',
'usageparameters': 'usageparameters',
'user': 'user',
'users': 'user',
'useraccounts': 'user_accounts',
'vault': 'vault',
}
REPORT_ACTIVITIES_UPPERCASE_EVENTS = {
'access_transparency',
'admin',
'chrome',
'context_aware_access',
'data_studio',
'gcp',
'jamboard',
'mobile'
}
REPORT_ACTIVITIES_TIME_OBJECTS = {'time'}
# gam report <ActivityApplictionName> [todrive <ToDriveAttribute>*]
# [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
# [([start <Time>] [end <Time>])|(range <Time> <Time>)|
# yesterday|today|thismonth|(previousmonths <Integer>)]
# [filtertime.* <Time>] [filter|filters <String>]
# [event|events <EventNameList>] [ip <String>]
# [groupidfilter <String>]
# [maxactivities <Number>] [maxevents <Number>] [maxresults <Number>]
# [countsonly [bydate|summary] [eventrowfilter]]
# (addcsvdata <FieldName> <String>)* [shownoactivities]
# gam report users|user [todrive <ToDriveAttribute>*]
# [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
# [allverifyuser <UserItem>]
# [(date <Date>)|(range <Date> <Date>)|
# yesterday|today|thismonth|(previousmonths <Integer>)]
# [nodatechange | (fulldatarequired all|<UserServiceNameList>)]
# [filtertime.* <Time>] [filter|filters <String>]
# [(fields|parameters <String>)|(services <UserServiceNameList>)]
# [aggregatebydate|aggregatebyuser [Boolean]]
# [maxresults <Number>]
# [convertmbtogb]
# gam report customers|customer|domain [todrive <ToDriveAttribute>*]
# [(date <Date>)|(range <Date> <Date>)|
# yesterday|today|thismonth|(previousmonths <Integer>)]
# [nodatechange | (fulldatarequired all|<CustomerServiceNameList>)]
# [(fields|parameters <String>)|(services <CustomerServiceNameList>)] [noauthorizedapps]
# [convertmbtogb]
def doReport():
def processUserUsage(usage, lastDate):
if not usage:
return (True, lastDate)
if lastDate == usage[0]['date']:
return (False, lastDate)
lastDate = usage[0]['date']
for user_report in usage:
if 'entity' not in user_report:
continue
row = {'date': user_report['date']}
if 'userEmail' in user_report['entity']:
row['email'] = user_report['entity']['userEmail']
if showOrgUnit:
row['orgUnitPath'] = userOrgUnits.get(row['email'], UNKNOWN)
else:
row['email'] = UNKNOWN
for item in user_report.get('parameters', []):
if 'name' not in item:
continue
name = item['name']
repsvc, _ = name.split(':', 1)
if repsvc not in includeServices:
continue
if name == 'accounts:disabled_reason' and 'stringValue' in item:
mg = DISABLED_REASON_TIME_PATTERN.match(item['stringValue'])
if mg:
try:
disabledTime = formatLocalTime(datetime.datetime.strptime(mg.group(1), '%Y/%m/%d-%H:%M:%S').replace(tzinfo=iso8601.UTC).strftime(YYYYMMDDTHHMMSSZ_FORMAT))
row['accounts:disabled_time'] = disabledTime
csvPF.AddTitles('accounts:disabled_time')
except ValueError:
pass
elif convertMbToGb and name.endswith('_in_mb'):
name = convertReportMBtoGB(name, item)
csvPF.AddTitles(name)
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
if ptype in item:
if ptype != 'datetimeValue':
row[name] = item[ptype]
else:
row[name] = formatLocalTime(item[ptype])
break
else:
row[name] = ''
csvPF.WriteRow(row)
return (True, lastDate)
def processAggregateUserUsageByUser(usage, lastDate):
if not usage:
return (True, lastDate)
if lastDate == usage[0]['date']:
return (False, lastDate)
lastDate = usage[0]['date']
for user_report in usage:
if 'entity' not in user_report:
continue
if 'userEmail' not in user_report['entity']:
continue
email = user_report['entity']['userEmail']
for item in user_report.get('parameters', []):
if 'name' not in item:
continue
name = item['name']
repsvc, _ = name.split(':', 1)
if repsvc not in includeServices:
continue
if 'intValue' in item:
if convertMbToGb and name.endswith('_in_mb'):
name = convertReportMBtoGB(name, None)
csvPF.AddTitles(name)
eventCounts.setdefault(email, {})
eventCounts[email].setdefault(name, 0)
eventCounts[email][name] += int(item['intValue'])
return (True, lastDate)
def processAggregateUserUsageByDate(usage, lastDate):
if not usage:
return (True, lastDate)
if lastDate == usage[0]['date']:
return (False, lastDate)
lastDate = usage[0]['date']
for user_report in usage:
if 'entity' not in user_report:
continue
for item in user_report.get('parameters', []):
if 'name' not in item:
continue
name = item['name']
repsvc, _ = name.split(':', 1)
if repsvc not in includeServices:
continue
if 'intValue' in item:
if convertMbToGb and name.endswith('_in_mb'):
name = convertReportMBtoGB(name, None)
csvPF.AddTitles(name)
eventCounts.setdefault(lastDate, {})
eventCounts[lastDate].setdefault(name, 0)
eventCounts[lastDate][name] += int(item['intValue'])
return (True, lastDate)
def processCustomerUsageOneRow(usage, lastDate):
if not usage:
return (True, lastDate)
if lastDate == usage[0]['date']:
return (False, lastDate)
lastDate = usage[0]['date']
row = {'date': lastDate}
for item in usage[0].get('parameters', []):
if 'name' not in item:
continue
name = item['name']
repsvc, _ = name.split(':', 1)
if repsvc not in includeServices:
continue
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
if ptype in item:
if convertMbToGb and name.endswith('_in_mb'):
name = convertReportMBtoGB(name, item)
csvPF.AddTitles(name)
if ptype != 'datetimeValue':
row[name] = item[ptype]
else:
row[name] = formatLocalTime(item[ptype])
break
else:
if 'msgValue' in item:
if name == 'accounts:authorized_apps':
if noAuthorizedApps:
continue
for app in item['msgValue']:
appName = f'App: {escapeCRsNLs(app["client_name"])}'
for key in ['num_users', 'client_id']:
title = f'{appName}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'
csvPF.AddTitles(title)
row[title] = app[key]
elif name == 'cros:device_version_distribution':
versions = {}
for version in item['msgValue']:
versions[version['version_number']] = version['num_devices']
for k, v in sorted(iter(versions.items()), reverse=True):
title = f'cros:device_version{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'
csvPF.AddTitles(title)
row[title] = v
else:
values = []
for subitem in item['msgValue']:
if 'count' in subitem:
mycount = myvalue = None
for key, value in iter(subitem.items()):
if key == 'count':
mycount = value
else:
myvalue = value
if mycount and myvalue:
values.append(f'{myvalue}:{mycount}')
value = ' '.join(values)
elif 'version_number' in subitem and 'num_devices' in subitem:
values.append(f'{subitem["version_number"]}:{subitem["num_devices"]}')
else:
continue
value = ' '.join(sorted(values, reverse=True))
csvPF.AddTitles(name)
row['name'] = value
csvPF.WriteRow(row)
return (True, lastDate)
def processCustomerUsage(usage, lastDate):
if not usage:
return (True, lastDate)
if lastDate == usage[0]['date']:
return (False, lastDate)
lastDate = usage[0]['date']
for item in usage[0].get('parameters', []):
if 'name' not in item:
continue
name = item['name']
repsvc, _ = name.split(':', 1)
if repsvc not in includeServices:
continue
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
if ptype in item:
if ptype != 'datetimeValue':
if convertMbToGb and name.endswith('_in_mb'):
name = convertReportMBtoGB(name, item)
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': item[ptype]})
else:
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': formatLocalTime(item[ptype])})
break
else:
if 'msgValue' in item:
if name == 'accounts:authorized_apps':
if noAuthorizedApps:
continue
for subitem in item['msgValue']:
app = {'date': lastDate}
for an_item in subitem:
if an_item == 'client_name':
app['name'] = f'App: {escapeCRsNLs(subitem[an_item])}'
elif an_item == 'num_users':
app['value'] = f'{subitem[an_item]} users'
elif an_item == 'client_id':
app['client_id'] = subitem[an_item]
authorizedApps.append(app)
elif name == 'cros:device_version_distribution':
values = []
for subitem in item['msgValue']:
values.append(f'{subitem["version_number"]}:{subitem["num_devices"]}')
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': ' '.join(sorted(values, reverse=True))})
else:
values = []
for subitem in item['msgValue']:
if 'count' in subitem:
mycount = myvalue = None
for key, value in iter(subitem.items()):
if key == 'count':
mycount = value
else:
myvalue = value
if mycount and myvalue:
values.append(f'{myvalue}:{mycount}')
else:
continue
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': ' '.join(sorted(values, reverse=True))})
csvPF.SortRowsTwoTitles('date', 'name', False)
if authorizedApps:
csvPF.AddTitle('client_id')
for row in sorted(authorizedApps, key=lambda k: (k['date'], k['name'].lower())):
csvPF.WriteRow(row)
return (True, lastDate)
# dynamically extend our choices with other reports Google dynamically adds
rep = buildGAPIObject(API.REPORTS)
dyn_choices = rep._rootDesc \
.get('resources', {}) \
.get('activities', {}) \
.get('methods', {}) \
.get('list', {}) \
.get('parameters', {}) \
.get('applicationName', {}) \
.get('enum', [])
for dyn_choice in dyn_choices:
if dyn_choice.replace('_', '') not in REPORT_CHOICE_MAP and \
dyn_choice not in REPORT_CHOICE_MAP.values():
REPORT_CHOICE_MAP[dyn_choice.replace('_', '')] = dyn_choice
report = getChoice(REPORT_CHOICE_MAP, mapChoice=True)
if report == 'usage':
doReportUsage()
return
if report == 'usageparameters':
doReportUsageParameters()
return
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId == GC.MY_CUSTOMER:
customerId = None
csvPF = CSVPrintFile()
filters = actorIpAddress = groupIdFilter = orgUnit = orgUnitId = None
showOrgUnit = False
parameters = set()
parameterServices = set()
eventCounts = {}
eventNames = []
startEndTime = StartEndTime('start', 'end')
oneDay = datetime.timedelta(days=1)
filterTimes = {}
maxActivities = maxEvents = 0
maxResults = 1000
aggregateByDate = aggregateByUser = convertMbToGb = countsOnly = countsByDate = countsSummary = \
eventRowFilter = exitUserLoop = noAuthorizedApps = normalizeUsers = select = userCustomerRange = False
limitDateChanges = -1
allVerifyUser = userKey = 'all'
cd = orgUnit = orgUnitId = None
userOrgUnits = {}
customerReports = userReports = False
if report == 'customer':
customerReports = True
service = rep.customerUsageReports()
fullDataServices = CUSTOMER_REPORT_SERVICES
elif report == 'user':
userReports = True
service = rep.userUsageReport()
fullDataServices = USER_REPORT_SERVICES
else:
service = rep.activities()
usageReports = customerReports or userReports
activityReports = not usageReports
dataRequiredServices = set()
addCSVData = {}
showNoActivities = False
if usageReports:
includeServices = set()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'range', 'thismonth', 'previousmonths'}:
startEndTime.Get(myarg)
userCustomerRange = True
elif myarg in {'ou', 'org', 'orgunit'}:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
orgUnit, orgUnitId = getOrgUnitId(cd)
select = False
elif myarg == 'showorgunit':
showOrgUnit = True
elif usageReports and myarg in {'date', 'yesterday', 'today'}:
startEndTime.Get('start' if myarg == 'date' else myarg)
startEndTime.endDateTime = startEndTime.startDateTime
userCustomerRange = False
elif usageReports and myarg in {'nodatechange', 'limitdatechanges'}:
if myarg == 'nodatechange':
limitDateChanges = 0
else:
limitDateChanges = getInteger(minVal=-1)
if (limitDateChanges == 0) and (startEndTime.startDateTime is not None) and (startEndTime.endDateTime == startEndTime.startDateTime):
userCustomerRange = True
elif usageReports and myarg in {'fields', 'parameters'}:
for field in getString(Cmd.OB_STRING).replace(',', ' ').split():
if ':' in field:
repsvc, _ = field.split(':', 1)
if repsvc in fullDataServices:
parameters.add(field)
parameterServices.add(repsvc)
includeServices.add(repsvc)
else:
invalidChoiceExit(repsvc, fullDataServices, True)
else:
Cmd.Backup()
invalidArgumentExit('service:parameter')
elif usageReports and myarg == 'fulldatarequired':
fdr = getString(Cmd.OB_SERVICE_NAME_LIST, minLen=0).lower()
if fdr:
if fdr != 'all':
for repsvc in fdr.replace(',', ' ').split():
if repsvc in fullDataServices:
dataRequiredServices.add(repsvc)
else:
invalidChoiceExit(repsvc, fullDataServices, True)
else:
dataRequiredServices = fullDataServices
elif usageReports and myarg in {'service', 'services'}:
for repsvc in getString(Cmd.OB_SERVICE_NAME_LIST).lower().replace(',', ' ').split():
if repsvc in fullDataServices:
parameterServices.add(repsvc)
includeServices.add(repsvc)
else:
invalidChoiceExit(repsvc, fullDataServices, True)
elif usageReports and myarg == 'convertmbtogb':
convertMbToGb = True
elif customerReports and myarg == 'noauthorizedapps':
noAuthorizedApps = True
elif activityReports and myarg == 'maxactivities':
maxActivities = getInteger(minVal=0)
elif activityReports and myarg == 'maxevents':
maxEvents = getInteger(minVal=0)
elif activityReports and myarg in {'start', 'starttime', 'end', 'endtime', 'yesterday', 'today'}:
startEndTime.Get(myarg)
elif activityReports and myarg in {'event', 'events'}:
for event in getString(Cmd.OB_EVENT_NAME_LIST).replace(',', ' ').split():
event = event.lower() if report not in REPORT_ACTIVITIES_UPPERCASE_EVENTS else event.upper()
if event not in eventNames:
eventNames.append(event)
elif activityReports and myarg == 'ip':
actorIpAddress = getString(Cmd.OB_STRING)
elif activityReports and myarg == 'countsonly':
countsOnly = True
elif activityReports and myarg == 'bydate':
countsByDate = True
elif activityReports and myarg == 'summary':
countsSummary = True
elif activityReports and myarg == 'eventrowfilter':
eventRowFilter = True
elif activityReports and myarg == 'groupidfilter':
groupIdFilter = getString(Cmd.OB_STRING)
elif activityReports and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif activityReports and myarg == 'shownoactivities':
showNoActivities = True
elif not customerReports and myarg.startswith('filtertime'):
filterTimes[myarg] = getTimeOrDeltaFromNow()
elif not customerReports and myarg in {'filter', 'filters'}:
filters = getString(Cmd.OB_STRING)
elif not customerReports and myarg == 'maxresults':
maxResults = getInteger(minVal=1, maxVal=1000)
elif not customerReports and myarg == 'user':
userKey = getString(Cmd.OB_EMAIL_ADDRESS)
orgUnit = orgUnitId = None
select = False
elif not customerReports and myarg == 'select':
_, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
orgUnit = orgUnitId = None
select = True
elif userReports and myarg == 'aggregatebydate':
aggregateByDate = getBoolean()
elif userReports and myarg == 'aggregatebyuser':
aggregateByUser = getBoolean()
elif userReports and myarg == 'allverifyuser':
allVerifyUser = getEmailAddress()
else:
unknownArgumentExit()
if aggregateByDate and aggregateByUser:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('aggregateByDate', 'aggregateByUser'))
if countsOnly and countsByDate and countsSummary:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('bydate', 'summary'))
parameters = ','.join(parameters) if parameters else None
if usageReports and not includeServices:
includeServices = set(fullDataServices)
if filterTimes and filters is not None:
for filterTimeName, filterTimeValue in iter(filterTimes.items()):
filters = filters.replace(f'#{filterTimeName}#', filterTimeValue)
if not orgUnitId:
showOrgUnit = False
if userReports:
if startEndTime.startDateTime is None:
startEndTime.startDateTime = startEndTime.endDateTime = todaysDate()
if select:
normalizeUsers = True
orgUnitId = None
Ent.SetGetting(Ent.REPORT)
elif userKey == 'all':
if orgUnitId:
if showOrgUnit:
userOrgUnits = getUserOrgUnits(cd, orgUnit, orgUnitId)
forWhom = f'users in orgUnit {orgUnit}'
else:
forWhom = 'all users'
printGettingEntityItemForWhom(Ent.REPORT, forWhom)
users = ['all']
else:
Ent.SetGetting(Ent.REPORT)
users = [normalizeEmailAddressOrUID(userKey)]
orgUnitId = None
if aggregateByDate:
titles = ['date']
elif aggregateByUser:
titles = ['email'] if not showOrgUnit else ['email', 'orgUnitPath']
else:
titles = ['email', 'date'] if not showOrgUnit else ['email', 'orgUnitPath', 'date']
csvPF.SetTitles(titles)
csvPF.SetSortAllTitles()
i = 0
count = len(users)
for user in users:
i += 1
if normalizeUsers:
user = normalizeEmailAddressOrUID(user)
if user != 'all':
printGettingEntityItemForWhom(Ent.REPORT, user, i, count)
verifyUser = user
else:
verifyUser = allVerifyUser
startDateTime = startEndTime.startDateTime
endDateTime = startEndTime.endDateTime
lastDate = None
numDateChanges = 0
while startDateTime <= endDateTime:
tryDate = startDateTime.strftime(YYYYMMDD_FORMAT)
try:
if not userCustomerRange:
result = callGAPI(service, 'get',
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
userKey=verifyUser, date=tryDate, customerId=customerId,
orgUnitID=orgUnitId, parameters=parameters,
fields='warnings,usageReports', maxResults=1)
prevTryDate = tryDate
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate,
dataRequiredServices, parameterServices, True)
if fullData < 0:
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_REPORT_AVAILABLE.format(report))
break
numDateChanges += 1
if fullData == 0:
if numDateChanges > limitDateChanges >= 0:
break
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
continue
if not select and userKey == 'all':
pageMessage = getPageMessageForWhom(forWhom, showDate=tryDate)
else:
pageMessage = getPageMessageForWhom(user, showDate=tryDate)
prevTryDate = tryDate
usage = callGAPIpages(service, 'get', 'usageReports',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=user, date=tryDate, customerId=customerId,
orgUnitID=orgUnitId, filters=filters, parameters=parameters,
maxResults=maxResults)
if aggregateByDate:
status, lastDate = processAggregateUserUsageByDate(usage, lastDate)
elif aggregateByUser:
status, lastDate = processAggregateUserUsageByUser(usage, lastDate)
else:
status, lastDate = processUserUsage(usage, lastDate)
if not status:
break
except GAPI.invalid as e:
if userCustomerRange:
break
numDateChanges += 1
tryDate = _adjustTryDate(str(e), numDateChanges, limitDateChanges, tryDate)
if not tryDate:
break
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
continue
except GAPI.invalidInput as e:
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
except GAPI.badRequest:
if user != 'all':
entityUnknownWarning(Ent.USER, user, i, count)
else:
printErrorMessage(BAD_REQUEST_RC, Msg.BAD_REQUEST)
exitUserLoop = True
break
except GAPI.forbidden as e:
accessErrorExit(None, str(e))
startDateTime += oneDay
if exitUserLoop:
break
if user != 'all' and lastDate is None and GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'date': prevTryDate, 'email': user})
if aggregateByDate:
for usageDate, events in iter(eventCounts.items()):
row = {'date': usageDate}
for event, count in iter(events.items()):
if convertMbToGb and event.endswith('_in_gb'):
count = f'{count/1024:.2f}'
row[event] = count
csvPF.WriteRow(row)
csvPF.SortRows('date', False)
csvPF.writeCSVfile(f'User Reports Aggregate - {tryDate}')
elif aggregateByUser:
for email, events in iter(eventCounts.items()):
row = {'email': email}
if showOrgUnit:
row['orgUnitPath'] = userOrgUnits.get(email, UNKNOWN)
for event, count in iter(events.items()):
if convertMbToGb and event.endswith('_in_gb'):
count = f'{count/1024:.2f}'
row[event] = count
csvPF.WriteRow(row)
csvPF.SortRows('email', False)
csvPF.writeCSVfile('User Reports Aggregate - User')
else:
csvPF.SortRowsTwoTitles('email', 'date', False)
csvPF.writeCSVfile(f'User Reports - {tryDate}')
elif customerReports:
if startEndTime.startDateTime is None:
startEndTime.startDateTime = startEndTime.endDateTime = todaysDate()
csvPF.SetTitles('date')
if not userCustomerRange or (startEndTime.startDateTime == startEndTime.endDateTime):
csvPF.AddTitles(['name', 'value'])
authorizedApps = []
startDateTime = startEndTime.startDateTime
endDateTime = startEndTime.endDateTime
lastDate = None
numDateChanges = 0
while startDateTime <= endDateTime:
tryDate = startDateTime.strftime(YYYYMMDD_FORMAT)
try:
if not userCustomerRange:
result = callGAPI(service, 'get',
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
date=tryDate, customerId=customerId, parameters=parameters, fields='warnings,usageReports')
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate,
dataRequiredServices, parameterServices)
if fullData < 0:
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_REPORT_AVAILABLE.format(report))
break
numDateChanges += 1
if fullData == 0:
if numDateChanges > limitDateChanges >= 0:
break
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
continue
usage = callGAPIpages(service, 'get', 'usageReports',
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN],
date=tryDate, customerId=customerId, parameters=parameters)
if not userCustomerRange or (startEndTime.startDateTime == startEndTime.endDateTime):
status, lastDate = processCustomerUsage(usage, lastDate)
else:
status, lastDate = processCustomerUsageOneRow(usage, lastDate)
if not status:
break
except GAPI.invalid as e:
if userCustomerRange:
break
numDateChanges += 1
tryDate = _adjustTryDate(str(e), numDateChanges, limitDateChanges, tryDate)
if not tryDate:
break
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
continue
except GAPI.invalidInput as e:
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
except GAPI.forbidden as e:
accessErrorExit(None, str(e))
startDateTime += oneDay
csvPF.writeCSVfile(f'Customer Report - {tryDate}')
else: # activityReports
csvPF.SetTitles('name')
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if select:
pageMessage = None
normalizeUsers = True
orgUnitId = None
elif userKey == 'all':
if orgUnitId:
if showOrgUnit:
userOrgUnits = getUserOrgUnits(cd, orgUnit, orgUnitId)
printGettingEntityItemForWhom(Ent.REPORT, f'users in orgUnit {orgUnit}')
else:
printGettingEntityItemForWhom(Ent.REPORT, 'all users')
pageMessage = getPageMessage()
users = ['all']
else:
Ent.SetGetting(Ent.ACTIVITY)
pageMessage = getPageMessage()
users = [normalizeEmailAddressOrUID(userKey)]
orgUnitId = None
zeroEventCounts = {}
if not eventNames:
eventNames.append(None)
else:
for eventName in eventNames:
zeroEventCounts[eventName] = 0
i = 0
count = len(users)
for user in users:
i += 1
if normalizeUsers:
user = normalizeEmailAddressOrUID(user)
if select or user != 'all':
printGettingEntityItemForWhom(Ent.ACTIVITY, user, i, count)
for eventName in eventNames:
try:
feed = callGAPIpages(service, 'list', 'items',
pageMessage=pageMessage, maxItems=maxActivities,
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.AUTH_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
applicationName=report, userKey=user, customerId=customerId,
actorIpAddress=actorIpAddress, orgUnitID=orgUnitId,
startTime=startEndTime.startTime, endTime=startEndTime.endTime,
eventName=eventName, filters=filters, groupIdFilter=groupIdFilter,
maxResults=maxResults)
except GAPI.badRequest:
if user != 'all':
entityUnknownWarning(Ent.USER, user, i, count)
continue
printErrorMessage(BAD_REQUEST_RC, Msg.BAD_REQUEST)
break
except (GAPI.invalid, GAPI.invalidInput, GAPI.serviceNotAvailable) as e:
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
except GAPI.authError:
accessErrorExit(None)
for activity in feed:
events = activity.pop('events')
actor = activity['actor'].get('email')
if not actor:
actor = 'id:'+activity['actor'].get('profileId', UNKNOWN)
if showOrgUnit:
activity['actor']['orgUnitPath'] = userOrgUnits.get(actor, UNKNOWN)
if countsOnly and countsByDate:
eventTime = activity.get('id', {}).get('time', UNKNOWN)
if eventTime != UNKNOWN:
try:
eventTime, _ = iso8601.parse_date(eventTime)
except (iso8601.ParseError, OverflowError):
eventTime = UNKNOWN
if eventTime != UNKNOWN:
eventDate = eventTime.strftime(YYYYMMDD_FORMAT)
else:
eventDate = UNKNOWN
if not countsOnly or eventRowFilter:
activity_row = flattenJSON(activity, timeObjects=REPORT_ACTIVITIES_TIME_OBJECTS)
purge_parameters = True
numEvents = 0
for event in events:
numEvents += 1
for item in event.get('parameters', []):
itemSet = set(item)
if not itemSet.symmetric_difference({'name'}):
event[item['name']] = ''
elif not itemSet.symmetric_difference({'value', 'name'}):
event[item['name']] = NL_SPACES_PATTERN.sub('', item['value'])
elif not itemSet.symmetric_difference({'intValue', 'name'}):
if item['name'] in {'start_time', 'end_time'}:
val = item.get('intValue')
if val is not None:
val = int(val)
if val >= 62135683200:
event[item['name']] = ISOformatTimeStamp(datetime.datetime.fromtimestamp(val-62135683200, GC.Values[GC.TIMEZONE]))
else:
event[item['name']] = val
else:
event[item['name']] = item['intValue']
elif not itemSet.symmetric_difference({'boolValue', 'name'}):
event[item['name']] = item['boolValue']
elif not itemSet.symmetric_difference({'multiValue', 'name'}):
event[item['name']] = ' '.join(item['multiValue'])
elif item['name'] == 'scope_data':
parts = {}
for message in item['multiMessageValue']:
for mess in message['parameter']:
value = mess.get('value', ' '.join(mess.get('multiValue', [])))
parts[mess['name']] = parts.get(mess['name'], [])+[value]
for part, v in iter(parts.items()):
if part == 'scope_name':
part = 'scope'
event[part] = ' '.join(v)
else:
purge_parameters = False
if purge_parameters:
event.pop('parameters', None)
row = flattenJSON(event)
row.update(activity_row)
if not countsOnly:
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(row)
if numEvents >= maxEvents > 0:
break
elif csvPF.CheckRowTitles(row):
eventName = event['name']
if not countsSummary:
eventCounts.setdefault(actor, {})
if not countsByDate:
eventCounts[actor].setdefault(eventName, 0)
eventCounts[actor][eventName] += 1
else:
eventCounts[actor].setdefault(eventDate, {})
eventCounts[actor][eventDate].setdefault(eventName, 0)
eventCounts[actor][eventDate][eventName] += 1
else:
eventCounts.setdefault(eventName, 0)
eventCounts[eventName] += 1
elif not countsSummary:
eventCounts.setdefault(actor, {})
if not countsByDate:
for event in events:
eventName = event['name']
eventCounts[actor].setdefault(eventName, 0)
eventCounts[actor][eventName] += 1
else:
for event in events:
eventName = event['name']
eventCounts[actor].setdefault(eventDate, {})
eventCounts[actor][eventDate].setdefault(eventName, 0)
eventCounts[actor][eventDate][eventName] += 1
else:
for event in events:
eventCounts.setdefault(event['name'], 0)
eventCounts[event['name']] += 1
if not countsOnly:
if not csvPF.rows and showNoActivities:
row = {'name': 'NoActivities'}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(row)
else:
if eventRowFilter:
csvPF.SetRowFilter([], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
if not countsSummary:
titles = ['emailAddress']
if countsOnly and countsByDate:
titles.append('date')
csvPF.SetTitles(titles)
csvPF.SetSortTitles(titles)
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if eventCounts:
if not countsByDate:
for actor, events in iter(eventCounts.items()):
row = {'emailAddress': actor}
row.update(zeroEventCounts)
for event, count in iter(events.items()):
row[event] = count
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(row)
else:
for actor, eventDates in iter(eventCounts.items()):
for eventDate, events in iter(eventDates.items()):
row = {'emailAddress': actor, 'date': eventDate}
row.update(zeroEventCounts)
for event, count in iter(events.items()):
row[event] = count
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(row)
elif showNoActivities:
row = {'emailAddress': 'NoActivities'}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
else:
csvPF.SetTitles(['event', 'count'])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if eventCounts:
for event, count in sorted(iter(eventCounts.items())):
row = {'event': event, 'count': count}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
elif showNoActivities:
row = {'event': 'NoActivities', 'count': 0}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
csvPF.writeCSVfile(f'{report.capitalize()} Activity Report')
# Substitute for #user#, #email#, #usernamne#
def _substituteForUser(field, user, userName):
if field.find('#') == -1:
return field
return field.replace('#user#', user).replace('#email#', user).replace('#username#', userName)
# Tag utilities
TAG_ADDRESS_ARGUMENT_TO_FIELD_MAP = {
'country': 'country',
'countrycode': 'countryCode',
'customtype': 'customType',
'extendedaddress': 'extendedAddress',
'formatted': 'formatted',
'locality': 'locality',
'pobox': 'poBox',
'postalcode': 'postalCode',
'primary': 'primary',
'region': 'region',
'streetaddress': 'streetAddress',
'type': 'type',
}
TAG_EMAIL_ARGUMENT_TO_FIELD_MAP = {
'domain': 'domain',
'primaryemail': 'primaryEmail',
'username': 'username',
}
TAG_EXTERNALID_ARGUMENT_TO_FIELD_MAP = {
'customtype': 'customType',
'type': 'type',
'value': 'value',
}
TAG_GENDER_ARGUMENT_TO_FIELD_MAP = {
'addressmeas': 'addressMeAs',
'customgender': 'customGender',
'type': 'type',
}
TAG_IM_ARGUMENT_TO_FIELD_MAP = {
'customprotocol': 'customProtocol',
'customtype': 'customType',
'im': 'im',
'protocol': 'protocol',
'primary': 'primary',
'type': 'type',
}
TAG_KEYWORD_ARGUMENT_TO_FIELD_MAP = {
'customtype': 'customType',
'type': 'type',
'value': 'value',
}
TAG_LOCATION_ARGUMENT_TO_FIELD_MAP = {
'area': 'area',
'buildingid': 'buildingId',
'buildingname': 'buildingName',
'customtype': 'customType',
'deskcode': 'deskCode',
'floorname': 'floorName',
'floorsection': 'floorSection',
'type': 'type',
}
TAG_NAME_ARGUMENT_TO_FIELD_MAP = {
'familyname': 'familyName',
'fullname': 'fullName',
'givenname': 'givenName',
}
TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
'costcenter': 'costCenter',
'costcentre': 'costCenter',
'customtype': 'customType',
'department': 'department',
'description': 'description',
'domain': 'domain',
'fulltimeequivalent': 'fullTimeEquivalent',
'location': 'location',
'name': 'name',
'primary': 'primary',
'symbol': 'symbol',
'title': 'title',
'type': 'type',
}
TAG_OTHEREMAIL_ARGUMENT_TO_FIELD_MAP = {
'address': 'address',
'customtype': 'customType',
'primary': 'primary',
'type': 'type',
}
TAG_PHONE_ARGUMENT_TO_FIELD_MAP = {
'customtype': 'customType',
'primary': 'primary',
'type': 'type',
'value': 'value',
}
TAG_POSIXACCOUNT_ARGUMENT_TO_FIELD_MAP = {
'accountid': 'accountId',
'gecos': 'gecos',
'gid': 'gid',
'homedirectory': 'homeDirectory',
'operatingsystemtype': 'operatingSystemType',
'primary': 'primary',
'shell': 'shell',
'systemid': 'systemId',
'uid': 'uid',
'username': 'username',
}
TAG_RELATION_ARGUMENT_TO_FIELD_MAP = {
'customtype': 'customType',
'type': 'type',
'value': 'value',
}
TAG_SSHPUBLICKEY_ARGUMENT_TO_FIELD_MAP = {
'expirationtimeusec': 'expirationTimeUsec',
'fingerprint': 'fingerprint',
'key': 'key',
}
TAG_WEBSITE_ARGUMENT_TO_FIELD_MAP = {
'customtype': 'customType',
'primary': 'primary',
'type': 'type',
'value': 'value',
}
TAG_FIELD_SUBFIELD_CHOICE_MAP = {
'address': ('addresses', TAG_ADDRESS_ARGUMENT_TO_FIELD_MAP),
'addresses': ('addresses', TAG_ADDRESS_ARGUMENT_TO_FIELD_MAP),
'email': ('primaryEmail', TAG_EMAIL_ARGUMENT_TO_FIELD_MAP),
'externalid': ('externalIds', TAG_EXTERNALID_ARGUMENT_TO_FIELD_MAP),
'externalids': ('externalIds', TAG_EXTERNALID_ARGUMENT_TO_FIELD_MAP),
'gender': ('gender', TAG_GENDER_ARGUMENT_TO_FIELD_MAP),
'im': ('ims', TAG_IM_ARGUMENT_TO_FIELD_MAP),
'ims': ('ims', TAG_IM_ARGUMENT_TO_FIELD_MAP),
'keyword': ('keywords', TAG_KEYWORD_ARGUMENT_TO_FIELD_MAP),
'keywords': ('keywords', TAG_KEYWORD_ARGUMENT_TO_FIELD_MAP),
'location': ('locations', TAG_LOCATION_ARGUMENT_TO_FIELD_MAP),
'locations': ('locations', TAG_LOCATION_ARGUMENT_TO_FIELD_MAP),
'name': ('name', TAG_NAME_ARGUMENT_TO_FIELD_MAP),
'organization': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
'organizations': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
'organisation': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
'organisations': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
'otheremail': ('emails', TAG_OTHEREMAIL_ARGUMENT_TO_FIELD_MAP),
'otheremails': ('emails', TAG_OTHEREMAIL_ARGUMENT_TO_FIELD_MAP),
'phone': ('phones', TAG_PHONE_ARGUMENT_TO_FIELD_MAP),
'phones': ('phones', TAG_PHONE_ARGUMENT_TO_FIELD_MAP),
'photourl': ('thumbnailPhotoUrl', {'': 'thumbnailPhotoUrl'}),
'posix': ('posixAccounts', TAG_POSIXACCOUNT_ARGUMENT_TO_FIELD_MAP),
'posixaccounts': ('posixAccounts', TAG_POSIXACCOUNT_ARGUMENT_TO_FIELD_MAP),
'relation': ('relations', TAG_RELATION_ARGUMENT_TO_FIELD_MAP),
'relations': ('relations', TAG_RELATION_ARGUMENT_TO_FIELD_MAP),
'sshkeys': ('sshPublicKeys', TAG_SSHPUBLICKEY_ARGUMENT_TO_FIELD_MAP),
'sshpublickeys': ('sshPublicKeys', TAG_SSHPUBLICKEY_ARGUMENT_TO_FIELD_MAP),
'website': ('websites', TAG_WEBSITE_ARGUMENT_TO_FIELD_MAP),
'websites': ('websites', TAG_WEBSITE_ARGUMENT_TO_FIELD_MAP),
}
def _initTagReplacements():
return {'cd': None, 'tags': {}, 'subs': False,
'fieldsSet': set(), 'fields': '',
'schemasSet': set(), 'customFieldMask': None}
def _getTagReplacement(myarg, tagReplacements, allowSubs):
if myarg == 'replace':
trregex = None
elif myarg == 'replaceregex':
trregex = getREPatternSubstitution(re.IGNORECASE)
else:
return False
matchTag = getString(Cmd.OB_TAG)
matchReplacement = getString(Cmd.OB_STRING, minLen=0)
if matchReplacement.startswith('field:'):
if not allowSubs:
usageErrorExit(Msg.USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT)
tagReplacements['subs'] = True
field = matchReplacement[6:].strip().lower()
if field.find('.') != -1:
args = field.split('.', 3)
field = args[0]
subfield = args[1]
if len(args) == 2:
matchfield = matchvalue = ''
elif len(args) == 4:
matchfield = args[2]
matchvalue = args[3]
if matchfield == 'primary':
matchvalue = matchvalue.lower()
if matchvalue == TRUE:
matchvalue = True
elif matchvalue == FALSE:
matchvalue = ''
else:
invalidChoiceExit(matchvalue, TRUE_FALSE, True)
else:
Cmd.Backup()
usageErrorExit(Msg.INVALID_TAG_SPECIFICATION)
elif field == 'photourl':
subfield = matchfield = matchvalue = ''
else:
field = ''
if not field or field not in TAG_FIELD_SUBFIELD_CHOICE_MAP:
invalidChoiceExit(field, TAG_FIELD_SUBFIELD_CHOICE_MAP, True)
field, subfieldsChoiceMap = TAG_FIELD_SUBFIELD_CHOICE_MAP[field]
if subfield not in subfieldsChoiceMap:
invalidChoiceExit(subfield, subfieldsChoiceMap, True)
subfield = subfieldsChoiceMap[subfield]
if matchfield:
if matchfield not in subfieldsChoiceMap:
invalidChoiceExit(matchfield, subfieldsChoiceMap, True)
matchfield = subfieldsChoiceMap[matchfield]
tagReplacements['fieldsSet'].add(field)
tagReplacements['fields'] = ','.join(tagReplacements['fieldsSet'])
tagReplacements['tags'][matchTag] = {'field': field, 'subfield': subfield,
'matchfield': matchfield, 'matchvalue': matchvalue, 'value': '',
'trregex': trregex}
if field == 'locations' and subfield == 'buildingName':
_makeBuildingIdNameMap()
elif matchReplacement.startswith('schema:'):
if not allowSubs:
usageErrorExit(Msg.USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT)
tagReplacements['subs'] = True
matchReplacement = matchReplacement[7:].strip()
if matchReplacement.find('.') != -1:
schemaName, schemaField = matchReplacement.split('.', 1)
else:
schemaName = ''
if not schemaName or not schemaField:
invalidArgumentExit(Cmd.OB_SCHEMA_NAME_FIELD_NAME)
tagReplacements['fieldsSet'].add('customSchemas')
tagReplacements['fields'] = ','.join(tagReplacements['fieldsSet'])
tagReplacements['schemasSet'].add(schemaName)
tagReplacements['customFieldMask'] = ','.join(tagReplacements['schemasSet'])
tagReplacements['tags'][matchTag] = {'schema': schemaName, 'schemafield': schemaField, 'value': '',
'trregex': trregex}
elif ((matchReplacement.find('#') >= 0) and
(matchReplacement.find('#user#') >= 0) or (matchReplacement.find('#email#') >= 0) or (matchReplacement.find('#username#') >= 0)):
if not allowSubs:
usageErrorExit(Msg.USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT)
tagReplacements['subs'] = True
tagReplacements['tags'][matchTag] = {'template': matchReplacement, 'value': '',
'trregex': trregex}
else:
if trregex is None:
tagReplacements['tags'][matchTag] = {'value': matchReplacement}
else:
tagReplacements['tags'][matchTag] = {'value': re.sub(trregex[0], trregex[1], matchReplacement)}
return True
def _getTagReplacementFieldValues(user, i, count, tagReplacements, results=None):
if results is None:
if tagReplacements['fields'] and tagReplacements['fields'] != 'primaryEmail':
if not tagReplacements['cd']:
tagReplacements['cd'] = buildGAPIObject(API.DIRECTORY)
try:
results = callGAPI(tagReplacements['cd'].users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=user, projection='custom', customFieldMask=tagReplacements['customFieldMask'], fields=tagReplacements['fields'])
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.backendError, GAPI.systemError):
entityUnknownWarning(Ent.USER, user, i, count)
return
else:
results = {'primaryEmail': user}
userName, domain = splitEmailAddress(user)
for _, tag in iter(tagReplacements['tags'].items()):
if tag.get('field'):
field = tag['field']
if field == 'primaryEmail':
subfield = tag['subfield']
if subfield == 'username':
tag['value'] = userName
elif subfield == 'domain':
tag['value'] = domain
else:
tag['value'] = user
else:
if field in ['addresses', 'emails', 'ims', 'organizations', 'phones', 'posixAccounts', 'websites']:
items = results.get(field, [])
if not tag['matchfield']:
for data in items:
if data.get('primary'):
break
else:
if items:
data = items[0]
else:
data = {}
else:
for data in items:
if data.get(tag['matchfield'], '') == tag['matchvalue']:
break
else:
data = {}
elif field in ['externalIds', 'relations', 'sshPublicKeys']:
items = results.get(field, [])
if not tag['matchfield']:
if items:
data = items[0]
else:
data = {}
else:
for data in items:
if data.get(tag['matchfield'], '') == tag['matchvalue']:
break
else:
data = {}
elif field in ['keywords', 'locations']:
items = results.get(field, [])
if not tag['matchfield']:
if items:
data = items[0]
data['buildingName'] = GM.Globals[GM.MAP_BUILDING_ID_TO_NAME].get(data.get('buildingId', ''), '')
else:
data = {}
else:
for data in items:
if data.get(tag['matchfield'], '') == tag['matchvalue']:
break
else:
data = {}
elif field == 'thumbnailPhotoUrl':
data = results
else:
data = results.get(field, {})
tag['value'] = str(data.get(tag['subfield'], ''))
elif tag.get('schema'):
tag['value'] = str(results.get('customSchemas', {}).get(tag['schema'], {}).get(tag['schemafield'], ''))
elif tag.get('template'):
tag['value'] = _substituteForUser(tag['template'], user, userName)
trregex = tag.get('trregex', None)
if trregex is not None:
tag['value'] = re.sub(trregex[0], trregex[1], tag['value'])
RTL_PATTERN = re.compile(r'(?s){RTL}.*?{/RTL}')
RT_PATTERN = re.compile(r'(?s){RT}.*?{/RT}')
TAG_REPLACE_PATTERN = re.compile(r'{(.+?)}')
RT_MARKERS = {'RT', '/RT', 'RTL', '/RTL'}
PC_PATTERN = re.compile(r'(?s){PC}.*?{/PC}')
UC_PATTERN = re.compile(r'(?s){UC}.*?{/UC}')
LC_PATTERN = re.compile(r'(?s){LC}.*?{/LC}')
CASE_MARKERS = {'PC', '/PC', 'UC', '/UC', 'LC', '/LC'}
SKIP_PATTERNS = [re.compile(r'<head>.*?</head>', flags=re.IGNORECASE),
re.compile(r'<script>.*?</script>', flags=re.IGNORECASE)]
def _processTagReplacements(tagReplacements, message):
def pcase(trstring):
new = ''
# state = True: Upshift 1st letter found
# state = False: Downshift subsequent letters
state = True
for c in trstring:
if state:
if c.isalpha():
new += c.upper()
state = False
else:
new += c
else:
if c.isalpha():
new += c.lower()
else:
state = True
new += c
return new
def ucase(trstring):
return trstring.upper()
def lcase(trstring):
return trstring.lower()
def _processCase(message, casePattern, caseFunc):
# Find all {xC}.*?{/xC} sequences
pos = 0
while True:
match = casePattern.search(message, pos)
if not match:
return message
start, end = match.span()
for skipArea in skipAreas:
if start >= skipArea[0] and end <= skipArea[1]:
break
else:
message = message[:start]+caseFunc(message[start+4:end-5])+message[end:]
pos = end
# Identify areas of message to avoid replacements
skipAreas = []
for pattern in SKIP_PATTERNS:
pos = 0
while True:
match = pattern.search(message, pos)
if not match:
break
skipAreas.append(match.span())
pos = match.end()
skipTags = set()
# Find all {tag}, note replacement value and starting location; note tags in skipAreas
tagFields = []
tagSubs = {}
pos = 0
while True:
match = TAG_REPLACE_PATTERN.search(message, pos)
if not match:
break
start, end = match.span()
tag = match.group(1)
if tag in CASE_MARKERS:
pass
elif tag not in RT_MARKERS:
for skipArea in skipAreas:
if start >= skipArea[0] and end <= skipArea[1]:
skipTags.add(tag)
break
else:
tagSubs.setdefault(tag, tagReplacements['tags'].get(tag, {'value': ''})['value'])
tagFields.append((tagSubs[tag], start))
pos = end
# Find all {RT}.*?{/RT} sequences
# If any non-empty {tag} replacement value falls between them, then mark {RT} and {/RT} to be stripped
# Otherwise, mark the entire {RT}.*?{/RT} sequence to be stripped
rtStrips = []
pos = 0
while True:
match = RT_PATTERN.search(message, pos)
if not match:
break
start, end = match.span()
stripEntireRT = True
hasTags = False
for tagField in tagFields:
if tagField[1] >= end:
break
if tagField[1] >= start:
hasTags = True
if tagField[0]:
rtStrips.append((False, start, start+4))
rtStrips.append((False, end-5, end))
stripEntireRT = False
break
if stripEntireRT:
if hasTags or start+4 == end-5:
rtStrips.append((True, start, end))
else:
rtStrips.append((False, start, start+4))
rtStrips.append((False, end-5, end))
pos = end
# Find all {RTL}.*?{/RTL} sequences
# If any non-empty {RT}...{tag}... {/RT} falls between them, then mark {RTL} and {/RTL} to be stripped
# Otherwise, mark the entire {RTL}.*{/RTL} sequence to be stripped
rtlStrips = []
pos = 0
while True:
match = RTL_PATTERN.search(message, pos)
if not match:
break
start, end = match.span()
stripEntireRTL = True
hasTags = False
for tagField in tagFields:
if tagField[1] >= end:
break
if tagField[1] >= start:
hasTags = True
if tagField[0]:
rtlStrips.append((False, start, start+5, end-6, end))
stripEntireRTL = False
break
if stripEntireRTL:
for rtStrip in rtStrips:
if rtStrip[1] >= end:
break
if rtStrip[1] >= start:
hasTags = True
if not rtStrip[0]:
rtlStrips.append((False, start, start+5, end-6, end))
stripEntireRTL = False
break
if stripEntireRTL:
if hasTags or start+5 == end-6:
rtlStrips.append((True, start, end))
else:
rtlStrips.append((False, start, start+5, end-6, end))
pos = end
if rtlStrips:
allStrips = []
i = 0
l = len(rtStrips)
for rtlStrip in rtlStrips:
while i < l and rtStrips[i][1] < rtlStrip[1]:
allStrips.append(rtStrips[i])
i += 1
allStrips.append((False, rtlStrip[1], rtlStrip[2]))
if not rtlStrip[0]:
while i < l and rtStrips[i][1] < rtlStrip[3]:
allStrips.append(rtStrips[i])
i += 1
allStrips.append((False, rtlStrip[3], rtlStrip[4]))
else:
while i < l and rtStrips[i][1] < rtlStrip[2]:
i += 1
while i < l:
allStrips.append(rtStrips[i])
i += 1
else:
allStrips = rtStrips
# Strip {RTL} {/RTL}, {RT} {/RT}, {RTL}.*?{/RTL}, {RT}.*?{/RT} sequences
for rtStrip in allStrips[::-1]:
message = message[:rtStrip[1]]+message[rtStrip[2]:]
# Strip {RTL} {/RTL}, {RT} {/RT}, {RTL}.*?{/RTL}, {RT}.*?{/RT} sequences
# Make {tag} replacements; ignore tags in skipAreas
pos = 0
while True:
match = TAG_REPLACE_PATTERN.search(message, pos)
if not match:
break
start, end = match.span()
tag = match.group(1)
if tag in CASE_MARKERS:
pos = end
elif tag not in RT_MARKERS:
if tag not in skipTags:
message = re.sub(match.group(0), tagSubs[tag], message)
pos = start+1
else:
pos = end
else:
# Replace invalid RT tags with ERROR(RT)
message = re.sub(match.group(0), f'ERROR({tag})', message)
pos = start+1
# Process case changes
message = _processCase(message, PC_PATTERN, pcase)
message = _processCase(message, UC_PATTERN, ucase)
message = _processCase(message, LC_PATTERN, lcase)
return message
def sendCreateUpdateUserNotification(body, basenotify, tagReplacements, i=0, count=0, msgFrom=None, createMessage=True):
def _makeSubstitutions(field):
notify[field] = _substituteForUser(notify[field], body['primaryEmail'], userName)
notify[field] = notify[field].replace('#domain#', domain)
notify[field] = notify[field].replace('#givenname#', body['name'].get('givenName', ''))
notify[field] = notify[field].replace('#familyname#', body['name'].get('familyName', ''))
def _makePasswordSubstitutions(field, html):
if not html:
notify[field] = notify[field].replace('#password#', notify['password'])
else:
notify[field] = notify[field].replace('#password#', notify['password'].replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))
userName, domain = splitEmailAddress(body['primaryEmail'])
notify = basenotify.copy()
if not notify['subject']:
notify['subject'] = Msg.CREATE_USER_NOTIFY_SUBJECT if createMessage else Msg.UPDATE_USER_PASSWORD_CHANGE_NOTIFY_SUBJECT
_makeSubstitutions('subject')
if not notify['message']:
notify['message'] = Msg.CREATE_USER_NOTIFY_MESSAGE if createMessage else Msg.UPDATE_USER_PASSWORD_CHANGE_NOTIFY_MESSAGE
elif notify['html']:
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '<br/>')
else:
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
_makeSubstitutions('message')
if tagReplacements['subs']:
_getTagReplacementFieldValues(body['primaryEmail'], i, count, tagReplacements, body if createMessage else None)
notify['subject'] = _processTagReplacements(tagReplacements, notify['subject'])
notify['message'] = _processTagReplacements(tagReplacements, notify['message'])
_makePasswordSubstitutions('subject', False)
_makePasswordSubstitutions('message', notify['html'])
if 'from' in notify:
msgFrom = notify['from']
msgReplyTo = notify.get('replyto', None)
mailBox = notify.get('mailbox', None)
for recipient in notify['recipients']:
send_email(notify['subject'], notify['message'], recipient, i, count,
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'], mailBox=mailBox)
def getRecipients():
if checkArgumentPresent('select'):
_, recipients = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
return [normalizeEmailAddressOrUID(emailAddress, noUid=True, noLower=True) for emailAddress in recipients]
return getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
# gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>]
# [<MessageContent>]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# gam <UserTypeEntity> sendemail recipient <RecipientEntity> [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>]
# [<MessageContent>]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# gam <UserTypeEntity> sendemail from <EmailAddress> [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>]
# [<MessageContent>]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
def doSendEmail(users=None):
body = {}
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
msgFroms = [_getAdminEmail()]
count = 1
if users is None:
checkArgumentPresent({'recipient', 'recipients', 'to'})
recipients = getRecipients()
else:
_, count, entityList = getEntityArgument(users)
if checkArgumentPresent({'recipient', 'recipients', 'to'}):
msgFroms = [normalizeEmailAddressOrUID(entity) for entity in entityList]
recipients = getRecipients()
else:
if checkArgumentPresent({'from'}):
msgFroms = [getString(Cmd.OB_EMAIL_ADDRESS)]
count = 1
recipients = [normalizeEmailAddressOrUID(entity) for entity in entityList]
msgHeaders = {}
ccRecipients = []
bccRecipients = []
mailBox = None
msgReplyTo = None
singleMessage = False
tagReplacements = _initTagReplacements()
attachments = []
embeddedImages = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if users is None and myarg == 'from':
msgFroms = [getString(Cmd.OB_EMAIL_ADDRESS)]
count = 1
elif myarg == 'replyto':
msgReplyTo = getString(Cmd.OB_EMAIL_ADDRESS)
elif myarg == 'subject':
notify['subject'] = getString(Cmd.OB_STRING)
elif myarg in SORF_MSG_FILE_ARGUMENTS:
notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
elif myarg == 'cc':
ccRecipients = getRecipients()
elif myarg == 'bcc':
bccRecipients = getRecipients()
elif myarg == 'mailbox':
mailBox = getString(Cmd.OB_EMAIL_ADDRESS)
elif myarg == 'singlemessage':
singleMessage = True
elif myarg == 'html':
notify['html'] = getBoolean()
elif myarg == 'newuser':
body['primaryEmail'] = getEmailAddress()
elif myarg in {'firstname', 'givenname'}:
body.setdefault('name', {})
body['name']['givenName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
elif myarg in {'lastname', 'familyname'}:
body.setdefault('name', {})
body['name']['familyName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
elif myarg in {'password', 'notifypassword'}:
body['password'] = notify['password'] = getString(Cmd.OB_PASSWORD, maxLen=100)
elif _getTagReplacement(myarg, tagReplacements, False):
pass
elif myarg == 'attach':
attachments.append((getFilename(), getCharSet()))
elif myarg == 'embedimage':
embeddedImages.append((getFilename(), getString(Cmd.OB_STRING)))
elif myarg in SMTP_HEADERS_MAP:
if myarg in SMTP_DATE_HEADERS:
msgDate, _, _ = getTimeOrDeltaFromNow(True)
msgHeaders[SMTP_HEADERS_MAP[myarg]] = formatdate(time.mktime(msgDate.timetuple()) + msgDate.microsecond/1E6, True)
else:
msgHeaders[SMTP_HEADERS_MAP[myarg]] = getString(Cmd.OB_STRING)
elif myarg == 'header':
header = getString(Cmd.OB_STRING, minLen=1)
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
if tagReplacements['tags']:
notify['message'] = _processTagReplacements(tagReplacements, notify['message'])
if tagReplacements['tags']:
notify['subject'] = _processTagReplacements(tagReplacements, notify['subject'])
jcount = len(recipients)
if body.get('primaryEmail'):
if (recipients and ('password' in body) and ('name' in body) and ('givenName' in body['name']) and ('familyName' in body['name'])):
notify['recipients'] = recipients
sendCreateUpdateUserNotification(body, notify, tagReplacements, msgFrom=msgFroms[0])
else:
usageErrorExit(Msg.NEWUSER_REQUIREMENTS, True)
return
if ccRecipients or bccRecipients:
singleMessage = True
i = 0
for msgFrom in msgFroms:
i += 1
if singleMessage:
entityPerformActionModifierNumItems([Ent.USER, msgFrom],
Act.MODIFIER_TO, jcount+len(ccRecipients)+len(bccRecipients), Ent.RECIPIENT, i, count)
send_email(notify['subject'], notify['message'], ','.join(recipients), i, count,
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders,
ccRecipients=','.join(ccRecipients), bccRecipients=','.join(bccRecipients),
mailBox=mailBox)
else:
entityPerformActionModifierNumItems([Ent.USER, msgFrom], Act.MODIFIER_TO, jcount, Ent.RECIPIENT, i, count)
Ind.Increment()
j = 0
for recipient in recipients:
j += 1
send_email(notify['subject'], notify['message'], recipient, j, jcount,
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox)
Ind.Decrement()
ADDRESS_FIELDS_PRINT_ORDER = ['contactName', 'organizationName', 'addressLine1', 'addressLine2', 'addressLine3', 'locality', 'region', 'postalCode', 'countryCode']
def _showCustomerAddressPhoneNumber(customerInfo):
if 'postalAddress' in customerInfo:
printKeyValueList(['Address', None])
Ind.Increment()
for field in ADDRESS_FIELDS_PRINT_ORDER:
if field in customerInfo['postalAddress']:
printKeyValueList([field, customerInfo['postalAddress'][field]])
Ind.Decrement()
if 'phoneNumber' in customerInfo:
printKeyValueList(['Phone', customerInfo['phoneNumber']])
ADDRESS_FIELDS_ARGUMENT_MAP = {
'contact': 'contactName', 'contactname': 'contactName',
'name': 'organizationName', 'organizationname': 'organizationName', 'organisationname': 'organizationName',
'address': 'addressLine1', 'address1': 'addressLine1', 'addressline1': 'addressLine1',
'address2': 'addressLine2', 'addressline2': 'addressLine2',
'address3': 'addressLine3', 'addressline3': 'addressLine3',
'city': 'locality', 'locality': 'locality',
'state': 'region', 'region': 'region',
'zipcode': 'postalCode', 'postal': 'postalCode', 'postalcode': 'postalCode',
'country': 'countryCode', 'countrycode': 'countryCode',
}
def _getResoldCustomerAttr():
body = {}
customerAuthToken = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ADDRESS_FIELDS_ARGUMENT_MAP:
body.setdefault('postalAddress', {})
body['postalAddress'][ADDRESS_FIELDS_ARGUMENT_MAP[myarg]] = getString(Cmd.OB_STRING, minLen=0, maxLen=255)
elif myarg in {'email', 'alternateemail'}:
body['alternateEmail'] = getEmailAddress(noUid=True)
elif myarg in {'phone', 'phonenumber'}:
body['phoneNumber'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'customerauthtoken', 'transfertoken'}:
customerAuthToken = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
return customerAuthToken, body
# gam create resoldcustomer <CustomerDomain> (customer_auth_token <String>) <ResoldCustomerAttribute>+
def doCreateResoldCustomer():
res = buildGAPIObject(API.RESELLER)
customerDomain = getString('customerDomain')
customerAuthToken, body = _getResoldCustomerAttr()
body['customerDomain'] = customerDomain
try:
result = callGAPI(res.customers(), 'insert',
throwReasons=GAPI.RESELLER_THROW_REASONS,
body=body, customerAuthToken=customerAuthToken, fields='customerId')
entityActionPerformed([Ent.CUSTOMER_DOMAIN, body['customerDomain'], Ent.CUSTOMER_ID, result['customerId']])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_DOMAIN, body['customerDomain']], str(e))
# gam update resoldcustomer <CustomerID> <ResoldCustomerAttribute>+
def doUpdateResoldCustomer():
res = buildGAPIObject(API.RESELLER)
customerId = getString(Cmd.OB_CUSTOMER_ID)
_, body = _getResoldCustomerAttr()
try:
callGAPI(res.customers(), 'patch',
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId, body=body, fields='')
entityActionPerformed([Ent.CUSTOMER_ID, customerId])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
# gam info resoldcustomer <CustomerID> [formatjson]
def doInfoResoldCustomer():
res = buildGAPIObject(API.RESELLER)
customerId = getString(Cmd.OB_CUSTOMER_ID)
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
FJQC.GetFormatJSON(myarg)
try:
customerInfo = callGAPI(res.customers(), 'get',
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId)
if not FJQC.formatJSON:
printKeyValueList(['Customer ID', customerInfo['customerId']])
printKeyValueList(['Customer Type', customerInfo['customerType']])
printKeyValueList(['Customer Domain', customerInfo['customerDomain']])
if 'customerDomainVerified' in customerInfo:
printKeyValueList(['Customer Domain Verified', customerInfo['customerDomainVerified']])
_showCustomerAddressPhoneNumber(customerInfo)
primaryEmail = customerInfo.get('primaryAdmin', {}).get('primaryEmail')
if primaryEmail:
printKeyValueList(['Customer Primary Email', primaryEmail])
if 'alternateEmail' in customerInfo:
printKeyValueList(['Customer Alternate Email', customerInfo['alternateEmail']])
printKeyValueList(['Customer Admin Console URL', customerInfo['resourceUiUrl']])
else:
printLine(json.dumps(cleanJSON(customerInfo), ensure_ascii=False, sort_keys=False))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
def getCustomerSubscription(res):
customerId = getString(Cmd.OB_CUSTOMER_ID)
productId, skuId = SKU.getProductAndSKU(getString(Cmd.OB_SKU_ID))
if not productId:
invalidChoiceExit(skuId, SKU.getSortedSKUList(), True)
try:
subscriptions = callGAPIpages(res.subscriptions(), 'list', 'subscriptions',
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId, fields='nextPageToken,subscriptions(skuId,subscriptionId,plan(planName))')
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.SUBSCRIPTION, None], str(e))
sys.exit(GM.Globals[GM.SYSEXITRC])
for subscription in subscriptions:
if skuId == subscription['skuId']:
return (customerId, skuId, subscription['subscriptionId'], subscription['plan']['planName'])
Cmd.Backup()
usageErrorExit(f'{Ent.FormatEntityValueList([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])}, {Msg.SUBSCRIPTION_NOT_FOUND}')
PLAN_NAME_MAP = {
'annualmonthlypay': 'ANNUAL_MONTHLY_PAY',
'annualyearlypay': 'ANNUAL_YEARLY_PAY',
'flexible': 'FLEXIBLE',
'trial': 'TRIAL',
}
def _getResoldSubscriptionAttr(customerId):
body = {'customerId': customerId,
'plan': {},
'seats': {},
'skuId': None,
}
customerAuthToken = None
seats1 = seats2 = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'deal', 'dealcode'}:
body['dealCode'] = getString('dealCode')
elif myarg in {'plan', 'planname'}:
body['plan']['planName'] = getChoice(PLAN_NAME_MAP, mapChoice=True)
elif myarg in {'purchaseorderid', 'po'}:
body['purchaseOrderId'] = getString('purchaseOrderId')
elif myarg == 'seats':
seats1 = getInteger(minVal=0)
if Cmd.ArgumentsRemaining() and Cmd.Current().isdigit():
seats2 = getInteger(minVal=0)
elif myarg in {'sku', 'skuid'}:
productId, body['skuId'] = SKU.getProductAndSKU(getString(Cmd.OB_SKU_ID))
if not productId:
invalidChoiceExit(body['skuId'], SKU.getSortedSKUList(), True)
elif myarg in {'customerauthtoken', 'transfertoken'}:
customerAuthToken = getString('customer_auth_token')
else:
unknownArgumentExit()
for field in ['plan', 'skuId']:
if not body[field]:
missingArgumentExit(field.lower())
if seats1 is None:
missingArgumentExit('seats')
if body['plan']['planName'].startswith('ANNUAL'):
body['seats']['numberOfSeats'] = seats1
else:
body['seats']['maximumNumberOfSeats'] = seats1 if seats2 is None else seats2
return customerAuthToken, body
SUBSCRIPTION_SKIP_OBJECTS = {'customerId', 'skuId', 'subscriptionId'}
SUBSCRIPTION_TIME_OBJECTS = {'creationTime', 'startTime', 'endTime', 'trialEndTime', 'transferabilityExpirationTime'}
def _showSubscription(subscription, FJQC=None):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(subscription, timeObjects=SUBSCRIPTION_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
return
Ind.Increment()
printEntity([Ent.SUBSCRIPTION, subscription['subscriptionId']])
showJSON(None, subscription, SUBSCRIPTION_SKIP_OBJECTS, SUBSCRIPTION_TIME_OBJECTS)
Ind.Decrement()
# gam create resoldsubscription <CustomerID> (sku <SKUID>)
# (plan annual_monthly_pay|annual_yearly_pay|flexible|trial) (seats <Number>)
# [customer_auth_token <String>] [deal <String>] [purchaseorderid <String>]
def doCreateResoldSubscription():
res = buildGAPIObject(API.RESELLER)
customerId = getString(Cmd.OB_CUSTOMER_ID)
customerAuthToken, body = _getResoldSubscriptionAttr(customerId)
try:
subscription = callGAPI(res.subscriptions(), 'insert',
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId, customerAuthToken=customerAuthToken, body=body)
entityActionPerformed([Ent.CUSTOMER_ID, customerId, Ent.SKU, subscription['skuId']])
_showSubscription(subscription)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
RENEWAL_TYPE_MAP = {
'autorenewmonthlypay': 'AUTO_RENEW_MONTHLY_PAY',
'autorenewyearlypay': 'AUTO_RENEW_YEARLY_PAY',
'cancel': 'CANCEL',
'renewcurrentusersmonthlypay': 'RENEW_CURRENT_USERS_MONTHLY_PAY',
'renewcurrentusersyearlypay': 'RENEW_CURRENT_USERS_YEARLY_PAY',
'switchtopayasyougo': 'SWITCH_TO_PAY_AS_YOU_GO',
}
# gam update resoldsubscription <CustomerID> <SKUID>
# activate|suspend|startpaidservice|
# (renewal auto_renew_monthly_pay|auto_renew_yearly_pay|cancel|renew_current_users_monthly_pay|renew_current_users_yearly_pay|switch_to_pay_as_you_go)|
# (seats <Number>)|
# (plan annual_monthly_pay|annual_yearly_pay|flexible|trial [deal <String>] [purchaseorderid <String>] [seats <Number>])
def doUpdateResoldSubscription():
def _getSeats():
seats1 = getInteger(minVal=0)
if Cmd.ArgumentsRemaining() and Cmd.Current().isdigit():
seats2 = getInteger(minVal=0)
else:
seats2 = None
if planName.startswith('ANNUAL'):
return {'numberOfSeats': seats1}
return {'maximumNumberOfSeats': seats1 if seats2 is None else seats2}
res = buildGAPIObject(API.RESELLER)
function = None
customerId, skuId, subscriptionId, planName = getCustomerSubscription(res)
kwargs = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'activate':
function = 'activate'
elif myarg == 'suspend':
function = 'suspend'
elif myarg == 'startpaidservice':
function = 'startPaidService'
elif myarg in {'renewal', 'renewaltype'}:
function = 'changeRenewalSettings'
kwargs['body'] = {'renewalType': getChoice(RENEWAL_TYPE_MAP, mapChoice=True)}
elif myarg == 'seats':
function = 'changeSeats'
kwargs['body'] = _getSeats()
elif myarg == 'plan':
function = 'changePlan'
planName = getChoice(PLAN_NAME_MAP, mapChoice=True)
kwargs['body'] = {'planName': planName}
while Cmd.ArgumentsRemaining():
planarg = getArgument()
if planarg == 'seats':
kwargs['body']['seats'] = _getSeats()
elif planarg in {'purchaseorderid', 'po'}:
kwargs['body']['purchaseOrderId'] = getString('purchaseOrderId')
elif planarg in {'dealcode', 'deal'}:
kwargs['body']['dealCode'] = getString('dealCode')
else:
unknownArgumentExit()
else:
unknownArgumentExit()
try:
subscription = callGAPI(res.subscriptions(), function,
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId, subscriptionId=subscriptionId, **kwargs)
entityActionPerformed([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])
if subscription:
_showSubscription(subscription)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
DELETION_TYPE_MAP = {
'cancel': 'cancel',
'downgrade': 'downgrade',
'transfertodirect': 'transfer_to_direct',
}
# gam delete resoldsubscription <CustomerID> <SKUID> cancel|downgrade|transfer_to_direct
def doDeleteResoldSubscription():
res = buildGAPIObject(API.RESELLER)
customerId, skuId, subscriptionId, _ = getCustomerSubscription(res)
deletionType = getChoice(DELETION_TYPE_MAP, mapChoice=True)
checkForExtraneousArguments()
try:
callGAPI(res.subscriptions(), 'delete',
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId, subscriptionId=subscriptionId, deletionType=deletionType)
entityActionPerformed([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId], str(e))
# gam info resoldsubscription <CustomerID> <SKUID>
def doInfoResoldSubscription():
res = buildGAPIObject(API.RESELLER)
customerId, skuId, subscriptionId, _ = getCustomerSubscription(res)
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
FJQC.GetFormatJSON(myarg)
try:
subscription = callGAPI(res.subscriptions(), 'get',
throwReasons=GAPI.RESELLER_THROW_REASONS,
customerId=customerId, subscriptionId=subscriptionId)
if not FJQC.formatJSON:
printEntity([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])
_showSubscription(subscription, FJQC)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId], str(e))
PRINT_RESOLD_SUBSCRIPTIONS_TITLES = ['customerId', 'skuId', 'subscriptionId']
# gam print resoldsubscriptions [todrive <ToDriveAttribute>*]
# [customerid <CustomerID>] [customer_auth_token <String>] [customer_prefix <String>]
# [maxresults <Number>]
# [formatjson [quotechar <Character>]]
# gam show resoldsubscriptions
# [customerid <CustomerID>] [customer_auth_token <String>] [customer_prefix <String>]
# [maxresults <Number>]
# [formatjson]
def doPrintShowResoldSubscriptions():
res = buildGAPIObject(API.RESELLER)
kwargs = {'maxResults': 100}
csvPF = CSVPrintFile(PRINT_RESOLD_SUBSCRIPTIONS_TITLES, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'customerid':
kwargs['customerId'] = getString(Cmd.OB_CUSTOMER_ID)
elif myarg in {'customerauthtoken', 'transfertoken'}:
kwargs['customerAuthToken'] = getString(Cmd.OB_CUSTOMER_AUTH_TOKEN)
elif myarg == 'customerprefix':
kwargs['customerNamePrefix'] = getString(Cmd.OB_STRING)
elif myarg == 'maxresults':
kwargs['maxResults'] = getInteger(minVal=1, maxVal=100)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
try:
subscriptions = callGAPIpages(res.subscriptions(), 'list', 'subscriptions',
throwReasons=GAPI.RESELLER_THROW_REASONS,
fields='nextPageToken,subscriptions', **kwargs)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.SUBSCRIPTION, None], str(e))
return
jcount = len(subscriptions)
if not csvPF:
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.SUBSCRIPTION)
Ind.Increment()
j = 0
for subscription in subscriptions:
j += 1
if not FJQC.formatJSON:
printEntity([Ent.CUSTOMER_ID, subscription['customerId'], Ent.SKU, subscription['skuId']], j, jcount)
_showSubscription(subscription, FJQC)
Ind.Decrement()
else:
for subscription in subscriptions:
row = flattenJSON(subscription, timeObjects=SUBSCRIPTION_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'customerId': subscription['customerId'],
'skuId': subscription['skuId'],
'subscriptionId': subscription['subscriptionId'],
'JSON': json.dumps(cleanJSON(subscription, timeObjects=SUBSCRIPTION_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
csvPF.writeCSVfile('Resold Subscriptions')
def normalizeChannelResellerID(resellerId):
if resellerId.startswith('accounts/'):
return resellerId
return f'accounts/{resellerId}'
def normalizeChannelCustomerID(customerId):
if customerId.startswith('customers/'):
return customerId
return f'customers/{customerId}'
def normalizeChannelProductID(productId):
if productId.startswith('products/'):
return productId
return f'products/{productId}'
CHANNEL_ENTITY_MAP = {
Ent.CHANNEL_CUSTOMER:
{'JSONtitles': ['name', 'domain', 'JSON'],
'timeObjects': ['createTime', 'updateTime'],
'items': 'customers',
'pageSize': 50,
'maxPageSize': 50,
'fields': {
'name': 'name',
'orgdisplayname': 'orgDisplayName',
'orgpostaladdress': 'orgPostalAddress',
'primarycontactinfo': 'primaryContactInfo',
'alternateemail': 'alternateEmail',
'domain': 'domain',
'createtime': 'createTime',
'updatetime': 'updateTime',
'cloudidentityid': 'cloudIdentityId',
'languagecode': 'languageCode',
'cloudidentityinfo': 'cloudIdentityInfo',
'channelpartnerid': 'channelPartnerId',
}
},
Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
{'JSONtitles': ['name', 'offer', 'JSON'],
'timeObjects': ['createTime', 'updateTime', 'startTime', 'endTime'],
'items': 'entitlements',
'pageSize': 100,
'maxPageSize': 100,
'fields': {
'name': 'name',
'createtime': 'createTime',
'updatetime': 'updateTime',
'offer': 'offer',
'commitmentsettings': 'commitmentSettings',
'provisioningstate': 'provisioningState',
'provisionedservice': 'provisionedService',
'suspensionreasons': 'suspensionReasons',
'purchaseorderid': 'purchaseOrderId',
'trialsettings': 'trialSettings',
'associationinfo': 'associationInfo',
'parameters': 'parameters',
}
},
Ent.CHANNEL_OFFER:
{'JSONtitles': ['name', 'sku', 'JSON'],
'timeObjects': ['startTime', 'endTime'],
'items': 'offers',
'pageSize': 1000,
'maxPageSize': 1000,
'fields': {
'name': 'name',
'marketinginfo': 'marketingInfo',
'sku': 'sku',
'plan': 'plan',
'constraints': 'constraints',
'pricebyresources': 'priceByResources',
'starttime': 'startTime',
'endtime': 'endTime',
'parameterdefinitions': 'parameterDefinitions',
}
},
Ent.CHANNEL_PRODUCT:
{'JSONtitles': ['name', 'JSON'],
'timeObjects': None,
'items': 'products',
'pageSize': 1000,
'maxPageSize': 1000,
'fields': {
'name': 'name',
'marketinginfo': 'marketingInfo',
}
},
Ent.CHANNEL_SKU:
{'JSOBtitles': ['name', 'JSON'],
'timeObjects': None,
'items': 'skus',
'pageSize': 1000,
'maxPageSize': 1000,
'fields': {
'name': 'name',
'marketinginfo': 'marketingInfo',
'product': 'product',
}
}
}
def doPrintShowChannelItems(entityType):
cchan = buildGAPIObject(API.CLOUDCHANNEL)
if entityType == Ent.CHANNEL_CUSTOMER:
service = cchan.accounts().customers()
elif entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
service = cchan.accounts().customers().entitlements()
elif entityType == Ent.CHANNEL_OFFER:
service = cchan.accounts().offers()
elif entityType == Ent.CHANNEL_PRODUCT:
service = cchan.products()
else: #Ent.CHANNEL_SKU
service = cchan.products().skus()
channelEntityMap = CHANNEL_ENTITY_MAP[entityType]
# csvPF = CSVPrintFile(channelEntityMap['titles'], 'sortall') if Act.csvFormat() else None
csvPF = CSVPrintFile(['name'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
resellerId = normalizeChannelResellerID(GC.Values[GC.RESELLER_ID] if GC.Values[GC.RESELLER_ID] else GC.Values[GC.CUSTOMER_ID])
customerId = normalizeChannelCustomerID(GC.Values[GC.CHANNEL_CUSTOMER_ID])
name = None
productId = 'products/-'
kwargs = {'pageSize': channelEntityMap['pageSize']}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'resellerid':
resellerId = normalizeChannelResellerID(getString(Cmd.OB_RESELLER_ID))
elif (entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT) and myarg in {'customerid', 'channelcustomerid'}:
customerId = normalizeChannelCustomerID(getString(Cmd.OB_CHANNEL_CUSTOMER_ID))
elif (entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT) and myarg == 'name':
name = getString(Cmd.OB_STRING)
parent = name.split('/')
if (len(parent) != 4) or (parent[0] != 'accounts') or (not parent[1]) or (parent[2] != 'customers') or (not parent[3]):
Cmd.Backup()
usageErrorExit(Msg.INVALID_RESELLER_CUSTOMER_NAME)
elif (entityType in {Ent.CHANNEL_OFFER, Ent.CHANNEL_PRODUCT, Ent.CHANNEL_SKU}) and myarg == 'language':
kwargs['languageCode'] = getLanguageCode(LANGUAGE_CODES_MAP)
elif (entityType in {Ent.CHANNEL_CUSTOMER, Ent.CHANNEL_OFFER}) and myarg == 'filter':
kwargs['filter'] = getString(Cmd.OB_STRING)
elif (entityType == Ent.CHANNEL_SKU) and myarg == 'productid':
productId = normalizeChannelProductID(getString(Cmd.OB_PRODUCT_ID))
elif myarg == 'fields':
if not fieldsList:
fieldsList.append('name')
for field in _getFieldsList():
if field in channelEntityMap['fields']:
fieldsList.append(channelEntityMap['fields'][field])
else:
invalidChoiceExit(field, list(channelEntityMap['fields']), True)
elif myarg == 'maxresults':
kwargs['pageSize'] = getInteger(minVal=1, maxVal=channelEntityMap['maxPageSize'])
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if entityType != Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
entityName = resellerId
if entityType in {Ent.CHANNEL_CUSTOMER, Ent.CHANNEL_OFFER}:
kwargs['parent'] = resellerId
else:
kwargs['account'] = resellerId
if entityType == Ent.CHANNEL_SKU:
kwargs['parent'] = productId
else:
if not name and customerId == 'customers/':
missingArgumentExit('channelcustomerid')
entityName = kwargs['parent'] = name if name else f'{resellerId}/{customerId}'
fields = getItemFieldsFromFieldsList(channelEntityMap['items'], fieldsList)
# if csvPF and FJQC.formatJSON and not fieldsList:
# csvPF.SetJSONTitles(channelEntityMap['JSONtitles'])
try:
results = callGAPIpages(service, 'list', channelEntityMap['items'],
bailOnInternalError=True,
throwReasons=[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.INTERNAL_ERROR, GAPI.NOT_FOUND],
fields=fields, **kwargs)
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.badRequest, GAPI.internalError, GAPI.notFound) as e:
entityActionFailedWarning([entityType, entityName], str(e))
return
jcount = len(results)
if not csvPF:
if not FJQC.formatJSON:
performActionNumItems(jcount, entityType)
Ind.Increment()
j = 0
for item in results:
j += 1
if not FJQC.formatJSON:
printEntity([entityType, item['name']], j, jcount)
Ind.Increment()
showJSON(None, item, timeObjects=channelEntityMap['timeObjects'])
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(item, timeObjects=channelEntityMap['timeObjects']),
ensure_ascii=False, sort_keys=False))
Ind.Decrement()
else:
for item in results:
row = flattenJSON(item, timeObjects=channelEntityMap['timeObjects'])
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'name': item['name'],
'JSON': json.dumps(cleanJSON(item, timeObjects=channelEntityMap['timeObjects']),
ensure_ascii=False, sort_keys=True)}
# if not fieldsList:
# if entityType == Ent.CHANNEL_CUSTOMER:
# row.update({'domain': item['domain']})
# elif entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
# row.update({'offer': item['offer']})
# elif entityType == Ent.CHANNEL_OFFER:
# row.update({'sku': item['sku']})
csvPF.WriteRowNoFilter(row)
csvPF.writeCSVfile(Ent.Plural(entityType))
# gam print channelcustomers [todrive <ToDriveAttribute>*]
# [resellerid <ResellerID>] [filter <String>]
# [fields <ChannelCustomerFieldList>]
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam show channelcustomers
# [resellerid <ResellerID>] [filter <String>]
# [fields <ChannelCustomerFieldList>]
# [maxresults <Integer>]
# [formatjson]
def doPrintShowChannelCustomers():
doPrintShowChannelItems(Ent.CHANNEL_CUSTOMER)
# gam print channelcustomercentitlements [todrive <ToDriveAttribute>*]
# ([resellerid <ResellerID>] [customerid <ChannelCustomerID>])|
# (name accounts/<ResellerID>/customers/<ChannelCustomerID>)
# [fields <ChannelCustomerEntitlementFieldList>]
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam show channelcustomerentitlements
# ([resellerid <ResellerID>] [customerid <ChannelCustomerID>])|
# (name accounts/<ResellerID>/customers/<ChannelCustomerID>)
# [fields <ChannelCustomerEntitlementFieldList>]
# [maxresults <Integer>]
# [formatjson]
def doPrintShowChannelCustomerEntitlements():
doPrintShowChannelItems(Ent.CHANNEL_CUSTOMER_ENTITLEMENT)
# gam print channeloffers [todrive <ToDriveAttribute>*]
# [resellerid <ResellerID>] [filter <String>] [language <LanguageCode]
# [fields <ChannelOfferFieldList>]
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam show channeloffers
# [resellerid <ResellerID>] [filter <String>] [language <LanguageCode]
# [fields <ChannelOfferFieldList>]
# [maxresults <Integer>]
# [formatjson]
def doPrintShowChannelOffers():
doPrintShowChannelItems(Ent.CHANNEL_OFFER)
# gam print channelproducts [todrive <ToDriveAttribute>*]
# [resellerid <ResellerID>] [language <LanguageCode]
# [fields <ChannelProductFieldList>]
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam show channelproducts
# [resellerid <ResellerID>] [language <LanguageCode]
# [fields <ChannelProductFieldList>]
# [maxresults <Integer>]
# [formatjson]
def doPrintShowChannelProducts():
doPrintShowChannelItems(Ent.CHANNEL_PRODUCT)
# gam print channelskus [todrive <ToDriveAttribute>*]
# [resellerid <ResellerID>] [language <LanguageCode] [productid <ProductID>]
# [fields <ChannelSKUFieldList>]
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam show channelskus
# [resellerid <ResellerID>] [language <LanguageCode] [productid <ProductID>]
# [fields <ChannelSKUFieldList>]
# [maxresults <Integer>]
# [formatjson]
def doPrintShowChannelSKUs():
doPrintShowChannelItems(Ent.CHANNEL_SKU)
ANALYTIC_ENTITY_MAP = {
Ent.ANALYTIC_ACCOUNT:
{'titles': ['User', 'name', 'displayName', 'createTime', 'updateTime', 'regionCode', 'deleted'],
'JSONtitles': ['User', 'name', 'displayName', 'JSON'],
'timeObjects': ['createTime', 'updateTime'],
'items': 'accounts',
'pageSize': 50,
'maxPageSize': 200,
},
Ent.ANALYTIC_ACCOUNT_SUMMARY:
{'titles': ['User', 'name', 'displayName', 'account'],
'JSONtitles': ['User', 'name', 'displayName', 'account', 'JSON'],
'timeObjects': ['createTime', 'updateTime', 'deleteTime', 'expireTime'],
'items': 'accountSummaries',
'pageSize': 50,
'maxPageSize': 200,
},
Ent.ANALYTIC_DATASTREAM:
{'titles': ['User', 'name', 'displayName', 'type', 'createTime', 'updateTime'],
'JSONtitles': ['User', 'name', 'displayName', 'type', 'JSON'],
'timeObjects': ['createTime', 'updateTime'],
'items': 'dataStreams',
'pageSize': 50,
'maxPageSize': 200,
},
Ent.ANALYTIC_PROPERTY:
{'titles': ['User', 'name', 'displayName', 'createTime', 'updateTime', 'propertyType', 'parent'],
'JSONtitles': ['User', 'name', 'displayName', 'propertyType', 'parent', 'JSON'],
'timeObjects': ['createTime', 'updateTime', 'deleteTime', 'expireTime'],
'items': 'properties',
'pageSize': 50,
'maxPageSize': 200,
},
Ent.ANALYTIC_UA_PROPERTY:
{'titles': ['User', 'accountId', 'name', 'id', 'created', 'updated'],
'JSONtitles': ['User', 'accountId', 'name', 'id', 'JSON'],
'timeObjects': ['created', 'updated'],
'items': 'items',
'pageSize': 50,
'maxPageSize': 200,
},
}
def printShowAnalyticItems(users, entityType):
analyticEntityMap = ANALYTIC_ENTITY_MAP[entityType]
csvPF = CSVPrintFile(analyticEntityMap['titles'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
if entityType != Ent.ANALYTIC_UA_PROPERTY:
kwargs = {'pageSize': analyticEntityMap['pageSize']}
api = API.ANALYTICS_ADMIN
else:
# kwargs = {'webPropertyId': '~all'}
kwargs = {}
api = API.ANALYTICS
if entityType in {Ent.ANALYTIC_ACCOUNT, Ent.ANALYTIC_PROPERTY}:
kwargs['showDeleted'] = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'maxresults':
kwargs['pageSize'] = getInteger(minVal=1, maxVal=analyticEntityMap['maxPageSize'])
elif entityType in {Ent.ANALYTIC_ACCOUNT, Ent.ANALYTIC_PROPERTY} and myarg == 'showdeleted':
kwargs['showDeleted'] = getBoolean()
elif entityType == Ent.ANALYTIC_PROPERTY and myarg == 'filter':
kwargs['filter'] = getString(Cmd.OB_STRING)
elif entityType == Ent.ANALYTIC_UA_PROPERTY and myarg == 'accountid':
kwargs['accountId'] = getString(Cmd.OB_STRING).replace('accounts/', '')
elif entityType == Ent.ANALYTIC_DATASTREAM and myarg == 'parent':
kwargs['parent'] = getString(Cmd.OB_STRING)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if entityType == Ent.ANALYTIC_PROPERTY and 'filter' not in kwargs:
missingArgumentExit('filter')
if entityType == Ent.ANALYTIC_UA_PROPERTY and 'accountId' not in kwargs:
missingArgumentExit('accountid')
if entityType == Ent.ANALYTIC_DATASTREAM and 'parent' not in kwargs:
missingArgumentExit('parent')
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(analyticEntityMap['JSONtitles'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, analytics = buildGAPIServiceObject(api, user, i, count)
if not analytics:
continue
if entityType == Ent.ANALYTIC_ACCOUNT:
service = analytics.accounts()
elif entityType == Ent.ANALYTIC_ACCOUNT_SUMMARY:
service = analytics.accountSummaries()
elif entityType == Ent.ANALYTIC_DATASTREAM:
service = analytics.properties().dataStreams()
elif entityType == Ent.ANALYTIC_PROPERTY:
service = analytics.properties()
else: #Ent.ANALYTIC_UA_PROPERTY:
service = analytics.management().webproperties()
# service = analytics.management().profiles()
if csvPF:
printGettingAllEntityItemsForWhom(entityType, user, i, count)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
try:
results = callGAPIpages(service, 'list', analyticEntityMap['items'],
pageMessage=pageMessage,
throwReasons=[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.INTERNAL_ERROR,
GAPI.SERVICE_NOT_AVAILABLE],
**kwargs)
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.badRequest, GAPI.internalError) as e:
entityActionFailedWarning([Ent.USER, user, entityType, None], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userAnalyticsServiceNotEnabledWarning(user, i, count)
continue
jcount = len(results)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, entityType)
Ind.Increment()
j = 0
for item in results:
j += 1
if not FJQC.formatJSON:
printEntity([entityType, item['name']], j, jcount)
Ind.Increment()
showJSON(None, item, timeObjects=analyticEntityMap['timeObjects'])
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(item, timeObjects=analyticEntityMap['timeObjects']),
ensure_ascii=False, sort_keys=False))
Ind.Decrement()
else:
for item in results:
row = flattenJSON(item, flattened={'User': user}, timeObjects=analyticEntityMap['timeObjects'])
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
if entityType != Ent.ANALYTIC_UA_PROPERTY:
row = {'User': user, 'name': item['name'], 'displayName': item['displayName']}
else:
row = {'User': user, 'accountId': item['accountId'], 'id': item['id'], 'name': item['name']}
for field in analyticEntityMap['JSONtitles'][2:-1]:
row[field] = item[field]
row['JSON'] = json.dumps(cleanJSON(item, timeObjects=analyticEntityMap['timeObjects']),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
if csvPF:
csvPF.writeCSVfile(Ent.Plural(entityType))
# gam <UserTypeEntity> print analyticaccounts [todrive <ToDriveAttribute>*]
# [maxresults <Integer>] [showdeleted [<Boolean>]]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show analyticaccounts
# [maxresults <Integer>] [showdeleted [<Boolean>]]
# [formatjson]
def printShowAnalyticAccounts(users):
printShowAnalyticItems(users, Ent.ANALYTIC_ACCOUNT)
# gam <UserTypeEntity> print analyticaccountsummaries [todrive <ToDriveAttribute>*]
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show analyticaccountsummaries
# [maxresults <Integer>]
# [formatjson]
def printShowAnalyticAccountSummaries(users):
printShowAnalyticItems(users, Ent.ANALYTIC_ACCOUNT_SUMMARY)
# gam <UserTypeEntity> print analyticproperties [todrive <ToDriveAttribute>*]
# filter <String>
# [maxresults <Integer>] [showdeleted [<Boolean>]]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show analyticproperties
# filter <String>
# [maxresults <Integer>] [showdeleted [<Boolean>]]
# [formatjson]
def printShowAnalyticProperties(users):
printShowAnalyticItems(users, Ent.ANALYTIC_PROPERTY)
# gam <UserTypeEntity> print analyticuaproperties [todrive <ToDriveAttribute>*]
# accountid [accounts/]<String>
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show analyticuaproperties
# accountid [accounts/]<String>
# [maxresults <Integer>]
# [formatjson]
def printShowAnalyticUAProperties(users):
printShowAnalyticItems(users, Ent.ANALYTIC_UA_PROPERTY)
# gam <UserTypeEntity> print analyticdatastreams [todrive <ToDriveAttribute>*]
# parent <String>
# [maxresults <Integer>]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show analyticdatastreams
# parent <String>
# [maxresults <Integer>]
# [formatjson]
def printShowAnalyticDatastreams(users):
printShowAnalyticItems(users, Ent.ANALYTIC_DATASTREAM)
# gam create domainalias|aliasdomain <DomainAlias> <DomainName>
def doCreateDomainAlias():
cd = buildGAPIObject(API.DIRECTORY)
body = {'domainAliasName': getString(Cmd.OB_DOMAIN_ALIAS)}
body['parentDomainName'] = getString(Cmd.OB_DOMAIN_NAME)
checkForExtraneousArguments()
try:
callGAPI(cd.domainAliases(), 'insert',
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.DUPLICATE, GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
GAPI.FORBIDDEN, GAPI.CONFLICT],
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformed([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']])
except GAPI.domainNotFound:
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName']], Msg.DOES_NOT_EXIST)
except GAPI.duplicate:
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']], Msg.DUPLICATE)
except (GAPI.invalid, GAPI.conflict) as e:
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
accessErrorExit(cd, str(e))
# gam delete domainalias|aliasdomain <DomainAlias>
def doDeleteDomainAlias():
cd = buildGAPIObject(API.DIRECTORY)
domainAliasName = getString(Cmd.OB_DOMAIN_ALIAS)
checkForExtraneousArguments()
try:
callGAPI(cd.domainAliases(), 'delete',
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], domainAliasName=domainAliasName)
entityActionPerformed([Ent.DOMAIN_ALIAS, domainAliasName])
except GAPI.domainAliasNotFound:
entityActionFailedWarning([Ent.DOMAIN_ALIAS, domainAliasName], Msg.DOES_NOT_EXIST)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
accessErrorExit(cd, str(e))
DOMAIN_TIME_OBJECTS = {'creationTime'}
DOMAIN_ALIAS_PRINT_ORDER = ['parentDomainName', 'creationTime', 'verified']
DOMAIN_ALIAS_SKIP_OBJECTS = {'domainAliasName'}
def _showDomainAlias(alias, FJQC, aliasSkipObjects, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(alias, timeObjects=DOMAIN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.DOMAIN_ALIAS, alias['domainAliasName']], i, count)
Ind.Increment()
if 'creationTime' in alias:
alias['creationTime'] = formatLocalTimestamp(alias['creationTime'])
for field in DOMAIN_ALIAS_PRINT_ORDER:
if field in alias:
printKeyValueList([field, alias[field]])
aliasSkipObjects.add(field)
showJSON(None, alias, aliasSkipObjects)
Ind.Decrement()
# gam info domainalias|aliasdomain <DomainAlias> [formatjson]
def doInfoDomainAlias():
cd = buildGAPIObject(API.DIRECTORY)
domainAliasName = getString(Cmd.OB_DOMAIN_ALIAS)
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
try:
result = callGAPI(cd.domainAliases(), 'get',
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], domainAliasName=domainAliasName)
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
_showDomainAlias(result, FJQC, aliasSkipObjects)
except GAPI.domainAliasNotFound:
entityActionFailedWarning([Ent.DOMAIN_ALIAS, domainAliasName], Msg.DOES_NOT_EXIST)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
accessErrorExit(cd, str(e))
def _printDomain(domain, csvPF):
row = {}
for attr in domain:
if attr not in DEFAULT_SKIP_OBJECTS:
if attr in DOMAIN_TIME_OBJECTS:
row[attr] = formatLocalTimestamp(domain[attr])
else:
row[attr] = domain[attr]
csvPF.AddTitles(attr)
csvPF.WriteRow(row)
DOMAIN_ALIAS_SORT_TITLES = ['domainAliasName', 'parentDomainName', 'creationTime', 'verified']
# gam print domainaliases [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
# gam show domainaliases
# [formatjson]
# [showitemcountonly]
def doPrintShowDomainAliases():
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['domainAliasName'], DOMAIN_ALIAS_SORT_TITLES) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
try:
domainAliases = callGAPIitems(cd.domainAliases(), 'list', 'domainAliases',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID])
count = len(domainAliases)
if showItemCountOnly:
writeStdout(f'{count}\n')
return
i = 0
for domainAlias in domainAliases:
i += 1
if not csvPF:
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
_showDomainAlias(domainAlias, FJQC, aliasSkipObjects, i, count)
elif not FJQC.formatJSON:
_printDomain(domainAlias, csvPF)
else:
csvPF.WriteRowNoFilter({'domainAliasName': domainAlias['domainAliasName'],
'JSON': json.dumps(cleanJSON(domainAlias, timeObjects=DOMAIN_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
accessErrorExit(cd, str(e))
if csvPF:
csvPF.writeCSVfile('Domain Aliases')
# gam create domain <DomainName>
def doCreateDomain():
cd = buildGAPIObject(API.DIRECTORY)
body = {'domainName': getString(Cmd.OB_DOMAIN_NAME)}
checkForExtraneousArguments()
try:
callGAPI(cd.domains(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.CONFLICT],
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformed([Ent.DOMAIN, body['domainName']])
except GAPI.duplicate:
entityDuplicateWarning([Ent.DOMAIN, body['domainName']])
except GAPI.conflict as e:
entityActionFailedWarning([Ent.DOMAIN, body['domainName']], str(e))
except (GAPI.domainNotFound, GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
accessErrorExit(cd, str(e))
# gam update domain <DomainName> primary
def doUpdateDomain():
cd = buildGAPIObject(API.DIRECTORY)
domainName = getString(Cmd.OB_DOMAIN_NAME)
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'primary':
body['customerDomain'] = domainName
else:
unknownArgumentExit()
if not body:
missingArgumentExit('primary')
try:
callGAPI(cd.customers(), 'update',
throwReasons=[GAPI.DOMAIN_NOT_VERIFIED_SECONDARY, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
customerKey=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformedMessage([Ent.DOMAIN, domainName], Msg.NOW_THE_PRIMARY_DOMAIN)
except GAPI.domainNotVerifiedSecondary:
entityActionFailedWarning([Ent.DOMAIN, domainName], Msg.DOMAIN_NOT_VERIFIED_SECONDARY)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalidInput) as e:
accessErrorExit(cd, str(e))
# gam delete domain <DomainName>
def doDeleteDomain():
cd = buildGAPIObject(API.DIRECTORY)
domainName = getString(Cmd.OB_DOMAIN_NAME)
checkForExtraneousArguments()
try:
callGAPI(cd.domains(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], domainName=domainName)
entityActionPerformed([Ent.DOMAIN, domainName])
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
accessErrorExit(cd, str(e))
CUSTOMER_LICENSE_MAP = {
'accounts:num_users': 'Total Users',
'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses',
'accounts:gsuite_basic_used_licenses': 'G Suite Basic Users',
'accounts:gsuite_enterprise_total_licenses': 'Workspace Enterprise Plus Licenses',
'accounts:gsuite_enterprise_used_licenses': 'Workspace Enterprise Plus Users',
'accounts:gsuite_unlimited_total_licenses': 'G Suite Business Licenses',
'accounts:gsuite_unlimited_used_licenses': 'G Suite Business Users',
'accounts:vault_total_licenses': 'Google Vault Licenses',
}
def _showCustomerLicenseInfo(customerInfo, FJQC):
def numUsersAvailable(result):
usageReports = result.get('usageReports', [])
if usageReports:
for item in usageReports[0].get('parameters', []):
if item['name'] == 'accounts:num_users':
return usageReports
return None
rep = buildGAPIObject(API.REPORTS)
parameters = ','.join(CUSTOMER_LICENSE_MAP)
tryDate = todaysDate().strftime(YYYYMMDD_FORMAT)
dataRequiredServices = {'accounts'}
while True:
try:
result = callGAPI(rep.customerUsageReports(), 'get',
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN],
date=tryDate, customerId=customerInfo['id'],
fields='warnings,usageReports', parameters=parameters)
usageReports = numUsersAvailable(result)
if usageReports:
break
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate, dataRequiredServices)
if fullData < 0:
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_USER_COUNTS_DATA_AVAILABLE)
return
if fullData == 0:
continue
break
except GAPI.invalid as e:
tryDate = _adjustTryDate(str(e), 0, -1, tryDate)
if not tryDate:
return
continue
except GAPI.forbidden:
return
if not FJQC.formatJSON:
printKeyValueList([f'User counts as of {tryDate}:'])
Ind.Increment()
for item in usageReports[0]['parameters']:
api_name = CUSTOMER_LICENSE_MAP.get(item['name'])
api_value = int(item.get('intValue', '0'))
if api_name and api_value:
if not FJQC.formatJSON:
printKeyValueList([api_name, f'{api_value:,}'])
else:
customerInfo[item['name']] = api_value
if not FJQC.formatJSON:
Ind.Decrement()
def setTrueCustomerId(cd=None):
if GC.Values[GC.CUSTOMER_ID] == GC.MY_CUSTOMER:
if not cd:
cd = buildGAPIObject(API.DIRECTORY)
try:
customerInfo = callGAPI(cd.customers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerKey=GC.MY_CUSTOMER,
fields='id')
GC.Values[GC.CUSTOMER_ID] = customerInfo['id']
except (GAPI.badRequest, GAPI.invalidInput, GAPI.resourceNotFound, GAPI.forbidden):
pass
def _getCustomerId():
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId != GC.MY_CUSTOMER and customerId[0] != 'C':
customerId = 'C' + customerId
return customerId
def _getCustomerIdNoC():
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId[0] == 'C':
return customerId[1:]
return customerId
def _getCustomersCustomerIdNoC():
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId.startswith('C'):
customerId = customerId[1:]
return f'customers/{customerId}'
def _getCustomersCustomerIdWithC():
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId != GC.MY_CUSTOMER and customerId[0] != 'C':
customerId = 'C' + customerId
return f'customers/{customerId}'
# gam info customer [formatjson]
def doInfoCustomer(returnCustomerInfo=None, FJQC=None):
cd = buildGAPIObject(API.DIRECTORY)
customerId = _getCustomerId()
if FJQC is None:
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
try:
customerInfo = callGAPI(cd.customers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerKey=customerId)
if 'customerCreationTime' in customerInfo:
customerInfo['customerCreationTime'] = formatLocalTime(customerInfo['customerCreationTime'])
else:
customerInfo['customerCreationTime'] = UNKNOWN
primaryDomain = {'domainName': UNKNOWN, 'verified': UNKNOWN}
try:
domains = callGAPIitems(cd.domains(), 'list', 'domains',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=customerInfo['id'], fields='domains(creationTime,domainName,isPrimary,verified)')
for domain in domains:
if domain.get('isPrimary'):
primaryDomain = domain
break
# From Jay Lee
# If customer has changed primary domain, customerCreationTime is date of current primary being added, not customer create date.
# We should get all domains and use oldest date
customerCreationTime = UNKNOWN
for domain in domains:
domainCreationTime = formatLocalTimestampUTC(domain['creationTime'])
if customerCreationTime == UNKNOWN or domainCreationTime < customerCreationTime:
customerCreationTime = domainCreationTime
customerInfo['customerCreationTime'] = formatLocalTime(customerCreationTime)
except (GAPI.badRequest, GAPI.notFound):
pass
customerInfo['customerDomain'] = primaryDomain['domainName']
customerInfo['verified'] = primaryDomain['verified']
if FJQC.formatJSON:
_showCustomerLicenseInfo(customerInfo, FJQC)
if returnCustomerInfo is not None:
returnCustomerInfo.update(customerInfo)
return
printLine(json.dumps(cleanJSON(customerInfo), ensure_ascii=False, sort_keys=True))
return
printKeyValueList(['Customer ID', customerInfo['id']])
printKeyValueList(['Primary Domain', customerInfo['customerDomain']])
printKeyValueList(['Primary Domain Verified', customerInfo['verified']])
printKeyValueList(['Customer Creation Time', customerInfo['customerCreationTime']])
printKeyValueList(['Default Language', customerInfo.get('language', 'Unset or Unknown (defaults to en)')])
_showCustomerAddressPhoneNumber(customerInfo)
printKeyValueList(['Admin Secondary Email', customerInfo.get('alternateEmail', UNKNOWN)])
_showCustomerLicenseInfo(customerInfo, FJQC)
except (GAPI.badRequest, GAPI.invalidInput, GAPI.domainNotFound, GAPI.notFound, GAPI.resourceNotFound):
accessErrorExit(cd)
except GAPI.forbidden as e:
entityActionFailedExit([Ent.CUSTOMER_ID, customerId], str(e))
# gam update customer [primary <DomainName>] [adminsecondaryemail|alternateemail <EmailAddress>] [language <LanguageCode] [phone|phonenumber <String>]
# [contact|contactname <String>] [name|organizationname <String>]
# [address1|addressline1 <String>] [address2|addressline2 <String>] [address3|addressline3 <String>]
# [locality <String>] [region <String>] [postalcode <String>] [country|countrycode <String>]
def doUpdateCustomer():
cd = buildGAPIObject(API.DIRECTORY)
customerId = _getCustomerId()
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ADDRESS_FIELDS_ARGUMENT_MAP:
body.setdefault('postalAddress', {})
body['postalAddress'][ADDRESS_FIELDS_ARGUMENT_MAP[myarg]] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'primary':
body['customerDomain'] = getString(Cmd.OB_DOMAIN_NAME)
elif myarg in {'adminsecondaryemail', 'alternateemail'}:
body['alternateEmail'] = getEmailAddress(noUid=True)
elif myarg in {'phone', 'phonenumber'}:
body['phoneNumber'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'language':
body['language'] = getLanguageCode(LANGUAGE_CODES_MAP)
else:
unknownArgumentExit()
if body:
try:
callGAPI(cd.customers(), 'patch',
throwReasons=[GAPI.DOMAIN_NOT_VERIFIED_SECONDARY, GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerKey=customerId, body=body, fields='')
entityActionPerformed([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID]])
except GAPI.domainNotVerifiedSecondary:
entityActionFailedWarning([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID], Ent.DOMAIN, body['customerDomain']], Msg.DOMAIN_NOT_VERIFIED_SECONDARY)
except (GAPI.invalid, GAPI.invalidInput) as e:
entityActionFailedWarning([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID]], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
# gam info instance [formatjson]
def doInfoInstance():
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
customerInfo = None if not FJQC.formatJSON else {}
doInfoCustomer(customerInfo, FJQC)
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(customerInfo), ensure_ascii=False, sort_keys=True))
DOMAIN_PRINT_ORDER = ['customerDomain', 'creationTime', 'isPrimary', 'verified']
DOMAIN_SKIP_OBJECTS = {'domainName', 'domainAliases'}
def _showDomain(result, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(result, timeObjects=DOMAIN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
skipObjects = DOMAIN_SKIP_OBJECTS
printEntity([Ent.DOMAIN, result['domainName']], i, count)
Ind.Increment()
if 'creationTime' in result:
result['creationTime'] = formatLocalTimestamp(result['creationTime'])
for field in DOMAIN_PRINT_ORDER:
if field in result:
printKeyValueList([field, result[field]])
skipObjects.add(field)
field = 'domainAliases'
aliases = result.get(field)
if aliases:
skipObjects.add(field)
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
for alias in aliases:
_showDomainAlias(alias, FJQC, aliasSkipObjects)
showJSON(None, alias, aliasSkipObjects)
showJSON(None, result, skipObjects)
Ind.Decrement()
# gam info domain [<DomainName>] [formatjson]
def doInfoDomain():
if (not Cmd.ArgumentsRemaining()) or (Cmd.Current().lower() == 'formatjson'):
doInfoInstance()
return
cd = buildGAPIObject(API.DIRECTORY)
domainName = getString(Cmd.OB_DOMAIN_NAME)
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
try:
result = callGAPI(cd.domains(), 'get',
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], domainName=domainName)
_showDomain(result, FJQC)
except GAPI.domainNotFound:
entityActionFailedWarning([Ent.DOMAIN, domainName], Msg.DOES_NOT_EXIST)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
DOMAIN_SORT_TITLES = ['domainName', 'parentDomainName', 'creationTime', 'type', 'verified']
# gam print domains [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
# gam show domains
# [formatjson]
# [showitemcountonly]
def doPrintShowDomains():
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['domainName'], DOMAIN_SORT_TITLES) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
try:
domains = callGAPIitems(cd.domains(), 'list', 'domains',
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID])
count = len(domains)
if showItemCountOnly:
writeStdout(f'{count}\n')
return
i = 0
for domain in domains:
i += 1
if not csvPF:
_showDomain(domain, FJQC, i, count)
elif not FJQC.formatJSON:
domain['type'] = 'primary' if domain.pop('isPrimary') else 'secondary'
domainAliases = domain.pop('domainAliases', [])
_printDomain(domain, csvPF)
for domainAlias in domainAliases:
domainAlias['type'] = 'alias'
domainAlias['domainName'] = domainAlias.pop('domainAliasName')
_printDomain(domainAlias, csvPF)
else:
csvPF.WriteRowNoFilter({'domainName': domain['domainName'],
'JSON': json.dumps(cleanJSON(domain, timeObjects=DOMAIN_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden, GAPI.domainNotFound) as e:
accessErrorExit(cd, str(e))
if csvPF:
csvPF.writeCSVfile('Domains')
PRINT_PRIVILEGES_FIELDS = ['serviceId', 'serviceName', 'privilegeName', 'isOuScopable', 'childPrivileges']
def _listPrivileges(cd):
fields = f'items({",".join(PRINT_PRIVILEGES_FIELDS)})'
try:
return callGAPIitems(cd.privileges(), 'list', 'items',
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
except (GAPI.badRequest, GAPI.customerNotFound, GAPI.forbidden):
accessErrorExit(cd)
# gam print privileges [todrive <ToDriveAttribute>*]
# gam show privileges
def doPrintShowPrivileges():
def _showPrivilege(privilege, i, count):
printEntity([Ent.PRIVILEGE, privilege['privilegeName']], i, count)
Ind.Increment()
printKeyValueList(['serviceId', privilege['serviceId']])
printKeyValueList(['serviceName', privilege.get('serviceName', UNKNOWN)])
printKeyValueList(['isOuScopable', privilege['isOuScopable']])
jcount = len(privilege.get('childPrivileges', []))
if jcount > 0:
printKeyValueList(['childPrivileges', jcount])
Ind.Increment()
j = 0
for childPrivilege in privilege['childPrivileges']:
j += 1
_showPrivilege(childPrivilege, j, jcount)
Ind.Decrement()
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(PRINT_PRIVILEGES_FIELDS, 'sortall') if Act.csvFormat() else None
getTodriveOnly(csvPF)
privileges = _listPrivileges(cd)
if not csvPF:
count = len(privileges)
performActionNumItems(count, Ent.PRIVILEGE)
Ind.Increment()
i = 0
for privilege in privileges:
i += 1
_showPrivilege(privilege, i, count)
Ind.Decrement()
else:
for privilege in privileges:
csvPF.WriteRowTitles(flattenJSON(privilege))
if csvPF:
csvPF.writeCSVfile('Privileges')
def makeRoleIdNameMap():
GM.Globals[GM.MAKE_ROLE_ID_NAME_MAP] = False
cd = buildGAPIObject(API.DIRECTORY)
try:
result = callGAPIpages(cd.roles(), 'list', 'items',
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID],
fields='nextPageToken,items(roleId,roleName)',
maxResults=100)
except (GAPI.badRequest, GAPI.customerNotFound, GAPI.forbidden):
accessErrorExit(cd)
for role in result:
GM.Globals[GM.MAP_ROLE_ID_TO_NAME][role['roleId']] = role['roleName']
GM.Globals[GM.MAP_ROLE_NAME_TO_ID][role['roleName'].lower()] = role['roleId']
def role_from_roleid(roleid):
if GM.Globals[GM.MAKE_ROLE_ID_NAME_MAP]:
makeRoleIdNameMap()
return GM.Globals[GM.MAP_ROLE_ID_TO_NAME].get(roleid, roleid)
def roleid_from_role(role):
if GM.Globals[GM.MAKE_ROLE_ID_NAME_MAP]:
makeRoleIdNameMap()
return GM.Globals[GM.MAP_ROLE_NAME_TO_ID].get(role.lower(), None)
def getRoleId():
role = getString(Cmd.OB_ROLE_ITEM)
cg = UID_PATTERN.match(role)
if cg:
roleId = cg.group(1)
else:
roleId = roleid_from_role(role)
if not roleId:
invalidChoiceExit(role, GM.Globals[GM.MAP_ROLE_NAME_TO_ID], True)
return (role, roleId)
# gam create adminrole <String> privileges all|all_ou|<PrivilegesList> [description <String>]
# gam update adminrole <RoleItem> [name <String>] [privileges all|all_ou|<PrivilegesList>] [description <String>]
def doCreateUpdateAdminRoles():
def expandChildPrivileges(privilege):
for childPrivilege in privilege.get('childPrivileges', []):
childPrivileges[childPrivilege['privilegeName']] = childPrivilege['serviceId']
expandChildPrivileges(childPrivilege)
cd = buildGAPIObject(API.DIRECTORY)
updateCmd = Act.Get() == Act.UPDATE
if not updateCmd:
body = {'roleName': getString(Cmd.OB_STRING)}
else:
body = {}
_, roleId = getRoleId()
allPrivileges = {}
ouPrivileges = {}
childPrivileges = {}
for privilege in _listPrivileges(cd):
allPrivileges[privilege['privilegeName']] = privilege['serviceId']
if privilege['isOuScopable']:
ouPrivileges[privilege['privilegeName']] = privilege['serviceId']
expandChildPrivileges(privilege)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'privileges':
privs = getString(Cmd.OB_PRIVILEGE_LIST).upper()
if privs == 'ALL':
body['rolePrivileges'] = [{'privilegeName': p, 'serviceId': v} for p, v in allPrivileges.items()]
elif privs == 'ALL_OU':
body['rolePrivileges'] = [{'privilegeName': p, 'serviceId': v} for p, v in ouPrivileges.items()]
else:
body.setdefault('rolePrivileges', [])
for p in privs.split(','):
if p in allPrivileges:
body['rolePrivileges'].append({'privilegeName': p, 'serviceId': allPrivileges[p]})
elif p in ouPrivileges:
body['rolePrivileges'].append({'privilegeName': p, 'serviceId': ouPrivileges[p]})
elif p in childPrivileges:
body['rolePrivileges'].append({'privilegeName': p, 'serviceId': childPrivileges[p]})
elif ':' in p:
priv, serv = p.split(':')
body['rolePrivileges'].append({'privilegeName': priv, 'serviceId': serv.lower()})
else:
invalidChoiceExit(p, list(allPrivileges.keys())+list(ouPrivileges.keys())+list(childPrivileges.keys()), True)
elif myarg == 'description':
body['roleDescription'] = getString(Cmd.OB_STRING)
elif myarg == 'name':
body['roleName'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
if not updateCmd and not body.get('rolePrivileges'):
missingArgumentExit('privileges')
try:
if not updateCmd:
result = callGAPI(cd.roles(), 'insert',
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN]+[GAPI.DUPLICATE],
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='roleId,roleName')
else:
result = callGAPI(cd.roles(), 'patch',
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN]+[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION],
customer=GC.Values[GC.CUSTOMER_ID], roleId=roleId, body=body, fields='roleId,roleName')
entityActionPerformed([Ent.ADMIN_ROLE, f"{result['roleName']}({result['roleId']})"])
except GAPI.duplicate as e:
entityActionFailedWarning([Ent.ADMIN_ROLE, f"{body['roleName']}"], str(e))
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.ADMIN_ROLE, roleId], str(e))
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
# gam delete adminrole <RoleItem>
def doDeleteAdminRole():
cd = buildGAPIObject(API.DIRECTORY)
role, roleId = getRoleId()
checkForExtraneousArguments()
try:
callGAPI(cd.roles(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN]+[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION],
customer=GC.Values[GC.CUSTOMER_ID], roleId=roleId)
entityActionPerformed([Ent.ADMIN_ROLE, f"{role}({roleId})"])
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.ADMIN_ROLE, roleId], str(e))
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
PRINT_ADMIN_ROLES_FIELDS = ['roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', 'isSystemRole']
def _showAdminRole(role, i=0, count=0):
printEntity([Ent.ADMIN_ROLE, role['roleName']], i, count)
Ind.Increment()
for field in PRINT_ADMIN_ROLES_FIELDS:
if field != 'roleName' and field in role:
printKeyValueList([field, role[field]])
jcount = len(role.get('rolePrivileges', []))
if jcount > 0:
printKeyValueList(['rolePrivileges', jcount])
Ind.Increment()
j = 0
for rolePrivilege in role['rolePrivileges']:
j += 1
printKeyValueList(['privilegeName', rolePrivilege['privilegeName']])
Ind.Increment()
printKeyValueList(['serviceId', rolePrivilege['serviceId']])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
# gam info adminrole <RoleItem> [privileges]
def doInfoAdminRole():
cd = buildGAPIObject(API.DIRECTORY)
fieldsList = PRINT_ADMIN_ROLES_FIELDS[:]
_, roleId = getRoleId()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'privileges':
fieldsList.append('rolePrivileges')
else:
unknownArgumentExit()
fields = getFieldsFromFieldsList(fieldsList)
try:
role = callGAPI(cd.roles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION,
GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
customer=GC.Values[GC.CUSTOMER_ID], roleId=roleId, fields=fields)
role.setdefault('isSuperAdminRole', False)
role.setdefault('isSystemRole', False)
_showAdminRole(role)
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.ADMIN_ROLE, roleId], str(e))
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
# gam print adminroles|roles [todrive <ToDriveAttribute>*]
# [privileges] [oneitemperrow]
# gam show adminroles|roles [privileges]
def doPrintShowAdminRoles():
cd = buildGAPIObject(API.DIRECTORY)
fieldsList = PRINT_ADMIN_ROLES_FIELDS[:]
csvPF = CSVPrintFile(fieldsList, PRINT_ADMIN_ROLES_FIELDS) if Act.csvFormat() else None
oneItemPerRow = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'privileges':
fieldsList.append('rolePrivileges')
elif myarg == 'oneitemperrow':
oneItemPerRow = True
else:
unknownArgumentExit()
if csvPF:
if not oneItemPerRow:
csvPF.AddTitles(['rolePrivileges'])
else:
csvPF.AddTitles(['privilegeName', 'serviceId'])
fields = getItemFieldsFromFieldsList('items', fieldsList)
printGettingAllAccountEntities(Ent.ADMIN_ROLE)
try:
roles = callGAPIpages(cd.roles(), 'list', 'items',
pageMessage=getPageMessage(),
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
except (GAPI.badRequest, GAPI.customerNotFound, GAPI.forbidden):
accessErrorExit(cd)
for role in roles:
role.setdefault('isSuperAdminRole', False)
role.setdefault('isSystemRole', False)
if not csvPF:
count = len(roles)
performActionNumItems(count, Ent.ADMIN_ROLE)
Ind.Increment()
i = 0
for role in roles:
i += 1
_showAdminRole(role, i, count)
Ind.Decrement()
else:
for role in roles:
if not oneItemPerRow or 'rolePrivileges' not in role:
csvPF.WriteRowTitles(flattenJSON(role))
else:
privileges = role.pop('rolePrivileges')
baserow = flattenJSON(role)
for privilege in privileges:
row = flattenJSON(privilege, flattened=baserow.copy())
csvPF.WriteRowTitles(row)
if csvPF:
csvPF.writeCSVfile('Admin Roles')
ADMIN_SCOPE_TYPE_CHOICE_MAP = {'customer': 'CUSTOMER', 'orgunit': 'ORG_UNIT', 'org': 'ORG_UNIT', 'ou': 'ORG_UNIT'}
SECURITY_GROUP_CONDITION = "api.getAttribute('cloudidentity.googleapis.com/groups.labels', []).hasAny(['groups.security']) && resource.type == 'cloudidentity.googleapis.com/Group'"
NONSECURITY_GROUP_CONDITION = f'!{SECURITY_GROUP_CONDITION}'
ADMIN_CONDITION_CHOICE_MAP = {
'securitygroup': SECURITY_GROUP_CONDITION,
'nonsecuritygroup': NONSECURITY_GROUP_CONDITION,
}
# gam create admin <EmailAddress>|<UniqueID> <RoleItem> customer|(org_unit <OrgUnitItem>)
# [condition securitygroup|nonsecuritygroup]
def doCreateAdmin():
cd = buildGAPIObject(API.DIRECTORY)
user = getEmailAddress(returnUIDprefix='uid:')
body = {'assignedTo': convertEmailAddressToUID(user, cd, emailType='any')}
role, roleId = getRoleId()
body['roleId'] = roleId
body['scopeType'] = getChoice(ADMIN_SCOPE_TYPE_CHOICE_MAP, mapChoice=True)
if body['scopeType'] == 'ORG_UNIT':
orgUnit, orgUnitId = getOrgUnitId(cd)
body['orgUnitId'] = orgUnitId[3:]
scope = f'ORG_UNIT {orgUnit}'
else:
scope = 'CUSTOMER'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'condition':
body['condition'] = getChoice(ADMIN_CONDITION_CHOICE_MAP, mapChoice=True)
else:
unknownArgumentExit()
try:
result = callGAPI(cd.roleAssignments(), 'insert',
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND,
GAPI.FORBIDDEN, GAPI.CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.INVALID_ORGUNIT, GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='roleAssignmentId,assigneeType')
assigneeType = result.get('assigneeType')
if assigneeType == 'user':
entityType = Ent.USER
elif assigneeType == 'group':
entityType = Ent.GROUP
else:
entityType = Ent.ADMINISTRATOR
entityActionPerformedMessage([Ent.ADMIN_ROLE_ASSIGNMENT, result['roleAssignmentId']],
f'{Ent.Singular(entityType)} {user}, {Ent.Singular(Ent.ADMIN_ROLE)} {role}, {Ent.Singular(Ent.SCOPE)} {scope}')
except GAPI.internalError:
pass
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
except (GAPI.forbidden, GAPI.customerExceededRoleAssignmentsLimit, GAPI.serviceNotAvailable, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.ADMINISTRATOR, user, Ent.ADMIN_ROLE, role], str(e))
except GAPI.invalidOrgunit:
entityActionFailedWarning([Ent.ADMINISTRATOR, user], Msg.INVALID_ORGUNIT)
except GAPI.duplicate:
entityActionFailedWarning([Ent.ADMINISTRATOR, user, Ent.ADMIN_ROLE, role], Msg.DUPLICATE)
# gam delete admin <RoleAssignmentId>
def doDeleteAdmin():
cd = buildGAPIObject(API.DIRECTORY)
roleAssignmentId = getString(Cmd.OB_ROLE_ASSIGNMENT_ID)
checkForExtraneousArguments()
try:
callGAPI(cd.roleAssignments(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.OPERATION_NOT_SUPPORTED, GAPI.FORBIDDEN,
GAPI.INVALID_INPUT, GAPI.SERVICE_NOT_AVAILABLE, GAPI.RESOURCE_NOT_FOUND,
GAPI.FAILED_PRECONDITION, GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], roleAssignmentId=roleAssignmentId)
entityActionPerformed([Ent.ADMIN_ROLE_ASSIGNMENT, roleAssignmentId])
except (GAPI.notFound, GAPI.operationNotSupported, GAPI.forbidden,
GAPI.invalidInput, GAPI.serviceNotAvailable, GAPI.resourceNotFound, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.ADMIN_ROLE_ASSIGNMENT, roleAssignmentId], str(e))
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
ASSIGNEE_EMAILTYPE_TOFIELD_MAP = {
'user': 'assignedToUser',
'group': 'assignedToGroup',
'serviceaccount': 'assignedToServiceAccount',
}
PRINT_ADMIN_FIELDS = ['roleAssignmentId', 'roleId', 'assignedTo', 'scopeType', 'orgUnitId']
PRINT_ADMIN_TITLES = ['roleAssignmentId', 'roleId', 'role',
'assignedTo', 'assignedToUser', 'assignedToGroup', 'assignedToServiceAccount', 'assignedToUnknown',
'scopeType', 'orgUnitId', 'orgUnit']
# gam print admins [todrive <ToDriveAttribute>*]
# [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>] [condition]
# [privileges] [oneitemperrow]
# gam show admins
# [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>] [condition] [privileges]
def doPrintShowAdmins():
def _getPrivileges(admin):
if showPrivileges:
roleId = admin['roleId']
if roleId not in rolePrivileges:
try:
rolePrivileges[roleId] = callGAPI(cd.roles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION,
GAPI.SERVICE_NOT_AVAILABLE, GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID],
roleId=roleId,
fields='rolePrivileges')
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
entityActionFailedExit([Ent.USER, userKey, Ent.ADMIN_ROLE, admin['roleId']], str(e))
rolePrivileges[roleId] = None
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
return rolePrivileges[roleId]
def _setNamesFromIds(admin, privileges):
admin['role'] = role_from_roleid(admin['roleId'])
assignedTo = admin['assignedTo']
admin['assignedToUnknown'] = False
if assignedTo not in assignedToIdEmailMap:
assigneeType = admin.get('assigneeType')
assignedToField = ASSIGNEE_EMAILTYPE_TOFIELD_MAP.get(assigneeType, None)
assigneeEmail, assigneeType = convertUIDtoEmailAddressWithType(f'uid:{assignedTo}',
cd,
sal,
emailTypes=list(ASSIGNEE_EMAILTYPE_TOFIELD_MAP.keys()))
if not assignedToField and assigneeType in ASSIGNEE_EMAILTYPE_TOFIELD_MAP:
assignedToField = ASSIGNEE_EMAILTYPE_TOFIELD_MAP[assigneeType]
if assigneeType == 'unknown':
assignedToField = 'assignedToUnknown'
assigneeEmail = True
assignedToIdEmailMap[assignedTo] = {'assignedToField': assignedToField, 'assigneeEmail': assigneeEmail}
admin[assignedToIdEmailMap[assignedTo]['assignedToField']] = assignedToIdEmailMap[assignedTo]['assigneeEmail']
if privileges is not None:
admin.update(privileges)
if 'orgUnitId' in admin:
admin['orgUnit'] = convertOrgUnitIDtoPath(cd, f'id:{admin["orgUnitId"]}')
if 'condition' in admin:
if admin['condition'] == SECURITY_GROUP_CONDITION:
admin['condition'] = 'securitygroup'
elif admin['condition'] == NONSECURITY_GROUP_CONDITION:
admin['condition'] = 'nonsecuritygroup'
cd = buildGAPIObject(API.DIRECTORY)
sal = buildGAPIObject(API.SERVICEACCOUNTLOOKUP)
csvPF = CSVPrintFile(PRINT_ADMIN_TITLES) if Act.csvFormat() else None
roleId = None
userKey = None
oneItemPerRow = showPrivileges = False
kwargs = {}
rolePrivileges = {}
fieldsList = PRINT_ADMIN_FIELDS
assignedToIdEmailMap = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'user', 'group'}:
userKey = kwargs['userKey'] = getEmailAddress()
elif myarg == 'role':
_, roleId = getRoleId()
elif myarg == 'condition':
fieldsList.append('condition')
if csvPF:
csvPF.AddTitle('condition')
elif myarg == 'privileges':
showPrivileges = True
elif myarg == 'oneitemperrow':
oneItemPerRow = True
else:
unknownArgumentExit()
if roleId and not kwargs:
kwargs['roleId'] = roleId
roleId = None
fields = getItemFieldsFromFieldsList('items', fieldsList)
printGettingAllAccountEntities(Ent.ADMIN_ROLE_ASSIGNMENT)
try:
admins = callGAPIpages(cd.roleAssignments(), 'list', 'items',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID, GAPI.USER_NOT_FOUND,
GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], fields=fields, **kwargs)
except (GAPI.invalid, GAPI.userNotFound):
entityUnknownWarning(Ent.ADMINISTRATOR, userKey)
return
except (GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityActionFailedExit([Ent.ADMINISTRATOR, userKey, Ent.ADMIN_ROLE, roleId], str(e))
except (GAPI.badRequest, GAPI.customerNotFound):
accessErrorExit(cd)
if not csvPF:
count = len(admins)
performActionNumItems(count, Ent.ADMIN_ROLE_ASSIGNMENT)
Ind.Increment()
i = 0
for admin in admins:
i += 1
if roleId and roleId != admin['roleId']:
continue
_setNamesFromIds(admin, _getPrivileges(admin))
printEntity([Ent.ADMIN_ROLE_ASSIGNMENT, admin['roleAssignmentId']], i, count)
Ind.Increment()
for field in PRINT_ADMIN_TITLES:
if field in admin:
if field == 'roleAssignmentId':
continue
if field != 'rolePrivileges':
printKeyValueList([field, admin[field]])
else:
showJSON(None, admin[field])
Ind.Decrement()
Ind.Decrement()
else:
for admin in admins:
if roleId and roleId != admin['roleId']:
continue
_setNamesFromIds(admin, _getPrivileges(admin))
if not oneItemPerRow or 'rolePrivileges' not in admin:
csvPF.WriteRowTitles(flattenJSON(admin))
else:
privileges = admin.pop('rolePrivileges')
baserow = flattenJSON(admin)
for privilege in privileges:
row = flattenJSON(privilege, flattened=baserow.copy())
csvPF.WriteRowTitles(row)
if csvPF:
csvPF.writeCSVfile('Admins')
def getTransferApplications(dt):
try:
return callGAPIpages(dt.applications(), 'list', 'applications',
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], fields='applications(id,name,transferParams)')
except (GAPI.unknownError, GAPI.forbidden):
accessErrorExit(None)
def _convertTransferAppIDtoName(apps, appID):
for app in apps:
if appID == app['id']:
return app['name']
return f'applicationId: {appID}'
DRIVE_AND_DOCS_APP_NAME = 'drive and docs'
GOOGLE_DATA_STUDIO_APP_NAME = 'google data studio'
SERVICE_NAME_CHOICE_MAP = {
'datastudio': GOOGLE_DATA_STUDIO_APP_NAME,
'drive': DRIVE_AND_DOCS_APP_NAME,
'googledrive': DRIVE_AND_DOCS_APP_NAME,
'gdrive': DRIVE_AND_DOCS_APP_NAME,
'lookerstudio': GOOGLE_DATA_STUDIO_APP_NAME,
}
def _validateTransferAppName(apps, appName):
appName = appName.strip().lower()
appName = SERVICE_NAME_CHOICE_MAP.get(appName, appName)
appNameList = []
for app in apps:
if appName == app['name'].lower():
return (app['name'], app['id'])
appNameList.append(app['name'].lower())
invalidChoiceExit(appName, appNameList, True)
PRIVACY_LEVEL_CHOICE_MAP = {
'private': ['PRIVATE'],
'shared': ['SHARED'],
'all': ['PRIVATE', 'SHARED'],
}
# gam create datatransfer|transfer <OldOwnerID> <ServiceNameList> <NewOwnerID>
# [private|shared|all] [release_resources] (<ParameterKey> <ParameterValue>)*
# [wait <Integer> <Integer>]
def doCreateDataTransfer():
def _assignAppParameter(key, value, doubleBackup=False):
keyValid = False
for app in apps:
for params in app.get('transferParams', []):
if key == params['key']:
appIndex = appIndicies.get(app['id'])
if appIndex is not None:
body['applicationDataTransfers'][appIndex].setdefault('applicationTransferParams', [])
body['applicationDataTransfers'][appIndex]['applicationTransferParams'].append({'key': key, 'value': value})
keyValid = True
break
if not keyValid:
Cmd.Backup()
if doubleBackup:
Cmd.Backup()
usageErrorExit(Msg.NO_DATA_TRANSFER_APP_FOR_PARAMETER.format(key))
dt = buildGAPIObject(API.DATATRANSFER)
apps = getTransferApplications(dt)
old_owner = getEmailAddress(returnUIDprefix='uid:')
body = {'oldOwnerUserId': convertEmailAddressToUID(old_owner)}
appIndicies = {}
appNameList = []
waitInterval = waitRetries = 0
i = 0
body['applicationDataTransfers'] = []
for appName in getString(Cmd.OB_SERVICE_NAME_LIST).split(','):
appName, appId = _validateTransferAppName(apps, appName)
body['applicationDataTransfers'].append({'applicationId': appId})
appIndicies[appId] = i
i += 1
appNameList.append(appName)
new_owner = getEmailAddress(returnUIDprefix='uid:')
body['newOwnerUserId'] = convertEmailAddressToUID(new_owner)
if body['oldOwnerUserId'] == body['newOwnerUserId']:
Cmd.Backup()
usageErrorExit(Msg.NEW_OWNER_MUST_DIFFER_FROM_OLD_OWNER)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in PRIVACY_LEVEL_CHOICE_MAP:
_assignAppParameter('PRIVACY_LEVEL', PRIVACY_LEVEL_CHOICE_MAP[myarg])
elif myarg == 'releaseresources':
if getBoolean():
_assignAppParameter('RELEASE_RESOURCES', ['TRUE'])
elif myarg == 'wait':
waitInterval = getInteger(minVal=5, maxVal=60)
waitRetries = getInteger(minVal=0)
else:
_assignAppParameter(Cmd.Previous().upper(), getString(Cmd.OB_PARAMETER_VALUE).upper().split(','), True)
try:
result = callGAPI(dt.transfers(), 'insert',
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN],
body=body, fields='id')
except (GAPI.unknownError, GAPI.forbidden) as e:
entityActionFailedExit([Ent.USER, old_owner], str(e))
entityActionPerformed([Ent.TRANSFER_REQUEST, None])
Ind.Increment()
printEntity([Ent.TRANSFER_ID, result['id']])
printEntity([Ent.SERVICE, ','.join(appNameList)])
printKeyValueList([Msg.FROM, old_owner])
printKeyValueList([Msg.TO, new_owner])
Ind.Decrement()
if waitRetries == 0:
return
retry = 0
status = 'inProgress'
dtId = result['id']
while True:
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_DATA_TRANSFER_TO_COMPLETE_SLEEPING.format(waitInterval))
time.sleep(waitInterval)
try:
result = callGAPI(dt.transfers(), 'get',
throwReasons=[GAPI.NOT_FOUND],
dataTransferId=dtId, fields='overallTransferStatusCode')
if result['overallTransferStatusCode'] == 'completed':
status = result['overallTransferStatusCode']
break
retry += 1
if retry >= waitRetries:
break
except GAPI.notFound:
entityActionFailedWarning([Ent.TRANSFER_ID, dtId], Msg.DOES_NOT_EXIST)
break
printEntity([Ent.TRANSFER_ID, dtId, Ent.STATUS, status])
def _showTransfer(apps, transfer, i, count):
printEntity([Ent.TRANSFER_ID, transfer['id']], i, count)
Ind.Increment()
printKeyValueList(['Request Time', formatLocalTime(transfer['requestTime'])])
printKeyValueList(['Old Owner', convertUserIDtoEmail(transfer['oldOwnerUserId'])])
printKeyValueList(['New Owner', convertUserIDtoEmail(transfer['newOwnerUserId'])])
printKeyValueList(['Overall Transfer Status', transfer['overallTransferStatusCode']])
for app in transfer['applicationDataTransfers']:
printKeyValueList(['Application', _convertTransferAppIDtoName(apps, app['applicationId'])])
Ind.Increment()
printKeyValueList(['Status', app['applicationTransferStatus']])
printKeyValueList(['Parameters'])
Ind.Increment()
if 'applicationTransferParams' in app:
for param in app['applicationTransferParams']:
key = param['key']
value = param.get('value', [])
if value:
printKeyValueList([key, ','.join(value)])
else:
printKeyValueList([key])
else:
printKeyValueList(['None'])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
# gam info datatransfer|transfer <TransferID>
def doInfoDataTransfer():
dt = buildGAPIObject(API.DATATRANSFER)
apps = getTransferApplications(dt)
dtId = getString(Cmd.OB_TRANSFER_ID)
checkForExtraneousArguments()
try:
transfer = callGAPI(dt.transfers(), 'get',
throwReasons=[GAPI.NOT_FOUND],
dataTransferId=dtId)
_showTransfer(apps, transfer, 0, 0)
except GAPI.notFound:
entityActionFailedWarning([Ent.TRANSFER_ID, dtId], Msg.DOES_NOT_EXIST)
DATA_TRANSFER_STATUS_MAP = {
'completed': 'completed',
'failed': 'failed',
# 'pending': 'pending',
'inprogress': 'inProgress',
}
DATA_TRANSFER_SORT_TITLES = ['id', 'requestTime', 'oldOwnerUserEmail', 'newOwnerUserEmail',
'overallTransferStatusCode', 'application', 'applicationId', 'status']
# gam print datatransfers|transfers [todrive <ToDriveAttribute>*]
# [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>]
# [status <String>] [delimiter <Character>]]
# (addcsvdata <FieldName> <String>)*
# gam show datatransfers|transfers
# [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>]
# [status <String>] [delimiter <Character>]]
def doPrintShowDataTransfers():
dt = buildGAPIObject(API.DATATRANSFER)
apps = getTransferApplications(dt)
newOwnerUserId = None
oldOwnerUserId = None
status = None
csvPF = CSVPrintFile(['id'], DATA_TRANSFER_SORT_TITLES) if Act.csvFormat() else None
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'olduser', 'oldowner'}:
oldOwnerUserId = convertEmailAddressToUID(getEmailAddress(returnUIDprefix='uid:'))
elif myarg in {'newuser', 'newowner'}:
newOwnerUserId = convertEmailAddressToUID(getEmailAddress(returnUIDprefix='uid:'))
elif myarg == 'status':
status = getChoice(DATA_TRANSFER_STATUS_MAP, mapChoice=True)
elif myarg == 'delimiter':
delimiter = getCharacter()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
unknownArgumentExit()
try:
transfers = callGAPIpages(dt.transfers(), 'list', 'dataTransfers',
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
customerId=GC.Values[GC.CUSTOMER_ID], status=status,
newOwnerUserId=newOwnerUserId, oldOwnerUserId=oldOwnerUserId)
except (GAPI.unknownError, GAPI.forbidden, GAPI.invalidInput) as e:
entityActionFailedExit([Ent.TRANSFER_REQUEST, None], str(e))
if not csvPF:
count = len(transfers)
performActionNumItems(count, Ent.TRANSFER_REQUEST)
Ind.Increment()
i = 0
for transfer in sorted(transfers, key=lambda k: k['requestTime']):
i += 1
_showTransfer(apps, transfer, i, count)
Ind.Decrement()
else:
for transfer in sorted(transfers, key=lambda k: k['requestTime']):
row = {}
row['id'] = transfer['id']
row['requestTime'] = formatLocalTime(transfer['requestTime'])
row['oldOwnerUserEmail'] = convertUserIDtoEmail(transfer['oldOwnerUserId'])
row['newOwnerUserEmail'] = convertUserIDtoEmail(transfer['newOwnerUserId'])
row['overallTransferStatusCode'] = transfer['overallTransferStatusCode']
if addCSVData:
row.update(addCSVData)
for app in transfer['applicationDataTransfers']:
xrow = row.copy()
xrow['application'] = _convertTransferAppIDtoName(apps, app['applicationId'])
xrow['applicationId'] = app['applicationId']
xrow['status'] = app['applicationTransferStatus']
for param in app.get('applicationTransferParams', []):
key = param['key']
xrow[key] = delimiter.join(param.get('value', [] if key != 'RELEASE_RESOURCES' else ['TRUE']))
csvPF.WriteRowTitles(xrow)
if csvPF:
csvPF.writeCSVfile('Data Transfers')
# gam show transferapps
def doShowTransferApps():
dt = buildGAPIObject(API.DATATRANSFER)
checkForExtraneousArguments()
Act.Set(Act.SHOW)
try:
apps = callGAPIpages(dt.applications(), 'list', 'applications',
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], fields='applications(id,name,transferParams)')
except (GAPI.unknownError, GAPI.forbidden):
accessErrorExit(None)
count = len(apps)
performActionNumItems(count, Ent.TRANSFER_APPLICATION)
Ind.Increment()
i = 0
for app in apps:
i += 1
printKeyValueListWithCount([app['name']], i, count)
Ind.Increment()
printKeyValueList(['id', app['id']])
transferParams = app.get('transferParams', [])
if transferParams:
printKeyValueList(['Parameters'])
Ind.Increment()
for param in transferParams:
printKeyValueList(['key', param['key']])
Ind.Increment()
printKeyValueList(['value', ','.join(param['value'])])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
def _getOrgInheritance(myarg, body):
if myarg == 'noinherit':
Cmd.Backup()
deprecatedArgumentExit(myarg)
elif myarg == 'inherit':
body['blockInheritance'] = False
elif myarg in {'blockinheritance', 'inheritanceblocked'}:
location = Cmd.Location()-1
if getBoolean():
Cmd.SetLocation(location)
deprecatedArgumentExit(myarg)
body['blockInheritance'] = False
else:
return False
return True
# gam create org|ou <String> [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)] [buildpath]
def doCreateOrg():
def _createOrg(body, parentPath, fullPath):
try:
callGAPI(cd.orgunits(), 'insert',
throwReasons=[GAPI.INVALID_PARENT_ORGUNIT, GAPI.INVALID_ORGUNIT, GAPI.BACKEND_ERROR, GAPI.BAD_REQUEST, GAPI.INVALID_CUSTOMER_ID, GAPI.LOGIN_REQUIRED],
customerId=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, fullPath])
except GAPI.invalidParentOrgunit:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, fullPath, Ent.PARENT_ORGANIZATIONAL_UNIT, parentPath], Msg.ENTITY_DOES_NOT_EXIST.format(Ent.Singular(Ent.PARENT_ORGANIZATIONAL_UNIT)))
return False
except (GAPI.invalidOrgunit, GAPI.backendError):
entityDuplicateWarning([Ent.ORGANIZATIONAL_UNIT, fullPath])
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, fullPath)
return True
cd = buildGAPIObject(API.DIRECTORY)
name = getOrgUnitItem(pathOnly=True, absolutePath=False)
parent = ''
body = {}
buildPath = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'description':
body['description'] = getStringWithCRsNLs()
elif myarg == 'parent':
parent = getOrgUnitItem()
elif _getOrgInheritance(myarg, body):
pass
elif myarg == 'buildpath':
buildPath = True
else:
unknownArgumentExit()
if parent.startswith('id:'):
parentPath = None
try:
parentPath = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=parent, fields='orgUnitPath')['orgUnitPath']
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
pass
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
errMsg = accessErrorMessage(cd)
if errMsg:
systemErrorExit(INVALID_DOMAIN_RC, errMsg)
if not parentPath and not buildPath:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, name, Ent.PARENT_ORGANIZATIONAL_UNIT, parent], Msg.ENTITY_DOES_NOT_EXIST.format(Ent.Singular(Ent.PARENT_ORGANIZATIONAL_UNIT)))
return
parent = parentPath
if parent == '/':
orgUnitPath = parent+name
else:
orgUnitPath = parent+'/'+name
if orgUnitPath.count('/') > 1:
body['parentOrgUnitPath'], body['name'] = orgUnitPath.rsplit('/', 1)
else:
body['parentOrgUnitPath'] = '/'
body['name'] = orgUnitPath[1:]
parent = body['parentOrgUnitPath']
if _createOrg(body, parent, orgUnitPath) or not buildPath:
return
description = body.pop('description', None)
fullPath = '/'
getPath = ''
orgNames = orgUnitPath.split('/')
n = len(orgNames)-1
for i in range(1, n+1):
body['parentOrgUnitPath'] = fullPath
if fullPath != '/':
fullPath += '/'
fullPath += orgNames[i]
if getPath != '':
getPath += '/'
getPath += orgNames[i]
try:
callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(getPath), fields='')
printKeyValueList([Ent.Singular(Ent.ORGANIZATIONAL_UNIT), fullPath, Msg.EXISTS])
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
body['name'] = orgNames[i]
if i == n and description:
body['description'] = description
if not _createOrg(body, body['parentOrgUnitPath'], fullPath):
return
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, fullPath)
def checkOrgUnitPathExists(cd, orgUnitPath, i=0, count=0, showError=False):
if orgUnitPath == '/':
_, orgUnitId = getOrgUnitId(cd, orgUnitPath)
return (True, orgUnitPath, orgUnitId)
try:
orgUnit = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)),
fields='orgUnitPath,orgUnitId')
return (True, orgUnit['orgUnitPath'], orgUnit['orgUnitId'])
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
pass
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
errMsg = accessErrorMessage(cd)
if errMsg:
systemErrorExit(INVALID_DOMAIN_RC, errMsg)
if showError:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
return (False, orgUnitPath, orgUnitPath)
def _batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, i, count, items, quickCrOSMove, fromOrgUnitPath=None):
def _callbackMoveCrOSesToOrgUnit(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
if not fromOrgUnitPath:
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
entityModifierActionPerformed([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], toOrgUnitPath, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason in [GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN]:
checkEntityItemValueAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
errMsg = getHTTPError({}, http_status, reason, message)
if not fromOrgUnitPath:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
entityModifierActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], toOrgUnitPath, errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
jcount = len(items)
if not fromOrgUnitPath:
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CROS_DEVICE, i, count)
else:
toOrgUnitPath = f'{Act.MODIFIER_TO} {Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnitPath}'
entityPerformActionNumItemsModifier([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath], jcount, Ent.CROS_DEVICE, toOrgUnitPath, i, count)
Ind.Increment()
if not quickCrOSMove:
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]),
('deviceId', None),
('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
if not GC.Values[GC.UPDATE_CROS_OU_WITH_ID]:
svcargs['body'] = {'orgUnitPath': orgUnitPath}
method = getattr(cd.chromeosdevices(), 'update')
else:
svcargs['body'] = {'orgUnitPath': orgUnitPath, 'orgUnitId': orgUnitId}
method = getattr(cd.chromeosdevices(), 'patch')
dbatch = cd.new_batch_http_request(callback=_callbackMoveCrOSesToOrgUnit)
bcount = 0
j = 0
for deviceId in items:
j += 1
svcparms = svcargs.copy()
svcparms['deviceId'] = deviceId
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, deviceId))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = cd.new_batch_http_request(callback=_callbackMoveCrOSesToOrgUnit)
bcount = 0
if bcount > 0:
dbatch.execute()
else:
bcount = 0
j = 0
while bcount < jcount:
kcount = min(jcount-bcount, GC.Values[GC.BATCH_SIZE])
try:
deviceIds = items[bcount:bcount+kcount]
callGAPI(cd.chromeosdevices(), 'moveDevicesToOu',
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitPath,
body={'deviceIds': deviceIds})
for deviceId in deviceIds:
j += 1
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, deviceId], j, jcount)
bcount += kcount
except GAPI.invalidOrgunit:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.INVALID_ORGUNIT, i, count)
break
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, None], str(e), i, count)
break
except GAPI.resourceNotFound as e:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ','.join(deviceIds)], str(e), i, count)
break
except (GAPI.badRequest, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath, i, count)
bcount += kcount
Ind.Decrement()
def _batchMoveUsersToOrgUnit(cd, orgUnitPath, i, count, items, fromOrgUnitPath=None):
_MOVE_USER_REASON_TO_MESSAGE_MAP = {GAPI.USER_NOT_FOUND: Msg.DOES_NOT_EXIST, GAPI.DOMAIN_NOT_FOUND: Msg.SERVICE_NOT_APPLICABLE, GAPI.FORBIDDEN: Msg.SERVICE_NOT_APPLICABLE}
def _callbackMoveUsersToOrgUnit(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
if not fromOrgUnitPath:
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.USER, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
entityModifierActionPerformed([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.USER, ri[RI_ITEM]], toOrgUnitPath, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
errMsg = getHTTPError(_MOVE_USER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
if not fromOrgUnitPath:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.USER, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
entityModifierActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.USER, ri[RI_ITEM]], toOrgUnitPath, errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
jcount = len(items)
if not fromOrgUnitPath:
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.USER, i, count)
else:
toOrgUnitPath = f'{Act.MODIFIER_TO} {Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnitPath}'
entityPerformActionNumItemsModifier([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath], jcount, Ent.USER, toOrgUnitPath, i, count)
Ind.Increment()
svcargs = dict([('userKey', None), ('body', {'orgUnitPath': orgUnitPath}), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.users(), 'update')
dbatch = cd.new_batch_http_request(callback=_callbackMoveUsersToOrgUnit)
bcount = 0
j = 0
for user in items:
j += 1
svcparms = svcargs.copy()
svcparms['userKey'] = normalizeEmailAddressOrUID(user)
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, svcparms['userKey']))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = cd.new_batch_http_request(callback=_callbackMoveUsersToOrgUnit)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
def _doUpdateOrgs(entityList):
cd = buildGAPIObject(API.DIRECTORY)
if checkArgumentPresent(['move', 'add']):
entityType, items = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS, crosAllowed=True)
orgItemLists = items if isinstance(items, dict) else None
quickCrOSMove = GC.Values[GC.QUICK_CROS_MOVE]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if entityType == Cmd.ENTITY_CROS and myarg == 'quickcrosmove':
quickCrOSMove = getBoolean()
else:
unknownArgumentExit()
Act.Set(Act.ADD)
i = 0
count = len(entityList)
for orgUnitPath in entityList:
i += 1
if orgItemLists:
items = orgItemLists[orgUnitPath]
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, orgUnitPath, i, count, True)
if not status:
continue
if entityType == Cmd.ENTITY_USERS:
_batchMoveUsersToOrgUnit(cd, orgUnitPath, i, count, items)
else:
_batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, i, count, items, quickCrOSMove)
elif checkArgumentPresent(['sync']):
entityType, syncMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS, crosAllowed=True)
cmdEntityType = Cmd.ENTITY_OU if entityType == Cmd.ENTITY_USERS else Cmd.ENTITY_CROS_OU
orgItemLists = syncMembers if isinstance(syncMembers, dict) else None
if orgItemLists is None:
syncMembersSet = set(syncMembers)
removeToOrgUnitPath = '/'
removeToOrgUnitId = None
quickCrOSMove = GC.Values[GC.QUICK_CROS_MOVE]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if entityType == Cmd.ENTITY_CROS and myarg == 'quickcrosmove':
quickCrOSMove = getBoolean()
elif myarg == 'removetoou':
status, removeToOrgUnitPath, removeToOrgUnitId = checkOrgUnitPathExists(cd, getOrgUnitItem())
if not status:
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, removeToOrgUnitPath)
else:
unknownArgumentExit()
if entityType == Cmd.ENTITY_CROS and not removeToOrgUnitId:
_, removeToOrgUnitPath, removeToOrgUnitId = checkOrgUnitPathExists(cd, removeToOrgUnitPath)
i = 0
count = len(entityList)
for orgUnitPath in entityList:
i += 1
if orgItemLists:
syncMembersSet = set(orgItemLists[orgUnitPath])
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, orgUnitPath, i, count, True)
if not status:
continue
currentMembersSet = set(getItemsToModify(cmdEntityType, orgUnitPath))
if entityType == Cmd.ENTITY_USERS:
Act.Set(Act.ADD)
_batchMoveUsersToOrgUnit(cd, orgUnitPath, i, count, list(syncMembersSet-currentMembersSet))
Act.Set(Act.REMOVE)
_batchMoveUsersToOrgUnit(cd, removeToOrgUnitPath, i, count, list(currentMembersSet-syncMembersSet), orgUnitPath)
else:
Act.Set(Act.ADD)
_batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, i, count, list(syncMembersSet-currentMembersSet), quickCrOSMove)
Act.Set(Act.REMOVE)
_batchMoveCrOSesToOrgUnit(cd, removeToOrgUnitPath, removeToOrgUnitId, i, count, list(currentMembersSet-syncMembersSet), quickCrOSMove, orgUnitPath)
else:
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
elif myarg == 'description':
body['description'] = getStringWithCRsNLs()
elif myarg == 'parent':
parent = getOrgUnitItem()
if parent.startswith('id:'):
body['parentOrgUnitId'] = parent
else:
body['parentOrgUnitPath'] = parent
elif _getOrgInheritance(myarg, body):
pass
else:
unknownArgumentExit()
i = 0
count = len(entityList)
for orgUnitPath in entityList:
i += 1
try:
callGAPI(cd.orgunits(), 'update',
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND, GAPI.BACKEND_ERROR, GAPI.INVALID_ORGUNIT_NAME,
GAPI.CONDITION_NOT_MET, GAPI.BAD_REQUEST, GAPI.INVALID_CUSTOMER_ID, GAPI.LOGIN_REQUIRED],
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)), body=body, fields='')
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], i, count)
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
except GAPI.invalidOrgunitName as e:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.NAME, body['name']], str(e), i, count)
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], str(e), i, count)
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
# gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
# gam update orgs|ous <OrgUnitEntity> add|move <CrosTypeEntity> [quickcrosmove [<Boolean>]]
# gam update orgs|ous <OrgUnitEntity> add|move <UserTypeEntity>
# gam update orgs|ous <OrgUnitEntity> sync <CrosTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
# gam update orgs|ous <OrgUnitEntity> sync <UserTypeEntity> [removetoou <OrgUnitItem>]
def doUpdateOrgs():
_doUpdateOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
# gam update org|ou <OrgUnitItem> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
# gam update org|ou <OrgUnitItem> add|move <CrosTypeEntity> [quickcrosmove [<Boolean>]]
# gam update org|ou <OrgUnitItem> add|move <UserTypeEntity>
# gam update org|ou <OrgUnitItem> sync <CrosTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
# gam update org|ou <OrgUnitItem> sync <UserTypeEntity> [removetoou <OrgUnitItem>]
def doUpdateOrg():
_doUpdateOrgs([getOrgUnitItem()])
def _doDeleteOrgs(entityList):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i = 0
count = len(entityList)
for orgUnitPath in entityList:
i += 1
try:
orgUnitPath = makeOrgUnitPathAbsolute(orgUnitPath)
callGAPI(cd.orgunits(), 'delete',
throwReasons=[GAPI.CONDITION_NOT_MET, GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND, GAPI.BACKEND_ERROR,
GAPI.INVALID_CUSTOMER_ID, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.BAD_REQUEST, GAPI.LOGIN_REQUIRED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)))
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], i, count)
except GAPI.conditionNotMet:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.HAS_CHILD_ORGS.format(Ent.Plural(Ent.ORGANIZATIONAL_UNIT)), i, count)
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.invalidCustomerId, GAPI.serviceNotAvailable) as e:
### Check for my_customer
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID]], str(e), i, count)
except (GAPI.badRequest, GAPI.loginRequired):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
# gam delete orgs|ous <OrgUnitEntity>
def doDeleteOrgs():
_doDeleteOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
# gam delete org|ou <OrgUnitItem>
def doDeleteOrg():
_doDeleteOrgs([getOrgUnitItem()])
ORG_FIELD_INFO_ORDER = ['orgUnitId', 'name', 'description', 'parentOrgUnitPath', 'parentOrgUnitId', 'blockInheritance']
ORG_FIELDS_WITH_CRS_NLS = {'description'}
def _doInfoOrgs(entityList):
cd = buildGAPIObject(API.DIRECTORY)
getUsers = True
isSuspended = None
entityType = Ent.USER
showChildren = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'nousers':
getUsers = False
elif myarg in SUSPENDED_ARGUMENTS:
isSuspended = _getIsSuspended(myarg)
entityType = Ent.USER_SUSPENDED if isSuspended else Ent.USER_NOT_SUSPENDED
elif myarg in {'children', 'child'}:
showChildren = True
else:
unknownArgumentExit()
i = 0
count = len(entityList)
for origOrgUnitPath in entityList:
i += 1
try:
if origOrgUnitPath == '/':
_, orgUnitPath = getOrgUnitId(cd, origOrgUnitPath)
else:
orgUnitPath = makeOrgUnitPathRelative(origOrgUnitPath)
result = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(orgUnitPath))
if 'orgUnitPath' not in result:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, origOrgUnitPath], Msg.DOES_NOT_EXIST, i, count)
continue
printEntity([Ent.ORGANIZATIONAL_UNIT, result['orgUnitPath']], i, count)
Ind.Increment()
for field in ORG_FIELD_INFO_ORDER:
value = result.get(field, None)
if value is not None:
if field not in ORG_FIELDS_WITH_CRS_NLS:
printKeyValueList([field, value])
else:
printKeyValueWithCRsNLs(field, value)
if getUsers:
orgUnitPath = result['orgUnitPath']
users = callGAPIpages(cd.users(), 'list', 'users',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, isSuspended), orderBy='email',
fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
printEntitiesCount(entityType, None)
usersInOU = 0
Ind.Increment()
orgUnitPath = orgUnitPath.lower()
for user in users:
if orgUnitPath == user['orgUnitPath'].lower():
printKeyValueList([user['primaryEmail']])
usersInOU += 1
elif showChildren:
printKeyValueList([f'{user["primaryEmail"]} (child)'])
usersInOU += 1
Ind.Decrement()
printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.ORGANIZATIONAL_UNIT)), usersInOU])
Ind.Decrement()
except (GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
# gam info orgs|ous <OrgUnitEntity> [nousers|notsuspended|suspended] [children|child]
def doInfoOrgs():
_doInfoOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
# gam info org|ou <OrgUnitItem> [nousers|notsuspended|suspended] [children|child]
def doInfoOrg():
_doInfoOrgs([getOrgUnitItem()])
ORG_ARGUMENT_TO_FIELD_MAP = {
'blockinheritance': 'blockInheritance',
'inheritanceblocked': 'blockInheritance',
'inherit': 'blockInheritance',
'description': 'description',
'id': 'orgUnitId',
'name': 'name',
'orgunitid': 'orgUnitId',
'orgunitpath': 'orgUnitPath',
'path': 'orgUnitPath',
'parentorgunitid': 'parentOrgUnitId',
'parentid': 'parentOrgUnitId',
'parentorgunitpath': 'parentOrgUnitPath',
'parent': 'parentOrgUnitPath',
}
ORG_FIELD_PRINT_ORDER = ['orgUnitPath', 'orgUnitId', 'name', 'description', 'parentOrgUnitPath', 'parentOrgUnitId', 'blockInheritance']
PRINT_ORGS_DEFAULT_FIELDS = ['orgUnitPath', 'orgUnitId', 'name', 'parentOrgUnitId']
ORG_UNIT_SELECTOR_FIELD = 'orgUnitSelector'
PRINT_OUS_SELECTOR_CHOICES = [
Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN,
Cmd.ENTITY_OU, Cmd.ENTITY_OU_NS, Cmd.ENTITY_OU_SUSP,
Cmd.ENTITY_OU_AND_CHILDREN, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OU_AND_CHILDREN_SUSP,
]
def _getOrgUnits(cd, orgUnitPath, fieldsList, listType, showParent, batchSubOrgs, childSelector=None, parentSelector=None):
def _callbackListOrgUnits(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
orgUnits.extend(response.get('organizationUnits', []))
else:
http_status, reason, message = checkGAPIError(exception)
errMsg = getHTTPError({}, http_status, reason, message)
if reason not in GAPI.DEFAULT_RETRY_REASONS:
if reason in [GAPI.BAD_REQUEST, GAPI.INVALID_CUSTOMER_ID, GAPI.LOGIN_REQUIRED]:
accessErrorExit(cd)
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, topLevelOrgUnits[int(ri[RI_I])]], errMsg)
return
waitOnFailure(1, 10, reason, message)
try:
response = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], type='all', orgUnitPath=topLevelOrgUnits[int(ri[RI_I])], fields=listfields)
orgUnits.extend(response.get('organizationUnits', []))
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, topLevelOrgUnits[int(ri[RI_I])]], Msg.DOES_NOT_EXIST)
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
accessErrorExit(cd)
def _batchListOrgUnits():
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]), ('orgUnitPath', None), ('type', 'all'), ('fields', listfields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.orgunits(), 'list')
dbatch = cd.new_batch_http_request(callback=_callbackListOrgUnits)
bcount = 0
i = 0
for orgUnitPath in topLevelOrgUnits:
svcparms = svcargs.copy()
svcparms['orgUnitPath'] = orgUnitPath
dbatch.add(method(**svcparms), request_id=batchRequestID('', i, 0, 0, 0, ''))
bcount += 1
i += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = cd.new_batch_http_request(callback=_callbackListOrgUnits)
bcount = 0
if bcount > 0:
dbatch.execute()
deleteOrgUnitId = deleteParentOrgUnitId = False
if showParent:
localFieldsList = fieldsList[:]
if 'orgUnitId' not in fieldsList:
localFieldsList.append('orgUnitId')
deleteOrgUnitId = True
if 'parentOrgUnitId' not in fieldsList:
localFieldsList.append('parentOrgUnitId')
deleteParentOrgUnitId = True
fields = getFieldsFromFieldsList(localFieldsList)
else:
fields = getFieldsFromFieldsList(fieldsList)
listfields = f'organizationUnits({fields})'
if listType == 'all' and orgUnitPath == '/':
printGettingAllAccountEntities(Ent.ORGANIZATIONAL_UNIT)
else:
printGettingAllEntityItemsForWhom(Ent.CHILD_ORGANIZATIONAL_UNIT, orgUnitPath,
qualifier=' (Direct Children)' if listType == 'children' else '', entityType=Ent.ORGANIZATIONAL_UNIT)
if listType == 'children':
batchSubOrgs = False
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], type=listType if not batchSubOrgs else 'children',
orgUnitPath=orgUnitPath, fields=listfields)
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST)
return None
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
accessErrorExit(cd)
orgUnits = orgs.get('organizationUnits', [])
topLevelOrgUnits = [orgUnit['orgUnitPath'] for orgUnit in orgUnits]
if batchSubOrgs:
_batchListOrgUnits()
if showParent:
parentOrgIds = []
retrievedOrgIds = []
if not orgUnits:
topLevelOrgId = getTopLevelOrgId(cd, orgUnitPath)
if topLevelOrgId:
parentOrgIds.append(topLevelOrgId)
for orgUnit in orgUnits:
retrievedOrgIds.append(orgUnit['orgUnitId'])
if orgUnit['parentOrgUnitId'] not in parentOrgIds:
parentOrgIds.append(orgUnit['parentOrgUnitId'])
missing_parents = set(parentOrgIds)-set(retrievedOrgIds)
for missing_parent in missing_parents:
try:
result = callGAPI(cd.orgunits(), 'get',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=missing_parent, fields=fields)
orgUnits.append(result)
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
pass
if listType == 'all' and orgUnitPath == '/':
printGotAccountEntities(len(orgUnits))
else:
printGotEntityItemsForWhom(len(orgUnits))
if childSelector is not None:
for orgUnit in orgUnits:
orgUnit[ORG_UNIT_SELECTOR_FIELD] = childSelector if orgUnit['orgUnitPath'] != orgUnitPath else parentSelector
if deleteOrgUnitId or deleteParentOrgUnitId:
for orgUnit in orgUnits:
if deleteOrgUnitId:
orgUnit.pop('orgUnitId', None)
if deleteParentOrgUnitId:
orgUnit.pop('parentOrgUnitId', None)
return orgUnits
def getOrgUnitIdToPathMap(cd=None):
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
orgUnits = _getOrgUnits(cd, '/', ['orgUnitPath', 'orgUnitId'], 'all', True, False)
return {ou['orgUnitId']:ou['orgUnitPath'] for ou in orgUnits}
# gam print orgs|ous [todrive <ToDriveAttribute>*]
# [fromparent <OrgUnitItem>] [showparent] [toplevelonly]
# [parentselector <OrgUnitSelector> childselector <OrgUnitSelector>]
# [allfields|<OrgUnitFieldName>*|(fields <OrgUnitFieldNameList>)] [convertcrnl] [batchsuborgs [<Boolean>]]
# [mincroscount <Number>] [maxcroscount <Number>]
# [minusercount <Number>] [maxusercount <Number>]
# [showitemcountonly]
def doPrintOrgs():
cd = buildGAPIObject(API.DIRECTORY)
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
fieldsList = []
csvPF = CSVPrintFile(sortTitles=ORG_FIELD_PRINT_ORDER)
orgUnitPath = '/'
listType = 'all'
batchSubOrgs = showParent = False
childSelector = parentSelector = None
minCrOSCounts = maxCrOSCounts = minUserCounts = maxUserCounts = -1
crosCounts = {}
userCounts = {}
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'fromparent':
orgUnitPath = getOrgUnitItem()
elif myarg == 'showparent':
showParent = getBoolean()
elif myarg == 'parentselector':
parentSelector = getChoice(PRINT_OUS_SELECTOR_CHOICES)
elif myarg == 'childselector':
childSelector = getChoice(PRINT_OUS_SELECTOR_CHOICES)
elif myarg == 'mincroscount':
minCrOSCounts = getInteger(minVal=-1)
elif myarg == 'maxcroscount':
maxCrOSCounts = getInteger(minVal=-1)
elif myarg == 'minusercount':
minUserCounts = getInteger(minVal=-1)
elif myarg == 'maxusercount':
maxUserCounts = getInteger(minVal=-1)
elif myarg == 'batchsuborgs':
batchSubOrgs = getBoolean()
elif myarg == 'toplevelonly':
listType = 'children'
elif myarg == 'allfields':
fieldsList = []
csvPF.SetTitles(fieldsList)
for field in ORG_FIELD_PRINT_ORDER:
csvPF.AddField(field, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
elif myarg in ORG_ARGUMENT_TO_FIELD_MAP:
if not fieldsList:
csvPF.AddField('orgUnitPath', ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
csvPF.AddField(myarg, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
elif myarg == 'fields':
if not fieldsList:
csvPF.AddField('orgUnitPath', ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
for field in _getFieldsList():
if field in ORG_ARGUMENT_TO_FIELD_MAP:
csvPF.AddField(field, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
else:
invalidChoiceExit(field, list(ORG_ARGUMENT_TO_FIELD_MAP), True)
elif myarg in {'convertcrnl', 'converttextnl'}:
convertCRNL = True
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
unknownArgumentExit()
if childSelector:
if showParent and parentSelector is None:
missingArgumentExit('parentselector')
csvPF.AddTitle(ORG_UNIT_SELECTOR_FIELD)
csvPF.AddSortTitle(ORG_UNIT_SELECTOR_FIELD)
showCrOSCounts = (minCrOSCounts >= 0 or maxCrOSCounts >= 0)
showUserCounts = (minUserCounts >= 0 or maxUserCounts >= 0)
if not fieldsList:
for field in PRINT_ORGS_DEFAULT_FIELDS:
csvPF.AddField(field, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
orgUnits = _getOrgUnits(cd, orgUnitPath, fieldsList, listType, showParent, batchSubOrgs, childSelector, parentSelector)
if showItemCountOnly:
writeStdout(f'{0 if orgUnits is None else (len(orgUnits))}\n')
return
if orgUnits is None:
return
if showUserCounts:
for orgUnit in orgUnits:
userCounts[orgUnit['orgUnitPath']] = [0, 0]
qualifier = Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
printGettingAllEntityItemsForWhom(Ent.USER, orgUnitPath, qualifier=qualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
try:
feed = yieldGAPIpages(cd.users(), 'list', 'users',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None), orderBy='email',
fields='nextPageToken,users(orgUnitPath,suspended)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
for users in feed:
for user in users:
if user['orgUnitPath'] in userCounts:
userCounts[user['orgUnitPath']][user['suspended']] += 1
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput, GAPI.badRequest, GAPI.backendError,
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
for orgUnit in sorted(orgUnits, key=lambda k: k['orgUnitPath']):
orgUnitPath = orgUnit['orgUnitPath']
if showCrOSCounts:
crosCounts[orgUnit['orgUnitPath']] = {}
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
pageToken = None
totalItems = 0
tokenRetries = 0
while True:
try:
feed = callGAPI(cd.chromeosdevices(), 'list', 'chromeosdevices',
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageToken=pageToken,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitPath, includeChildOrgunits=False,
fields='nextPageToken,chromeosdevices(status)', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
tokenRetries = 0
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
if feed:
for cros in feed.get('chromeosdevices', []):
crosCounts[orgUnitPath].setdefault(cros['status'], 0)
crosCounts[orgUnitPath][cros['status']] += 1
del feed
if not pageToken:
_finalizeGAPIpagesResult(pageMessage)
break
except GAPI.invalidInput as e:
message = str(e)
# Invalid Input: xyz - Check for invalid pageToken!!
# 0123456789012345
if message[15:] == pageToken:
tokenRetries += 1
if tokenRetries <= 2:
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
time.sleep(tokenRetries*5)
continue
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, None], message)
break
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.badRequest, GAPI.backendError,
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
break
row = {}
for field in fieldsList:
if convertCRNL and field in ORG_FIELDS_WITH_CRS_NLS:
row[field] = escapeCRsNLs(orgUnit.get(field, ''))
else:
row[field] = orgUnit.get(field, '')
if childSelector:
row[ORG_UNIT_SELECTOR_FIELD] = orgUnit[ORG_UNIT_SELECTOR_FIELD]
if showCrOSCounts or showUserCounts:
if showCrOSCounts:
total = 0
for k, v in sorted(iter(crosCounts[orgUnitPath].items())):
row[f'CrOS{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
total += v
row[f'CrOS{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Total'] = total
if ((minCrOSCounts != -1 and total < minCrOSCounts) or
(maxCrOSCounts != -1 and total > maxCrOSCounts)):
continue
if showUserCounts:
row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}NotSuspended'] = userCounts[orgUnitPath][0]
row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Suspended'] = userCounts[orgUnitPath][1]
row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Total'] = total = userCounts[orgUnitPath][0]+userCounts[orgUnitPath][1]
if ((minUserCounts != -1 and total < minUserCounts) or
(maxUserCounts != -1 and total > maxUserCounts)):
continue
csvPF.WriteRowTitles(row)
else:
csvPF.WriteRow(row)
csvPF.writeCSVfile('Orgs')
# gam show orgtree [fromparent <OrgUnitItem>] [batchsuborgs [Boolean>]]
def doShowOrgTree():
def addOrgUnitToTree(orgPathList, i, n, tree):
if orgPathList[i] not in tree:
tree[orgPathList[i]] = {}
if i < n:
addOrgUnitToTree(orgPathList, i+1, n, tree[orgPathList[i]])
def printOrgUnit(parentOrgUnit, tree):
printKeyValueList([parentOrgUnit])
Ind.Increment()
for childOrgUnit in sorted(tree[parentOrgUnit]):
printOrgUnit(childOrgUnit, tree[parentOrgUnit])
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
orgUnitPath = '/'
fieldsList = ['orgUnitPath']
listType = 'all'
batchSubOrgs = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'fromparent':
orgUnitPath = getOrgUnitItem()
elif myarg == 'batchsuborgs':
batchSubOrgs = getBoolean()
else:
unknownArgumentExit()
orgUnits = _getOrgUnits(cd, orgUnitPath, fieldsList, listType, False, batchSubOrgs)
if orgUnits is None:
return
orgTree = {}
for orgUnit in orgUnits:
orgPath = orgUnit['orgUnitPath'].split('/')
addOrgUnitToTree(orgPath, 1, len(orgPath)-1, orgTree)
for org in sorted(orgTree):
printOrgUnit(org, orgTree)
ORG_ITEMS_FIELD_MAP = {
'browsers': 'browsers',
'devices': 'devices',
'shareddrives': 'sharedDrives',
'subous': 'subOus',
'users': 'users',
}
# gam check ou|org <OrgUnitItem> [todrive <ToDriveAttribute>*]
# [<OrgUnitCheckName>*|(fields <OrgUnitCheckNameList>)]
# [filename <FileName>] [movetoou <OrgUnitItem>]
# [formatjson [quotechar <Character>]]
def doCheckOrgUnit():
def writeCommandInfo(field):
nonlocal commitBatch
if commitBatch:
f.write(f'{Cmd.COMMIT_BATCH_CMD}\n')
else:
commitBatch = True
f.write(f'{Cmd.PRINT_CMD} Move {field} from {orgUnitPath} to {moveToOrgUnitPath}\n')
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['orgUnitPath', 'orgUnitId', 'empty'])
FJQC = FormatJSONQuoteChar(csvPF)
f = orgUnitPath = None
fieldsList = []
titlesList = []
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, getOrgUnitItem())
if not status:
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
orgUnitPathLower = orgUnitPath.lower()
fileName = 'CleanOuBatch.txt'
moveToOrgUnitPath = moveToOrgUnitPathLower = None
commitBatch = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORG_ITEMS_FIELD_MAP:
fieldsList.append(myarg)
elif myarg == 'fields':
for field in _getFieldsList():
if field in ORG_ITEMS_FIELD_MAP:
fieldsList.append(field)
else:
invalidChoiceExit(field, list(ORG_ITEMS_FIELD_MAP), True)
elif myarg == 'filename':
fileName = setFilePath(getString(Cmd.OB_FILE_NAME))
elif myarg == 'movetoou':
movetoouLocation = Cmd.Location()
status, moveToOrgUnitPath, _ = checkOrgUnitPathExists(cd, getOrgUnitItem())
moveToOrgUnitPathLower = moveToOrgUnitPath.lower()
if not status:
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, moveToOrgUnitPath)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not fieldsList:
fieldsList = ORG_ITEMS_FIELD_MAP.keys()
if moveToOrgUnitPath is not None:
Cmd.SetLocation(movetoouLocation)
if orgUnitPathLower == moveToOrgUnitPathLower:
usageErrorExit(Msg.OU_AND_MOVETOOU_CANNOT_BE_IDENTICAL.format(orgUnitPath, moveToOrgUnitPath))
if 'subous' in fieldsList and moveToOrgUnitPathLower.startswith(orgUnitPathLower):
usageErrorExit(Msg.OU_SUBOUS_CANNOT_BE_MOVED_TO_MOVETOOU.format(orgUnitPath, moveToOrgUnitPath))
fileName = setFilePath(fileName)
f = openFile(fileName, DEFAULT_FILE_WRITE_MODE)
orgUnitItemCounts = {}
for field in sorted(fieldsList):
title = ORG_ITEMS_FIELD_MAP[field]
orgUnitItemCounts[title] = 0
if not FJQC.formatJSON:
titlesList.append(title)
if 'browsers' in fieldsList:
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
printGettingAllEntityItemsForWhom(Ent.CHROME_BROWSER, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessage()
try:
feed = yieldGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
pageMessage=pageMessage, messageAttribute='deviceId',
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customerId, orgUnitPath=orgUnitPath, projection='BASIC',
fields='nextPageToken,browsers(deviceId)')
for browsers in feed:
orgUnitItemCounts['browsers'] += len(browsers)
if f is not None and orgUnitItemCounts['browsers'] > 0:
writeCommandInfo('browsers')
f.write(f'gam move browsers ou {moveToOrgUnitPath} browserou {orgUnitPath}\n')
except (GAPI.invalidInput, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e))
except GAPI.invalidOrgunit as e:
entityActionFailedExit([Ent.CHROME_BROWSER, None], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound):
accessErrorExit(None)
if 'devices' in fieldsList:
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
pageToken = None
totalItems = 0
tokenRetries = 0
while True:
try:
feed = callGAPI(cd.chromeosdevices(), 'list',
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageToken=pageToken, customerId=GC.Values[GC.CUSTOMER_ID],
orgUnitPath=orgUnitPath, fields='nextPageToken,chromeosdevices(deviceId)', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
tokenRetries = 0
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
if feed:
orgUnitItemCounts['devices'] += len(feed.get('chromeosdevices', []))
del feed
if not pageToken:
_finalizeGAPIpagesResult(pageMessage)
printGotAccountEntities(totalItems)
if f is not None and orgUnitItemCounts['devices'] > 0:
writeCommandInfo('devices')
f.write(f'gam update ou {moveToOrgUnitPath} add cros_ou {orgUnitPath}\n')
break
except GAPI.invalidInput as e:
message = str(e)
# Invalid Input: xyz - Check for invalid pageToken!!
# 0123456789012345
if message[15:] == pageToken:
tokenRetries += 1
if tokenRetries <= 2:
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
time.sleep(tokenRetries*5)
continue
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
break
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
break
except GAPI.invalidOrgunit as e:
entityActionFailedExit([Ent.CROS_DEVICE, None], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
if 'shareddrives' in fieldsList:
ci = buildGAPIObject(API.CLOUDIDENTITY_ORGUNITS_BETA)
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
sds = callGAPIpages(ci.orgUnits().memberships(), 'list', 'orgMemberships',
pageMessage=getPageMessageForWhom(),
parent=f'orgUnits/{orgUnitId[3:]}',
customer=_getCustomersCustomerIdWithC(),
filter="type == 'shared_drive'")
orgUnitItemCounts['sharedDrives'] = len(sds)
if f is not None and orgUnitItemCounts['sharedDrives'] > 0:
writeCommandInfo('Shared Drives')
for sd in sds:
name = sd['name'].split(';')[1]
f.write(f'gam update shareddrive {name} ou {moveToOrgUnitPath}\n')
if 'subous' in fieldsList:
subOus = _getOrgUnits(cd, orgUnitPath, ['orgUnitPath'], 'children', False, False, None, None)
orgUnitItemCounts['subOus'] = len(subOus)
if f is not None and orgUnitItemCounts['subOus'] > 0:
writeCommandInfo('Sub OrgUnit')
for ou in subOus:
f.write(f'gam update ou {ou["orgUnitPath"]} parent {moveToOrgUnitPath}\n')
if 'users' in fieldsList:
printGettingAllEntityItemsForWhom(Ent.USER, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
try:
feed = yieldGAPIpages(cd.users(), 'list', 'users',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None),
fields='nextPageToken,users(orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
for users in feed:
for user in users:
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
orgUnitItemCounts['users'] += 1
if f is not None and orgUnitItemCounts['users'] > 0:
writeCommandInfo('users')
f.write(f'gam update ou {moveToOrgUnitPath} add ou {orgUnitPath}\n')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput, GAPI.badRequest, GAPI.backendError,
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
if f is not None:
closeFile(f)
writeStderr(Msg.GAM_BATCH_FILE_WRITTEN.format(fileName))
empty = True
for count in orgUnitItemCounts.values():
if count > 0:
empty = False
break
baseRow = {'orgUnitPath': orgUnitPath, 'orgUnitId': orgUnitId, 'empty': empty}
row = flattenJSON(orgUnitItemCounts, baseRow.copy())
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
baseRow['JSON'] = json.dumps(cleanJSON(orgUnitItemCounts), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(baseRow)
csvPF.writeCSVfile(f'OrgUnit {orgUnitPath} Item Counts')
if not empty and GM.Globals[GM.SYSEXITRC] == 0:
setSysExitRC(ORGUNIT_NOT_EMPTY_RC)
ALIAS_TARGET_TYPES = ['user', 'group', 'target']
# gam create aliases|nicknames <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
# [verifynotinvitable]
# gam update aliases|nicknames <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
# [notargetverify] [waitafterdelete <Integer>]
def doCreateUpdateAliases():
def verifyAliasTargetExists():
if targetType != 'group':
try:
callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=targetEmail, fields='primaryEmail')
return 'user'
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
if targetType == 'user':
return None
try:
callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=targetEmail, fields='email')
return 'group'
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.systemError):
return None
def deleteAliasOnUpdate():
# User alias
if targetType != 'group':
try:
callGAPI(cd.users().aliases(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=aliasEmail, alias=aliasEmail)
printEntityKVList([Ent.USER_ALIAS, aliasEmail], [Act.PerformedName(Act.DELETE)], i, count)
time.sleep(waitAfterDelete)
return True
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail], str(e), i, count)
return False
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
if targetType == 'user':
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
return False
# Group alias
try:
callGAPI(cd.groups().aliases(), 'delete',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=aliasEmail, alias=aliasEmail)
time.sleep(waitAfterDelete)
return True
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
return False
except GAPI.forbidden:
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
return False
except (GAPI.groupNotFound, GAPI.badRequest, GAPI.invalid, GAPI.invalidResource):
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
return False
cd = buildGAPIObject(API.DIRECTORY)
ci = None
updateCmd = Act.Get() == Act.UPDATE
aliasList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
targetType = getChoice(ALIAS_TARGET_TYPES)
targetEmails = getEntityList(Cmd.OB_GROUP_ENTITY)
entityLists = targetEmails if isinstance(targetEmails, dict) else None
verifyNotInvitable = False
verifyTarget = updateCmd
waitAfterDelete = 2
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if (not updateCmd) and myarg == 'verifynotinvitable':
verifyNotInvitable = True
elif updateCmd and myarg == 'notargetverify':
verifyTarget = False
elif updateCmd and myarg == 'waitafterdelete':
waitAfterDelete = getInteger(minVal=2, maxVal=10)
else:
unknownArgumentExit()
i = 0
count = len(aliasList)
for aliasEmail in aliasList:
i += 1
if entityLists:
targetEmails = entityLists[aliasEmail]
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True, noLower=True)
if verifyNotInvitable:
isInvitableUser, ci = _getIsInvitableUser(ci, aliasEmail)
if isInvitableUser:
entityActionNotPerformedWarning([Ent.ALIAS_EMAIL, aliasEmail], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
continue
body = {'alias': aliasEmail}
jcount = len(targetEmails)
if jcount > 0:
# Only process first target
targetEmail = normalizeEmailAddressOrUID(targetEmails[0])
if verifyTarget:
targetType = verifyAliasTargetExists()
if targetType is None:
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
continue
if updateCmd and not deleteAliasOnUpdate():
continue
# User alias
if targetType != 'group':
try:
callGAPI(cd.users().aliases(), 'insert',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN, GAPI.DUPLICATE,
GAPI.CONDITION_NOT_MET, GAPI.LIMIT_EXCEEDED],
userKey=targetEmail, body=body, fields='')
entityActionPerformed([Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], i, count)
continue
except (GAPI.conditionNotMet, GAPI.limitExceeded) as e:
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], str(e), i, count)
continue
except GAPI.duplicate:
duplicateAliasGroupUserWarning(cd, [Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], i, count)
continue
except (GAPI.invalid, GAPI.invalidInput):
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], Msg.INVALID_ALIAS, i, count)
continue
except (GAPI.userNotFound, GAPI.badRequest, GAPI.forbidden):
if targetType == 'user':
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
continue
# Group alias
try:
callGAPI(cd.groups().aliases(), 'insert',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN, GAPI.DUPLICATE,
GAPI.CONDITION_NOT_MET, GAPI.LIMIT_EXCEEDED],
groupKey=targetEmail, body=body, fields='')
entityActionPerformed([Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], i, count)
except (GAPI.conditionNotMet, GAPI.limitExceeded) as e:
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], str(e), i, count)
except GAPI.duplicate:
duplicateAliasGroupUserWarning(cd, [Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], i, count)
except (GAPI.invalid, GAPI.invalidInput):
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], Msg.INVALID_ALIAS, i, count)
except (GAPI.groupNotFound, GAPI.userNotFound, GAPI.badRequest, GAPI.forbidden):
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
# gam delete aliases|nicknames [user|group|target] <EmailAddressEntity>
def doDeleteAliases():
cd = buildGAPIObject(API.DIRECTORY)
targetType = getChoice(ALIAS_TARGET_TYPES, defaultChoice='target')
entityList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
checkForExtraneousArguments()
i = 0
count = len(entityList)
for aliasEmail in entityList:
i += 1
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True)
aliasDeleted = False
if targetType != 'group':
try:
result = callGAPI(cd.users().aliases(), 'list',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=aliasEmail, fields='aliases(alias)')
for aliasEntry in result.get('aliases', []):
if aliasEmail == aliasEntry['alias'].lower():
aliasEmail = aliasEntry['alias']
callGAPI(cd.users().aliases(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=aliasEmail, alias=aliasEmail)
entityActionPerformed([Ent.USER_ALIAS, aliasEmail], i, count)
aliasDeleted = True
break
if aliasDeleted:
continue
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail], str(e), i, count)
continue
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
pass
if targetType == 'user':
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
continue
try:
result = callGAPI(cd.groups().aliases(), 'list',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=aliasEmail, fields='aliases(alias)')
for aliasEntry in result.get('aliases', []):
if aliasEmail == aliasEntry['alias'].lower():
aliasEmail = aliasEntry['alias']
callGAPI(cd.groups().aliases(), 'delete',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=aliasEmail, alias=aliasEmail)
entityActionPerformed([Ent.GROUP_ALIAS, aliasEmail], i, count)
aliasDeleted = True
break
if aliasDeleted:
continue
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
continue
except (GAPI.groupNotFound, GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
pass
if targetType == 'group':
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
continue
entityUnknownWarning(Ent.ALIAS, aliasEmail, i, count)
# gam remove aliases|nicknames <EmailAddress> user|group <EmailAddressEntity>
def doRemoveAliases():
cd = buildGAPIObject(API.DIRECTORY)
targetEmail = getEmailAddress()
targetType = getChoice(['user', 'group'])
entityList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
checkForExtraneousArguments()
count = len(entityList)
i = 0
if targetType == 'user':
try:
for aliasEmail in entityList:
i += 1
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True)
callGAPI(cd.users().aliases(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=targetEmail, alias=aliasEmail)
entityActionPerformed([Ent.USER, targetEmail, Ent.USER_ALIAS, aliasEmail], i, count)
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.USER, targetEmail, Ent.USER_ALIAS, aliasEmail], str(e), i, count)
except GAPI.invalidResource:
entityActionFailedWarning([Ent.USER, targetEmail, Ent.USER_ALIAS, aliasEmail], Msg.DOES_NOT_EXIST, i, count)
else:
try:
for aliasEmail in entityList:
i += 1
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True)
callGAPI(cd.groups().aliases(), 'delete',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=targetEmail, alias=aliasEmail)
entityActionPerformed([Ent.GROUP, targetEmail, Ent.GROUP_ALIAS, aliasEmail], i, count)
except (GAPI.groupNotFound, GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.GROUP, targetEmail, Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
except GAPI.invalidResource:
entityActionFailedWarning([Ent.GROUP, targetEmail, Ent.GROUP_ALIAS, aliasEmail], Msg.DOES_NOT_EXIST, i, count)
def _addUserAliases(cd, user, aliasList, i, count):
jcount = len(aliasList)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.USER_ALIAS, i, count)
Ind.Increment()
j = 0
for aliasEmail in aliasList:
j += 1
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True, noLower=True)
body = {'alias': aliasEmail}
try:
callGAPI(cd.users().aliases(), 'insert',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN, GAPI.DUPLICATE,
GAPI.CONDITION_NOT_MET, GAPI.LIMIT_EXCEEDED],
userKey=user, body=body, fields='')
entityActionPerformed([Ent.USER, user, Ent.USER_ALIAS, aliasEmail], j, jcount)
except (GAPI.conditionNotMet, GAPI.limitExceeded) as e:
entityActionFailedWarning([Ent.USER, user, Ent.USER_ALIAS, aliasEmail], str(e), j, jcount)
except GAPI.duplicate:
duplicateAliasGroupUserWarning(cd, [Ent.USER, user, Ent.USER_ALIAS, aliasEmail], j, jcount)
except (GAPI.invalid, GAPI.invalidInput):
entityActionFailedWarning([Ent.USER, user, Ent.USER_ALIAS, aliasEmail], Msg.INVALID_ALIAS, j, jcount)
except (GAPI.userNotFound, GAPI.badRequest, GAPI.forbidden):
entityUnknownWarning(Ent.USER, user, i, count)
Ind.Decrement()
# gam <UserTypeEntity> delete alias|aliases
def deleteUsersAliases(users):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
try:
user_aliases = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=user, fields='id,primaryEmail,aliases')
user_id = user_aliases['id']
user_primary = user_aliases['primaryEmail']
jcount = len(user_aliases['aliases']) if ('aliases' in user_aliases) else 0
entityPerformActionNumItems([Ent.USER, user_primary], jcount, Ent.ALIAS, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
j = 0
for an_alias in user_aliases['aliases']:
j += 1
try:
callGAPI(cd.users().aliases(), 'delete',
throwReasons=[GAPI.RESOURCE_ID_NOT_FOUND],
userKey=user_id, alias=an_alias)
entityActionPerformed([Ent.USER, user_primary, Ent.ALIAS, an_alias], j, jcount)
except GAPI.resourceIdNotFound:
entityActionFailedWarning([Ent.USER, user_primary, Ent.ALIAS, an_alias], Msg.DOES_NOT_EXIST, j, jcount)
Ind.Decrement()
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
entityUnknownWarning(Ent.USER, user, i, count)
def infoAliases(entityList):
def _showAliasInfo(uid, email, aliasEmail, entityType, aliasEntityType, i, count):
if email.lower() != aliasEmail:
printEntity([aliasEntityType, aliasEmail], i, count)
Ind.Increment()
printEntity([entityType, email])
printEntity([Ent.UNIQUE_ID, uid])
Ind.Decrement()
else:
setSysExitRC(ENTITY_IS_NOT_AN_ALIAS_RC)
printEntityKVList([Ent.EMAIL, aliasEmail],
[f'Is a {Ent.Singular(entityType)}, not a {Ent.Singular(aliasEntityType)}'],
i, count)
cd = buildGAPIObject(API.DIRECTORY)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
# Ignore info group/user arguments that may have come from whatis
if (myarg in INFO_GROUP_OPTIONS) or (myarg in INFO_USER_OPTIONS):
if myarg == 'schemas':
getString(Cmd.OB_SCHEMA_NAME_LIST)
else:
unknownArgumentExit()
i = 0
count = len(entityList)
for aliasEmail in entityList:
i += 1
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True, noLower=True)
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=aliasEmail, fields='id,primaryEmail')
_showAliasInfo(result['id'], result['primaryEmail'], aliasEmail, Ent.USER_EMAIL, Ent.USER_ALIAS, i, count)
continue
except (GAPI.userNotFound, GAPI.badRequest):
pass
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.backendError, GAPI.systemError):
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
continue
try:
result = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
groupKey=aliasEmail, fields='id,email')
_showAliasInfo(result['id'], result['email'], aliasEmail, Ent.GROUP_EMAIL, Ent.GROUP_ALIAS, i, count)
continue
except GAPI.groupNotFound:
pass
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
continue
entityUnknownWarning(Ent.EMAIL, aliasEmail, i, count)
# gam info aliases|nicknames <EmailAddressEntity>
def doInfoAliases():
infoAliases(getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY))
def initUserGroupDomainQueryFilters():
if not GC.Values[GC.PRINT_AGU_DOMAINS]:
return {'list': [{'customer': GC.Values[GC.CUSTOMER_ID]}], 'queries': [None]}
return {'list': [{'domain': domain.lower()} for domain in GC.Values[GC.PRINT_AGU_DOMAINS].replace(',', ' ').split()], 'queries': [None]}
def getUserGroupDomainQueryFilters(myarg, kwargsDict):
if myarg in {'domain', 'domains'}:
kwargsDict['list'] = [{'domain': domain.lower()} for domain in getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)]
elif myarg in {'query', 'queries'}:
kwargsDict['queries'] = getQueries(myarg)
else:
return False
return True
def makeUserGroupDomainQueryFilters(kwargsDict):
kwargsQueries = []
for kwargs in kwargsDict['list']:
for query in kwargsDict['queries']:
kwargsQueries.append((kwargs, query))
return kwargsQueries
def userFilters(kwargs, query, orgUnitPath, isSuspended):
queryTitle = ''
if kwargs.get('domain'):
queryTitle += f'domain={kwargs["domain"]}, '
if orgUnitPath is not None:
if query is not None and query.find(orgUnitPath) == -1:
query += f" orgUnitPath='{orgUnitPath}'"
else:
if query is None:
query = ''
else:
query += ' '
query += f"orgUnitPath='{orgUnitPath}'"
if isSuspended is not None:
if query is None:
query = ''
else:
query += ' '
query += f'isSuspended={isSuspended}'
if query is not None:
queryTitle += f'query="{query}", '
if queryTitle:
return query, queryTitle[:-2]
return query, queryTitle
# gam print aliases|nicknames [todrive <ToDriveAttribute>*]
# ([domain|domains <DomainNameEntity>] [(query <QueryUser>)|(queries <QueryUserList>)]
# [limittoou <OrgUnitItem>])
# [user|users <EmailAddressList>] [group|groups <EmailAddressList>]
# [select <UserTypeEntity>]
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
# [shownoneditable] [nogroups] [nousers]
# [onerowpertarget] [delimiter <Character>]
# [suppressnoaliasrows]
# (addcsvdata <FieldName> <String>)*
def doPrintAliases():
def writeAliases(target, targetEmail, targetType):
if not oneRowPerTarget:
for alias in target.get('aliases', []):
if aliasMatchPattern.match(alias):
row = {'Alias': alias, 'Target': targetEmail, 'TargetType': targetType}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
if showNonEditable:
for alias in target.get('nonEditableAliases', []):
if aliasMatchPattern.match(alias):
row = {'NonEditableAlias': alias, 'Target': targetEmail, 'TargetType': targetType}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
else:
aliases = [alias for alias in target.get('aliases', []) if aliasMatchPattern.match(alias)]
if showNonEditable:
nealiases = [alias for alias in target.get('nonEditableAliases', []) if aliasMatchPattern.match(alias)]
else:
nealiases = []
if suppressNoAliasRows and not aliases and not nealiases:
return
row = {'Target': targetEmail, 'TargetType': targetType, 'Aliases': delimiter.join(aliases)}
if showNonEditable:
row['NonEditableAliases'] = delimiter.join(nealiases)
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile()
userFields = ['primaryEmail', 'aliases']
groupFields = ['email', 'aliases']
oneRowPerTarget = showNonEditable = suppressNoAliasRows = False
kwargsDict = initUserGroupDomainQueryFilters()
getGroups = getUsers = True
groups = []
users = []
aliasMatchPattern = re.compile(r'^.*$')
isSuspended = orgUnitPath = None
addCSVData = {}
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'shownoneditable':
showNonEditable= True
userFields.append('nonEditableAliases')
groupFields.append('nonEditableAliases')
elif myarg == 'nogroups':
getGroups = False
elif myarg == 'nousers':
getUsers = False
elif myarg == 'limittoou':
orgUnitPath = getOrgUnitItem(pathOnly=True, cd=cd)
orgUnitPathLower = orgUnitPath.lower()
userFields.append('orgUnitPath')
getGroups = False
elif getUserGroupDomainQueryFilters(myarg, kwargsDict):
pass
elif myarg == 'select':
_, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
elif myarg == 'issuspended':
isSuspended = getBoolean()
elif myarg in {'user','users'}:
users.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
elif myarg in {'group', 'groups'}:
groups.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
elif myarg == 'aliasmatchpattern':
aliasMatchPattern = getREPattern(re.IGNORECASE)
elif myarg == 'onerowpertarget':
oneRowPerTarget = True
elif myarg == 'suppressnoaliasrows':
suppressNoAliasRows = True
elif myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
unknownArgumentExit()
if (users or groups) and kwargsDict['queries'][0] is None:
getUsers = getGroups = False
if not oneRowPerTarget:
titlesList = ['Alias', 'Target', 'TargetType']
if showNonEditable:
titlesList.insert(1, 'NonEditableAlias')
else:
titlesList = ['Target', 'TargetType', 'Aliases']
if showNonEditable:
titlesList.append('NonEditableAliases')
csvPF.SetTitles(titlesList)
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if getUsers:
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
kwargs = kwargsQuery[0]
query = kwargsQuery[1]
query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended)
printGettingAllAccountEntities(Ent.USER, pquery)
try:
entityList = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='primaryEmail',
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.INVALID_INPUT, GAPI.DOMAIN_NOT_FOUND,
GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
retryReasons=[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
query=query, orderBy='email',
fields=f'nextPageToken,users({",".join(userFields)})',
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
for user in entityList:
if orgUnitPath is None or orgUnitPathLower == user.get('orgUnitPath', '').lower():
writeAliases(user, user['primaryEmail'], 'User')
except (GAPI.invalidOrgunit, GAPI.invalidInput):
entityActionFailedWarning([Ent.ALIAS, None], invalidQuery(query))
continue
except GAPI.domainNotFound as e:
entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e))
continue
except (GAPI.unknownError, GAPI.failedPrecondition) as e:
entityActionFailedExit([Ent.USER, None], str(e))
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
count = len(users)
i = 0
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
printGettingEntityItemForWhom(Ent.USER_ALIAS, user, i, count)
try:
result = callGAPI(cd.users().aliases(), 'list',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=user, fields='aliases(alias)')
aliases = {'aliases': [alias['alias'] for alias in result.get('aliases', [])]}
writeAliases(aliases, user, 'User')
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
if getGroups:
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
kwargs = kwargsQuery[0]
query = kwargsQuery[1]
query, pquery = groupFilters(kwargs, query)
printGettingAllAccountEntities(Ent.GROUP, pquery)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
query=query, orderBy='email',
fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs)
for group in entityList:
writeAliases(group, group['email'], 'Group')
except (GAPI.invalidMember, GAPI.invalidInput) as e:
if not invalidMember(query):
entityActionFailedExit([Ent.GROUP, None], str(e))
except GAPI.domainNotFound as e:
entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e))
continue
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
count = len(groups)
i = 0
for group in groups:
i += 1
group = normalizeEmailAddressOrUID(group)
printGettingEntityItemForWhom(Ent.GROUP_ALIAS, group, i, count)
try:
result = callGAPI(cd.groups().aliases(), 'list',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=group, fields='aliases(alias)')
aliases = {'aliases': [alias['alias'] for alias in result.get('aliases', [])]}
writeAliases(aliases, group, 'Group')
except (GAPI.groupNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.GROUP, group], str(e), i, count)
csvPF.writeCSVfile('Aliases')
# gam print addresses [todrive <ToDriveAttribute>*]
# [domain <DomainName>]
def doPrintAddresses():
cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile()
titlesList = ['Type', 'Email', 'Target']
userFields = ['primaryEmail', 'aliases', 'nonEditableAliases', 'suspended']
groupFields = ['email', 'aliases', 'nonEditableAliases']
domainFields = ['domainName', 'isPrimary', 'domainAliases']
resourceFields = ['resourceEmail']
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'domain':
kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower()
kwargs.pop('customer', None)
else:
unknownArgumentExit()
csvPF.SetTitles(titlesList)
printGettingAllAccountEntities(Ent.USER)
try:
entityList = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='primaryEmail',
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
retryReasons=[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
orderBy='email', fields=f'nextPageToken,users({",".join(userFields)})',
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
except (GAPI.unknownError, GAPI.failedPrecondition) as e:
entityActionFailedExit([Ent.USER, None], str(e))
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
for user in entityList:
userEmail = user['primaryEmail']
prefix = '' if not user['suspended'] else 'Suspended'
csvPF.WriteRow({'Type': f'{prefix}User', 'Email': userEmail})
for alias in user.get('aliases', []):
csvPF.WriteRow({'Type': f'{prefix}UserAlias', 'Email': alias, 'Target': userEmail})
for alias in user.get('nonEditableAliases', []):
csvPF.WriteRow({'Type': f'{prefix}UserNEAlias', 'Email': alias, 'Target': userEmail})
printGettingAllAccountEntities(Ent.GROUP)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
throwReasons=GAPI.GROUP_LIST_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
for group in entityList:
groupEmail = group['email']
csvPF.WriteRow({'Type': 'Group', 'Email': groupEmail})
for alias in group.get('aliases', []):
csvPF.WriteRow({'Type': 'GroupAlias', 'Email': alias, 'Target': groupEmail})
for alias in group.get('nonEditableAliases', []):
csvPF.WriteRow({'Type': 'GroupNEAlias', 'Email': alias, 'Target': groupEmail})
printGettingAllAccountEntities(Ent.RESOURCE_CALENDAR)
try:
entityList = callGAPIpages(cd.resources().calendars(), 'list', 'items',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='resourceEmail',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
customer=GC.Values[GC.CUSTOMER_ID], fields=f'nextPageToken,items({",".join(resourceFields)})')
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, ''], str(e))
return
for resource in entityList:
csvPF.WriteRow({'Type': 'Resource', 'Email': resource['resourceEmail']})
try:
entityList = callGAPIitems(cd.domains(), 'list', 'domains',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], fields=f'domains({",".join(domainFields)})')
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
for domain in entityList:
domainEmail = domain['domainName']
csvPF.WriteRow({'Type': 'DomainPrimary' if domain['isPrimary'] else 'DomainSecondary', 'Email': domainEmail})
for alias in domain.get('domainAliases', []):
csvPF.WriteRow({'Type': 'DomainAlias', 'Email': alias['domainAliasName'], 'Target': domainEmail})
csvPF.SortRowsTwoTitles('Type', 'Email', False)
csvPF.writeCSVfile('Addresses')
# Contact commands utilities
#
def _getCreateContactReturnOptions(parameters):
myarg = getArgument()
if myarg == 'returnidonly':
parameters['returnIdOnly'] = True
elif myarg == 'csv':
parameters['csvPF'] = CSVPrintFile(parameters['titles'], 'sortall')
elif parameters['csvPF'] and myarg == 'todrive':
parameters['csvPF'].GetTodriveParameters()
elif parameters['csvPF'] and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
parameters['addCSVData'][k] = getString(Cmd.OB_STRING, minLen=0)
else:
return False
return True
#
CONTACT_JSON = 'JSON'
CONTACT_ID = 'ContactID'
CONTACT_UPDATED = 'Updated'
CONTACT_NAME_PREFIX = 'Name Prefix'
CONTACT_GIVEN_NAME = 'Given Name'
CONTACT_ADDITIONAL_NAME = 'Additional Name'
CONTACT_FAMILY_NAME = 'Family Name'
CONTACT_NAME_SUFFIX = 'Name Suffix'
CONTACT_NAME = 'Name'
CONTACT_NICKNAME = 'Nickname'
CONTACT_MAIDENNAME = 'Maiden Name'
CONTACT_SHORTNAME = 'Short Name'
CONTACT_INITIALS = 'Initials'
CONTACT_BIRTHDAY = 'Birthday'
CONTACT_GENDER = 'Gender'
CONTACT_LOCATION = 'Location'
CONTACT_PRIORITY = 'Priority'
CONTACT_SENSITIVITY = 'Sensitivity'
CONTACT_SUBJECT = 'Subject'
CONTACT_LANGUAGE = 'Language'
CONTACT_NOTES = 'Notes'
CONTACT_OCCUPATION = 'Occupation'
CONTACT_BILLING_INFORMATION = 'Billing Information'
CONTACT_MILEAGE = 'Mileage'
CONTACT_DIRECTORY_SERVER = 'Directory Server'
CONTACT_ADDRESSES = 'Addresses'
CONTACT_CALENDARS = 'Calendars'
CONTACT_EMAILS = 'Emails'
CONTACT_EXTERNALIDS = 'External IDs'
CONTACT_EVENTS = 'Events'
CONTACT_HOBBIES = 'Hobbies'
CONTACT_IMS = 'IMs'
CONTACT_JOTS = 'Jots'
CONTACT_ORGANIZATIONS = 'Organizations'
CONTACT_PHONES = 'Phones'
CONTACT_RELATIONS = 'Relations'
CONTACT_USER_DEFINED_FIELDS = 'User Defined Fields'
CONTACT_WEBSITES = 'Websites'
#
class ContactsManager():
CONTACT_ARGUMENT_TO_PROPERTY_MAP = {
'json': CONTACT_JSON,
'name': CONTACT_NAME,
'prefix': CONTACT_NAME_PREFIX,
'givenname': CONTACT_GIVEN_NAME,
'additionalname': CONTACT_ADDITIONAL_NAME,
'familyname': CONTACT_FAMILY_NAME,
'firstname': CONTACT_GIVEN_NAME,
'middlename': CONTACT_ADDITIONAL_NAME,
'lastname': CONTACT_FAMILY_NAME,
'suffix': CONTACT_NAME_SUFFIX,
'nickname': CONTACT_NICKNAME,
'maidenname': CONTACT_MAIDENNAME,
'shortname': CONTACT_SHORTNAME,
'initials': CONTACT_INITIALS,
'birthday': CONTACT_BIRTHDAY,
'gender': CONTACT_GENDER,
'location': CONTACT_LOCATION,
'priority': CONTACT_PRIORITY,
'sensitivity': CONTACT_SENSITIVITY,
'subject': CONTACT_SUBJECT,
'language': CONTACT_LANGUAGE,
'note': CONTACT_NOTES,
'notes': CONTACT_NOTES,
'occupation': CONTACT_OCCUPATION,
'billinginfo': CONTACT_BILLING_INFORMATION,
'mileage': CONTACT_MILEAGE,
'directoryserver': CONTACT_DIRECTORY_SERVER,
'address': CONTACT_ADDRESSES,
'addresses': CONTACT_ADDRESSES,
'calendar': CONTACT_CALENDARS,
'calendars': CONTACT_CALENDARS,
'email': CONTACT_EMAILS,
'emails': CONTACT_EMAILS,
'externalid': CONTACT_EXTERNALIDS,
'externalids': CONTACT_EXTERNALIDS,
'event': CONTACT_EVENTS,
'events': CONTACT_EVENTS,
'hobby': CONTACT_HOBBIES,
'hobbies': CONTACT_HOBBIES,
'im': CONTACT_IMS,
'ims': CONTACT_IMS,
'jot': CONTACT_JOTS,
'jots': CONTACT_JOTS,
'organization': CONTACT_ORGANIZATIONS,
'organizations': CONTACT_ORGANIZATIONS,
'organisation': CONTACT_ORGANIZATIONS,
'organisations': CONTACT_ORGANIZATIONS,
'phone': CONTACT_PHONES,
'phones': CONTACT_PHONES,
'relation': CONTACT_RELATIONS,
'relations': CONTACT_RELATIONS,
'userdefinedfield': CONTACT_USER_DEFINED_FIELDS,
'userdefinedfields': CONTACT_USER_DEFINED_FIELDS,
'website': CONTACT_WEBSITES,
'websites': CONTACT_WEBSITES,
'updated': CONTACT_UPDATED,
}
GENDER_CHOICE_MAP = {'male': 'male', 'female': 'female'}
PRIORITY_CHOICE_MAP = {'low': 'low', 'normal': 'normal', 'high': 'high'}
SENSITIVITY_CHOICE_MAP = {
'confidential': 'confidential',
'normal': 'normal',
'personal': 'personal',
'private': 'private',
}
CONTACT_NAME_FIELDS = (
CONTACT_NAME_PREFIX,
CONTACT_GIVEN_NAME,
CONTACT_ADDITIONAL_NAME,
CONTACT_FAMILY_NAME,
CONTACT_NAME_SUFFIX,
)
ADDRESS_TYPE_ARGUMENT_TO_REL = {
'work': gdata.apps.contacts.REL_WORK,
'home': gdata.apps.contacts.REL_HOME,
'other': gdata.apps.contacts.REL_OTHER,
}
ADDRESS_REL_TO_TYPE_ARGUMENT = {
gdata.apps.contacts.REL_WORK: 'work',
gdata.apps.contacts.REL_HOME: 'home',
gdata.apps.contacts.REL_OTHER: 'other',
}
ADDRESS_ARGUMENT_TO_FIELD_MAP = {
'streetaddress': 'street',
'pobox': 'pobox',
'neighborhood': 'neighborhood',
'locality': 'city',
'region': 'region',
'postalcode': 'postcode',
'country': 'country',
'formatted': 'value', 'unstructured': 'value',
}
ADDRESS_FIELD_TO_ARGUMENT_MAP = {
'street': 'streetaddress',
'pobox': 'pobox',
'neighborhood': 'neighborhood',
'city': 'locality',
'region': 'region',
'postcode': 'postalcode',
'country': 'country',
}
ADDRESS_FIELD_PRINT_ORDER = [
'street',
'pobox',
'neighborhood',
'city',
'region',
'postcode',
'country',
]
CALENDAR_TYPE_ARGUMENT_TO_REL = {
'work': 'work',
'home': 'home',
'free-busy': 'free-busy',
}
CALENDAR_REL_TO_TYPE_ARGUMENT = {
'work': 'work',
'home': 'home',
'free-busy': 'free-busy',
}
EMAIL_TYPE_ARGUMENT_TO_REL = {
'work': gdata.apps.contacts.REL_WORK,
'home': gdata.apps.contacts.REL_HOME,
'other': gdata.apps.contacts.REL_OTHER,
}
EMAIL_REL_TO_TYPE_ARGUMENT = {
gdata.apps.contacts.REL_WORK: 'work',
gdata.apps.contacts.REL_HOME: 'home',
gdata.apps.contacts.REL_OTHER: 'other',
}
EVENT_TYPE_ARGUMENT_TO_REL = {
'anniversary': 'anniversary',
'other': 'other',
}
EVENT_REL_TO_TYPE_ARGUMENT = {
'anniversary': 'anniversary',
'other': 'other',
}
EXTERNALID_TYPE_ARGUMENT_TO_REL = {
'account': 'account',
'customer': 'customer',
'network': 'network',
'organization': 'organization',
'organisation': 'organization',
}
EXTERNALID_REL_TO_TYPE_ARGUMENT = {
'account': 'account',
'customer': 'customer',
'network': 'network',
'organization': 'organization',
'organisation': 'organization',
}
IM_TYPE_ARGUMENT_TO_REL = {
'work': gdata.apps.contacts.REL_WORK,
'home': gdata.apps.contacts.REL_HOME,
'other': gdata.apps.contacts.REL_OTHER,
}
IM_REL_TO_TYPE_ARGUMENT = {
gdata.apps.contacts.REL_WORK: 'work',
gdata.apps.contacts.REL_HOME: 'home',
gdata.apps.contacts.REL_OTHER: 'other',
}
IM_PROTOCOL_TO_REL_MAP = {
'aim': gdata.apps.contacts.IM_AIM,
'gtalk': gdata.apps.contacts.IM_GOOGLE_TALK,
'icq': gdata.apps.contacts.IM_ICQ,
'jabber': gdata.apps.contacts.IM_JABBER,
'msn': gdata.apps.contacts.IM_MSN,
'netmeeting': gdata.apps.contacts.IM_NETMEETING,
'qq': gdata.apps.contacts.IM_QQ,
'skype': gdata.apps.contacts.IM_SKYPE,
'xmpp': gdata.apps.contacts.IM_JABBER,
'yahoo': gdata.apps.contacts.IM_YAHOO,
}
IM_REL_TO_PROTOCOL_MAP = {
gdata.apps.contacts.IM_AIM: 'aim',
gdata.apps.contacts.IM_GOOGLE_TALK: 'gtalk',
gdata.apps.contacts.IM_ICQ: 'icq',
gdata.apps.contacts.IM_JABBER: 'jabber',
gdata.apps.contacts.IM_MSN: 'msn',
gdata.apps.contacts.IM_NETMEETING: 'netmeeting',
gdata.apps.contacts.IM_QQ: 'qq',
gdata.apps.contacts.IM_SKYPE: 'skype',
gdata.apps.contacts.IM_YAHOO: 'yahoo',
}
JOT_TYPE_ARGUMENT_TO_REL = {
'work': 'work',
'home': 'home',
'other': 'other',
'keywords': 'keywords',
'user': 'user',
}
JOT_REL_TO_TYPE_ARGUMENT = {
'work': 'work',
'home': 'home',
'other': 'other',
'keywords': 'keywords',
'user': 'user',
}
ORGANIZATION_TYPE_ARGUMENT_TO_REL = {
'work': gdata.apps.contacts.REL_WORK,
'other': gdata.apps.contacts.REL_OTHER,
}
ORGANIZATION_REL_TO_TYPE_ARGUMENT = {
gdata.apps.contacts.REL_WORK: 'work',
gdata.apps.contacts.REL_OTHER: 'other',
}
ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
'location': 'where',
'department': 'department',
'title': 'title',
'jobdescription': 'jobdescription',
'symbol': 'symbol',
}
ORGANIZATION_FIELD_TO_ARGUMENT_MAP = {
'where': 'location',
'department': 'department',
'title': 'title',
'jobdescription': 'jobdescription',
'symbol': 'symbol',
}
ORGANIZATION_FIELD_PRINT_ORDER = [
'where',
'department',
'title',
'jobdescription',
'symbol',
]
PHONE_TYPE_ARGUMENT_TO_REL = {
'work': gdata.apps.contacts.PHONE_WORK,
'home': gdata.apps.contacts.PHONE_HOME,
'other': gdata.apps.contacts.PHONE_OTHER,
'fax': gdata.apps.contacts.PHONE_FAX,
'home_fax': gdata.apps.contacts.PHONE_HOME_FAX,
'work_fax': gdata.apps.contacts.PHONE_WORK_FAX,
'other_fax': gdata.apps.contacts.PHONE_OTHER_FAX,
'main': gdata.apps.contacts.PHONE_MAIN,
'company_main': gdata.apps.contacts.PHONE_COMPANY_MAIN,
'assistant': gdata.apps.contacts.PHONE_ASSISTANT,
'mobile': gdata.apps.contacts.PHONE_MOBILE,
'work_mobile': gdata.apps.contacts.PHONE_WORK_MOBILE,
'pager': gdata.apps.contacts.PHONE_PAGER,
'work_pager': gdata.apps.contacts.PHONE_WORK_PAGER,
'car': gdata.apps.contacts.PHONE_CAR,
'radio': gdata.apps.contacts.PHONE_RADIO,
'callback': gdata.apps.contacts.PHONE_CALLBACK,
'isdn': gdata.apps.contacts.PHONE_ISDN,
'telex': gdata.apps.contacts.PHONE_TELEX,
'tty_tdd': gdata.apps.contacts.PHONE_TTY_TDD,
}
PHONE_REL_TO_TYPE_ARGUMENT = {
gdata.apps.contacts.PHONE_WORK: 'work',
gdata.apps.contacts.PHONE_HOME: 'home',
gdata.apps.contacts.PHONE_OTHER: 'other',
gdata.apps.contacts.PHONE_FAX: 'fax',
gdata.apps.contacts.PHONE_HOME_FAX: 'home_fax',
gdata.apps.contacts.PHONE_WORK_FAX: 'work_fax',
gdata.apps.contacts.PHONE_OTHER_FAX: 'other_fax',
gdata.apps.contacts.PHONE_MAIN: 'main',
gdata.apps.contacts.PHONE_COMPANY_MAIN: 'company_main',
gdata.apps.contacts.PHONE_ASSISTANT: 'assistant',
gdata.apps.contacts.PHONE_MOBILE: 'mobile',
gdata.apps.contacts.PHONE_WORK_MOBILE: 'work_mobile',
gdata.apps.contacts.PHONE_PAGER: 'pager',
gdata.apps.contacts.PHONE_WORK_PAGER: 'work_pager',
gdata.apps.contacts.PHONE_CAR: 'car',
gdata.apps.contacts.PHONE_RADIO: 'radio',
gdata.apps.contacts.PHONE_CALLBACK: 'callback',
gdata.apps.contacts.PHONE_ISDN: 'isdn',
gdata.apps.contacts.PHONE_TELEX: 'telex',
gdata.apps.contacts.PHONE_TTY_TDD: 'tty_tdd',
}
RELATION_TYPE_ARGUMENT_TO_REL = {
'spouse': 'spouse',
'child': 'child',
'mother': 'mother',
'father': 'father',
'parent': 'parent',
'brother': 'brother',
'sister': 'sister',
'friend': 'friend',
'relative': 'relative',
'manager': 'manager',
'assistant': 'assistant',
'referredby': 'referred-by',
'partner': 'partner',
'domesticpartner': 'domestic-partner',
}
RELATION_REL_TO_TYPE_ARGUMENT = {
'spouse' : 'spouse',
'child' : 'child',
'mother' : 'mother',
'father' : 'father',
'parent' : 'parent',
'brother' : 'brother',
'sister' : 'sister',
'friend' : 'friend',
'relative' : 'relative',
'manager' : 'manager',
'assistant' : 'assistant',
'referred-by' : 'referred_by',
'partner' : 'partner',
'domestic-partner' : 'domestic_partner',
}
WEBSITE_TYPE_ARGUMENT_TO_REL = {
'home-page': 'home-page',
'blog': 'blog',
'profile': 'profile',
'work': 'work',
'home': 'home',
'other': 'other',
'ftp': 'ftp',
'reservations': 'reservations',
'app-install-page': 'app-install-page',
}
WEBSITE_REL_TO_TYPE_ARGUMENT = {
'home-page': 'home-page',
'blog': 'blog',
'profile': 'profile',
'work': 'work',
'home': 'home',
'other': 'other',
'ftp': 'ftp',
'reservations': 'reservations',
'app-install-page': 'app-install-page',
}
CONTACT_NAME_PROPERTY_PRINT_ORDER = [
CONTACT_UPDATED,
CONTACT_NAME,
CONTACT_NAME_PREFIX,
CONTACT_GIVEN_NAME,
CONTACT_ADDITIONAL_NAME,
CONTACT_FAMILY_NAME,
CONTACT_NAME_SUFFIX,
CONTACT_NICKNAME,
CONTACT_MAIDENNAME,
CONTACT_SHORTNAME,
CONTACT_INITIALS,
CONTACT_BIRTHDAY,
CONTACT_GENDER,
CONTACT_LOCATION,
CONTACT_PRIORITY,
CONTACT_SENSITIVITY,
CONTACT_SUBJECT,
CONTACT_LANGUAGE,
CONTACT_NOTES,
CONTACT_OCCUPATION,
CONTACT_BILLING_INFORMATION,
CONTACT_MILEAGE,
CONTACT_DIRECTORY_SERVER,
]
CONTACT_ARRAY_PROPERTY_PRINT_ORDER = [
CONTACT_ADDRESSES,
CONTACT_EMAILS,
CONTACT_IMS,
CONTACT_PHONES,
CONTACT_CALENDARS,
CONTACT_ORGANIZATIONS,
CONTACT_EXTERNALIDS,
CONTACT_EVENTS,
CONTACT_HOBBIES,
CONTACT_JOTS,
CONTACT_RELATIONS,
CONTACT_WEBSITES,
CONTACT_USER_DEFINED_FIELDS,
]
CONTACT_ARRAY_PROPERTIES = {
CONTACT_ADDRESSES: {'relMap': ADDRESS_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'formatted', 'primary': True},
CONTACT_EMAILS: {'relMap': EMAIL_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'address', 'primary': True},
CONTACT_IMS: {'relMap': IM_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'address', 'primary': True},
CONTACT_PHONES: {'relMap': PHONE_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': True},
CONTACT_CALENDARS: {'relMap': CALENDAR_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'address', 'primary': True},
CONTACT_ORGANIZATIONS: {'relMap': ORGANIZATION_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'name', 'primary': True},
CONTACT_EXTERNALIDS: {'relMap': EXTERNALID_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': False},
CONTACT_EVENTS: {'relMap': EVENT_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'date', 'primary': False},
CONTACT_HOBBIES: {'relMap': None, 'infoTitle': 'value', 'primary': False},
CONTACT_JOTS: {'relMap': JOT_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': False},
CONTACT_RELATIONS: {'relMap': RELATION_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': False},
CONTACT_USER_DEFINED_FIELDS: {'relMap': None, 'infoTitle': 'value', 'primary': False},
CONTACT_WEBSITES: {'relMap': WEBSITE_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': True},
}
@staticmethod
def GetContactShortId(contactEntry):
full_id = contactEntry.id.text
return full_id[full_id.rfind('/')+1:]
@staticmethod
def GetContactFields(parameters=None):
fields = {}
def CheckClearFieldsList(fieldName):
if checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
fields.pop(fieldName, None)
fields[fieldName] = []
return True
return False
def InitArrayItem(choices):
item = {}
rel = getChoice(choices, mapChoice=True, defaultChoice=None)
if rel:
item['rel'] = rel
item['label'] = None
else:
item['rel'] = None
item['label'] = getString(Cmd.OB_STRING)
return item
def PrimaryNotPrimary(pnp, entry):
if pnp == 'notprimary':
entry['primary'] = 'false'
return True
if pnp == 'primary':
entry['primary'] = 'true'
primary['location'] = Cmd.Location()
return True
return False
def GetPrimaryNotPrimaryChoice(entry):
if not getChoice({'primary': True, 'notprimary': False}, mapChoice=True):
entry['primary'] = 'false'
else:
entry['primary'] = 'true'
primary['location'] = Cmd.Location()
def AppendItemToFieldsList(fieldName, fieldValue, checkBlankField=None):
fields.setdefault(fieldName, [])
if checkBlankField is None or fieldValue[checkBlankField]:
if isinstance(fieldValue, dict) and fieldValue.get('primary', 'false') == 'true':
for citem in fields[fieldName]:
if citem.get('primary', 'false') == 'true':
Cmd.SetLocation(primary['location']-1)
usageErrorExit(Msg.MULTIPLE_ITEMS_MARKED_PRIMARY.format(fieldName))
fields[fieldName].append(fieldValue)
primary = {}
while Cmd.ArgumentsRemaining():
if parameters is not None:
if _getCreateContactReturnOptions(parameters):
continue
Cmd.Backup()
fieldName = getChoice(ContactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP, mapChoice=True)
if fieldName == CONTACT_BIRTHDAY:
fields[fieldName] = getYYYYMMDD(minLen=0)
elif fieldName == CONTACT_GENDER:
fields[fieldName] = getChoice(ContactsManager.GENDER_CHOICE_MAP, mapChoice=True)
elif fieldName == CONTACT_PRIORITY:
fields[fieldName] = getChoice(ContactsManager.PRIORITY_CHOICE_MAP, mapChoice=True)
elif fieldName == CONTACT_SENSITIVITY:
fields[fieldName] = getChoice(ContactsManager.SENSITIVITY_CHOICE_MAP, mapChoice=True)
elif fieldName == CONTACT_LANGUAGE:
fields[fieldName] = getLanguageCode(LANGUAGE_CODES_MAP)
elif fieldName == CONTACT_NOTES:
fields[fieldName] = getStringWithCRsNLsOrFile()[0]
elif fieldName == CONTACT_ADDRESSES:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.ADDRESS_TYPE_ARGUMENT_TO_REL)
entry['primary'] = 'false'
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument in ContactsManager.ADDRESS_ARGUMENT_TO_FIELD_MAP:
value = getString(Cmd.OB_STRING, minLen=0)
if value:
entry[ContactsManager.ADDRESS_ARGUMENT_TO_FIELD_MAP[argument]] = value.replace('\\n', '\n')
elif PrimaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
AppendItemToFieldsList(fieldName, entry)
elif fieldName == CONTACT_CALENDARS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.CALENDAR_TYPE_ARGUMENT_TO_REL)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
GetPrimaryNotPrimaryChoice(entry)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_EMAILS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.EMAIL_TYPE_ARGUMENT_TO_REL)
entry['value'] = getEmailAddress(noUid=True, minLen=0)
GetPrimaryNotPrimaryChoice(entry)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_EVENTS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.EVENT_TYPE_ARGUMENT_TO_REL)
entry['value'] = getYYYYMMDD(minLen=0)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_EXTERNALIDS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.EXTERNALID_TYPE_ARGUMENT_TO_REL)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_HOBBIES:
if CheckClearFieldsList(fieldName):
continue
entry = {'value': getString(Cmd.OB_STRING, minLen=0)}
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_IMS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.IM_TYPE_ARGUMENT_TO_REL)
entry['protocol'] = getChoice(ContactsManager.IM_PROTOCOL_TO_REL_MAP, mapChoice=True)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
GetPrimaryNotPrimaryChoice(entry)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_JOTS:
if CheckClearFieldsList(fieldName):
continue
entry = {'rel': getChoice(ContactsManager.JOT_TYPE_ARGUMENT_TO_REL, mapChoice=True)}
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_ORGANIZATIONS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.ORGANIZATION_TYPE_ARGUMENT_TO_REL)
entry['primary'] = 'false'
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument in ContactsManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP:
value = getString(Cmd.OB_STRING, minLen=0)
if value:
entry[ContactsManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP[argument]] = value
elif PrimaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_PHONES:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.PHONE_TYPE_ARGUMENT_TO_REL)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
GetPrimaryNotPrimaryChoice(entry)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_RELATIONS:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.RELATION_TYPE_ARGUMENT_TO_REL)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_USER_DEFINED_FIELDS:
if CheckClearFieldsList(fieldName):
continue
entry = {'rel': getString(Cmd.OB_STRING, minLen=0), 'value': getString(Cmd.OB_STRING, minLen=0)}
if not entry['rel'] or entry['rel'].lower() == 'none':
entry['rel'] = None
AppendItemToFieldsList(fieldName, entry, 'value')
elif fieldName == CONTACT_WEBSITES:
if CheckClearFieldsList(fieldName):
continue
entry = InitArrayItem(ContactsManager.WEBSITE_TYPE_ARGUMENT_TO_REL)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
GetPrimaryNotPrimaryChoice(entry)
AppendItemToFieldsList(fieldName, entry, 'value')
else:
fields[fieldName] = getString(Cmd.OB_STRING, minLen=0)
return fields
@staticmethod
def FieldsToContact(fields):
def GetField(fieldName):
return fields.get(fieldName)
def SetClassAttribute(value, fieldClass, processNLs, attr):
if value:
if processNLs:
value = value.replace('\\n', '\n')
if attr == 'text':
return fieldClass(text=value)
if attr == 'code':
return fieldClass(code=value)
if attr == 'rel':
return fieldClass(rel=value)
if attr == 'value':
return fieldClass(value=value)
if attr == 'value_string':
return fieldClass(value_string=value)
if attr == 'when':
return fieldClass(when=value)
return None
def GetContactField(fieldName, fieldClass, processNLs=False, attr='text'):
return SetClassAttribute(fields.get(fieldName), fieldClass, processNLs, attr)
def GetListEntryField(entry, fieldName, fieldClass, processNLs=False, attr='text'):
return SetClassAttribute(entry.get(fieldName), fieldClass, processNLs, attr)
contactEntry = gdata.apps.contacts.ContactEntry()
value = GetField(CONTACT_NAME)
if not value:
value = ' '.join([fields[fieldName] for fieldName in ContactsManager.CONTACT_NAME_FIELDS if fieldName in fields])
contactEntry.name = gdata.apps.contacts.Name(full_name=gdata.apps.contacts.FullName(text=value))
contactEntry.name.name_prefix = GetContactField(CONTACT_NAME_PREFIX, gdata.apps.contacts.NamePrefix)
contactEntry.name.given_name = GetContactField(CONTACT_GIVEN_NAME, gdata.apps.contacts.GivenName)
contactEntry.name.additional_name = GetContactField(CONTACT_ADDITIONAL_NAME, gdata.apps.contacts.AdditionalName)
contactEntry.name.family_name = GetContactField(CONTACT_FAMILY_NAME, gdata.apps.contacts.FamilyName)
contactEntry.name.name_suffix = GetContactField(CONTACT_NAME_SUFFIX, gdata.apps.contacts.NameSuffix)
contactEntry.nickname = GetContactField(CONTACT_NICKNAME, gdata.apps.contacts.Nickname)
contactEntry.maidenName = GetContactField(CONTACT_MAIDENNAME, gdata.apps.contacts.MaidenName)
contactEntry.shortName = GetContactField(CONTACT_SHORTNAME, gdata.apps.contacts.ShortName)
contactEntry.initials = GetContactField(CONTACT_INITIALS, gdata.apps.contacts.Initials)
contactEntry.birthday = GetContactField(CONTACT_BIRTHDAY, gdata.apps.contacts.Birthday, attr='when')
contactEntry.gender = GetContactField(CONTACT_GENDER, gdata.apps.contacts.Gender, attr='value')
contactEntry.where = GetContactField(CONTACT_LOCATION, gdata.apps.contacts.Where, attr='value_string')
contactEntry.priority = GetContactField(CONTACT_PRIORITY, gdata.apps.contacts.Priority, attr='rel')
contactEntry.sensitivity = GetContactField(CONTACT_SENSITIVITY, gdata.apps.contacts.Sensitivity, attr='rel')
contactEntry.subject = GetContactField(CONTACT_SUBJECT, gdata.apps.contacts.Subject)
contactEntry.language = GetContactField(CONTACT_LANGUAGE, gdata.apps.contacts.Language, attr='code')
contactEntry.content = GetContactField(CONTACT_NOTES, gdata.apps.contacts.Content, processNLs=True)
contactEntry.occupation = GetContactField(CONTACT_OCCUPATION, gdata.apps.contacts.Occupation)
contactEntry.billingInformation = GetContactField(CONTACT_BILLING_INFORMATION, gdata.apps.contacts.BillingInformation, processNLs=True)
contactEntry.mileage = GetContactField(CONTACT_MILEAGE, gdata.apps.contacts.Mileage)
contactEntry.directoryServer = GetContactField(CONTACT_DIRECTORY_SERVER, gdata.apps.contacts.DirectoryServer)
value = GetField(CONTACT_ADDRESSES)
if value:
for address in value:
street = GetListEntryField(address, 'street', gdata.apps.contacts.Street)
pobox = GetListEntryField(address, 'pobox', gdata.apps.contacts.PoBox)
neighborhood = GetListEntryField(address, 'neighborhood', gdata.apps.contacts.Neighborhood)
city = GetListEntryField(address, 'city', gdata.apps.contacts.City)
region = GetListEntryField(address, 'region', gdata.apps.contacts.Region)
postcode = GetListEntryField(address, 'postcode', gdata.apps.contacts.Postcode)
country = GetListEntryField(address, 'country', gdata.apps.contacts.Country)
formatted_address = GetListEntryField(address, 'value', gdata.apps.contacts.FormattedAddress, processNLs=True)
contactEntry.structuredPostalAddress.append(gdata.apps.contacts.StructuredPostalAddress(street=street, pobox=pobox, neighborhood=neighborhood,
city=city, region=region,
postcode=postcode, country=country,
formatted_address=formatted_address,
rel=address['rel'], label=address['label'], primary=address['primary']))
value = GetField(CONTACT_CALENDARS)
if value:
for calendarLink in value:
contactEntry.calendarLink.append(gdata.apps.contacts.CalendarLink(href=calendarLink['value'], rel=calendarLink['rel'], label=calendarLink['label'], primary=calendarLink['primary']))
value = GetField(CONTACT_EMAILS)
if value:
for emailaddr in value:
contactEntry.email.append(gdata.apps.contacts.Email(address=emailaddr['value'], rel=emailaddr['rel'], label=emailaddr['label'], primary=emailaddr['primary']))
value = GetField(CONTACT_EXTERNALIDS)
if value:
for externalid in value:
contactEntry.externalId.append(gdata.apps.contacts.ExternalId(value=externalid['value'], rel=externalid['rel'], label=externalid['label']))
value = GetField(CONTACT_EVENTS)
if value:
for event in value:
contactEntry.event.append(gdata.apps.contacts.Event(rel=event['rel'], label=event['label'],
when=gdata.apps.contacts.When(startTime=event['value'])))
value = GetField(CONTACT_HOBBIES)
if value:
for hobby in value:
contactEntry.hobby.append(gdata.apps.contacts.Hobby(text=hobby['value']))
value = GetField(CONTACT_IMS)
if value:
for im in value:
contactEntry.im.append(gdata.apps.contacts.IM(address=im['value'], protocol=im['protocol'], rel=im['rel'], label=im['label'], primary=im['primary']))
value = GetField(CONTACT_JOTS)
if value:
for jot in value:
contactEntry.jot.append(gdata.apps.contacts.Jot(text=jot['value'], rel=jot['rel']))
value = GetField(CONTACT_ORGANIZATIONS)
if value:
for organization in value:
org_name = gdata.apps.contacts.OrgName(text=organization['value'])
department = GetListEntryField(organization, 'department', gdata.apps.contacts.OrgDepartment)
title = GetListEntryField(organization, 'title', gdata.apps.contacts.OrgTitle)
job_description = GetListEntryField(organization, 'jobdescription', gdata.apps.contacts.OrgJobDescription)
symbol = GetListEntryField(organization, 'symbol', gdata.apps.contacts.OrgSymbol)
where = GetListEntryField(organization, 'where', gdata.apps.contacts.Where, attr='value_string')
contactEntry.organization.append(gdata.apps.contacts.Organization(name=org_name, department=department,
title=title, job_description=job_description,
symbol=symbol, where=where,
rel=organization['rel'], label=organization['label'], primary=organization['primary']))
value = GetField(CONTACT_PHONES)
if value:
for phone in value:
contactEntry.phoneNumber.append(gdata.apps.contacts.PhoneNumber(text=phone['value'], rel=phone['rel'], label=phone['label'], primary=phone['primary']))
value = GetField(CONTACT_RELATIONS)
if value:
for relation in value:
contactEntry.relation.append(gdata.apps.contacts.Relation(text=relation['value'], rel=relation['rel'], label=relation['label']))
value = GetField(CONTACT_USER_DEFINED_FIELDS)
if value:
for userdefinedfield in value:
contactEntry.userDefinedField.append(gdata.apps.contacts.UserDefinedField(key=userdefinedfield['rel'], value=userdefinedfield['value']))
value = GetField(CONTACT_WEBSITES)
if value:
for website in value:
contactEntry.website.append(gdata.apps.contacts.Website(href=website['value'], rel=website['rel'], label=website['label'], primary=website['primary']))
return contactEntry
@staticmethod
def ContactToFields(contactEntry):
fields = {}
def GetContactField(fieldName, attrlist):
objAttr = contactEntry
for attr in attrlist:
objAttr = getattr(objAttr, attr)
if not objAttr:
return
fields[fieldName] = objAttr
def GetListEntryField(entry, attrlist):
objAttr = entry
for attr in attrlist:
objAttr = getattr(objAttr, attr)
if not objAttr:
return None
return objAttr
def AppendItemToFieldsList(fieldName, fieldValue):
fields.setdefault(fieldName, [])
fields[fieldName].append(fieldValue)
fields[CONTACT_ID] = ContactsManager.GetContactShortId(contactEntry)
GetContactField(CONTACT_UPDATED, ['updated', 'text'])
if not contactEntry.deleted:
GetContactField(CONTACT_NAME, ['title', 'text'])
else:
fields[CONTACT_NAME] = 'Deleted'
GetContactField(CONTACT_NAME_PREFIX, ['name', 'name_prefix', 'text'])
GetContactField(CONTACT_GIVEN_NAME, ['name', 'given_name', 'text'])
GetContactField(CONTACT_ADDITIONAL_NAME, ['name', 'additional_name', 'text'])
GetContactField(CONTACT_FAMILY_NAME, ['name', 'family_name', 'text'])
GetContactField(CONTACT_NAME_SUFFIX, ['name', 'name_suffix', 'text'])
GetContactField(CONTACT_NICKNAME, ['nickname', 'text'])
GetContactField(CONTACT_MAIDENNAME, ['maidenName', 'text'])
GetContactField(CONTACT_SHORTNAME, ['shortName', 'text'])
GetContactField(CONTACT_INITIALS, ['initials', 'text'])
GetContactField(CONTACT_BIRTHDAY, ['birthday', 'when'])
GetContactField(CONTACT_GENDER, ['gender', 'value'])
GetContactField(CONTACT_SUBJECT, ['subject', 'text'])
GetContactField(CONTACT_LANGUAGE, ['language', 'code'])
GetContactField(CONTACT_PRIORITY, ['priority', 'rel'])
GetContactField(CONTACT_SENSITIVITY, ['sensitivity', 'rel'])
GetContactField(CONTACT_NOTES, ['content', 'text'])
GetContactField(CONTACT_LOCATION, ['where', 'value_string'])
GetContactField(CONTACT_OCCUPATION, ['occupation', 'text'])
GetContactField(CONTACT_BILLING_INFORMATION, ['billingInformation', 'text'])
GetContactField(CONTACT_MILEAGE, ['mileage', 'text'])
GetContactField(CONTACT_DIRECTORY_SERVER, ['directoryServer', 'text'])
for address in contactEntry.structuredPostalAddress:
AppendItemToFieldsList(CONTACT_ADDRESSES,
{'rel': address.rel,
'label': address.label,
'value': GetListEntryField(address, ['formatted_address', 'text']),
'street': GetListEntryField(address, ['street', 'text']),
'pobox': GetListEntryField(address, ['pobox', 'text']),
'neighborhood': GetListEntryField(address, ['neighborhood', 'text']),
'city': GetListEntryField(address, ['city', 'text']),
'region': GetListEntryField(address, ['region', 'text']),
'postcode': GetListEntryField(address, ['postcode', 'text']),
'country': GetListEntryField(address, ['country', 'text']),
'primary': address.primary})
for calendarLink in contactEntry.calendarLink:
AppendItemToFieldsList(CONTACT_CALENDARS,
{'rel': calendarLink.rel,
'label': calendarLink.label,
'value': calendarLink.href,
'primary': calendarLink.primary})
for emailaddr in contactEntry.email:
AppendItemToFieldsList(CONTACT_EMAILS,
{'rel': emailaddr.rel,
'label': emailaddr.label,
'value': emailaddr.address,
'primary': emailaddr.primary})
for externalid in contactEntry.externalId:
AppendItemToFieldsList(CONTACT_EXTERNALIDS,
{'rel': externalid.rel,
'label': externalid.label,
'value': externalid.value})
for event in contactEntry.event:
AppendItemToFieldsList(CONTACT_EVENTS,
{'rel': event.rel,
'label': event.label,
'value': GetListEntryField(event, ['when', 'startTime'])})
for hobby in contactEntry.hobby:
AppendItemToFieldsList(CONTACT_HOBBIES,
{'value': hobby.text})
for im in contactEntry.im:
AppendItemToFieldsList(CONTACT_IMS,
{'rel': im.rel,
'label': im.label,
'value': im.address,
'protocol': im.protocol,
'primary': im.primary})
for jot in contactEntry.jot:
AppendItemToFieldsList(CONTACT_JOTS,
{'rel': jot.rel,
'value': jot.text})
for organization in contactEntry.organization:
AppendItemToFieldsList(CONTACT_ORGANIZATIONS,
{'rel': organization.rel,
'label': organization.label,
'value': GetListEntryField(organization, ['name', 'text']),
'department': GetListEntryField(organization, ['department', 'text']),
'title': GetListEntryField(organization, ['title', 'text']),
'symbol': GetListEntryField(organization, ['symbol', 'text']),
'jobdescription': GetListEntryField(organization, ['job_description', 'text']),
'where': GetListEntryField(organization, ['where', 'value_string']),
'primary': organization.primary})
for phone in contactEntry.phoneNumber:
AppendItemToFieldsList(CONTACT_PHONES,
{'rel': phone.rel,
'label': phone.label,
'value': phone.text,
'primary': phone.primary})
for relation in contactEntry.relation:
AppendItemToFieldsList(CONTACT_RELATIONS,
{'rel': relation.rel,
'label': relation.label,
'value': relation.text})
for userdefinedfield in contactEntry.userDefinedField:
AppendItemToFieldsList(CONTACT_USER_DEFINED_FIELDS,
{'rel': userdefinedfield.key,
'value': userdefinedfield.value})
for website in contactEntry.website:
AppendItemToFieldsList(CONTACT_WEBSITES,
{'rel': website.rel,
'label': website.label,
'value': website.href,
'primary': website.primary})
return fields
CONTACTS_PROJECTION_CHOICE_MAP = {'basic': 'thin', 'thin': 'thin', 'full': 'full'}
CONTACTS_ORDERBY_CHOICE_MAP = {'lastmodified': 'lastmodified'}
def normalizeContactId(contactId):
if contactId.startswith('id:'):
return contactId[3:]
return contactId
def _initContactQueryAttributes():
return {'query': None, 'projection': 'full', 'url_params': {'max-results': str(GC.Values[GC.CONTACT_MAX_RESULTS])},
'emailMatchPattern': None, 'emailMatchType': None}
def _getContactQueryAttributes(contactQuery, myarg, unknownAction, printShowCmd):
if myarg == 'query':
contactQuery['query'] = getString(Cmd.OB_QUERY)
elif myarg == 'emailmatchpattern':
contactQuery['emailMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'emailmatchtype':
contactQuery['emailMatchType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
elif myarg == 'updatedmin':
contactQuery['url_params']['updated-min'] = getYYYYMMDD()
elif myarg == 'endquery':
return False
elif not printShowCmd:
if unknownAction < 0:
unknownArgumentExit()
if unknownAction > 0:
Cmd.Backup()
return False
elif myarg == 'orderby':
contactQuery['url_params']['orderby'], contactQuery['url_params']['sortorder'] = getOrderBySortOrder(CONTACTS_ORDERBY_CHOICE_MAP, 'ascending', False)
elif myarg in CONTACTS_PROJECTION_CHOICE_MAP:
contactQuery['projection'] = CONTACTS_PROJECTION_CHOICE_MAP[myarg]
elif myarg == 'showdeleted':
contactQuery['url_params']['showdeleted'] = 'true'
else:
if unknownAction < 0:
unknownArgumentExit()
if unknownAction > 0:
Cmd.Backup()
return False
return True
CONTACT_SELECT_ARGUMENTS = {'query', 'emailmatchpattern', 'emailmatchtype', 'updatedmin'}
def _getContactEntityList(unknownAction, printShowCmd):
contactQuery = _initContactQueryAttributes()
if Cmd.PeekArgumentPresent(CONTACT_SELECT_ARGUMENTS):
entityList = None
queriedContacts = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if not _getContactQueryAttributes(contactQuery, myarg, unknownAction, printShowCmd):
break
else:
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
queriedContacts = False
if unknownAction < 0:
checkForExtraneousArguments()
return (entityList, contactQuery, queriedContacts)
def queryContacts(contactsObject, contactQuery):
entityType = Ent.DOMAIN
user = GC.Values[GC.DOMAIN]
if contactQuery['query']:
uri = getContactsQuery(feed=contactsObject.GetContactFeedUri(contact_list=user, projection=contactQuery['projection']),
text_query=contactQuery['query']).ToUri()
else:
uri = contactsObject.GetContactFeedUri(contact_list=user, projection=contactQuery['projection'])
printGettingAllEntityItemsForWhom(Ent.CONTACT, user, query=contactQuery['query'])
try:
entityList = callGDataPages(contactsObject, 'GetContactsFeed',
pageMessage=getPageMessageForWhom(),
throwErrors=[GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
uri=uri, url_params=contactQuery['url_params'])
return entityList
except GDATA.badRequest as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT, ''], str(e))
except GDATA.forbidden:
entityServiceNotApplicableWarning(entityType, user)
return None
def localContactSelects(contactsManager, contactQuery, fields):
if contactQuery['emailMatchPattern']:
emailMatchType = contactQuery['emailMatchType']
for item in fields.get(CONTACT_EMAILS, []):
if contactQuery['emailMatchPattern'].match(item['value']):
if (not emailMatchType or
emailMatchType == item.get('label') or
emailMatchType == contactsManager.CONTACT_ARRAY_PROPERTIES[CONTACT_EMAILS]['relMap'].get(item['rel'], 'custom')):
break
else:
return False
return True
def countLocalContactSelects(contactsManager, contacts, contactQuery):
if contacts is not None and contactQuery:
jcount = 0
for contact in contacts:
fields = contactsManager.ContactToFields(contact)
if localContactSelects(contactsManager, contactQuery, fields):
jcount += 1
else:
jcount = len(contacts) if contacts is not None else 0
return jcount
def clearEmailAddressMatches(contactsManager, contactClear, fields):
savedAddresses = []
updateRequired = False
emailMatchType = contactClear['emailClearType']
for item in fields.get(CONTACT_EMAILS, []):
if (contactClear['emailClearPattern'].match(item['value']) and
(not emailMatchType or
emailMatchType == item.get('label') or
emailMatchType == contactsManager.CONTACT_ARRAY_PROPERTIES[CONTACT_EMAILS]['relMap'].get(item['rel'], 'custom'))):
updateRequired = True
else:
savedAddresses.append(item)
if updateRequired:
fields[CONTACT_EMAILS] = savedAddresses
return updateRequired
def dedupEmailAddressMatches(contactsManager, emailMatchType, fields):
sai = -1
savedAddresses = []
matches = {}
updateRequired = False
for item in fields.get(CONTACT_EMAILS, []):
emailAddr = item['value']
emailType = item.get('label')
if emailType is None:
emailType = contactsManager.CONTACT_ARRAY_PROPERTIES[CONTACT_EMAILS]['relMap'].get(item['rel'], 'custom')
if (emailAddr in matches) and (not emailMatchType or emailType in matches[emailAddr]['types']):
if item['primary'] == 'true':
savedAddresses[matches[emailAddr]['sai']]['primary'] = 'true'
updateRequired = True
else:
savedAddresses.append(item)
sai += 1
matches.setdefault(emailAddr, {'types': set(), 'sai': sai})
matches[emailAddr]['types'].add(emailType)
if updateRequired:
fields[CONTACT_EMAILS] = savedAddresses
return updateRequired
def _createContact():
entityType = Ent.DOMAIN
contactsManager = ContactsManager()
parameters = {'csvPF': None, 'titles': ['Domain', CONTACT_ID], 'addCSVData': {}, 'returnIdOnly': False}
fields = contactsManager.GetContactFields(parameters)
csvPF = parameters['csvPF']
addCSVData = parameters['addCSVData']
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
returnIdOnly = parameters['returnIdOnly']
contactEntry = contactsManager.FieldsToContact(fields)
user, contactsObject = getContactsObject(True)
try:
contact = callGData(contactsObject, 'CreateContact',
throwErrors=[GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
new_contact=contactEntry, insert_uri=contactsObject.GetContactFeedUri(contact_list=user))
contactId = contactsManager.GetContactShortId(contact)
if returnIdOnly:
writeStdout(f'{contactId}\n')
elif not csvPF:
entityActionPerformed([entityType, user, Ent.CONTACT, contactId])
else:
row = {'Domain': user, CONTACT_ID: contactId}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
except GDATA.badRequest as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT, ''], str(e))
except GDATA.forbidden:
entityServiceNotApplicableWarning(entityType, user)
except GDATA.serviceNotApplicable:
entityUnknownWarning(entityType, user)
if csvPF:
csvPF.writeCSVfile('Contacts')
# gam create contact <ContactAttribute>+
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*))| returnidonly]
def doCreateDomainContact():
_createContact()
def _clearUpdateContacts(updateContacts):
entityType = Ent.DOMAIN
contactsManager = ContactsManager()
entityList, contactQuery, queriedContacts = _getContactEntityList(1, False)
if updateContacts:
update_fields = contactsManager.GetContactFields()
else:
contactClear = {'emailClearPattern': contactQuery['emailMatchPattern'], 'emailClearType': contactQuery['emailMatchType']}
deleteClearedContactsWithNoEmails = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'emailclearpattern':
contactClear['emailClearPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'emailcleartype':
contactClear['emailClearType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
elif myarg == 'deleteclearedcontactswithnoemails':
deleteClearedContactsWithNoEmails = True
else:
unknownArgumentExit()
if not contactClear['emailClearPattern']:
missingArgumentExit('emailclearpattern')
user, contactsObject = getContactsObject(True)
if queriedContacts:
entityList = queryContacts(contactsObject, contactQuery)
if entityList is None:
return
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
for contact in entityList:
j += 1
try:
if not queriedContacts:
contactId = normalizeContactId(contact)
contact = callGData(contactsObject, 'GetContact',
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId))
fields = contactsManager.ContactToFields(contact)
else:
contactId = contactsManager.GetContactShortId(contact)
fields = contactsManager.ContactToFields(contact)
if not localContactSelects(contactsManager, contactQuery, fields):
continue
if updateContacts:
for field, value in iter(update_fields.items()):
fields[field] = value
contactEntry = contactsManager.FieldsToContact(fields)
else:
if not clearEmailAddressMatches(contactsManager, contactClear, fields):
continue
if deleteClearedContactsWithNoEmails and not fields[CONTACT_EMAILS]:
Act.Set(Act.DELETE)
callGData(contactsObject, 'DeleteContact',
throwErrors=[GDATA.NOT_FOUND, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), extra_headers={'If-Match': contact.etag})
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
continue
contactEntry = contactsManager.FieldsToContact(fields)
contactEntry.category = contact.category
contactEntry.link = contact.link
contactEntry.etag = contact.etag
contactEntry.id = contact.id
Act.Set(Act.UPDATE)
callGData(contactsObject, 'UpdateContact',
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.PRECONDITION_FAILED, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), updated_contact=contactEntry, extra_headers={'If-Match': contact.etag})
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
except (GDATA.notFound, GDATA.badRequest, GDATA.preconditionFailed) as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
except (GDATA.forbidden, GDATA.notImplemented):
entityServiceNotApplicableWarning(entityType, user)
break
except GDATA.serviceNotApplicable:
entityUnknownWarning(entityType, user)
break
Ind.Decrement()
# gam clear contacts <ContactEntity>|<ContactSelection>
# [clearmatchpattern <REMatchPattern>] [clearmatchtype work|home|other|<String>]
# [deleteclearedcontactswithnoemails]
def doClearDomainContacts():
_clearUpdateContacts(False)
# gam update contacts <ContactEntity>|<ContactSelection> <ContactAttribute>+
def doUpdateDomainContacts():
_clearUpdateContacts(True)
def _dedupContacts():
entityType = Ent.DOMAIN
contactsManager = ContactsManager()
contactQuery = _initContactQueryAttributes()
emailMatchType = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matchtype':
emailMatchType = getBoolean()
else:
_getContactQueryAttributes(contactQuery, myarg, -1, False)
user, contactsObject = getContactsObject(True)
contacts = queryContacts(contactsObject, contactQuery)
if contacts is None:
return
j = 0
jcount = len(contacts)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
for contact in contacts:
j += 1
try:
fields = contactsManager.ContactToFields(contact)
if not localContactSelects(contactsManager, contactQuery, fields):
continue
if not dedupEmailAddressMatches(contactsManager, emailMatchType, fields):
continue
contactId = fields[CONTACT_ID]
contactEntry = contactsManager.FieldsToContact(fields)
contactEntry.category = contact.category
contactEntry.link = contact.link
contactEntry.etag = contact.etag
contactEntry.id = contact.id
Act.Set(Act.UPDATE)
callGData(contactsObject, 'UpdateContact',
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.PRECONDITION_FAILED, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), updated_contact=contactEntry, extra_headers={'If-Match': contact.etag})
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
except (GDATA.notFound, GDATA.badRequest, GDATA.preconditionFailed) as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
except (GDATA.forbidden, GDATA.notImplemented):
entityServiceNotApplicableWarning(entityType, user)
break
except GDATA.serviceNotApplicable:
entityUnknownWarning(entityType, user)
break
Ind.Decrement()
# gam dedup contacts <ContactEntity>|<ContactSelection> [matchType [<Boolean>]]
def doDedupDomainContacts():
_dedupContacts()
def _deleteContacts():
entityType = Ent.DOMAIN
contactsManager = ContactsManager()
entityList, contactQuery, queriedContacts = _getContactEntityList(-1, False)
user, contactsObject = getContactsObject(True)
if queriedContacts:
entityList = queryContacts(contactsObject, contactQuery)
if entityList is None:
return
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
for contact in entityList:
j += 1
try:
if not queriedContacts:
contactId = normalizeContactId(contact)
contact = callGData(contactsObject, 'GetContact',
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId))
else:
contactId = contactsManager.GetContactShortId(contact)
fields = contactsManager.ContactToFields(contact)
if not localContactSelects(contactsManager, contactQuery, fields):
continue
callGData(contactsObject, 'DeleteContact',
throwErrors=[GDATA.NOT_FOUND, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), extra_headers={'If-Match': contact.etag})
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
except (GDATA.notFound, GDATA.badRequest) as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
except (GDATA.forbidden, GDATA.notImplemented):
entityServiceNotApplicableWarning(entityType, user)
break
except GDATA.serviceNotApplicable:
entityUnknownWarning(entityType, user)
break
Ind.Decrement()
# gam delete contacts <ContactEntity>|<ContactSelection>
def doDeleteDomainContacts():
_deleteContacts()
CONTACT_TIME_OBJECTS = {CONTACT_UPDATED}
CONTACT_FIELDS_WITH_CRS_NLS = {CONTACT_NOTES, CONTACT_BILLING_INFORMATION}
def _showContact(contactsManager, fields, displayFieldsList, j, jcount, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(fields, timeObjects=CONTACT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CONTACT, fields[CONTACT_ID]], j, jcount)
Ind.Increment()
for key in contactsManager.CONTACT_NAME_PROPERTY_PRINT_ORDER:
if displayFieldsList and key not in displayFieldsList:
continue
if key in fields:
if key in CONTACT_TIME_OBJECTS:
printKeyValueList([key, formatLocalTime(fields[key])])
elif key not in CONTACT_FIELDS_WITH_CRS_NLS:
printKeyValueList([key, fields[key]])
else:
printKeyValueWithCRsNLs(key, fields[key])
for key in contactsManager.CONTACT_ARRAY_PROPERTY_PRINT_ORDER:
if displayFieldsList and key not in displayFieldsList:
continue
if key in fields:
keymap = contactsManager.CONTACT_ARRAY_PROPERTIES[key]
printKeyValueList([key, None])
Ind.Increment()
for item in fields[key]:
fn = item.get('label')
if keymap['relMap']:
if not fn:
fn = keymap['relMap'].get(item['rel'], 'custom')
printKeyValueList(['type', fn])
Ind.Increment()
if keymap['primary']:
printKeyValueList(['rank', ['notprimary', 'primary'][item['primary'] == 'true']])
value = item['value']
if value is None:
value = ''
if key == CONTACT_IMS:
printKeyValueList(['protocol', contactsManager.IM_REL_TO_PROTOCOL_MAP.get(item['protocol'], item['protocol'])])
printKeyValueList([keymap['infoTitle'], value])
elif key == CONTACT_ADDRESSES:
printKeyValueWithCRsNLs(keymap['infoTitle'], value)
for org_key in contactsManager.ADDRESS_FIELD_PRINT_ORDER:
if item[org_key]:
printKeyValueList([contactsManager.ADDRESS_FIELD_TO_ARGUMENT_MAP[org_key], item[org_key]])
elif key == CONTACT_ORGANIZATIONS:
printKeyValueList([keymap['infoTitle'], value])
for org_key in contactsManager.ORGANIZATION_FIELD_PRINT_ORDER:
if item[org_key]:
printKeyValueList([contactsManager.ORGANIZATION_FIELD_TO_ARGUMENT_MAP[org_key], item[org_key]])
elif key == CONTACT_USER_DEFINED_FIELDS:
printKeyValueList([item.get('rel') or 'None', value])
else:
printKeyValueList([keymap['infoTitle'], value])
if keymap['relMap']:
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
def _getContactFieldsList(contactsManager, displayFieldsList):
for field in _getFieldsList():
if field in contactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP:
displayFieldsList.append(contactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP[field])
else:
invalidChoiceExit(field, contactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP, True)
def _infoContacts(contactFeed):
entityType = Ent.DOMAIN
contactsManager = ContactsManager()
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
contactQuery = _initContactQueryAttributes()
FJQC = FormatJSONQuoteChar()
displayFieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in CONTACTS_PROJECTION_CHOICE_MAP:
contactQuery['projection'] = CONTACTS_PROJECTION_CHOICE_MAP[myarg]
elif myarg == 'fields':
_getContactFieldsList(contactsManager, displayFieldsList)
else:
FJQC.GetFormatJSON(myarg)
user, contactsObject = getContactsObject(contactFeed)
j = 0
jcount = len(entityList)
if not FJQC.formatJSON:
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
for contact in entityList:
j += 1
try:
contactId = normalizeContactId(contact)
contact = callGData(contactsObject, 'GetContact',
bailOnInternalServerError=True,
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE,
GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED, GDATA.INTERNAL_SERVER_ERROR],
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId, projection=contactQuery['projection']))
fields = contactsManager.ContactToFields(contact)
_showContact(contactsManager, fields, displayFieldsList, j, jcount, FJQC)
except (GDATA.notFound, GDATA.badRequest, GDATA.forbidden, GDATA.notImplemented, GDATA.internalServerError) as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
except GDATA.serviceNotApplicable:
entityUnknownWarning(entityType, user)
break
Ind.Decrement()
# gam info contacts <ContactEntity>
# [basic|full]
# [fields <ContactFieldNameList>] [formatjson]
def doInfoDomainContacts():
_infoContacts(True)
# gam info gal <ContactEntity>
# [basic|full]
# [fields <ContactFieldNameList>] [formatjson]
def doInfoGAL():
_infoContacts(False)
def _printShowContacts(contactFeed):
entityType = Ent.DOMAIN
entityTypeName = Ent.Singular(entityType)
contactsManager = ContactsManager()
csvPF = CSVPrintFile([entityTypeName, CONTACT_ID, CONTACT_NAME], 'sortall',
contactsManager.CONTACT_ARRAY_PROPERTY_PRINT_ORDER) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
CSVTitle = 'Contacts'
contactQuery = _initContactQueryAttributes()
countsOnly = False
displayFieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'fields':
_getContactFieldsList(contactsManager, displayFieldsList)
elif myarg == 'countsonly':
countsOnly = True
contactQuery['projection'] = CONTACTS_PROJECTION_CHOICE_MAP['basic']
if csvPF:
csvPF.SetTitles([entityTypeName, CSVTitle])
elif _getContactQueryAttributes(contactQuery, myarg, 0, True):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
user, contactsObject = getContactsObject(contactFeed)
contacts = queryContacts(contactsObject, contactQuery)
if countsOnly:
jcount = countLocalContactSelects(contactsManager, contacts, contactQuery)
if csvPF:
csvPF.WriteRowTitles({entityTypeName: user, CSVTitle: jcount})
else:
printEntityKVList([entityType, user], [CSVTitle, jcount])
elif contacts is not None:
jcount = len(contacts)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
Ind.Increment()
j = 0
for contact in contacts:
j += 1
fields = contactsManager.ContactToFields(contact)
if not localContactSelects(contactsManager, contactQuery, fields):
continue
_showContact(contactsManager, fields, displayFieldsList, j, jcount, FJQC)
Ind.Decrement()
elif contacts:
for contact in contacts:
fields = contactsManager.ContactToFields(contact)
if not localContactSelects(contactsManager, contactQuery, fields):
continue
contactRow = {entityTypeName: user, CONTACT_ID: fields[CONTACT_ID]}
for key in contactsManager.CONTACT_NAME_PROPERTY_PRINT_ORDER:
if displayFieldsList and key not in displayFieldsList:
continue
if key in fields:
if key == CONTACT_UPDATED:
contactRow[key] = formatLocalTime(fields[key])
elif key not in (CONTACT_NOTES, CONTACT_BILLING_INFORMATION):
contactRow[key] = fields[key]
else:
contactRow[key] = escapeCRsNLs(fields[key])
for key in contactsManager.CONTACT_ARRAY_PROPERTY_PRINT_ORDER:
if displayFieldsList and key not in displayFieldsList:
continue
if key in fields:
keymap = contactsManager.CONTACT_ARRAY_PROPERTIES[key]
j = 0
contactRow[f'{key}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}count'] = len(fields[key])
for item in fields[key]:
j += 1
fn = f'{key}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}'
fnt = item.get('label')
if fnt:
contactRow[fn+'type'] = fnt
elif keymap['relMap']:
contactRow[fn+'type'] = keymap['relMap'].get(item['rel'], 'custom')
if keymap['primary']:
contactRow[fn+'rank'] = 'primary' if item['primary'] == 'true' else 'notprimary'
value = item['value']
if value is None:
value = ''
if key == CONTACT_IMS:
contactRow[fn+'protocol'] = contactsManager.IM_REL_TO_PROTOCOL_MAP.get(item['protocol'], item['protocol'])
contactRow[fn+keymap['infoTitle']] = value
elif key == CONTACT_ADDRESSES:
contactRow[fn+keymap['infoTitle']] = escapeCRsNLs(value)
for org_key in contactsManager.ADDRESS_FIELD_PRINT_ORDER:
if item[org_key]:
contactRow[fn+contactsManager.ADDRESS_FIELD_TO_ARGUMENT_MAP[org_key]] = escapeCRsNLs(item[org_key])
elif key == CONTACT_ORGANIZATIONS:
contactRow[fn+keymap['infoTitle']] = value
for org_key in contactsManager.ORGANIZATION_FIELD_PRINT_ORDER:
if item[org_key]:
contactRow[fn+contactsManager.ORGANIZATION_FIELD_TO_ARGUMENT_MAP[org_key]] = item[org_key]
elif key == CONTACT_USER_DEFINED_FIELDS:
contactRow[fn+'type'] = item.get('rel') or 'None'
contactRow[fn+keymap['infoTitle']] = value
else:
contactRow[fn+keymap['infoTitle']] = value
if not FJQC.formatJSON:
csvPF.WriteRowTitles(contactRow)
elif csvPF.CheckRowTitles(contactRow):
csvPF.WriteRowNoFilter({entityTypeName: user, CONTACT_ID: fields[CONTACT_ID],
CONTACT_NAME: fields.get(CONTACT_NAME, ''),
'JSON': json.dumps(cleanJSON(fields, timeObjects=CONTACT_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile(CSVTitle)
# gam print contacts [todrive <ToDriveAttribute>*] <ContactSelection>
# [basic|full|countsonly] [showdeleted] [orderby <ContactOrderByFieldName> [ascending|descending]]
# [fields <ContactFieldNameList>] [formatjson [quotechar <Character>]]
# gam show contacts <ContactSelection>
# [basic|full|countsonly] [showdeleted] [orderby <ContactOrderByFieldName> [ascending|descending]]
# [fields <ContactFieldNameList>] [formatjson]
def doPrintShowDomainContacts():
_printShowContacts(True)
# gam print gal [todrive <ToDriveAttribute>*] <ContactSelection>
# [basic|full] [orderby <ContactOrderByFieldName> [ascending|descending]]
# [fields <ContactFieldNameList>] [formatjson [quotechar <Character>]]
# gam show gal <ContactSelection>
# [basic|full|countsonly] [orderby <ContactOrderByFieldName> [ascending|descending]]
# [fields <ContactFieldNameList>] [formatjson]
def doPrintShowGAL():
_printShowContacts(False)
# Prople commands utilities
#
def normalizePeopleResourceName(resourceName):
if resourceName.startswith('people/'):
return resourceName
return f'people/{resourceName}'
def normalizeContactGroupResourceName(resourceName):
if resourceName.startswith('contactGroups/'):
return resourceName
return f'contactGroups/{resourceName}'
def normalizeOtherContactsResourceName(resourceName):
if resourceName.startswith('otherContacts/'):
return resourceName
return f'otherContacts/{resourceName}'
PEOPLE_JSON = 'JSON'
PEOPLE_ADDRESSES = 'addresses'
PEOPLE_BIOGRAPHIES = 'biographies'
PEOPLE_BIRTHDAYS = 'birthdays'
PEOPLE_CALENDAR_URLS = 'calendarUrls'
PEOPLE_CLIENT_DATA = 'clientData'
PEOPLE_COVER_PHOTOS = 'coverPhotos'
PEOPLE_EMAIL_ADDRESSES = 'emailAddresses'
PEOPLE_EVENTS = 'events'
PEOPLE_EXTERNAL_IDS = 'externalIds'
PEOPLE_FILE_ASES = 'fileAses'
PEOPLE_GENDERS = 'genders'
PEOPLE_IM_CLIENTS = 'imClients'
PEOPLE_INTERESTS = 'interests'
PEOPLE_LOCALES = 'locales'
PEOPLE_LOCATIONS = 'locations'
PEOPLE_MEMBERSHIPS = 'memberships'
PEOPLE_METADATA = 'metadata'
PEOPLE_MISC_KEYWORDS = 'miscKeywords'
PEOPLE_MISC_KEYWORDS_BILLING_INFORMATION = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_BILLING_INFORMATION'
PEOPLE_MISC_KEYWORDS_DIRECTORY_SERVER = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_DIRECTORY_SERVER'
PEOPLE_MISC_KEYWORDS_JOT = PEOPLE_MISC_KEYWORDS+'.jot'
PEOPLE_MISC_KEYWORDS_MILEAGE = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_MILEAGE'
PEOPLE_MISC_KEYWORDS_PRIORITY = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_PRIORITY'
PEOPLE_MISC_KEYWORDS_SENSITIVITY = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_SENSITIVITY'
PEOPLE_MISC_KEYWORDS_SUBJECT = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_SUBJECT'
PEOPLE_NAMES = 'names'
PEOPLE_NAMES_FAMILY_NAME = PEOPLE_NAMES+'.familyName'
PEOPLE_NAMES_GIVEN_NAME = PEOPLE_NAMES+'.givenName'
PEOPLE_NAMES_HONORIFIC_PREFIX = PEOPLE_NAMES+'.honorificPrefix'
PEOPLE_NAMES_HONORIFIC_SUFFIX = PEOPLE_NAMES+'.honorificSuffix'
PEOPLE_NAMES_MIDDLE_NAME = PEOPLE_NAMES+'.middleName'
PEOPLE_NAMES_PHONETIC_FAMILY_NAME = PEOPLE_NAMES+'.phoneticFamilyName'
PEOPLE_NAMES_PHONETIC_GIVEN_NAME = PEOPLE_NAMES+'.phoneticGivenName'
PEOPLE_NAMES_PHONETIC_HONORIFIC_PREFIX = PEOPLE_NAMES+'.phoneticHonorificPrefix'
PEOPLE_NAMES_PHONETIC_HONORIFIC_SUFFIX = PEOPLE_NAMES+'.phoneticHonorificSuffix'
PEOPLE_NAMES_PHONETIC_MIDDLE_NAME = PEOPLE_NAMES+'.phoneticMiddleName'
PEOPLE_NAMES_UNSTRUCTURED_NAME = PEOPLE_NAMES+'.unstructuredName'
PEOPLE_NICKNAMES = 'nicknames'
PEOPLE_NICKNAMES_INITIALS = PEOPLE_NICKNAMES+'.INITIALS'
PEOPLE_NICKNAMES_MAIDENNAME = PEOPLE_NICKNAMES+'.MAIDEN_NAME'
PEOPLE_NICKNAMES_NICKNAME = PEOPLE_NICKNAMES+'.DEFAULT'
PEOPLE_NICKNAMES_SHORTNAME = PEOPLE_NICKNAMES+'.SHORT_NAME'
PEOPLE_OCCUPATIONS = 'occupations'
PEOPLE_ORGANIZATIONS = 'organizations'
PEOPLE_PHONE_NUMBERS = 'phoneNumbers'
PEOPLE_PHOTOS = 'photos'
PEOPLE_RELATIONS = 'relations'
PEOPLE_SIP_ADDRESSES = 'sipAddresses'
PEOPLE_SKILLS = 'skills'
PEOPLE_UPDATE_TIME = 'updateTime'
PEOPLE_URLS = 'urls'
PEOPLE_USER_DEFINED = 'userDefined'
PEOPLE_GROUPS = 'ContactGroups'
PEOPLE_GROUPS_LIST = 'ContactGroupsList'
PEOPLE_ADD_GROUPS = 'ContactAddGroups'
PEOPLE_ADD_GROUPS_LIST = 'ContactAddGroupsList'
PEOPLE_REMOVE_GROUPS = 'ContactRemoveGroups'
PEOPLE_REMOVE_GROUPS_LIST = 'ContactRemoveGroupsList'
PEOPLE_GROUP_NAME = 'name'
PEOPLE_GROUP_CLIENT_DATA = 'clientData'
#
class PeopleManager():
PEOPLE_ARGUMENT_TO_PROPERTY_MAP = {
'json': PEOPLE_JSON,
'additionalname': PEOPLE_NAMES_MIDDLE_NAME,
'address': PEOPLE_ADDRESSES,
'addresses': PEOPLE_ADDRESSES,
'billinginfo': PEOPLE_MISC_KEYWORDS_BILLING_INFORMATION,
'biography': PEOPLE_BIOGRAPHIES,
'biographies': PEOPLE_BIOGRAPHIES,
'birthday': PEOPLE_BIRTHDAYS,
'birthdays': PEOPLE_BIRTHDAYS,
'calendar': PEOPLE_CALENDAR_URLS,
'calendars': PEOPLE_CALENDAR_URLS,
'clientdata': PEOPLE_CLIENT_DATA,
# 'coverphoto': PEOPLE_COVER_PHOTOS,
# 'coverphotos': PEOPLE_COVER_PHOTOS,
'directoryserver': PEOPLE_MISC_KEYWORDS_DIRECTORY_SERVER,
'email': PEOPLE_EMAIL_ADDRESSES,
'emails': PEOPLE_EMAIL_ADDRESSES,
'emailadresses': PEOPLE_EMAIL_ADDRESSES,
'event': PEOPLE_EVENTS,
'events': PEOPLE_EVENTS,
'externalid': PEOPLE_EXTERNAL_IDS,
'externalids': PEOPLE_EXTERNAL_IDS,
'familyname': PEOPLE_NAMES_FAMILY_NAME,
'fileas': PEOPLE_FILE_ASES,
'firstname': PEOPLE_NAMES_GIVEN_NAME,
'gender': PEOPLE_GENDERS,
'genders': PEOPLE_GENDERS,
'givenname': PEOPLE_NAMES_GIVEN_NAME,
'hobby': PEOPLE_INTERESTS,
'hobbies': PEOPLE_INTERESTS,
'im': PEOPLE_IM_CLIENTS,
'ims': PEOPLE_IM_CLIENTS,
'imclients': 'imClients',
'initials': PEOPLE_NICKNAMES_INITIALS,
'interests': PEOPLE_INTERESTS,
'jot': PEOPLE_MISC_KEYWORDS_JOT,
'jots': PEOPLE_MISC_KEYWORDS_JOT,
'language': PEOPLE_LOCALES,
'lastname': PEOPLE_NAMES_FAMILY_NAME,
'locale': PEOPLE_LOCALES,
'location': PEOPLE_LOCATIONS,
'locations': PEOPLE_LOCATIONS,
'maidenname': PEOPLE_NICKNAMES_MAIDENNAME,
'middlename': PEOPLE_NAMES_MIDDLE_NAME,
'mileage': PEOPLE_MISC_KEYWORDS_MILEAGE,
'misckeywords': PEOPLE_MISC_KEYWORDS,
'name': PEOPLE_NAMES_UNSTRUCTURED_NAME,
'names': PEOPLE_NAMES_UNSTRUCTURED_NAME,
'nickname': PEOPLE_NICKNAMES_NICKNAME,
'nicknames': PEOPLE_NICKNAMES_NICKNAME,
'note': PEOPLE_BIOGRAPHIES,
'notes': PEOPLE_BIOGRAPHIES,
'occupation': PEOPLE_OCCUPATIONS,
'occupations': PEOPLE_OCCUPATIONS,
'organization': PEOPLE_ORGANIZATIONS,
'organizations': PEOPLE_ORGANIZATIONS,
'organisation': PEOPLE_ORGANIZATIONS,
'organisations': PEOPLE_ORGANIZATIONS,
'phone': PEOPLE_PHONE_NUMBERS,
'phones': PEOPLE_PHONE_NUMBERS,
'phonenumbers': PEOPLE_PHONE_NUMBERS,
# 'photo': PEOPLE_PHOTOS,
# 'photos': PEOPLE_PHOTOS,
'prefix': PEOPLE_NAMES_HONORIFIC_PREFIX,
'priority': PEOPLE_MISC_KEYWORDS_PRIORITY,
'relation': PEOPLE_RELATIONS,
'relations': PEOPLE_RELATIONS,
'sensitivity': PEOPLE_MISC_KEYWORDS_SENSITIVITY,
'shortname': PEOPLE_NICKNAMES_SHORTNAME,
'sipaddress': PEOPLE_SIP_ADDRESSES,
'sipaddresses': PEOPLE_SIP_ADDRESSES,
'skills': PEOPLE_SKILLS,
'subject': PEOPLE_MISC_KEYWORDS_SUBJECT,
'suffix': PEOPLE_NAMES_HONORIFIC_SUFFIX,
'url': PEOPLE_URLS,
'urls': PEOPLE_URLS,
'userdefined': PEOPLE_USER_DEFINED,
'userdefinedfield': PEOPLE_USER_DEFINED,
'userdefinedfields': PEOPLE_USER_DEFINED,
'website': PEOPLE_URLS,
'websites': PEOPLE_URLS,
'contactgroup': PEOPLE_GROUPS,
'contactgroups': PEOPLE_GROUPS,
'addcontactgroup': PEOPLE_ADD_GROUPS,
'addcontactgroups': PEOPLE_ADD_GROUPS,
'removecontactgroup': PEOPLE_REMOVE_GROUPS,
'removecontactgroups': PEOPLE_REMOVE_GROUPS,
}
ADDRESS_ARGUMENT_TO_FIELD_MAP = {
'formatted': 'formattedValue',
'unstructured': 'formattedValue',
'pobox': 'poBox',
'street': 'streetAddress',
'streetaddress': 'streetAddress',
'extended': 'extendedAddress',
'neighborhood': 'extendedAddress',
'city': 'city',
'locality': 'city',
'region': 'region',
'postalcode': 'postalCode',
'country': 'country',
'countrycode': 'countryCode',
}
JOT_TYPE_MAP = {
'work': 'WORK',
'home': 'HOME',
'other': 'OTHER',
'keyword': 'KEYWORD',
'keywords': 'KEYWORD',
'user': 'USER',
}
IM_PROTOCOLS = {
'aim': 'aim',
'googletalk': 'googleTalk',
'gtalk': 'googleTalk',
'icq': 'icq',
'jabber': 'jabber',
'msn': 'msn',
'netmeeting': 'netMeeting',
'qq': 'qq',
'skype': 'skype',
'xmpp': 'jabber',
'yahoo': 'yahoo',
}
ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
'startdate': 'startDate',
'enddate': 'endDate',
'current': 'current',
'phoneticname': 'phoneticName',
'title': 'title',
'department': 'department',
'jobdescription': 'jobDescription',
'symbol': 'symbol',
'domain': 'domain',
'location': 'location',
}
# Fields with a key and value
KEY_VALUE_FIELDS = {
PEOPLE_CLIENT_DATA,
PEOPLE_USER_DEFINED,
}
# Fields with a type and value
TYPE_VALUE_FIELDS = {
PEOPLE_EVENTS: {
'anniversary': 'anniversary',
'other': 'other',
},
PEOPLE_EXTERNAL_IDS: {
'account': 'account',
'customer': 'customer',
'loginid': 'loginId',
'network': 'network',
'organization': 'organization',
'organisation': 'organization',
},
PEOPLE_RELATIONS: {
'spouse' : 'spouse',
'child' : 'child',
'mother' : 'mother',
'father' : 'father',
'parent' : 'parent',
'brother' : 'brother',
'sister' : 'sister',
'friend' : 'friend',
'relative' : 'relative',
'domesticpartner' : 'domesticPartner',
'manager' : 'manager',
'assistant' : 'assistant',
'referredby' : 'referredBy',
'partner' : 'partner',
},
PEOPLE_SIP_ADDRESSES: {
'work': 'work',
'home': 'home',
'other': 'other',
'mobile': 'mobile',
},
}
# Fields with a type, value and primary|notprimary; some fields may have additional arguments
TYPE_VALUE_PNP_FIELDS = {
PEOPLE_ADDRESSES: {
'work': 'work',
'home': 'home',
'other': 'other',
},
PEOPLE_CALENDAR_URLS: {
'work': 'work',
'home': 'home',
'freebusy': 'freeBusy',
},
PEOPLE_EMAIL_ADDRESSES: {
'work': 'work',
'home': 'home',
'other': 'other',
},
PEOPLE_IM_CLIENTS: {
'work': 'work',
'home': 'home',
'other': 'other',
},
PEOPLE_ORGANIZATIONS: {
'school': 'school',
'work': 'work',
'other': 'other',
},
PEOPLE_PHONE_NUMBERS: {
'work': 'work',
'home': 'home',
'other': 'other',
'googlevoice': 'googleVoice',
'fax': 'homeFax',
'homefax': 'homeFax',
'workfax': 'workFax',
'otherfax': 'otherFax',
'main': 'main',
'company_main': 'companyMain',
'assistant': 'assistant',
'mobile': 'mobile',
'workmobile': 'workMobile',
'pager': 'pager',
'workpager': 'workPager',
'car': 'car',
'radio': 'radio',
'callback': 'callback',
'isdn': 'isdn',
'telex': 'telex',
'ttytdd': 'TTY_TDD',
},
PEOPLE_SIP_ADDRESSES: {
'work': 'work',
'home': 'home',
'mobile': 'mobile',
'other': 'other',
},
PEOPLE_URLS: {
'appinstallpage': 'appInstallPage',
'blog': 'blog',
'ftp': 'ftp',
'home': 'homePage',
'homepage': 'homePage',
'other': 'other',
'profile': 'profile',
'reservations': 'reservations',
'resume': 'resume',
'work': 'work',
}
}
# Fields that allow an empty type
EMPTY_TYPE_ALLOWED_FIELDS = {PEOPLE_ADDRESSES, PEOPLE_EMAIL_ADDRESSES, PEOPLE_PHONE_NUMBERS, PEOPLE_URLS}
# Fields with just a URL
# URL_FIELDS = {
# PEOPLE_COVER_PHOTOS,
# PEOPLE_PHOTOS,
# }
# Fields with a single value
SINGLE_VALUE_FIELDS = {
PEOPLE_FILE_ASES,
}
# Fields with multiple values
MULTI_VALUE_FIELDS = {
PEOPLE_INTERESTS,
PEOPLE_LOCALES,
PEOPLE_LOCATIONS,
PEOPLE_OCCUPATIONS,
PEOPLE_SKILLS,
}
@staticmethod
def GetPersonFields(entityType, allowAddRemove, parameters=None):
person = {}
contactGroupsLists = {
PEOPLE_GROUPS_LIST: [],
PEOPLE_ADD_GROUPS_LIST: [],
PEOPLE_REMOVE_GROUPS_LIST: []
}
locations = {'primary': None}
def CheckClearPersonField(fieldName):
if checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
person.pop(fieldName, None)
person[fieldName] = []
return True
return False
def GetSingleFieldEntry(fieldName):
person.setdefault(fieldName, [])
if not person[fieldName]:
person[fieldName].append({})
return person[fieldName][0]
def InitArrayFieldEntry(choices, typeMinLen=1):
entry = {'metadata': {'primary': False}}
if choices is not None:
ftype = getChoice(choices, mapChoice=True, defaultChoice=None)
if ftype:
entry['type'] = ftype
else:
entry['type'] = getString(Cmd.OB_STRING, minLen=typeMinLen)
return entry
def GetMultiFieldEntry(fieldName):
person.setdefault(fieldName, [])
person[fieldName].append({})
return person[fieldName][-1]
def getDate(entry, fieldName):
event = getYYYYMMDD(minLen=0, returnDateTime=True)
if event:
entry[fieldName] = {'year': event.year, 'month': event.month, 'day': event.day}
def PrimaryNotPrimary(pnp, entry):
if pnp == 'notprimary':
entry['metadata']['primary'] = 'false'
return True
if pnp == 'primary':
entry['metadata']['primary'] = 'true'
locations['primary'] = Cmd.Location()
return True
return False
def GetPrimaryNotPrimaryChoice(entry):
pnp = getChoice({'primary': True, 'notprimary': False}, mapChoice=True)
entry['metadata']['primary'] = pnp
if pnp:
locations['primary'] = Cmd.Location()
def AppendArrayEntryToFields(fieldName, entry, checkBlankField=None):
person.setdefault(fieldName, [])
if checkBlankField is None or entry[checkBlankField]:
if entry.get('metadata', {}).get('primary', False):
for centry in person[fieldName][1:]:
if centry.get('metadata', {}).get('primary', False):
Cmd.SetLocation(locations['primary']-1)
usageErrorExit(Msg.MULTIPLE_ITEMS_MARKED_PRIMARY.format(fieldName))
person[fieldName].append(entry)
while Cmd.ArgumentsRemaining():
if parameters is not None:
if _getCreateContactReturnOptions(parameters):
continue
Cmd.Backup()
locations['fieldName'] = Cmd.Location()
fieldName = getChoice(PeopleManager.PEOPLE_ARGUMENT_TO_PROPERTY_MAP, mapChoice=True)
if '.' in fieldName:
fieldName, subFieldName = fieldName.split('.')
if fieldName == PEOPLE_ADDRESSES:
if CheckClearPersonField(fieldName):
continue
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_PNP_FIELDS[fieldName], typeMinLen=0)
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument in PeopleManager.ADDRESS_ARGUMENT_TO_FIELD_MAP:
subFieldName = PeopleManager.ADDRESS_ARGUMENT_TO_FIELD_MAP[argument]
value = getString(Cmd.OB_STRING, minLen=0)
if value: ### Delete?
entry[subFieldName] = value.replace('\\n', '\n')
elif PrimaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
AppendArrayEntryToFields(fieldName, entry, None)
elif fieldName == PEOPLE_BIRTHDAYS:
entry = GetSingleFieldEntry(fieldName)
getDate(entry, 'date')
elif fieldName == PEOPLE_BIOGRAPHIES:
entry = GetSingleFieldEntry(fieldName)
text, _, html = getStringWithCRsNLsOrFile()
entry['value' ] = text
entry['contentType'] = ['TEXT_PLAIN', 'TEXT_HTML'][html]
elif fieldName == PEOPLE_GENDERS:
entry = GetSingleFieldEntry(fieldName)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
elif fieldName == PEOPLE_MISC_KEYWORDS:
entry = GetMultiFieldEntry(fieldName)
if subFieldName == 'jot':
subFieldName = getChoice(PeopleManager.JOT_TYPE_MAP, mapChoice=True)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
entry['type'] = subFieldName
elif fieldName == PEOPLE_NAMES:
entry = GetSingleFieldEntry(fieldName)
entry[subFieldName] = getString(Cmd.OB_STRING, minLen=0)
elif fieldName == PEOPLE_NICKNAMES:
entry = GetMultiFieldEntry(fieldName)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
entry['type'] = subFieldName
elif fieldName == PEOPLE_ORGANIZATIONS:
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_PNP_FIELDS[fieldName])
entry['name'] = getString(Cmd.OB_STRING, minLen=0)
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument in PeopleManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP:
subFieldName = PeopleManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP[argument]
if subFieldName == 'current':
entry[subFieldName] = getBoolean()
elif subFieldName in {'startDate', 'endDate'}:
getDate(entry, subFieldName)
else:
value = getString(Cmd.OB_STRING, minLen=0)
if value: ### Delete?
entry[subFieldName] = value
elif PrimaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
AppendArrayEntryToFields(fieldName, entry, None)
elif fieldName in PeopleManager.KEY_VALUE_FIELDS:
if CheckClearPersonField(fieldName):
continue
entry = InitArrayFieldEntry(None)
entry['key'] = getString(Cmd.OB_STRING, minLen=1)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
AppendArrayEntryToFields(fieldName, entry, 'value')
elif fieldName in PeopleManager.TYPE_VALUE_FIELDS:
if CheckClearPersonField(fieldName):
continue
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_FIELDS[fieldName])
if fieldName == PEOPLE_EVENTS:
checkBlankField = 'date'
getDate(entry, checkBlankField)
elif fieldName == PEOPLE_RELATIONS:
checkBlankField = 'type'
entry['person'] = getString(Cmd.OB_STRING, minLen=0)
else:
checkBlankField = 'value'
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
AppendArrayEntryToFields(fieldName, entry, checkBlankField)
elif fieldName in PeopleManager.TYPE_VALUE_PNP_FIELDS:
if CheckClearPersonField(fieldName):
continue
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_PNP_FIELDS[fieldName],
typeMinLen=0 if fieldName in PeopleManager.EMPTY_TYPE_ALLOWED_FIELDS else 1)
if fieldName == PEOPLE_IM_CLIENTS:
checkBlankField = None
entry['protocol'] = getChoice(PeopleManager.IM_PROTOCOLS, mapChoice=True)
entry['username'] = getString(Cmd.OB_STRING, minLen=0)
elif fieldName == PEOPLE_EMAIL_ADDRESSES:
checkBlankField = 'value'
entry[checkBlankField] = getString(Cmd.OB_STRING, minLen=0)
if checkArgumentPresent(['displayname']):
entry['displayName'] = getString(Cmd.OB_STRING, minLen=0)
elif fieldName == PEOPLE_CALENDAR_URLS:
checkBlankField = 'url'
entry[checkBlankField] = getString(Cmd.OB_STRING, minLen=0)
else:
checkBlankField = 'value'
entry[checkBlankField] = getString(Cmd.OB_STRING, minLen=0)
GetPrimaryNotPrimaryChoice(entry)
AppendArrayEntryToFields(fieldName, entry, checkBlankField)
elif fieldName in PeopleManager.SINGLE_VALUE_FIELDS:
entry = GetSingleFieldEntry(fieldName)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
elif fieldName in PeopleManager.MULTI_VALUE_FIELDS:
if CheckClearPersonField(fieldName):
continue
entry = InitArrayFieldEntry(None)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
AppendArrayEntryToFields(fieldName, entry, 'value')
# elif fieldName in PeopleManager.URL_FIELDS:
# if CheckClearPersonField(fieldName):
# continue
# entry = InitArrayFieldEntry(None)
# entry['url'] = getString(Cmd.OB_STRING, minLen=0)
# entry['default'] = False
# AppendArrayEntryToFields(fieldName, entry, 'url')
elif fieldName == PEOPLE_GROUPS:
if entityType != Ent.USER:
Cmd.Backup()
unknownArgumentExit()
contactGroupsLists[PEOPLE_GROUPS_LIST].append(getString(Cmd.OB_STRING))
elif fieldName == PEOPLE_ADD_GROUPS:
if not allowAddRemove:
unknownArgumentExit()
if entityType != Ent.USER:
Cmd.Backup()
unknownArgumentExit()
contactGroupsLists[PEOPLE_ADD_GROUPS_LIST].append(getString(Cmd.OB_STRING))
elif fieldName == PEOPLE_REMOVE_GROUPS:
if not allowAddRemove:
unknownArgumentExit()
if entityType != Ent.USER:
Cmd.Backup()
unknownArgumentExit()
contactGroupsLists[PEOPLE_REMOVE_GROUPS_LIST].append(getString(Cmd.OB_STRING))
elif fieldName == PEOPLE_JSON:
jsonData = getJSON(['resourceName', 'etag', 'metadata', PEOPLE_COVER_PHOTOS, PEOPLE_PHOTOS, PEOPLE_UPDATE_TIME])
for membership in jsonData.pop('memberships', []):
contactGroupName = membership.get('contactGroupMembership', {}).get('contactGroupName', '')
if contactGroupName:
contactGroupsLists[PEOPLE_GROUPS_LIST].append(contactGroupName)
newClientData = []
for clientData in jsonData.pop('clientData', []):
if clientData['key'] not in {'ContactId', 'CtsContactHash'}:
newClientData.append({'key': clientData['key'], 'value': clientData['value']})
if newClientData:
person.setdefault(PEOPLE_CLIENT_DATA, [])
person[PEOPLE_CLIENT_DATA].extend(newClientData)
person.update(jsonData)
return (person, set(person.keys()), contactGroupsLists)
PEOPLE_GROUP_ARGUMENT_TO_PROPERTY_MAP = {
'json': PEOPLE_JSON,
'name': PEOPLE_GROUP_NAME,
'clientdata': PEOPLE_GROUP_CLIENT_DATA,
}
@staticmethod
def AddContactGroupsToContact(contactEntry, contactGroupsList):
contactEntry[PEOPLE_MEMBERSHIPS] = []
for groupId in contactGroupsList:
if groupId != 'clear':
contactEntry[PEOPLE_MEMBERSHIPS].append({'contactGroupMembership': {'contactGroupResourceName': groupId}})
else:
contactEntry[PEOPLE_MEMBERSHIPS] = []
@staticmethod
def AddFilteredContactGroupsToContact(contactEntry, contactGroupsList, contactRemoveGroupsList):
contactEntry[PEOPLE_MEMBERSHIPS] = []
for groupId in contactGroupsList:
if groupId not in contactRemoveGroupsList:
contactEntry[PEOPLE_MEMBERSHIPS].append({'contactGroupMembership': {'contactGroupResourceName': groupId}})
@staticmethod
def AddAdditionalContactGroupsToContact(contactEntry, contactGroupsList):
for groupId in contactGroupsList:
contactEntry[PEOPLE_MEMBERSHIPS].append({'contactGroupMembership': {'contactGroupResourceName': groupId}})
@staticmethod
def GetContactGroupFields(parameters=None):
contactGroup = {}
while Cmd.ArgumentsRemaining():
if parameters is not None:
if _getCreateContactReturnOptions(parameters):
continue
Cmd.Backup()
fieldName = getChoice(PeopleManager.PEOPLE_GROUP_ARGUMENT_TO_PROPERTY_MAP, mapChoice=True)
if fieldName == PEOPLE_GROUP_NAME:
contactGroup[PEOPLE_GROUP_NAME] = getString(Cmd.OB_STRING)
elif fieldName == PEOPLE_GROUP_CLIENT_DATA:
entry = {}
entry['key'] = getString(Cmd.OB_STRING, minLen=1)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
if entry['value']:
contactGroup.setdefault(fieldName, [])
contactGroup[fieldName].append(entry)
elif fieldName == PEOPLE_JSON:
jsonData = getJSON(['resourceName', 'etag', 'metadata', 'formattedName', 'memberResourceNames', 'memberCount'])
if jsonData.get('groupType', '') != 'SYSTEM_CONTACT_GROUP':
contactGroup[PEOPLE_GROUP_NAME] = jsonData['name']
return (contactGroup, ','.join(contactGroup.keys()))
PEOPLE_DIRECTORY_SOURCES_CHOICE_MAP = {
'contact': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
'contacts': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
'domaincontact': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
'comaincontacts': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
'profile': 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE',
'profiles': 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'
}
PEOPLE_READ_SOURCES_CHOICE_MAP = {
'contact': 'READ_SOURCE_TYPE_CONTACT',
'contacts': 'READ_SOURCE_TYPE_CONTACT',
'domaincontact': 'READ_SOURCE_TYPE_DOMAIN_CONTACT',
'domaincontacts': 'READ_SOURCE_TYPE_DOMAIN_CONTACT',
'profile': 'READ_SOURCE_TYPE_PROFILE',
'profiles': 'READ_SOURCE_TYPE_PROFILE'
}
PEOPLE_DIRECTORY_MERGE_SOURCES_CHOICE_MAP = {
'contact': 'DIRECTORY_MERGE_SOURCE_TYPE_CONTACT',
'contacts': 'DIRECTORY_MERGE_SOURCE_TYPE_CONTACT',
}
def _initPeopleContactQueryAttributes(printShowCmd):
return {'query': None, 'updateTime': None,
'contactGroupSelect': None, 'contactGroupFilter': None, 'group': None, 'dropMemberships': False,
'mainContacts': True, 'otherContacts': printShowCmd, 'emailMatchPattern': None, 'emailMatchType': None}
def _getPeopleContactQueryAttributes(contactQuery, myarg, entityType, unknownAction, printShowCmd):
if myarg == 'query':
contactQuery['query'] = getString(Cmd.OB_QUERY)
elif myarg in {'contactgroup', 'selectcontactgroup'}:
if entityType == Ent.USER:
contactQuery['contactGroupSelect'] = getString(Cmd.OB_CONTACT_GROUP_ITEM)
contactQuery['contactGroupFilter'] = None
contactQuery['mainContacts'] = True
contactQuery['otherContacts'] = False
else:
unknownArgumentExit()
elif myarg == 'emailmatchpattern':
contactQuery['emailMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'emailmatchtype':
contactQuery['emailMatchType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
elif myarg == 'updatedmin':
deprecatedArgument(myarg)
getYYYYMMDD()
elif myarg == 'endquery':
return False
elif not printShowCmd:
if unknownAction < 0:
unknownArgumentExit()
if unknownAction > 0:
Cmd.Backup()
return False
elif myarg == 'filtercontactgroup':
if entityType == Ent.USER:
contactQuery['contactGroupFilter'] = getString(Cmd.OB_CONTACT_GROUP_ITEM)
contactQuery['contactGroupSelect'] = None
contactQuery['mainContacts'] = True
contactQuery['otherContacts'] = False
else:
unknownArgumentExit()
elif myarg in {'maincontacts', 'selectmaincontacts'}:
if entityType == Ent.USER:
contactQuery['contactGroupSelect'] = None
contactQuery['contactGroupFilter'] = None
contactQuery['mainContacts'] = True
contactQuery['otherContacts'] = False
else:
unknownArgumentExit()
elif myarg in {'othercontacts', 'selectothercontacts'}:
if entityType == Ent.USER:
contactQuery['contactGroupSelect'] = None
contactQuery['contactGroupFilter'] = None
contactQuery['mainContacts'] = False
contactQuery['otherContacts'] = True
else:
unknownArgumentExit()
elif myarg == 'orderby':
getOrderBySortOrder(CONTACTS_ORDERBY_CHOICE_MAP, 'ascending', False)
deprecatedArgument(myarg)
elif myarg in CONTACTS_PROJECTION_CHOICE_MAP:
deprecatedArgument(myarg)
elif myarg == 'showdeleted':
deprecatedArgument(myarg)
else:
if unknownAction < 0:
unknownArgumentExit()
if unknownAction > 0:
Cmd.Backup()
return False
return True
PEOPLE_CONTACT_SELECT_ARGUMENTS = {
'query', 'contactgroup', 'selectcontactgroup',
'maincontacts', 'selectmaincontacts',
'othercontacts', 'selectothercontacts',
'emailmatchpattern', 'emailmatchtype',
}
PEOPLE_CONTACT_DEPRECATED_SELECT_ARGUMENTS = {
'orderby', 'basic', 'thin', 'full', 'showdeleted',
}
def _getPeopleContactEntityList(entityType, unknownAction, noEntityArguments=None):
contactQuery = _initPeopleContactQueryAttributes(False)
if noEntityArguments is not None and (not Cmd.ArgumentsRemaining() or Cmd.PeekArgumentPresent(noEntityArguments)):
# <PeopleResourceNameEntity>|<PeopleUserContactSelection> are optional in dedup|replacedomain contacts
entityList = None
queriedContacts = True
elif Cmd.PeekArgumentPresent(PEOPLE_CONTACT_SELECT_ARGUMENTS.union(PEOPLE_CONTACT_DEPRECATED_SELECT_ARGUMENTS)):
entityList = None
queriedContacts = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if not _getPeopleContactQueryAttributes(contactQuery, myarg, entityType, unknownAction, False):
break
else:
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
queriedContacts = False
if unknownAction < 0:
checkForExtraneousArguments()
return (entityList, entityList if isinstance(entityList, dict) else None, contactQuery, queriedContacts)
def _initPeopleOtherContactQueryAttributes():
return {'query': None,
'otherContacts': True, 'emailMatchPattern': None, 'emailMatchType': None}
def _getPeopleOtherContactQueryAttributes(contactQuery, myarg, unknownAction):
if myarg == 'query':
contactQuery['query'] = getString(Cmd.OB_QUERY)
elif myarg == 'emailmatchpattern':
contactQuery['emailMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'emailmatchtype':
contactQuery['emailMatchType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
elif myarg == 'endquery':
return False
else:
if unknownAction < 0:
unknownArgumentExit()
if unknownAction > 0:
Cmd.Backup()
return False
return True
PEOPLE_OTHERCONTACT_SELECT_ARGUMENTS = {'query', 'emailmatchpattern', 'emailmatchtype'}
def _getPeopleOtherContactEntityList(unknownAction):
contactQuery = _initPeopleOtherContactQueryAttributes()
if Cmd.PeekArgumentPresent(PEOPLE_OTHERCONTACT_SELECT_ARGUMENTS):
entityList = None
queriedContacts = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if not _getPeopleOtherContactQueryAttributes(contactQuery, myarg, unknownAction):
break
else:
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
queriedContacts = False
if unknownAction < 0:
checkForExtraneousArguments()
return (entityList, entityList if isinstance(entityList, dict) else None, contactQuery, queriedContacts)
def _getPeopleOtherContacts(people, entityType, user, i=0, count=0):
try:
printGettingAllEntityItemsForWhom(Ent.OTHER_CONTACT, user, i, count)
results = callGAPIpages(people.otherContacts(), 'list', 'otherContacts',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageSize=1000,
readMask='emailAddresses', fields='nextPageToken,otherContacts(etag,resourceName,emailAddresses(value,type))')
otherContacts = {}
for contact in results:
resourceName = contact.pop('resourceName')
otherContacts[resourceName] = contact
return otherContacts
except GAPI.permissionDenied as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
entityUnknownWarning(entityType, user, i, count)
return None
def queryPeopleContacts(people, contactQuery, fields, sortOrder, entityType, user, i=0, count=0):
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['domaincontact' if entityType == Ent.DOMAIN else 'contact']]
printGettingAllEntityItemsForWhom(Ent.PEOPLE_CONTACT, user, i, count, query=contactQuery['query'])
pageMessage = getPageMessageForWhom()
try:
# Contact group not selected
if not contactQuery['contactGroupSelect']:
if not contactQuery['query']:
results = callGAPIpages(people.people().connections(), 'list', 'connections',
pageMessage=pageMessage,
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
resourceName='people/me', sources=sources, personFields=fields,
sortOrder=sortOrder, fields='nextPageToken,connections')
if contactQuery['contactGroupFilter']:
entityList = []
for person in results:
for membership in person.get(PEOPLE_MEMBERSHIPS, []):
if membership.get('contactGroupMembership', {}).get('contactGroupResourceName', '') == contactQuery['group']:
if contactQuery['dropMemberships']:
person.pop(PEOPLE_MEMBERSHIPS)
entityList.append(person)
break
else:
entityList = results
else:
results = callGAPI(people.people(), 'searchContacts',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
sources=sources, readMask=fields, query=contactQuery['query'])
entityList = [person['person'] for person in results.get('results', [])]
totalItems = len(entityList)
# Contact group selected
else:
totalItems = callGAPI(people.contactGroups(), 'get',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=contactQuery['group'], groupFields='memberCount').get('memberCount', 0)
entityList = []
if totalItems > 0:
results = callGAPI(people.contactGroups(), 'get',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=contactQuery['group'], maxMembers=totalItems, groupFields='name')
for resourceName in results.get('memberResourceNames', []):
result = callGAPI(people.people(), 'get',
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName, sources=sources, personFields=fields)
entityList.append(result)
if pageMessage and (contactQuery['contactGroupSelect'] or contactQuery['contactGroupFilter'] or contactQuery['query']):
showMessage = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
writeGotMessage(showMessage.replace('{0}', str(Ent.Choose(Ent.PEOPLE_CONTACT, totalItems))))
return entityList
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
entityUnknownWarning(entityType, user, i, count)
return None
def queryPeopleOtherContacts(people, contactQuery, fields, entityType, user, i=0, count=0):
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
printGettingAllEntityItemsForWhom(Ent.OTHER_CONTACT, user, i, count, query=contactQuery['query'])
pageMessage = getPageMessageForWhom()
try:
if not contactQuery['query']:
entityList = callGAPIpages(people.otherContacts(), 'list', 'otherContacts',
pageMessage=pageMessage,
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
readMask=fields, fields='nextPageToken,otherContacts', sources=sources)
else:
results = callGAPI(people.otherContacts(), 'search',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
pageSize=30, readMask=fields, query=contactQuery['query'])
entityList = [person['person'] for person in results.get('results', [])]
totalItems = len(entityList)
if pageMessage:
showMessage = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
writeGotMessage(showMessage.replace('{0}', str(Ent.Choose(Ent.OTHER_CONTACT, totalItems))))
return entityList
except GAPI.permissionDenied as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
entityUnknownWarning(entityType, user, i, count)
return None
def getPeopleContactGroupsInfo(people, entityType, entityName, i, count):
contactGroupIDs = {}
contactGroupNames = {}
try:
groups = callGAPIpages(people.contactGroups(), 'list', 'contactGroups',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
groupFields='name', fields='nextPageToken,contactGroups(resourceName,name,formattedName)')
if groups:
for group in groups:
contactGroupIDs[group['resourceName']] = group['formattedName']
contactGroupNames.setdefault(group['formattedName'], [])
contactGroupNames[group['formattedName']].append(group['resourceName'])
if group['formattedName'] != group['name']:
contactGroupNames.setdefault(group['name'], [])
contactGroupNames[group['name']].append(group['resourceName'])
except GAPI.permissionDenied as e:
ClientAPIAccessDeniedExit(str(e))
except GAPI.forbidden:
userPeopleServiceNotEnabledWarning(entityName, i, count)
return (contactGroupIDs, False)
except GAPI.serviceNotAvailable:
entityUnknownWarning(entityType, entityName, i, count)
return (contactGroupIDs, False)
return (contactGroupIDs, contactGroupNames)
def validatePeopleContactGroup(people, contactGroupName,
contactGroupIDs, contactGroupNames, entityType, entityName, i, count):
if not contactGroupNames:
contactGroupIDs, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, entityName, i, count)
if contactGroupNames is False:
return (None, contactGroupIDs, contactGroupNames)
if contactGroupName == 'clear':
return (contactGroupName, contactGroupIDs, contactGroupNames)
cg = UID_PATTERN.match(contactGroupName)
if cg:
contactGroupName = cg.group(1)
if contactGroupName in contactGroupIDs:
return (contactGroupName, contactGroupIDs, contactGroupNames)
normalizedContactGroupName = normalizeContactGroupResourceName(contactGroupName)
if normalizedContactGroupName in contactGroupIDs:
return (normalizedContactGroupName, contactGroupIDs, contactGroupNames)
else:
if contactGroupName in contactGroupIDs:
return (contactGroupName, contactGroupIDs, contactGroupNames)
if contactGroupName in contactGroupNames:
return (contactGroupNames[contactGroupName][0], contactGroupIDs, contactGroupNames)
normalizedContactGroupName = normalizeContactGroupResourceName(contactGroupName)
if normalizedContactGroupName != contactGroupName and normalizedContactGroupName in contactGroupIDs:
return (normalizedContactGroupName, contactGroupIDs, contactGroupNames)
return (None, contactGroupIDs, contactGroupNames)
def validatePeopleContactGroupsList(people, contactId,
contactGroupsList, entityType, entityName, i, count):
result = True
contactGroupIDs = contactGroupNames = None
validatedContactGroupsList = []
for contactGroup in contactGroupsList:
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
contactGroupIDs, contactGroupNames, entityType, entityName, i, count)
if groupId:
validatedContactGroupsList.append(groupId)
else:
if contactGroupNames:
entityActionNotPerformedWarning([entityType, entityName, Ent.CONTACT, contactId],
Ent.TypeNameMessage(Ent.CONTACT_GROUP, contactGroup, Msg.DOES_NOT_EXIST))
result = False
return (result, validatedContactGroupsList, contactGroupIDs)
# gam <UserTypeEntity> create contact <PeopleContactAttribute>+
# (contactgroup <ContactGroupItem>)*
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*))| returnidonly]
def createUserPeopleContact(users):
entityType = Ent.USER
peopleManager = PeopleManager()
peopleEntityType = Ent.CONTACT
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
parameters = {'csvPF': None, 'titles': ['User', 'resourceName'], 'addCSVData': {}, 'returnIdOnly': False}
body, personFields, contactGroupsLists = peopleManager.GetPersonFields(entityType, False, parameters)
csvPF = parameters['csvPF']
addCSVData = parameters['addCSVData']
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
returnIdOnly = parameters['returnIdOnly']
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if contactGroupsLists[PEOPLE_GROUPS_LIST]:
result, validatedContactGroupsList, _ = validatePeopleContactGroupsList(people, '',
contactGroupsLists[PEOPLE_GROUPS_LIST], entityType, user, i, count)
if not result:
continue
peopleManager.AddContactGroupsToContact(body, validatedContactGroupsList)
personFields.add(PEOPLE_MEMBERSHIPS)
else:
personFields.discard(PEOPLE_MEMBERSHIPS)
try:
result = callGAPI(people.people(), 'createContact',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS+[GAPI.INVALID_ARGUMENT],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
personFields=','.join(personFields), body=body, sources=sources)
resourceName = result['resourceName']
if returnIdOnly:
writeStdout(f'{resourceName}\n')
elif not csvPF:
entityActionPerformed([entityType, user, peopleEntityType, resourceName], i, count)
else:
row = {'User': user, 'resourceName': resourceName}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
except GAPI.invalidArgument as e:
entityActionFailedWarning([entityType, user, peopleEntityType, None], str(e), i, count)
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
if csvPF:
csvPF.writeCSVfile('People Contacts')
def localPeopleContactSelects(contactQuery, contact):
if contactQuery['emailMatchPattern']:
emailMatchType = contactQuery['emailMatchType']
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
if contactQuery['emailMatchPattern'].match(item['value']):
if (not emailMatchType or emailMatchType == item.get('type', '')):
break
else:
return False
return True
def countLocalPeopleContactSelects(contactQuery, contacts):
if contacts is not None and contactQuery['emailMatchPattern']:
jcount = 0
for contact in contacts:
if localPeopleContactSelects(contactQuery, contact):
jcount += 1
else:
jcount = len(contacts) if contacts is not None else 0
return jcount
def clearPeopleEmailAddressMatches(contactClear, contact):
savedAddresses = []
updateRequired = False
emailMatchType = contactClear['emailClearType']
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
if (contactClear['emailClearPattern'].match(item['value']) and
(not emailMatchType or emailMatchType == item.get('type', ''))):
updateRequired = True
else:
savedAddresses.append(item)
if updateRequired:
contact[PEOPLE_EMAIL_ADDRESSES] = savedAddresses
return updateRequired
def _clearUpdatePeopleContacts(users, updateContacts):
action = Act.Get()
entityType = Ent.USER
peopleManager = PeopleManager()
peopleEntityType = Ent.PEOPLE_CONTACT
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, 1)
if updateContacts:
body, updatePersonFields, contactGroupsLists = peopleManager.GetPersonFields(entityType, True)
else:
contactClear = {'emailClearPattern': contactQuery['emailMatchPattern'], 'emailClearType': contactQuery['emailMatchType']}
deleteClearedContactsWithNoEmails = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'emailclearpattern':
contactClear['emailClearPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'emailcleartype':
contactClear['emailClearType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
elif myarg == 'deleteclearedcontactswithnoemails':
deleteClearedContactsWithNoEmails = True
else:
unknownArgumentExit()
if not contactClear['emailClearPattern']:
missingArgumentExit('emailclearpattern')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if contactQuery['contactGroupSelect']:
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
None, None, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
continue
contactQuery['group'] = groupId
if queriedContacts:
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses,memberships', None, entityType, user, i, count)
if entityList is None:
continue
Act.Set(action)
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
validatedContactGroupsLists = {
PEOPLE_GROUPS_LIST: [],
PEOPLE_ADD_GROUPS_LIST: [],
PEOPLE_REMOVE_GROUPS_LIST: []
}
Ind.Increment()
for contact in entityList:
j += 1
try:
if not queriedContacts:
resourceName = contact
contact = callGAPI(people.people(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=contact, sources=sources, personFields='emailAddresses,memberships')
else:
if not localPeopleContactSelects(contactQuery, contact):
continue
resourceName = contact['resourceName']
if updateContacts:
body['etag'] = contact['etag']
existingContactGroupsList = []
for contactGroup in contact.get(PEOPLE_MEMBERSHIPS, []):
if 'contactGroupMembership' in contactGroup:
existingContactGroupsList.append(contactGroup['contactGroupMembership']['contactGroupResourceName'])
groupError = False
for field in [PEOPLE_GROUPS_LIST, PEOPLE_ADD_GROUPS_LIST, PEOPLE_REMOVE_GROUPS_LIST]:
if contactGroupsLists[field] and not validatedContactGroupsLists[field]:
status, validatedContactGroupsLists[field], _ = validatePeopleContactGroupsList(people, resourceName,
contactGroupsLists[field], entityType, user, i, count)
if not status:
groupError = True
if groupError:
break
if validatedContactGroupsLists[PEOPLE_GROUPS_LIST]:
peopleManager.AddContactGroupsToContact(body, validatedContactGroupsLists[PEOPLE_GROUPS_LIST])
updatePersonFields.add(PEOPLE_MEMBERSHIPS)
elif validatedContactGroupsLists[PEOPLE_ADD_GROUPS_LIST] or validatedContactGroupsLists[PEOPLE_REMOVE_GROUPS_LIST]:
body[PEOPLE_MEMBERSHIPS] = []
if contact.get(PEOPLE_MEMBERSHIPS):
peopleManager.AddFilteredContactGroupsToContact(body, existingContactGroupsList,
validatedContactGroupsLists[PEOPLE_REMOVE_GROUPS_LIST])
if validatedContactGroupsLists[PEOPLE_ADD_GROUPS_LIST]:
peopleManager.AddAdditionalContactGroupsToContact(body, validatedContactGroupsLists[PEOPLE_ADD_GROUPS_LIST])
updatePersonFields.add(PEOPLE_MEMBERSHIPS)
elif existingContactGroupsList:
updatePersonFields.discard(PEOPLE_MEMBERSHIPS)
else:
if not clearPeopleEmailAddressMatches(contactClear, contact):
continue
if deleteClearedContactsWithNoEmails and not contact[PEOPLE_EMAIL_ADDRESSES]:
Act.Set(Act.DELETE)
callGAPI(people.people(), 'deleteContact',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName)
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
continue
body = contact
updatePersonFields = [PEOPLE_EMAIL_ADDRESSES]
person = callGAPI(people.people(), 'updateContact',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName,
updatePersonFields=','.join(updatePersonFields), body=body, sources=sources)
entityActionPerformed([entityType, user, peopleEntityType, person['resourceName']], j, jcount)
except GAPI.invalidArgument as e:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
Ind.Decrement()
# gam <UserTypeEntity> clear contacts <PeopleResourceNameEntity>|<PeopleUserContactSelection>
# [emailclearpattern <REMatchPattern>] [emailcleartype work|home|other|<String>]
# [deleteclearedcontactswithnoemails]
def clearUserPeopleContacts(users):
_clearUpdatePeopleContacts(users, False)
# gam <UserTypeEntity> update contacts <PeopleResourceNameEntity>|(<PeopleUserContactSelection> endquery)
# <PeopleContactAttribute>+
# (contactgroup <ContactGroupItem>)*|((addcontactgroup <ContactGroupItem>)* (removecontactgroup <ContactGroupItem>)*)
# gam <UserTypeEntity> update contacts
def updateUserPeopleContacts(users):
_clearUpdatePeopleContacts(users, True)
def dedupPeopleEmailAddressMatches(emailMatchType, contact):
sai = -1
savedAddresses = []
matches = {}
updateRequired = False
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
emailAddr = item['value']
emailType = item.get('type', '')
if (emailAddr in matches) and (not emailMatchType or emailType in matches[emailAddr]['types']):
if item['metadata'].get('primary', False):
savedAddresses[matches[emailAddr]['sai']]['metadata']['primary'] = True
updateRequired = True
else:
savedAddresses.append(item)
sai += 1
matches.setdefault(emailAddr, {'types': set(), 'sai': sai})
matches[emailAddr]['types'].add(emailType)
if updateRequired:
contact[PEOPLE_EMAIL_ADDRESSES] = savedAddresses
return updateRequired
def replaceDomainPeopleEmailAddressMatches(contactQuery, contact, replaceDomains):
updateRequired = False
if contactQuery['emailMatchPattern']:
emailMatchType = contactQuery['emailMatchType']
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
emailAddr = item['value']
emailType = item.get('type', '')
if contactQuery['emailMatchPattern'].match(emailAddr):
userName, domain = splitEmailAddress(emailAddr)
domain = domain.lower()
if ((domain in replaceDomains) and
(not emailMatchType or emailType == emailMatchType)):
item['value'] = f'{userName}@{replaceDomains[domain]}'
updateRequired = True
else:
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
emailAddr = item['value']
emailType = item.get('type', '')
userName, domain = splitEmailAddress(emailAddr)
domain = domain.lower()
if domain in replaceDomains:
item['value'] = f'{userName}@{replaceDomains[domain]}'
updateRequired = True
return updateRequired
# gam <UserTypeEntity> dedup contacts
# [<PeopleResourceNameEntity>|<PeopleUserContactSelection>]
# [matchType [<Boolean>]]
# gam <UserTypeEntity> replacedomain contacts
# [<PeopleResourceNameEntity>|<PeopleUserContactSelection>]
# (domain <DomainName> <DomainName>)+
def dedupReplaceDomainUserPeopleContacts(users):
action = Act.Get()
entityType = Ent.USER
peopleEntityType = Ent.PEOPLE_CONTACT
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, 1, {'matchtype', 'domain'})
emailMatchType = False
replaceDomains = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if action == Act.DEDUP and myarg == 'matchtype':
emailMatchType = getBoolean()
elif action == Act.REPLACE_DOMAIN and myarg == 'domain':
domain = getString(Cmd.OB_DOMAIN_NAME).lower()
replaceDomains[domain] = getString(Cmd.OB_DOMAIN_NAME).lower()
else:
unknownArgumentExit()
if action == Act.REPLACE_DOMAIN and not replaceDomains:
missingArgumentExit('domain')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if contactQuery['contactGroupSelect']:
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
None, None, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
continue
contactQuery['group'] = groupId
if queriedContacts:
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses,memberships', None, entityType, user, i, count)
if entityList is None:
continue
Act.Set(action)
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Act.Set(Act.UPDATE)
Ind.Increment()
for contact in entityList:
j += 1
try:
if not queriedContacts:
resourceName = contact
contact = callGAPI(people.people(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=contact, sources=sources, personFields='emailAddresses,memberships')
else:
if action == Act.DEDUP and not localPeopleContactSelects(contactQuery, contact):
continue
resourceName = contact['resourceName']
if action == Act.DEDUP:
if not dedupPeopleEmailAddressMatches(emailMatchType, contact):
continue
else:
if not replaceDomainPeopleEmailAddressMatches(contactQuery, contact, replaceDomains):
continue
Act.Set(Act.UPDATE)
callGAPI(people.people(), 'updateContact',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName,
updatePersonFields='emailAddresses', body=contact)
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
except GAPI.invalidArgument as e:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
Ind.Decrement()
# gam <UserTypeEntity> delete contacts <PeopleResourceNameEntity>|<PeopleUserContactSelection>
def deleteUserPeopleContacts(users):
entityType = Ent.USER
peopleEntityType = Ent.PEOPLE_CONTACT
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, -1)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if contactQuery['contactGroupSelect']:
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
None, None, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
continue
contactQuery['group'] = groupId
if queriedContacts:
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses', None, entityType, user, i, count)
if entityList is None:
continue
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contact in entityList:
j += 1
if isinstance(contact, dict):
if not localPeopleContactSelects(contactQuery, contact):
continue
resourceName = contact['resourceName']
else:
resourceName = normalizePeopleResourceName(contact)
try:
callGAPI(people.people(), 'deleteContact',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName)
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
Ind.Decrement()
def _initPersonMetadataParameters():
return {'strip': True, 'mapUpdateTime': False, 'sourceTypes': set()}
def _processPersonMetadata(person, parameters):
metadata = person.get(PEOPLE_METADATA, None)
if metadata is not None:
if parameters['mapUpdateTime']:
sources = person[PEOPLE_METADATA].get('sources', [])
if sources and sources[0].get(PEOPLE_UPDATE_TIME, None) is not None:
person[PEOPLE_UPDATE_TIME] = formatLocalTime(sources[0][PEOPLE_UPDATE_TIME])
if parameters['sourceTypes']:
stripKeys = []
for k, v in iter(person.items()):
if isinstance(v, list):
person[k] = []
for entry in v:
if isinstance(entry, dict):
if entry.get('metadata', {}).get('source', {}).get('type', None) in parameters['sourceTypes']:
person[k].append(entry)
else:
person[k].append(entry)
if not person[k]:
stripKeys.append(k)
for k in stripKeys:
person.pop(k, None)
if parameters['strip']:
person.pop(PEOPLE_METADATA, None)
for _, v in iter(person.items()):
if isinstance(v, list):
for entry in v:
if isinstance(entry, dict):
entry.pop(PEOPLE_METADATA, None)
def addContactGroupNamesToContacts(contacts, contactGroupIDs, showContactGroupNamesList):
for contact in contacts:
if showContactGroupNamesList:
contact[PEOPLE_GROUPS_LIST] = []
for membership in contact.get('memberships', []):
if 'contactGroupMembership' in membership:
membership['contactGroupMembership']['contactGroupName'] = contactGroupIDs.get(membership['contactGroupMembership']['contactGroupResourceName'], UNKNOWN)
if showContactGroupNamesList:
contact[PEOPLE_GROUPS_LIST].append(membership['contactGroupMembership']['contactGroupName'])
def _printPerson(entityTypeName, user, person, csvPF, FJQC, parameters):
_processPersonMetadata(person, parameters)
row = flattenJSON(person, flattened={entityTypeName: user})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({entityTypeName: user, 'resourceName': person['resourceName'],
'JSON': json.dumps(cleanJSON(person),
ensure_ascii=False, sort_keys=True)})
PEOPLE_CONTACT_OBJECT_KEYS = {
'addresses': 'type',
'calendarUrls': 'type',
'emailAddresses': 'type',
'events': 'type',
'externalIds': 'type',
'genders': 'value',
'imClients': 'type',
'locations': 'type',
'miscKeywords': 'type',
'nicknames': 'type',
'organizations': 'type',
'relations': 'type',
'urls': 'type',
'userDefined': 'key',
}
def _showPerson(userEntityType, user, entityType, person, i, count, FJQC, parameters):
_processPersonMetadata(person, parameters)
if not FJQC.formatJSON:
printEntity([userEntityType, user, entityType, person['resourceName']], i, count)
Ind.Increment()
showJSON(None, person, dictObjectsKey=PEOPLE_CONTACT_OBJECT_KEYS)
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(person), ensure_ascii=False, sort_keys=True))
def _printPersonEntityList(entityType, entityList, userEntityType, user, i, count, csvPF, FJQC, parameters, contactQuery):
if not csvPF:
jcount = len(entityList)
if not FJQC.formatJSON:
entityPerformActionModifierNumItems([userEntityType, user], Msg.MAXIMUM_OF, jcount, entityType, i, count)
Ind.Increment()
j = 0
for person in entityList:
j += 1
if not contactQuery or localPeopleContactSelects(contactQuery, person):
_showPerson(userEntityType, user, entityType, person, j, jcount, FJQC, parameters)
Ind.Decrement()
else:
entityTypeName = Ent.Singular(userEntityType)
for person in entityList:
if not contactQuery or localPeopleContactSelects(contactQuery, person):
_printPerson(entityTypeName, user, person, csvPF, FJQC, parameters)
PEOPLE_FIELDS_CHOICE_MAP = {
'additionalname': PEOPLE_NAMES,
'address': PEOPLE_ADDRESSES,
'addresses': PEOPLE_ADDRESSES,
'ageranges': 'ageRanges',
'billinginfo': PEOPLE_MISC_KEYWORDS,
'biography': PEOPLE_BIOGRAPHIES,
'biographies': PEOPLE_BIOGRAPHIES,
'birthday': PEOPLE_BIRTHDAYS,
'birthdays': PEOPLE_BIRTHDAYS,
'calendar': PEOPLE_CALENDAR_URLS,
'calendars': PEOPLE_CALENDAR_URLS,
'calendarurls': PEOPLE_CALENDAR_URLS,
'clientdata': PEOPLE_CLIENT_DATA,
'coverphotos': PEOPLE_COVER_PHOTOS,
'directoryserver': PEOPLE_MISC_KEYWORDS,
'email': PEOPLE_EMAIL_ADDRESSES,
'emails': PEOPLE_EMAIL_ADDRESSES,
'emailaddresses': PEOPLE_EMAIL_ADDRESSES,
'event': PEOPLE_EVENTS,
'events': PEOPLE_EVENTS,
'externalid': PEOPLE_EXTERNAL_IDS,
'externalids': PEOPLE_EXTERNAL_IDS,
'familyname': PEOPLE_NAMES,
'fileas': PEOPLE_FILE_ASES,
'firstname': PEOPLE_NAMES,
'gender': PEOPLE_GENDERS,
'genders': PEOPLE_GENDERS,
'givenname': PEOPLE_NAMES,
'hobby': PEOPLE_INTERESTS,
'hobbies': PEOPLE_INTERESTS,
'im': PEOPLE_IM_CLIENTS,
'ims': PEOPLE_IM_CLIENTS,
'imclients': PEOPLE_IM_CLIENTS,
'initials': PEOPLE_NICKNAMES,
'interests': PEOPLE_INTERESTS,
'jot': PEOPLE_MISC_KEYWORDS,
'jots': PEOPLE_MISC_KEYWORDS,
'language': PEOPLE_LOCALES,
'languages': PEOPLE_LOCALES,
'lastname': PEOPLE_NAMES,
'locales': PEOPLE_LOCALES,
'location': PEOPLE_LOCATIONS,
'locations': PEOPLE_LOCATIONS,
'maidenname': PEOPLE_NAMES,
'memberships': PEOPLE_MEMBERSHIPS,
'metadata': PEOPLE_METADATA,
'middlename': PEOPLE_NAMES,
'mileage': PEOPLE_MISC_KEYWORDS,
'misckeywords': PEOPLE_MISC_KEYWORDS,
'name': PEOPLE_NAMES,
'names': PEOPLE_NAMES,
'nickname': PEOPLE_NICKNAMES,
'nicknames': PEOPLE_NICKNAMES,
'note': PEOPLE_BIOGRAPHIES,
'notes': PEOPLE_BIOGRAPHIES,
'occupation': PEOPLE_OCCUPATIONS,
'occupations': PEOPLE_OCCUPATIONS,
'organization': PEOPLE_ORGANIZATIONS,
'organizations': PEOPLE_ORGANIZATIONS,
'organisation': PEOPLE_ORGANIZATIONS,
'organisations': PEOPLE_ORGANIZATIONS,
'phone': PEOPLE_PHONE_NUMBERS,
'phones': PEOPLE_PHONE_NUMBERS,
'phonenumbers': PEOPLE_PHONE_NUMBERS,
'photo': PEOPLE_PHOTOS,
'photos': PEOPLE_PHOTOS,
'prefix': PEOPLE_NAMES,
'priority': PEOPLE_MISC_KEYWORDS,
'relation': PEOPLE_RELATIONS,
'relations': PEOPLE_RELATIONS,
'sensitivity': PEOPLE_MISC_KEYWORDS,
'shortname': PEOPLE_NICKNAMES,
'sipaddress': PEOPLE_SIP_ADDRESSES,
'sipaddresses': PEOPLE_SIP_ADDRESSES,
'skills': PEOPLE_SKILLS,
'subject': PEOPLE_MISC_KEYWORDS,
'suffix': PEOPLE_NAMES,
'updated': PEOPLE_UPDATE_TIME,
'updatetime': PEOPLE_UPDATE_TIME,
'urls': PEOPLE_URLS,
'userdefined': PEOPLE_USER_DEFINED,
'userdefinedfield': PEOPLE_USER_DEFINED,
'userdefinedfields': PEOPLE_USER_DEFINED,
'website': PEOPLE_URLS,
'websites': PEOPLE_URLS,
}
PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP = {
'email': PEOPLE_EMAIL_ADDRESSES,
'emails': PEOPLE_EMAIL_ADDRESSES,
'emailaddresses': PEOPLE_EMAIL_ADDRESSES,
'metadata': PEOPLE_METADATA,
'names': PEOPLE_NAMES,
'phone': PEOPLE_PHONE_NUMBERS,
'phones': PEOPLE_PHONE_NUMBERS,
'phonenumbers': PEOPLE_PHONE_NUMBERS,
'photo': PEOPLE_PHOTOS,
'photos': PEOPLE_PHOTOS,
}
PEOPLE_CONTACTS_DEFAULT_FIELDS = ['names', 'emailaddresses', 'phonenumbers']
PEOPLE_ORDERBY_CHOICE_MAP = {
'firstname': 'FIRST_NAME_ASCENDING',
'lastname': 'LAST_NAME_ASCENDING',
'lastmodified': 'LAST_MODIFIED_',
}
def getPersonFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField=None, fieldsArg='fields'):
if fieldsList is None:
fieldsList = []
return getFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField, fieldsArg)
def _getPersonFields(fieldsChoiceMap, defaultFields, fieldsList, parameters):
if fieldsList is None:
fieldsList = []
for field in fieldsChoiceMap:
addFieldToFieldsList(field, fieldsChoiceMap, fieldsList)
elif not fieldsList:
for field in defaultFields:
addFieldToFieldsList(field, fieldsChoiceMap, fieldsList)
fieldsList = list(set(fieldsList))
if PEOPLE_UPDATE_TIME in fieldsList:
parameters['mapUpdateTime'] = True
fieldsList.remove(PEOPLE_UPDATE_TIME)
fieldsList.append(PEOPLE_METADATA)
return ','.join(fieldsList)
def _infoPeople(users, entityType, source):
if entityType == Ent.DOMAIN:
people = buildGAPIObject(API.PEOPLE)
peopleEntityType = Ent.DOMAIN_PROFILE if source == 'profile' else Ent.PEOPLE_CONTACT
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP[source]]
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
resourceNameLists = entityList if isinstance(entityList, dict) else None
showContactGroups = False
FJQC = FormatJSONQuoteChar()
fieldsList = []
parameters = _initPersonMetadataParameters()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'showgroups':
showContactGroups = True
elif myarg == 'showmetadata':
parameters['strip'] = False
else:
FJQC.GetFormatJSON(myarg)
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
contactGroupIDs = contactGroupNames = None
if entityType != Ent.DOMAIN:
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if showContactGroups:
contactGroupIDs, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, user, i, count)
if contactGroupNames is False:
continue
j = 0
jcount = len(entityList)
if not FJQC.formatJSON:
entityPerformActionNumItems([entityType, user], jcount, peopleEntityType, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contact in entityList:
j += 1
if isinstance(contact, dict):
resourceName = contact['resourceName']
else:
resourceName = normalizePeopleResourceName(contact)
try:
result = callGAPI(people.people(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR, GAPI.INVALID_ARGUMENT]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName, sources=sources, personFields=fields)
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
continue
except GAPI.invalidArgument as e:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
continue
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
if showContactGroups and contactGroupIDs:
addContactGroupNamesToContacts([result], contactGroupIDs, False)
_showPerson(entityType, user, peopleEntityType, result, j, jcount, FJQC, parameters)
Ind.Decrement()
# gam <UserTypeEntity> info contacts <PeopleResourceNameEntity>
# [showgroups]
# [allFields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson]
def infoUserPeopleContacts(users):
_infoPeople(users, Ent.USER, 'contact')
# gam <UserTypeEntity> print contacts [todrive <ToDriveAttribute>*] <PeoplePrintShowUserContactSelection>
# [showgroups|showgroupnameslist] [orderby firstname|lastname|(lastmodified ascending)|(lastnodified descending)
# [countsonly|allfields|fields <PeopleFieldNameList>] [showmetadata]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show contacts <PeoplePrintShowUserContactSelection>
# [showgroups] [orderby firstname|lastname|(lastmodified ascending)|(lastnodified descending)
# [countsonly|allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson]
def printShowUserPeopleContacts(users):
entityType = Ent.USER
entityTypeName = Ent.Singular(entityType)
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
CSVTitle = 'People Contacts'
fieldsList = []
parameters = _initPersonMetadataParameters()
sortOrder = None
countsOnly = showContactGroups = showContactGroupNamesList = False
contactQuery = _initPeopleContactQueryAttributes(True)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showgroups':
showContactGroups = True
elif myarg == 'showgroupnameslist':
showContactGroups = showContactGroupNamesList = True
elif myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'countsonly':
countsOnly = True
if csvPF:
csvPF.SetTitles([entityTypeName, CSVTitle])
elif myarg == 'showmetadata':
parameters['strip'] = False
elif myarg == 'orderby':
sortOrder = getChoice(PEOPLE_ORDERBY_CHOICE_MAP, mapChoice=True)
if sortOrder == 'LAST_MODIFIED_':
sortOrder += getChoice(SORTORDER_CHOICE_MAP, defaultChoice='DESCENDING', mapChoice=True)
elif _getPeopleContactQueryAttributes(contactQuery, myarg, entityType, 0, True):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if countsOnly:
fieldsList = ['emailAddresses']
if contactQuery['mainContacts']:
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
if contactQuery['contactGroupFilter'] and 'memberships' not in fields:
fields += ',memberships'
contactQuery['dropMemberships'] = True
if contactQuery['otherContacts']:
if not fieldsList:
ofields = _getPersonFields(PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
else:
ofields = getFieldsFromFieldsList([PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP[field.lower()] for field in fieldsList if field.lower() in PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if contactQuery['otherContacts']:
_, opeople = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
if not opeople:
continue
contactGroupIDs = contactGroupNames = None
if showContactGroups:
contactGroupIDs, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, user, i, count)
if contactGroupNames is False:
continue
contactGroupSelectFilter = contactQuery['contactGroupSelect'] or contactQuery['contactGroupFilter']
if contactGroupSelectFilter:
groupId, _, contactGroupNames =\
validatePeopleContactGroup(people, contactGroupSelectFilter,
contactGroupIDs, contactGroupNames, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroupSelectFilter], Msg.DOES_NOT_EXIST, i, count)
continue
contactQuery['group'] = groupId
if contactQuery['mainContacts']:
contacts = queryPeopleContacts(people, contactQuery, fields, sortOrder, entityType, user, i, count)
else:
contacts = []
if contactQuery['otherContacts']:
ocontacts = queryPeopleOtherContacts(opeople, contactQuery, ofields, entityType, user, i, count)
else:
ocontacts = []
if countsOnly:
jcount = countLocalPeopleContactSelects(contactQuery, contacts)+countLocalPeopleContactSelects(contactQuery, ocontacts)
if csvPF:
csvPF.WriteRowTitles({entityTypeName: user, CSVTitle: jcount})
else:
printEntityKVList([entityType, user], [CSVTitle, jcount], i, count)
elif contacts is not None or ocontacts is not None:
if not csvPF:
if contacts is not None and contactQuery['mainContacts']:
if showContactGroups and contactGroupIDs:
addContactGroupNamesToContacts(contacts, contactGroupIDs, False)
_printPersonEntityList(Ent.PEOPLE_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
if ocontacts is not None and contactQuery['otherContacts']:
_printPersonEntityList(Ent.OTHER_CONTACT, ocontacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
elif contacts or ocontacts:
if contacts:
if showContactGroups and contactGroupIDs:
addContactGroupNamesToContacts(contacts, contactGroupIDs, showContactGroupNamesList and FJQC.formatJSON)
_printPersonEntityList(Ent.PEOPLE_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
if ocontacts:
_printPersonEntityList(Ent.OTHER_CONTACT, ocontacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({Ent.Singular(entityType): user})
if csvPF:
csvPF.writeCSVfile(CSVTitle)
CONTACTGROUPS_MYCONTACTS_ID = 'contactGroups/myContacts'
CONTACTGROUPS_MYCONTACTS_NAME = 'My Contacts'
# gam <UserTypeEntity> copy othercontacts
# <OtherContactResourceNameEntity>|<OtherContactSelection>
def copyUserPeopleOtherContacts(users):
entityType = Ent.USER
peopleEntityType = Ent.OTHER_CONTACT
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
copyMask = ['emailAddresses', 'names', 'phoneNumbers']
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleOtherContactEntityList(-1)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
if not people:
continue
if queriedContacts:
entityList = queryPeopleOtherContacts(people, contactQuery, 'emailAddresses', entityType, user, i, count)
if entityList is None:
continue
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contact in entityList:
j += 1
if isinstance(contact, dict):
if not localPeopleContactSelects(contactQuery, contact):
continue
resourceName = contact['resourceName']
else:
resourceName = normalizeOtherContactsResourceName(contact)
try:
callGAPI(people.otherContacts(), 'copyOtherContactToMyContactsGroup',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=resourceName, body={'copyMask': ','.join(copyMask), 'sources': sources})
entityModifierNewValueActionPerformed([entityType, user, peopleEntityType, resourceName], Act.MODIFIER_TO, CONTACTGROUPS_MYCONTACTS_NAME)
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
continue
except GAPI.permissionDenied as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
Ind.Decrement()
# gam <UserTypeEntity> delete othercontacts
# <OtherContactResourceNameEntity>|<OtherContactSelection>
# gam <UserTypeEntity> move othercontacts
# <OtherContactResourceNameEntity>|<OtherContactSelection>
# gam <UserTypeEntity> update othercontacts
# <OtherResourceNameEntity>|<OtherContactSelection>
# <PeopleContactAttribute>*
# (contactgroup <ContactGroupItem>)*
def processUserPeopleOtherContacts(users):
action = Act.Get()
entityType = Ent.USER
peopleEntityType = Ent.OTHER_CONTACT
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleOtherContactEntityList(1)
if action == Act.UPDATE:
Act.Set(Act.UPDATE_MOVE)
peopleManager = PeopleManager()
body, updatePersonFields, contactGroupsLists = peopleManager.GetPersonFields(entityType, False)
else:
body = {PEOPLE_MEMBERSHIPS: [{'contactGroupMembership': {'contactGroupResourceName': CONTACTGROUPS_MYCONTACTS_ID}}]}
updatePersonFields = [PEOPLE_MEMBERSHIPS]
checkForExtraneousArguments()
validatedContactGroupsList = [CONTACTGROUPS_MYCONTACTS_ID]
contactGroupIDs = {CONTACTGROUPS_MYCONTACTS_ID: CONTACTGROUPS_MYCONTACTS_NAME}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
if not people:
continue
_, upeople = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not upeople:
continue
if queriedContacts:
entityList = queryPeopleOtherContacts(people, contactQuery, 'emailAddresses', entityType, user, i, count)
if entityList is None:
continue
else:
otherContacts = _getPeopleOtherContacts(people, entityType, user, i=0, count=0)
if otherContacts is None:
continue
if action == Act.UPDATE:
if contactGroupsLists[PEOPLE_GROUPS_LIST]:
result, validatedContactGroupsList, contactGroupIDs =\
validatePeopleContactGroupsList(people, '', contactGroupsLists[PEOPLE_GROUPS_LIST], entityType, user, i, count)
if not result:
continue
if CONTACTGROUPS_MYCONTACTS_ID not in validatedContactGroupsList:
validatedContactGroupsList.insert(0, CONTACTGROUPS_MYCONTACTS_ID)
updatePersonFields.add(PEOPLE_MEMBERSHIPS)
contactGroupNamesList = []
for resourceName in validatedContactGroupsList:
contactGroupNamesList.append(contactGroupIDs[resourceName])
contactGroupNames = ','.join(contactGroupNamesList)
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contact in entityList:
j += 1
if isinstance(contact, dict):
if not localPeopleContactSelects(contactQuery, contact):
continue
resourceName = contact['resourceName']
else:
resourceName = normalizeOtherContactsResourceName(contact)
contact = otherContacts.get(resourceName)
if contact is None:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
continue
peopleResourceName = resourceName.replace('otherContacts', 'people')
body['etag'] = contact['etag']
if action == Act.UPDATE and validatedContactGroupsList:
peopleManager.AddContactGroupsToContact(body, validatedContactGroupsList)
try:
callGAPI(upeople.people(), 'updateContact',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=peopleResourceName,
updatePersonFields=','.join(updatePersonFields), body=body, sources=sources)
if action != Act.DELETE:
entityModifierNewValueActionPerformed([entityType, user, peopleEntityType, resourceName],
Act.MODIFIER_TO, contactGroupNames, j, jcount)
else:
maxRetries = 5
for retry in range(1, maxRetries+1):
try:
callGAPI(upeople.people(), 'deleteContact',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=peopleResourceName)
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
break
except (GAPI.notFound, GAPI.internalError):
if retry == maxRetries:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
break
time.sleep(retry*2)
except GAPI.invalidArgument as e:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
continue
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
continue
except GAPI.permissionDenied as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
Ind.Decrement()
# gam <UserTypeEntity> print othercontacts [todrive <ToDriveAttribute>*] <OtherContactSelection>
# [countsonly|allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show othercontacts <OtherContactSelection>
# [countsonly|allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
# [formatjson]
def printShowUserPeopleOtherContacts(users):
entityType = Ent.USER
entityTypeName = Ent.Singular(entityType)
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
CSVTitle = 'Other Contacts'
fieldsList = []
parameters = _initPersonMetadataParameters()
countsOnly = False
contactQuery = _initPeopleOtherContactQueryAttributes()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'countsonly':
countsOnly = True
if csvPF:
csvPF.SetTitles([entityTypeName, CSVTitle])
elif myarg == 'showmetadata':
parameters['strip'] = False
elif _getPeopleOtherContactQueryAttributes(contactQuery, myarg, 0):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if countsOnly:
fieldsList = ['emailAddresses']
fields = _getPersonFields(PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, people = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
if not people:
continue
contacts = queryPeopleOtherContacts(people, contactQuery, fields, entityType, user, i, count)
if countsOnly:
jcount = countLocalPeopleContactSelects(contactQuery, contacts)
if csvPF:
csvPF.WriteRowTitles({entityTypeName: user, CSVTitle: jcount})
else:
printEntityKVList([entityType, user], [CSVTitle, jcount], i, count)
elif contacts is not None:
if not csvPF:
_printPersonEntityList(Ent.OTHER_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
elif contacts:
_printPersonEntityList(Ent.OTHER_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({Ent.Singular(entityType): user})
if csvPF:
csvPF.writeCSVfile(CSVTitle)
def _printShowPeople(source):
people = buildGAPIObject(API.PEOPLE_DIRECTORY)
entityType = Ent.DOMAIN
entityTypeName = Ent.Singular(entityType)
sources = [PEOPLE_DIRECTORY_SOURCES_CHOICE_MAP[source]]
if sources[0] == 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE':
peopleEntityType = Ent.DOMAIN_PROFILE
CSVTitle = 'People Profiles'
else:
peopleEntityType = Ent.DOMAIN_PEOPLE_CONTACT
CSVTitle = 'People Contacts'
function = 'listDirectoryPeople'
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
parameters = _initPersonMetadataParameters()
mergeSources = []
fieldsList = []
parameters = _initPersonMetadataParameters()
countsOnly = False
kwargs = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif source is None and myarg in {'source', 'sources'}:
sources = [getChoice(PEOPLE_DIRECTORY_SOURCES_CHOICE_MAP, mapChoice=True)]
elif myarg in {'mergesource', 'mergesources'}:
mergeSources = [getChoice(PEOPLE_DIRECTORY_MERGE_SOURCES_CHOICE_MAP, mapChoice=True)]
elif myarg == 'showmetadata':
parameters['strip'] = False
elif myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'countsonly':
countsOnly = True
fieldsList = [PEOPLE_METADATA]
if csvPF:
csvPF.SetTitles([entityTypeName, CSVTitle])
elif myarg == 'query':
kwargs['query'] = getString(Cmd.OB_QUERY)
function = 'searchDirectoryPeople'
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
printGettingAllEntityItemsForWhom(peopleEntityType, GC.Values[GC.DOMAIN], query=kwargs.get('query'))
try:
entityList = callGAPIpages(people.people(), function, 'people',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
sources=sources, mergeSources=mergeSources,
readMask=fields, fields='nextPageToken,people', **kwargs)
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
if not countsOnly:
_printPersonEntityList(peopleEntityType, entityList, Ent.DOMAIN, GC.Values[GC.DOMAIN], 0, 0, csvPF, FJQC, parameters, None)
else:
jcount = len(entityList)
if csvPF:
csvPF.WriteRowTitles({entityTypeName: GC.Values[GC.DOMAIN], CSVTitle: jcount})
else:
printEntityKVList([entityType, GC.Values[GC.DOMAIN]], [CSVTitle, jcount])
if csvPF:
csvPF.writeCSVfile(CSVTitle)
# gam info people|peopleprofile <PeopleResourceNameEntity>
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson]
def doInfoDomainPeopleProfile():
_infoPeople([GC.Values[GC.DOMAIN]], Ent.DOMAIN, 'profile')
# gam info peoplecontact <PeopleResourceNameEntity>
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson]
def doInfoDomainPeopleContacts():
_infoPeople([GC.Values[GC.DOMAIN]], Ent.DOMAIN, 'domaincontact')
# gam print people|peopleprofile [todrive <ToDriveAttribute>*]
# [query <String>]
# [mergesources <PeopleMergeSourceName>]
# [countsonly]
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson [quotechar <Character>]]
# gam show people|peopleprofile
# [query <String>]
# [mergesources <PeopleMergeSourceName>]
# [countsonly]
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson]
# gam print domaincontacts|peoplecontacts [todrive <ToDriveAttribute>*]
# [sources <PeopleSourceName>]
# [query <String>]
# [mergesources <PeopleMergeSourceName>]
# [countsonly]
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson [quotechar <Character>]]
# gam show domaincontacts|peoplecontacts
# [sources <PeopleSourceName>]
# [query <String>]
# [mergesources <PeopleMergeSourceName>]
# [countsonly]
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [formatjson]
def doPrintShowDomainPeopleProfiles():
_printShowPeople('profile')
def doPrintShowDomainPeopleContacts():
_printShowPeople('domaincontact')
PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP = {
'account': 'ACCOUNT',
'accounts': 'ACCOUNT',
'domain': 'DOMAIN_PROFILE',
'domains': 'DOMAIN_PROFILE',
'profile': 'PROFILE',
'profiles': 'PROFILE',
}
# gam <UserTypeEntity> print peopleprofile [todrive <ToDriveAttribute>*]
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [sources <PeopleProfileSourceNameList>]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show peopleprofile
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
# [sources <PeopleProfileSourceNameList>]
# [formatjson]
def printShowUserPeopleProfiles(users):
entityType = Ent.USER
entityTypeName = Ent.Singular(entityType)
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['profile']]
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
parameters = _initPersonMetadataParameters()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg in {'source', 'sources'}:
for field in _getFieldsList():
if field in PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP:
parameters['sourceTypes'].add(PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP[field])
else:
invalidChoiceExit(field, PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP, True)
elif myarg == 'showmetadata':
parameters['strip'] = False
elif myarg == 'peoplelookupuser':
deprecatedArgument(myarg)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
# user, people = buildGAPIServiceObject(API.PEOPLE_DIRECTORY, user, i, count)
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.PEOPLE_PROFILE, user, i, count)
try:
result = callGAPI(people.people(), 'get',
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName='people/me', sources=sources, personFields=fields)
except GAPI.notFound:
entityUnknownWarning(Ent.PEOPLE_PROFILE, user, i, count)
continue
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
if not csvPF:
_showPerson(entityType, user, Ent.PEOPLE_PROFILE, result, i, count, FJQC, parameters)
else:
_printPerson(entityTypeName, user, result, csvPF, FJQC, parameters)
if csvPF:
csvPF.writeCSVfile('People Profiles')
def _processPeopleContactPhotos(users, function):
def _makeFilenameFromPattern(resourceName):
filename = filenamePattern[:]
if subForContactId:
filename = filename.replace('#contactid#', resourceName.split('/')[1])
if subForEmail:
for email in result.get(PEOPLE_EMAIL_ADDRESSES, []):
if email.get(PEOPLE_METADATA, {}).get('primary', False):
filename = filename.replace('#email#', email['value'])
break
else:
filename = filename.replace('#email#', resourceName.split('/')[1])
return filename
if users is not None:
entityType = Ent.USER
peopleEntityType = Ent.PEOPLE_CONTACT
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
else:
users = [None]
people = buildGAPIObject(API.PEOPLE_DIRECTORY)
entityType = Ent.DOMAIN
peopleEntityType = Ent.PEOPLE_CONTACT
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['domaincontact']]
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, 1)
if function in {'updateContactPhoto', 'getContactPhoto'}:
targetFolder = os.getcwd()
filenamePattern = '#contactid#.jpg'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'drivedir':
targetFolder = GC.Values[GC.DRIVE_DIR]
elif myarg in {'sourcefolder', 'targetfolder'}:
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if function == 'GetContactPhoto' and not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
elif myarg == 'filename':
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
else:
unknownArgumentExit()
subForContactId = filenamePattern.find('#contactid#') != -1
subForEmail = filenamePattern.find('#email#') != -1
if not subForContactId and not subForEmail:
filename = filenamePattern
else: #elif function == 'deleteContactPhoto':
checkForExtraneousArguments()
subForContactId = subForEmail = False
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if resourceNameLists:
entityList = resourceNameLists[user]
if user is not None:
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
else:
user = Ent.Singular(entityType)
if contactQuery['contactGroupSelect']:
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
None, None, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
continue
contactQuery['group'] = groupId
if queriedContacts:
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses,photos', None, entityType, user, i, count)
if entityList is None:
continue
j = 0
jcount = len(entityList)
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.PHOTO, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contact in entityList:
j += 1
if isinstance(contact, dict):
if not localPeopleContactSelects(contactQuery, contact):
continue
resourceName = contact['resourceName']
else:
resourceName = normalizePeopleResourceName(contact)
try:
if subForEmail:
result = callGAPI(people.people(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName, sources=sources, personFields='emailAddresses')
if function == 'updateContactPhoto':
if subForContactId or subForEmail:
filename = _makeFilenameFromPattern(resourceName)
filename = os.path.join(targetFolder, filename)
with open(os.path.expanduser(filename), 'rb') as f:
image_data = f.read()
callGAPI(people.people(), function,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=resourceName,
body={'photoBytes': base64.urlsafe_b64encode(image_data).decode(UTF8)})
entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
elif function == 'getContactPhoto':
if subForContactId or subForEmail:
filename = _makeFilenameFromPattern(resourceName)
filename = os.path.join(targetFolder, filename)
result = callGAPI(people.people(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=resourceName, personFields='photos')
url = None
for photo in result.get('photos', []):
if photo['metadata']['source']['type'] == 'CONTACT':
url = photo['url']
break
if not url:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, None], Msg.CONTACT_PHOTO_NOT_FOUND, j, jcount)
continue
try:
status, photo_data = getHttpObj().request(url, 'GET')
if status['status'] != '200':
entityActionFailedWarning([entityType, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, j, jcount)
continue
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
continue
status, e = writeFileReturnError(filename, photo_data, mode='wb')
if status:
entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
else:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
else: #elif function == 'deleteContactPhoto':
filename = ''
callGAPI(people.people(), function,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR, GAPI.PHOTO_NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=resourceName)
entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
except (GAPI.notFound, GAPI.internalError):
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
continue
except GAPI.photoNotFound:
entityDoesNotHaveItemWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
except (GAPI.invalidArgument, OSError, IOError) as e:
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
break
Ind.Decrement()
# gam <UserTypeEntity> update contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
# [drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]
def updateUserPeopleContactPhoto(users):
_processPeopleContactPhotos(users, 'updateContactPhoto')
# gam <UserTypeEntity> get contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
# [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
def getUserPeopleContactPhoto(users):
_processPeopleContactPhotos(users, 'getContactPhoto')
# gam <UserTypeEntity> delete contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
def deleteUserPeopleContactPhoto(users):
_processPeopleContactPhotos(users, 'deleteContactPhoto')
# gam update contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
# [drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]
def doUpdateDomainContactPhoto():
_processPeopleContactPhotos(None, 'updateContactPhoto')
# gam get contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
# [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
def doGetDomainContactPhoto():
_processPeopleContactPhotos(None, 'getContactPhoto')
# gam delete contactphoto <PeopleResourceNameEntity>|<PeopleUserContactSelection>
def doDeleteDomainContactPhoto():
_processPeopleContactPhotos(None, 'deleteContactPhoto')
# gam <UserTypeEntity> create contactgroup <ContactGroupAttribute>+
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*))| returnidonly]
def createUserPeopleContactGroup(users):
peopleManager = PeopleManager()
entityType = Ent.USER
parameters = {'csvPF': None, 'titles': ['User', 'resourceName'], 'addCSVData': {}, 'returnIdOnly': False}
body, _ = peopleManager.GetContactGroupFields(parameters)
if PEOPLE_GROUP_NAME not in body:
return
csvPF = parameters['csvPF']
addCSVData = parameters['addCSVData']
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
returnIdOnly = parameters['returnIdOnly']
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
_, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, user, i, count)
if contactGroupNames is False:
continue
contactGroup = body[PEOPLE_GROUP_NAME]
if contactGroup in contactGroupNames:
entityActionFailedWarning([entityType, user], Ent.TypeNameMessage(Ent.CONTACT_GROUP, contactGroup, Msg.DUPLICATE), i, count)
continue
try:
result = callGAPI(people.contactGroups(), 'create',
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
body={'contactGroup': body}, fields='resourceName')
resourceName = result['resourceName']
if returnIdOnly:
writeStdout(f'{resourceName}\n')
elif not csvPF:
entityActionPerformed([entityType, user, Ent.CONTACT_GROUP, resourceName], i, count)
else:
row = {'User': user, 'resourceName': resourceName}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
except (GAPI.forbidden, GAPI.permissionDenied):
userPeopleServiceNotEnabledWarning(user, i, count)
except GAPI.serviceNotAvailable:
entityUnknownWarning(entityType, user, i, count)
if csvPF:
csvPF.writeCSVfile('People Contact Groups')
# gam <UserTypeEntity> update contactgroups <ContactGroupItem> <ContactAttribute>+
def updateUserPeopleContactGroup(users):
peopleManager = PeopleManager()
entityType = Ent.USER
entityList = getStringReturnInList(Cmd.OB_CONTACT_GROUP_ITEM)
body, fields = peopleManager.GetContactGroupFields()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
contactGroupIDs = contactGroupNames = None
j = 0
jcount = len(entityList)
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT_GROUP, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contactGroup in entityList:
j += 1
try:
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
contactGroupIDs, contactGroupNames, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], Msg.DOES_NOT_EXIST, j, jcount)
continue
break
result = callGAPI(people.contactGroups(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=groupId)
body['etag'] = result['etag']
newContactGroup = body.get(PEOPLE_GROUP_NAME)
if newContactGroup:
if newContactGroup in contactGroupNames and groupId not in contactGroupNames[newContactGroup]:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup],
Ent.TypeNameMessage(Ent.CONTACT_GROUP, newContactGroup, Msg.DUPLICATE), i, count)
continue
callGAPI(people.contactGroups(), 'update',
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=groupId, body={'contactGroup': body, 'updateGroupFields': fields})
entityActionPerformed([entityType, user, Ent.CONTACT_GROUP, contactGroup], j, jcount)
except (GAPI.notFound, GAPI.internalError) as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], str(e), j, jcount)
except (GAPI.forbidden, GAPI.permissionDenied):
userPeopleServiceNotEnabledWarning(user, i, count)
break
except GAPI.serviceNotAvailable:
entityUnknownWarning(entityType, user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> delete contactgroups <ContactGroupEntity>
def deleteUserPeopleContactGroups(users):
entityType = Ent.USER
entityList = getEntityList(Cmd.OB_CONTACT_GROUP_ENTITY, shlexSplit=True)
contactGroupIdLists = entityList if isinstance(entityList, dict) else None
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if contactGroupIdLists:
entityList = contactGroupIdLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
contactGroupIDs = contactGroupNames = None
j = 0
jcount = len(entityList)
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT_GROUP, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contactGroup in entityList:
j += 1
try:
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
contactGroupIDs, contactGroupNames, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], Msg.DOES_NOT_EXIST, j, jcount)
continue
break
callGAPI(people.contactGroups(), 'delete',
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=groupId)
entityActionPerformed([entityType, user, Ent.CONTACT_GROUP, contactGroup], j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], str(e), j, jcount)
except (GAPI.forbidden, GAPI.permissionDenied):
userPeopleServiceNotEnabledWarning(user, i, count)
break
except GAPI.serviceNotAvailable:
entityUnknownWarning(entityType, user, i, count)
break
Ind.Decrement()
PEOPLE_GROUP_TIME_OBJECTS = {'updateTime'}
def _normalizeContactGroupMetadata(contactGroup):
normalizedContactGroup = contactGroup.copy()
for k, v in iter(normalizedContactGroup.pop('metadata', {}).items()):
normalizedContactGroup[k] = v
return normalizedContactGroup
def _showContactGroup(userEntityType, user, entityType, contactGroup, i, count, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(contactGroup, timeObjects=PEOPLE_GROUP_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True))
return
normalizedContactGroup = _normalizeContactGroupMetadata(contactGroup)
printEntity([userEntityType, user, entityType, contactGroup['resourceName']], i, count)
Ind.Increment()
showJSON(None, normalizedContactGroup, timeObjects=PEOPLE_GROUP_TIME_OBJECTS)
Ind.Decrement()
def _printContactGroup(entityTypeName, user, contactGroup, csvPF, FJQC):
normalizedContactGroup = _normalizeContactGroupMetadata(contactGroup)
row = flattenJSON(normalizedContactGroup, flattened={entityTypeName: user}, timeObjects=PEOPLE_GROUP_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({entityTypeName: user, 'resourceName': contactGroup['resourceName'],
'JSON': json.dumps(cleanJSON(contactGroup, timeObjects=PEOPLE_GROUP_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP = {
'clientdata': 'clientData',
'grouptype': 'groupType',
'membercount': 'memberCount',
'metadata': 'metadata',
'name': 'name',
}
PEOPLE_CONTACTGROUPS_DEFAULT_FIELDS = ['name', 'metadata', 'grouptype', 'membercount']
# gam <UserTypeEntity> info contactgroups <PeopleContactGroupEntity>
# [allfields|(fields <PeoplaContactGroupFieldList>)] [showmetadata]
# [formatjson]
def infoUserPeopleContactGroups(users):
entityType = Ent.USER
entityList = getEntityList(Cmd.OB_CONTACT_GROUP_ENTITY, shlexSplit=True)
contactGroupIdLists = entityList if isinstance(entityList, dict) else None
FJQC = FormatJSONQuoteChar()
fieldsList = []
parameters = _initPersonMetadataParameters()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'showmetadata':
parameters['strip'] = False
else:
FJQC.GetFormatJSON(myarg)
fields = _getPersonFields(PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTGROUPS_DEFAULT_FIELDS, fieldsList, parameters)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if contactGroupIdLists:
entityList = contactGroupIdLists[user]
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
contactGroupIDs = contactGroupNames = None
j = 0
jcount = len(entityList)
if not FJQC.formatJSON:
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT_GROUP, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
for contactGroup in entityList:
j += 1
try:
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
contactGroupIDs, contactGroupNames, entityType, user, i, count)
if not groupId:
if contactGroupNames:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], Msg.DOES_NOT_EXIST, j, jcount)
continue
break
group = callGAPI(people.contactGroups(), 'get',
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
resourceName=groupId, groupFields=fields)
_showContactGroup(entityType, user, Ent.CONTACT_GROUP, group, j, jcount, FJQC)
except GAPI.notFound as e:
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], str(e), j, jcount)
except (GAPI.forbidden, GAPI.permissionDenied):
userPeopleServiceNotEnabledWarning(user, i, count)
break
except GAPI.serviceNotAvailable:
entityUnknownWarning(entityType, user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> print contactgroups [todrive <ToDriveAttribute>*]
# [allfields|(fields <PeoplaContactGroupFieldList>)] [showmetadata]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show contactgroups
# [allfields|(fields <PeoplaContactGroupFieldList>)] [showmetadata]
# [formatjson]
def printShowUserPeopleContactGroups(users):
entityType = Ent.USER
entityTypeName = Ent.Singular(entityType)
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
if csvPF:
csvPF.SetNoEscapeChar(True)
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
parameters = _initPersonMetadataParameters()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'allfields':
fieldsList = None
elif getPersonFieldsList(myarg, PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'showmetadata':
parameters['strip'] = False
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = _getPersonFields(PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTGROUPS_DEFAULT_FIELDS, fieldsList, parameters)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
printGettingAllEntityItemsForWhom(Ent.PEOPLE_CONTACT_GROUP, user, i, count)
try:
entityList = callGAPIpages(people.contactGroups(), 'list', 'contactGroups',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
groupFields=fields, fields='nextPageToken,contactGroups')
except GAPI.permissionDenied as e:
ClientAPIAccessDeniedExit(str(e))
except (GAPI.serviceNotAvailable, GAPI.forbidden):
ClientAPIAccessDeniedExit()
_printPersonEntityList(Ent.PEOPLE_CONTACT_GROUP, entityList, entityType, user, i, count, csvPF, FJQC, parameters, None)
if csvPF:
csvPF.writeCSVfile('People Contact Groups')
# Delegate command utilities
def _validateUserGetDelegateList(cd, user, i, count, entity):
if entity['dict']:
entityList = entity['dict'][user]
else:
entityList = entity['list']
user = checkUserExists(cd, user, Ent.USER, i, count)
if not user:
return (user, None, 0)
jcount = len(entityList)
entityPerformActionNumItems([Ent.USER, user], jcount, entity['item'], i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (user, entityList, jcount)
def _getDelegateName(cd, delegateEmail, delegateNames):
if delegateEmail in delegateNames:
return delegateNames[delegateEmail]
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=delegateEmail, fields='name(fullName)')
delegateName = result.get('name', {'fullName': delegateEmail}).get('fullName', delegateEmail)
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
delegateName = delegateEmail
delegateNames[delegateEmail] = delegateName
return delegateName
# gam <UserTypeEntity> create contactdelegate <UserEntity>
# gam <UserTypeEntity> delete contactdelegate <UserEntity>
def processContactDelegates(users):
condel = buildGAPIObject(API.CONTACTDELEGATION)
cd = buildGAPIObject(API.DIRECTORY)
function = 'delete' if Act.Get() == Act.DELETE else 'create'
delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, delegates, jcount = _validateUserGetDelegateList(cd, user, i, count, delegateEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for delegate in delegates:
j += 1
delegateEmail = checkUserExists(cd, delegate, Ent.CONTACT_DELEGATE, j, jcount)
if not delegateEmail:
continue
try:
if function == 'create':
callGAPI(condel.delegates(), function,
throwReasons=GAPI.CONTACT_DELEGATE_THROW_REASONS,
user=user, body={'email': delegateEmail})
else:
callGAPI(condel.delegates(), function,
throwReasons=GAPI.CONTACT_DELEGATE_THROW_REASONS,
user=user, delegate=delegateEmail)
entityActionPerformed([Ent.USER, user, Ent.CONTACT_DELEGATE, delegateEmail], j, jcount)
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.CONTACT_DELEGATE, delegateEmail], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.badRequest):
userContactDelegateServiceNotEnabledWarning(user, i, count)
Ind.Decrement()
# gam <UserTypeEntity> print contactdelegates [todrive <ToDriveAttribute>*] [shownames]
# gam <UserTypeEntity> show contactdelegates [shownames] [csv]
def printShowContactDelegates(users):
condel = buildGAPIObject(API.CONTACTDELEGATION)
titlesList = ['User', 'delegateAddress']
csvPF = CSVPrintFile() if Act.csvFormat() else None
cd = buildGAPIObject(API.DIRECTORY)
csvStyle = showNames = False
delegateNames = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif not csvPF and myarg == 'csv':
csvStyle = True
elif myarg == 'shownames':
titlesList = ['User', 'delegateName', 'delegateAddress']
showNames = True
else:
unknownArgumentExit()
if csvPF:
csvPF.AddTitles(titlesList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = checkUserExists(cd, user, Ent.USER, i, count)
if not user:
continue
printGettingAllEntityItemsForWhom(Ent.CONTACT_DELEGATE, user, i, count)
try:
delegates = callGAPIpages(condel.delegates(), 'list', 'delegates',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.CONTACT_DELEGATE_THROW_REASONS,
user=user)
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.CONTACT_DELEGATE, None], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.badRequest):
userContactDelegateServiceNotEnabledWarning(user, i, count)
continue
jcount = len(delegates)
if not csvPF:
if not csvStyle:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CONTACT_DELEGATE, i, count)
Ind.Increment()
j = 0
for delegate in delegates:
j += 1
delegateEmail = delegate['email']
if showNames:
printEntity([Ent.CONTACT_DELEGATE, _getDelegateName(cd, delegateEmail, delegateNames)], j, jcount)
Ind.Increment()
printKeyValueList(['Delegate Email', delegateEmail])
Ind.Decrement()
else:
printEntity([Ent.DELEGATE, delegateEmail], j, jcount)
Ind.Decrement()
else:
j = 0
for delegate in delegates:
j += 1
delegateEmail = delegate['email']
if showNames:
writeStdout(f'{user},{_getDelegateName(cd, delegateEmail, delegateNames)},{delegateEmail}\n')
else:
writeStdout(f'{user},{delegateEmail}\n')
else:
if delegates:
if showNames:
for delegate in delegates:
csvPF.WriteRow({'User': user, 'delegateName': _getDelegateName(cd, delegate['email'], delegateNames),
'delegateAddress': delegate['email']})
else:
for delegate in delegates:
csvPF.WriteRow({'User': user, 'delegateAddress': delegate['email']})
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
if csvPF:
csvPF.writeCSVfile('Contact Delegates')
# CrOS commands utilities
def getCrOSDeviceEntity():
if checkArgumentPresent('crossn'):
return getItemsToModify(Cmd.ENTITY_CROS_SN, getString(Cmd.OB_SERIAL_NUMBER_LIST))
if checkArgumentPresent('query'):
return getItemsToModify(Cmd.ENTITY_CROS_QUERY, getString(Cmd.OB_QUERY))
deviceId = getString(Cmd.OB_CROS_DEVICE_ENTITY)
if deviceId[:6].lower() == 'query:':
query = deviceId[6:]
if query[:12].lower() == 'orgunitpath:':
return getItemsToModify(Cmd.ENTITY_CROS_OU, query[12:])
return getItemsToModify(Cmd.ENTITY_CROS_QUERY, query)
Cmd.Backup()
return getEntityList(Cmd.OB_CROS_ENTITY)
UPDATE_CROS_ARGUMENT_TO_PROPERTY_MAP = {
'annotatedassetid': 'annotatedAssetId',
'annotatedlocation': 'annotatedLocation',
'annotateduser': 'annotatedUser',
'asset': 'annotatedAssetId',
'assetid': 'annotatedAssetId',
'location': 'annotatedLocation',
'notes': 'notes',
'org': 'orgUnitPath',
'orgunitpath': 'orgUnitPath',
'ou': 'orgUnitPath',
'tag': 'annotatedAssetId',
'updatenotes': 'notes',
'user': 'annotatedUser',
}
CROS_ACTION_CHOICE_MAP = {
'deprovisiondifferentmodelreplace': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_DIFFERENT_MODEL_REPLACEMENT'),
'deprovisiondifferentmodelreplacement': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_DIFFERENT_MODEL_REPLACEMENT'),
'deprovisionretiringdevice': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_RETIRING_DEVICE'),
'deprovisionsamemodelreplace': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_SAME_MODEL_REPLACEMENT'),
'deprovisionsamemodelreplacement': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_SAME_MODEL_REPLACEMENT'),
'deprovisionupgradetransfer': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_UPGRADE_TRANSFER'),
'disable': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DISABLE', None),
'reenable': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_REENABLE', None),
# 'preprovisioneddisable': ('pre_provisioned_disable', None),
# 'preprovisionedreenable': ('pre_provisioned_reenable', None)
}
CROS_ACTION_NAME_MAP = {
'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION': Act.DEPROVISION,
'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DISABLE': Act.DISABLE,
'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_REENABLE': Act.REENABLE,
# 'pre_provisioned_disable': Act.PRE_PROVISIONED_DISABLE,
# 'pre_provisioned_reenable': Act.PRE_PROVISIONED_REENABLE
}
# gam <CrOSTypeEntity> update <CrOSAttribute>+ [quickcrosmove [<Boolean>]] [nobatchupdate]
# gam <CrOSTypeEntity> update action <CrOSAction> [acknowledge_device_touch_requirement]
# [actionbatchsize <Integer>]
def updateCrOSDevices(entityList):
cd = buildGAPIObject(API.DIRECTORY)
noBatchUpdate = False
update_body = {}
action_body = {}
orgUnitPath = updateNotes = None
ackWipe = False
quickCrOSMove = GC.Values[GC.QUICK_CROS_MOVE]
actionBatchSize = 10
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in UPDATE_CROS_ARGUMENT_TO_PROPERTY_MAP:
up = UPDATE_CROS_ARGUMENT_TO_PROPERTY_MAP[myarg]
if up == 'orgUnitPath':
orgUnitPath = getOrgUnitItem()
elif up == 'notes':
update_body[up] = getStringWithCRsNLs()
updateNotes = update_body[up] if myarg == 'updatenotes' and update_body[up].find('#notes#') != -1 else None
else:
update_body[up] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'action':
actionLocation = Cmd.Location()
action_body['changeChromeOsDeviceStatusAction'], deprovisionReason = getChoice(CROS_ACTION_CHOICE_MAP, mapChoice=True)
if deprovisionReason:
action_body['deprovisionReason'] = deprovisionReason
Act.Set(CROS_ACTION_NAME_MAP[action_body['changeChromeOsDeviceStatusAction']])
elif myarg == 'acknowledgedevicetouchrequirement':
ackWipe = True
elif myarg == 'quickcrosmove':
quickCrOSMove = getBoolean()
elif myarg == 'nobatchupdate':
noBatchUpdate = getBoolean()
elif myarg == 'actionbatchsize':
actionBatchSize = getInteger(minVal=10, maxVal=250)
else:
unknownArgumentExit()
if action_body and update_body:
Cmd.SetLocation(actionLocation-1)
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('action', '<CrOSAttribute>'))
if orgUnitPath:
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, orgUnitPath)
if not status:
entityActionFailedWarning([Ent.CROS_DEVICE, ''], f'{Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnitPath}, {Msg.DOES_NOT_EXIST}')
return
i, count, entityList = getEntityArgument(entityList)
# Action
if action_body:
if action_body['changeChromeOsDeviceStatusAction'] == 'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION' and not ackWipe:
stderrWarningMsg(Msg.REFUSING_TO_DEPROVISION_DEVICES.format(count))
systemErrorExit(ACTION_NOT_PERFORMED_RC, None)
while i < count:
bcount = min(count-i, actionBatchSize)
action_body['deviceIds'] = entityList[i:i+bcount]
try:
result = callGAPI(cd.customer().devices().chromeos(), 'batchChangeStatus',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.CONDITION_NOT_MET, GAPI.INVALID_INPUT,
GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], body=action_body)
for status in result['changeChromeOsDeviceStatusResults']:
i += 1
deviceId = status['deviceId']
if 'error' not in status:
entityActionPerformed([Ent.CROS_DEVICE, deviceId], i, count)
else:
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], status['error']['message'], i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.conditionNotMet, GAPI.invalidInput, GAPI.badRequest, GAPI.forbidden) as e:
entityActionFailedExit([Ent.CROS_DEVICE, None], str(e))
return
# Update
function = None
if update_body or noBatchUpdate:
if orgUnitPath and (not quickCrOSMove or noBatchUpdate):
update_body['orgUnitPath'] = orgUnitPath
if GC.Values[GC.UPDATE_CROS_OU_WITH_ID]:
update_body['orgUnitId'] = orgUnitId
orgUnitPath = None
function = 'update'
parmId = 'deviceId'
kwargs = {parmId: None, 'body': update_body, 'fields': ''}
if orgUnitPath:
Act.Set(Act.ADD)
_batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, 0, 0, entityList, quickCrOSMove)
Act.Set(Act.UPDATE)
if function is None:
return
for deviceId in entityList:
i += 1
kwargs[parmId] = deviceId
try:
if updateNotes:
oldNotes = callGAPI(cd.chromeosdevices(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, fields='notes').get('notes', '')
update_body['notes'] = updateNotes.replace('#notes#', oldNotes)
callGAPI(cd.chromeosdevices(), function,
throwReasons=[GAPI.INVALID, GAPI.CONDITION_NOT_MET, GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], **kwargs)
entityActionPerformed([Ent.CROS_DEVICE, deviceId], i, count)
except (GAPI.invalid, GAPI.conditionNotMet, GAPI.invalidInput) as e:
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], str(e), i, count)
except GAPI.invalidOrgunit:
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], Msg.INVALID_ORGUNIT, i, count)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, deviceId, i, count)
# gam update cros|croses <CrOSEntity> <CrOSAttribute>+ [quickcrosmove [<Boolean>]] [nobatchupdate]
# gam update cros|croses <CrOSEntity> action <CrOSAction> [acknowledge_device_touch_requirement]
def doUpdateCrOSDevices():
updateCrOSDevices(getCrOSDeviceEntity())
CROS_COMMAND_CHOICE_MAP = {
'reboot': 'REBOOT',
'remotepowerwash': 'REMOTE_POWERWASH',
'setvolume': 'SET_VOLUME',
'takeascreenshot': 'TAKE_A_SCREENSHOT',
'wipeusers': 'WIPE_USERS'
}
CROS_DOIT_REQUIRED_COMMANDS = {'WIPE_USERS', 'REMOTE_POWERWASH'}
CROS_KIOSK_COMMANDS = {'REBOOT', 'SET_VOLUME', 'TAKE_A_SCREENSHOT'}
CROS_COMMAND_FINAL_STATES = {'EXPIRED', 'CANCELLED', 'EXECUTED_BY_CLIENT'}
CROS_COMMAND_TIME_OBJECTS = {'executeTime', 'issueTime', 'commandExpireTime'}
def displayCrOSCommandResult(cd, deviceId, commandId, checkResultRetries, i, count):
Ind.Increment()
try:
for _ in range(0, checkResultRetries):
time.sleep(2)
result = callGAPI(cd.customer().devices().chromeos().commands(), 'get',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, commandId=commandId)
showJSON(None, result, timeObjects=CROS_COMMAND_TIME_OBJECTS)
state = result.get('state')
if state in CROS_COMMAND_FINAL_STATES:
break
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId, Ent.COMMAND_ID, commandId], str(e), i, count)
Ind.Decrement()
# gam <CrOSTypeEntity> issuecommand command <CrOSCommand> [times_to_check_status <Integer>] [doit]
def issueCommandCrOSDevices(entityList):
cd = buildGAPIObject(API.DIRECTORY)
body = {}
checkResultRetries = 1
doit = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'command':
body['commandType'] = getChoice(CROS_COMMAND_CHOICE_MAP, mapChoice=True)
if body['commandType'] == 'SET_VOLUME':
body['payload'] = json.dumps({'volume': getInteger(minVal=0, maxVal=100)})
elif myarg == 'timestocheckstatus':
checkResultRetries = getInteger(minVal=0)
elif myarg == 'doit':
doit = True
else:
unknownArgumentExit()
if not body:
missingArgumentExit('command <CrOSCommand>')
i, count, entityList = getEntityArgument(entityList)
if body['commandType'] in CROS_DOIT_REQUIRED_COMMANDS and not doit:
actionNotPerformedNumItemsWarning(count, Ent.CROS_DEVICE, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION)
return
for deviceId in entityList:
i += 1
try:
result = callGAPI(cd.customer().devices().chromeos(), 'issueCommand',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND],
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, body=body)
commandId = result.get('commandId')
entityActionPerformed([Ent.CROS_DEVICE, deviceId, Ent.ACTION, body['commandType'], Ent.COMMAND_ID, commandId], i, count)
displayCrOSCommandResult(cd, deviceId, commandId, checkResultRetries, i, count)
except GAPI.invalidArgument as e:
errMsg = str(e)
if body['commandType'] in CROS_KIOSK_COMMANDS:
errMsg += Msg.KIOSK_MODE_REQUIRED.format(body['commandType'])
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], errMsg, i, count)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], str(e), i, count)
# gam issuecommand <CrOSEntity> command <CrOSCommand> [times_to_check_status <Integer>] [doit]
def doIssueCommandCrOSDevices():
issueCommandCrOSDevices(getCrOSDeviceEntity())
# gam <CrOSTypeEntity> getcommand commandid <CommandID> [times_to_check_status <Integer>]
def getCommandResultCrOSDevices(entityList):
cd = buildGAPIObject(API.DIRECTORY)
commandId = ''
checkResultRetries = 1
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'commandid':
commandId = getString(Cmd.OB_COMMAND_ID)
elif myarg == 'timestocheckstatus':
checkResultRetries = getInteger(minVal=0)
else:
unknownArgumentExit()
if not commandId:
missingArgumentExit('commandid <CommandID>')
i, count, entityList = getEntityArgument(entityList)
for deviceId in entityList:
i += 1
printEntity([Ent.CROS_DEVICE, deviceId, Ent.COMMAND_ID, commandId], i, count)
displayCrOSCommandResult(cd, deviceId, commandId, checkResultRetries, i, count)
# gam getcommand <CrOSEntity> commandid <CommandID> [times_to_check_status <Integer>]
def doGetCommandResultCrOSDevices():
getCommandResultCrOSDevices(getCrOSDeviceEntity())
# From https://www.chromium.org/chromium-os/tpm_firmware_update
CROS_TPM_VULN_VERSIONS = ['41f', '420', '628', '8520']
CROS_TPM_FIXED_VERSIONS = ['422', '62b', '8521']
def checkTPMVulnerability(cros):
if 'tpmVersionInfo' in cros and 'firmwareVersion' in cros['tpmVersionInfo']:
if cros['tpmVersionInfo']['firmwareVersion'] in CROS_TPM_VULN_VERSIONS:
cros['tpmVersionInfo']['tpmVulnerability'] = 'VULNERABLE'
elif cros['tpmVersionInfo']['firmwareVersion'] in CROS_TPM_FIXED_VERSIONS:
cros['tpmVersionInfo']['tpmVulnerability'] = 'UPDATED'
else:
cros['tpmVersionInfo']['tpmVulnerability'] = 'NOT IMPACTED'
def _filterActiveTimeRanges(cros, selected, listLimit, startDate, endDate, activeTimeRangesOrder):
if not selected:
cros.pop('activeTimeRanges', None)
return []
filteredItems = []
activeTimeRanges = cros.get('activeTimeRanges', [])
if activeTimeRangesOrder == 'DESCENDING':
activeTimeRanges.reverse()
i = 0
for item in activeTimeRanges:
activityDate = datetime.datetime.strptime(item['date'], YYYYMMDD_FORMAT)
if ((startDate is None) or (activityDate >= startDate)) and ((endDate is None) or (activityDate <= endDate)):
item['duration'] = formatMilliSeconds(item['activeTime'])
item['minutes'] = item['activeTime']//60000
item['activeTime'] = str(item['activeTime'])
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros['activeTimeRanges'] = filteredItems
return cros['activeTimeRanges']
def _filterDeviceFiles(cros, selected, listLimit, startTime, endTime):
if not selected:
cros.pop('deviceFiles', None)
return []
filteredItems = []
i = 0
for item in cros.get('deviceFiles', []):
timeValue, _ = iso8601.parse_date(item['createTime'])
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
item['createTime'] = formatLocalTime(item['createTime'])
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros['deviceFiles'] = filteredItems
return cros['deviceFiles']
def _filterCPUStatusReports(cros, selected, listLimit, startTime, endTime):
if not selected:
cros.pop('cpuStatusReports', None)
return []
filteredItems = []
i = 0
for item in cros.get('cpuStatusReports', []):
timeValue, _ = iso8601.parse_date(item['reportTime'])
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
item['reportTime'] = formatLocalTime(item['reportTime'])
for tempInfo in item.get('cpuTemperatureInfo', []):
tempInfo['label'] = tempInfo['label'].strip()
if 'cpuUtilizationPercentageInfo' in item:
item['cpuUtilizationPercentageInfo'] = ','.join([str(x) for x in item['cpuUtilizationPercentageInfo']])
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros['cpuStatusReports'] = filteredItems
return cros['cpuStatusReports']
def _filterSystemRamFreeReports(cros, selected, listLimit, startTime, endTime):
if not selected:
cros.pop('systemRamFreeReports', None)
return []
filteredItems = []
i = 0
for item in cros.get('systemRamFreeReports', []):
timeValue, _ = iso8601.parse_date(item['reportTime'])
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
item['reportTime'] = formatLocalTime(item['reportTime'])
item['systemRamFreeInfo'] = ','.join([str(x) for x in item['systemRamFreeInfo']])
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros['systemRamFreeReports'] = filteredItems
return cros['systemRamFreeReports']
def _filterRecentUsers(cros, selected, listLimit):
if not selected:
cros.pop('recentUsers', None)
return []
filteredItems = []
i = 0
for item in cros.get('recentUsers', []):
item['email'] = item.get('email', [UNKNOWN, 'UnmanagedUser'][item['type'] == 'USER_TYPE_UNMANAGED'])
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros['recentUsers'] = filteredItems
return cros['recentUsers']
def _filterScreenshotFiles(cros, selected, listLimit, startTime, endTime):
if not selected:
cros.pop('screenshotFiles', None)
return []
filteredItems = []
i = 0
for item in cros.get('screenshotFiles', []):
timeValue, _ = iso8601.parse_date(item['createTime'])
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
item['createTime'] = formatLocalTime(item['createTime'])
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros['screenshotFiles'] = filteredItems
return cros['screenshotFiles']
def _filterBasicList(cros, field, selected, listLimit):
if not selected:
cros.pop(field, None)
return []
if listLimit:
filteredItems = []
i = 0
for item in cros.get(field, []):
filteredItems.append(item)
i += 1
if listLimit and i == listLimit:
break
cros[field] = filteredItems
return cros[field]
return cros.get(field, [])
def _computeDVRstorageFreePercentage(cros):
for diskVolumeReport in cros.get('diskVolumeReports', []):
volumeInfo = diskVolumeReport['volumeInfo']
for volume in volumeInfo:
if volume['storageTotal'] != '0':
volume['storageFreePercentage'] = str(int(int(volume['storageFree'])/int(volume['storageTotal'])*100))
else:
volume['storageFreePercentage'] = '0'
def _getFilterDateTime():
filterDate = getYYYYMMDD(returnDateTime=True)
return (filterDate, filterDate.replace(tzinfo=iso8601.UTC))
CROS_FIELDS_CHOICE_MAP = {
'activetimeranges': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
'annotatedassetid': 'annotatedAssetId',
'annotatedlocation': 'annotatedLocation',
'annotateduser': 'annotatedUser',
'asset': 'annotatedAssetId',
'assetid': 'annotatedAssetId',
'autoupdateexpiration': 'autoUpdateExpiration',
'autoupdatethrough': 'autoUpdateThrough',
'backlightinfo': 'backlightInfo',
'bootmode': 'bootMode',
'cpuinfo': 'cpuInfo',
'cpustatusreports': 'cpuStatusReports',
'deprovisionreason': 'deprovisionReason',
'devicefiles': ['deviceFiles.type', 'deviceFiles.createTime'],
'deviceid': 'deviceId',
'devicelicensetype': 'deviceLicenseType',
'diskvolumereports': 'diskVolumeReports',
'dockmacaddress': 'dockMacAddress',
'ethernetmacaddress': 'ethernetMacAddress',
'ethernetmacaddress0': 'ethernetMacAddress0',
'extendedsupporteligible': 'extendedSupportEligible',
'extendedsupportstart': 'extendedSupportStart',
'extendedsupportenabled': 'extendedSupportEnabled',
'firmwareversion': 'firmwareVersion',
'firstenrollmenttime': 'firstEnrollmentTime',
'lastdeprovisiontimestamp': 'lastDeprovisionTimestamp',
'lastenrollmenttime': 'lastEnrollmentTime',
'lastknownnetwork': 'lastKnownNetwork',
'lastsync': 'lastSync',
'location': 'annotatedLocation',
'macaddress': 'macAddress',
'manufacturedate': 'manufactureDate',
'meid': 'meid',
'model': 'model',
'notes': 'notes',
'ordernumber': 'orderNumber',
'org': 'orgUnitPath',
'orgunitid': 'orgUnitId',
'orgunitpath': 'orgUnitPath',
'osupdatestatus': 'osUpdateStatus',
'osversion': 'osVersion',
'ou': 'orgUnitPath',
'platformversion': 'platformVersion',
'recentusers': ['recentUsers.email', 'recentUsers.type'],
'screenshotfiles': 'screenshotFiles',
'serialnumber': 'serialNumber',
'status': 'status',
'supportenddate': 'supportEndDate',
'systemramfreereports': 'systemRamFreeReports',
'systemramtotal': 'systemRamTotal',
'tag': 'annotatedAssetId',
'timeranges': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
'times': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
'tpmversioninfo': 'tpmVersionInfo',
'user': 'annotatedUser',
'users': ['recentUsers.email', 'recentUsers.type'],
'willautorenew': 'willAutoRenew',
}
CROS_BASIC_FIELDS_LIST = ['deviceId', 'annotatedAssetId', 'annotatedLocation', 'annotatedUser', 'lastSync', 'notes', 'serialNumber', 'status']
CROS_SCALAR_PROPERTY_PRINT_ORDER = [
'orgUnitId',
'orgUnitPath',
'annotatedAssetId',
'annotatedLocation',
'annotatedUser',
'lastSync',
'notes',
'serialNumber',
'status',
'deviceLicenseType',
'model',
'firmwareVersion',
'platformVersion',
'osVersion',
'bootMode',
'meid',
'dockMacAddress',
'ethernetMacAddress',
'ethernetMacAddress0',
'macAddress',
'systemRamTotal',
'firstEnrollmentTime',
'lastEnrollmentTime',
'deprovisionReason',
'lastDeprovisionTimestamp',
'orderNumber',
'manufactureDate',
'supportEndDate',
'autoUpdateExpiration',
'willAutoRenew',
]
CROS_LIST_FIELDS_CHOICE_MAP = {
'activetimeranges': 'activeTimeRanges',
'cpustatusreports': 'cpuStatusReports',
'devicefiles': 'deviceFiles',
'diskvolumereports': 'diskVolumeReports',
'files': 'deviceFiles',
'lastknownnetwork': 'lastKnownNetwork',
'recentusers': 'recentUsers',
'screenshotfiles': 'screenshotFiles',
'systemramfreereports': 'systemRamFreeReports',
'timeranges': 'activeTimeRanges',
'times': 'activeTimeRanges',
'users': 'recentUsers',
}
CROS_TIME_OBJECTS = {
'createTime',
'firstEnrollmentTime',
'lastDeprovisionTimestamp',
'lastEnrollmentTime',
'lastSync',
'reportTime',
'supportEndDate',
}
CROS_FIELDS_WITH_CRS_NLS = {'notes'}
CROS_START_ARGUMENTS = ['start', 'startdate', 'oldestdate']
CROS_END_ARGUMENTS = ['end', 'enddate']
# gam info cros <CrOSEntity>
# gam <CrOSTypeEntity> info
# [basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
# [nolists]
# [start <Date>] [end <Date>] [listlimit <Number>]
# [reverselists <CrOSListFieldNameList>]
# [timerangeorder ascending|descending] [showdvrsfp]
# [downloadfile latest|<Time>] [targetfolder <FilePath>]
# [formatjson]
def infoCrOSDevices(entityList):
cd = buildGAPIObject(API.DIRECTORY)
downloadfile = None
targetFolder = GC.Values[GC.DRIVE_DIR]
projection = None
fieldsList = []
reverseLists = []
noLists = showDVRstorageFreePercentage = False
FJQC = FormatJSONQuoteChar()
listLimit = 0
startDate = endDate = startTime = endTime = None
activeTimeRangesOrder = 'ASCENDING'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'nolists':
noLists = True
elif myarg == 'listlimit':
listLimit = getInteger(minVal=0)
elif myarg in CROS_START_ARGUMENTS:
startDate, startTime = _getFilterDateTime()
elif myarg in CROS_END_ARGUMENTS:
endDate, endTime = _getFilterDateTime()
elif myarg == 'timerangeorder':
activeTimeRangesOrder = getChoice(SORTORDER_CHOICE_MAP, mapChoice=True)
elif myarg == 'allfields':
projection = 'FULL'
fieldsList = []
elif myarg in PROJECTION_CHOICE_MAP:
projection = PROJECTION_CHOICE_MAP[myarg]
if projection == 'FULL':
fieldsList = []
else:
fieldsList = CROS_BASIC_FIELDS_LIST[:]
elif myarg in CROS_FIELDS_CHOICE_MAP:
addFieldToFieldsList(myarg, CROS_FIELDS_CHOICE_MAP, fieldsList)
elif myarg == 'fields':
for field in _getFieldsList():
if field in CROS_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CROS_FIELDS_CHOICE_MAP, fieldsList)
if field in CROS_LIST_FIELDS_CHOICE_MAP:
projection = 'FULL'
noLists = False
else:
invalidChoiceExit(field, CROS_FIELDS_CHOICE_MAP, True)
elif myarg == 'reverselists':
for field in _getFieldsList():
if field in CROS_LIST_FIELDS_CHOICE_MAP:
reverseLists.append(CROS_LIST_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, CROS_LIST_FIELDS_CHOICE_MAP, True)
elif myarg == 'downloadfile':
downloadfile = getString(Cmd.OB_STRING).lower()
if downloadfile != 'latest':
Cmd.Backup()
downloadfile = formatLocalTime(getTimeOrDeltaFromNow())
elif myarg == 'targetfolder':
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
elif myarg == 'showdvrsfp':
showDVRstorageFreePercentage = True
else:
FJQC.GetFormatJSON(myarg)
if fieldsList:
fieldsList.append('deviceId')
if downloadfile:
fieldsList.append('deviceFiles.downloadUrl')
fields = getFieldsFromFieldsList(fieldsList)
i, count, entityList = getEntityArgument(entityList)
for deviceId in entityList:
i += 1
try:
cros = callGAPI(cd.chromeosdevices(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, projection=projection, fields=fields)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, deviceId, i, count)
continue
checkTPMVulnerability(cros)
if 'autoUpdateExpiration' in cros:
cros['autoUpdateExpiration'] = formatLocalDatestamp(cros['autoUpdateExpiration'])
if showDVRstorageFreePercentage:
_computeDVRstorageFreePercentage(cros)
for field in reverseLists:
if field in cros:
cros[field].reverse()
if 'orgUnitId' in cros:
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(cros, timeObjects=CROS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
continue
printEntity([Ent.CROS_DEVICE, deviceId], i, count)
Ind.Increment()
for up in CROS_SCALAR_PROPERTY_PRINT_ORDER:
if up in cros:
if up not in CROS_TIME_OBJECTS:
if up not in CROS_FIELDS_WITH_CRS_NLS:
printKeyValueList([up, cros[up]])
else:
printKeyValueWithCRsNLs(up, cros[up])
else:
printKeyValueList([up, formatLocalTime(cros[up])])
up = 'tpmVersionInfo'
if up in cros:
printKeyValueList([up, ''])
Ind.Increment()
for key, value in sorted(iter(cros[up].items())):
printKeyValueList([key, value])
Ind.Decrement()
if not noLists:
activeTimeRanges = _filterActiveTimeRanges(cros, True, listLimit, startDate, endDate, activeTimeRangesOrder)
if activeTimeRanges:
printKeyValueList(['activeTimeRanges'])
Ind.Increment()
for activeTimeRange in activeTimeRanges:
printKeyValueList(['date', activeTimeRange['date']])
Ind.Increment()
for key in ['activeTime', 'duration', 'minutes']:
printKeyValueList([key, activeTimeRange[key]])
Ind.Decrement()
Ind.Decrement()
recentUsers = _filterRecentUsers(cros, True, listLimit)
if recentUsers:
printKeyValueList(['recentUsers'])
Ind.Increment()
for recentUser in recentUsers:
printKeyValueList(['type', recentUser['type']])
Ind.Increment()
printKeyValueList(['email', recentUser['email']])
Ind.Decrement()
Ind.Decrement()
deviceFiles = _filterDeviceFiles(cros, True, listLimit, startTime, endTime)
if deviceFiles:
printKeyValueList(['deviceFiles'])
Ind.Increment()
for deviceFile in deviceFiles:
printKeyValueList([deviceFile['type'], deviceFile['createTime']])
Ind.Decrement()
if downloadfile:
if downloadfile == 'latest':
deviceFile = deviceFiles[-1]
else:
for deviceFile in deviceFiles:
if deviceFile['createTime'] == downloadfile:
break
else:
deviceFile = None
if deviceFile:
downloadfilename = os.path.join(targetFolder, f'cros-logs-{deviceId}-{deviceFile["createTime"]}.zip')
_, content = cd._http.request(deviceFile['downloadUrl'])
writeFile(downloadfilename, content, mode='wb', continueOnError=True)
printKeyValueList(['Downloaded', downloadfilename])
else:
Act.Set(Act.DOWNLOAD)
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId, Ent.DEVICE_FILE, downloadfile],
Msg.DOES_NOT_EXIST, i, count)
Act.Set(Act.INFO)
elif downloadfile:
Act.Set(Act.DOWNLOAD)
entityActionNotPerformedWarning([Ent.CROS_DEVICE, deviceId, Ent.DEVICE_FILE, downloadfile],
Msg.NO_ENTITIES_FOUND.format(Ent.Plural(Ent.DEVICE_FILE)), i, count)
Act.Set(Act.INFO)
cpuStatusReports = _filterCPUStatusReports(cros, True, listLimit, startTime, endTime)
if cpuStatusReports:
printKeyValueList(['cpuStatusReports'])
Ind.Increment()
for cpuStatusReport in cpuStatusReports:
printKeyValueList(['reportTime', formatLocalTime(cpuStatusReport['reportTime'])])
Ind.Increment()
if 'cpuTemperatureInfo' in cpuStatusReport:
printKeyValueList(['cpuTemperatureInfo'])
Ind.Increment()
for tempInfo in cpuStatusReport.get('cpuTemperatureInfo', []):
printKeyValueList([tempInfo['label'], tempInfo['temperature']])
Ind.Decrement()
if 'cpuUtilizationPercentageInfo' in cpuStatusReport:
printKeyValueList(['cpuUtilizationPercentageInfo', cpuStatusReport['cpuUtilizationPercentageInfo']])
Ind.Decrement()
Ind.Decrement()
diskVolumeReports = _filterBasicList(cros, 'diskVolumeReports', True, listLimit)
if diskVolumeReports:
printKeyValueList(['diskVolumeReports'])
Ind.Increment()
printKeyValueList(['volumeInfo'])
for diskVolumeReport in diskVolumeReports:
volumeInfo = diskVolumeReport['volumeInfo']
Ind.Increment()
for volume in volumeInfo:
printKeyValueList(['volumeId', volume['volumeId']])
Ind.Increment()
printKeyValueList(['storageFree', volume['storageFree']])
printKeyValueList(['storageTotal', volume['storageTotal']])
if showDVRstorageFreePercentage:
printKeyValueList(['storageFreePercentage', volume['storageFreePercentage']])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
lastKnownNetworks = _filterBasicList(cros, 'lastKnownNetwork', True, listLimit)
if lastKnownNetworks:
printKeyValueList(['lastKnownNetwork'])
Ind.Increment()
for lastKnownNetwork in lastKnownNetworks:
printKeyValueList(['ipAddress', lastKnownNetwork['ipAddress']])
Ind.Increment()
printKeyValueList(['wanIpAddress', lastKnownNetwork['wanIpAddress']])
Ind.Decrement()
Ind.Decrement()
screenshotFiles = _filterScreenshotFiles(cros, True, listLimit, startTime, endTime)
if screenshotFiles:
printKeyValueList(['screenshotFiles'])
Ind.Increment()
for screenshotFile in screenshotFiles:
printKeyValueList(['name', screenshotFile['name']])
Ind.Increment()
printKeyValueList(['type', screenshotFile['type']])
printKeyValueList(['downloadUrl', screenshotFile['downloadUrl']])
printKeyValueList(['createTime', screenshotFile['createTime']])
Ind.Decrement()
Ind.Decrement()
systemRamFreeReports = _filterSystemRamFreeReports(cros, True, listLimit, startTime, endTime)
if systemRamFreeReports:
printKeyValueList(['systemRamFreeReports'])
Ind.Increment()
for systemRamFreeReport in systemRamFreeReports:
printKeyValueList(['reportTime', systemRamFreeReport['reportTime']])
Ind.Increment()
printKeyValueList(['systemRamFreeInfo', systemRamFreeReport['systemRamFreeInfo']])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
def doInfoCrOSDevices():
infoCrOSDevices(getCrOSDeviceEntity())
def getDeviceFilesEntity():
deviceFilesEntity = {'list': [], 'dict': None, 'count': None, 'time': None, 'range': None}
startEndTime = StartEndTime()
entitySelector = getEntitySelector()
if entitySelector:
entityList = getEntitySelection(entitySelector, False)
if isinstance(entityList, dict):
deviceFilesEntity['dict'] = entityList
else:
deviceFilesEntity['list'] = entityList
else:
myarg = getString(Cmd.OB_DEVICE_FILE_ENTITY, checkBlank=True)
mycmd = myarg.lower()
if mycmd in {'first', 'last', 'allexceptfirst', 'allexceptlast'}:
deviceFilesEntity['count'] = (mycmd, getInteger(minVal=1))
elif mycmd in {'before', 'after'}:
dateTime, _, _ = getTimeOrDeltaFromNow(True)
deviceFilesEntity['time'] = (mycmd, dateTime)
elif mycmd == 'range':
startEndTime.Get(mycmd)
deviceFilesEntity['range'] = (mycmd, startEndTime.startDateTime, startEndTime.endDateTime)
else:
for timeItem in myarg.split(','):
try:
timestamp, _ = iso8601.parse_date(timeItem)
deviceFilesEntity['list'].append(ISOformatTimeStamp(timestamp.astimezone(GC.Values[GC.TIMEZONE])))
except (iso8601.ParseError, OverflowError):
Cmd.Backup()
invalidArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
return deviceFilesEntity
def _selectDeviceFiles(deviceId, deviceFiles, deviceFilesEntity):
numDeviceFiles = len(deviceFiles)
if numDeviceFiles == 0:
return deviceFiles
for deviceFile in deviceFiles:
deviceFile['createTime'] = formatLocalTime(deviceFile['createTime'])
if deviceFilesEntity['count']:
countType = deviceFilesEntity['count'][0]
count = deviceFilesEntity['count'][1]
if countType == 'first':
return deviceFiles[:count]
if countType == 'last':
return deviceFiles[-count:]
if countType == 'allexceptfirst':
return deviceFiles[count:]
# if countType == 'allexceptlast':
return deviceFiles[:-count]
if deviceFilesEntity['time']:
dateTime = deviceFilesEntity['time'][1]
count = 0
if deviceFilesEntity['time'][0] == 'before':
for deviceFile in deviceFiles:
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
if createTime >= dateTime:
break
count += 1
return deviceFiles[:count]
# if deviceFilesEntity['time'][0] == 'after':
for deviceFile in deviceFiles:
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
if createTime >= dateTime:
break
count += 1
return deviceFiles[count:]
if deviceFilesEntity['range']:
dateTime = deviceFilesEntity['range'][1]
spos = 0
for deviceFile in deviceFiles:
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
if createTime >= dateTime:
break
spos += 1
dateTime = deviceFilesEntity['range'][2]
epos = spos
for deviceFile in deviceFiles[spos:]:
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
if createTime >= dateTime:
break
epos += 1
return deviceFiles[spos:epos]
# if deviceFilesEntity['range'] or deviceFilesEntity['dict']:
if deviceFilesEntity['dict']:
deviceFileCreateTimes = deviceFilesEntity['dict'][deviceId]
else:
deviceFileCreateTimes = deviceFilesEntity['list']
return [deviceFile for deviceFile in deviceFiles if deviceFile['createTime'] in deviceFileCreateTimes]
# gam <CrOSTypeEntity> get devicefile [select <DeviceFileEntity>] [targetfolder <FilePath>]
def getCrOSDeviceFiles(entityList):
cd = buildGAPIObject(API.DIRECTORY)
targetFolder = GC.Values[GC.DRIVE_DIR]
deviceFilesEntity = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'select':
deviceFilesEntity = getDeviceFilesEntity()
elif myarg == 'targetfolder':
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
else:
unknownArgumentExit()
fields = 'deviceFiles(type,createTime,downloadUrl)'
i, count, entityList = getEntityArgument(entityList)
for deviceId in entityList:
i += 1
try:
deviceFiles = callGAPIitems(cd.chromeosdevices(), 'get', 'deviceFiles',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, fields=fields)
if deviceFilesEntity:
deviceFiles = _selectDeviceFiles(deviceId, deviceFiles, deviceFilesEntity)
jcount = len(deviceFiles)
entityPerformActionNumItems([Ent.CROS_DEVICE, deviceId], jcount, Ent.DEVICE_FILE, i, count)
Ind.Increment()
j = 0
for deviceFile in deviceFiles:
j += 1
downloadfilename = os.path.join(targetFolder, f'cros-logs-{deviceId}-{deviceFile["createTime"]}.zip')
_, content = cd._http.request(deviceFile['downloadUrl'])
writeFile(downloadfilename, content, mode='wb', continueOnError=True)
entityActionPerformed([Ent.DEVICE_FILE, downloadfilename], j, jcount)
Ind.Decrement()
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, deviceId, i, count)
# gam get devicefile <CrOSEntity> [select <DeviceFileEntity>] [targetfolder <FilePath>]
def doGetCrOSDeviceFiles():
getCrOSDeviceFiles(getCrOSDeviceEntity())
def substituteQueryTimes(queries, queryTimes):
if queryTimes:
for i, query in enumerate(queries):
if query is not None:
for queryTimeName, queryTimeValue in iter(queryTimes.items()):
query = query.replace(f'#{queryTimeName}#', queryTimeValue)
queries[i] = query
# Get CrOS devices from gam.cfg print_cros_ous and print_cros_ous_and_children
def getCfgCrOSEntities():
if GC.Values[GC.PRINT_CROS_OUS]:
entityList = getItemsToModify(Cmd.ENTITY_CROS_OUS, GC.Values[GC.PRINT_CROS_OUS])
else:
entityList = []
if GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]:
entityList.extend(getItemsToModify(Cmd.ENTITY_CROS_OUS_AND_CHILDREN, GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]))
return entityList
CROS_ORDERBY_CHOICE_MAP = {
'lastsync': 'lastSync',
'location': 'annotatedLocation',
'notes': 'notes',
'serialnumber': 'serialNumber',
'status': 'status',
'supportenddate': 'supportEndDate',
'user': 'annotatedUser',
}
CROS_ENTITIES_MAP = {
'crosorg': Cmd.ENTITY_CROS_OU,
'crosorgandchildren': Cmd.ENTITY_CROS_OU_AND_CHILDREN,
'crosorgs': Cmd.ENTITY_CROS_OUS,
'crosorgsandchildren': Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
'crosou': Cmd.ENTITY_CROS_OU,
'crosouandchildren': Cmd.ENTITY_CROS_OU_AND_CHILDREN,
'crosous': Cmd.ENTITY_CROS_OUS,
'crosousandchildren': Cmd.ENTITY_CROS_OUS_AND_CHILDREN
}
CROS_INDEXED_TITLES = ['activeTimeRanges', 'recentUsers', 'deviceFiles',
'cpuStatusReports', 'diskVolumeReports', 'lastKnownNetwork', 'screenshotFiles', 'systemRamFreeReports']
# gam print cros [todrive <ToDriveAttribute>*]
# [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
# [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
# (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
# [showitemcountonly]
# gam print cros [todrive <ToDriveAttribute>*] select <CrOSTypeEntity>
# gam <CrOSTypeEntity> print cros [todrive <ToDriveAttribute>*]
# [orderby <CrOSOrderByFieldName> [ascending|descending]]
# [basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
# [nolists|(<CrOSListFieldName>* [onerow])]
# [start <Date>] [end <Date>] [listlimit <Number>]
# [reverselists <CrOSListFieldNameList>]
# [timerangeorder ascending|descending] [showdvrsfp]
# (addcsvdata <FieldName> <String>)*
# [sortheaders]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintCrOSDevices(entityList=None):
def _printCrOS(cros):
checkTPMVulnerability(cros)
if 'autoUpdateExpiration' in cros:
cros['autoUpdateExpiration'] = formatLocalDatestamp(cros['autoUpdateExpiration'])
if showDVRstorageFreePercentage:
_computeDVRstorageFreePercentage(cros)
for field in reverseLists:
if field in cros:
cros[field].reverse()
if 'orgUnitId' in cros:
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
if addCSVData:
cros.update(addCSVData)
if FJQC.formatJSON:
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS)):
csvPF.WriteRowNoFilter({'deviceId': cros['deviceId'],
'JSON': json.dumps(cleanJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
return
if 'notes' in cros:
cros['notes'] = escapeCRsNLs(cros['notes'])
for cpuStatusReport in cros.get('cpuStatusReports', []):
for tempInfo in cpuStatusReport.get('cpuTemperatureInfo', []):
tempInfo['label'] = tempInfo['label'].strip()
if not noLists and not selectedLists:
csvPF.WriteRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS))
return
attrib = 'tpmVersionInfo'
if attrib in cros:
for key, value in sorted(iter(cros[attrib].items())):
attribKey = f'{attrib}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'
cros[attribKey] = value
cros.pop(attrib)
activeTimeRanges = _filterActiveTimeRanges(cros, selectedLists.get('activeTimeRanges', False), listLimit, startDate, endDate, activeTimeRangesOrder)
recentUsers = _filterRecentUsers(cros, selectedLists.get('recentUsers', False), listLimit)
deviceFiles = _filterDeviceFiles(cros, selectedLists.get('deviceFiles', False), listLimit, startTime, endTime)
cpuStatusReports = _filterCPUStatusReports(cros, selectedLists.get('cpuStatusReports', False), listLimit, startTime, endTime)
diskVolumeReports = _filterBasicList(cros, 'diskVolumeReports', selectedLists.get('diskVolumeReports', False), listLimit)
lastKnownNetworks = _filterBasicList(cros, 'lastKnownNetwork', selectedLists.get('lastKnownNetwork', False), listLimit)
screenshotFiles = _filterScreenshotFiles(cros, selectedLists.get('screenshotFiles', False), listLimit, startTime, endTime)
systemRamFreeReports = _filterSystemRamFreeReports(cros, selectedLists.get('systemRamFreeReports', False), listLimit, startTime, endTime)
if oneRow:
csvPF.WriteRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS))
return
row = {}
for attrib in cros:
if attrib not in {'kind', 'etag', 'tpmVersionInfo', 'recentUsers', 'activeTimeRanges',
'deviceFiles', 'cpuStatusReports', 'diskVolumeReports', 'lastKnownNetwork', 'screenshotFiles', 'systemRamFreeReports'}:
if attrib not in CROS_TIME_OBJECTS:
row[attrib] = cros[attrib]
else:
row[attrib] = formatLocalTime(cros[attrib])
if addCSVData:
row.update(addCSVData)
if noLists or (not activeTimeRanges and not recentUsers and not deviceFiles and
not cpuStatusReports and not diskVolumeReports and not lastKnownNetworks and not screenshotFiles and not systemRamFreeReports):
csvPF.WriteRowTitles(row)
return
lenATR = len(activeTimeRanges)
lenRU = len(recentUsers)
lenDF = len(deviceFiles)
lenCSR = len(cpuStatusReports)
lenDVR = len(diskVolumeReports)
lenLKN = len(lastKnownNetworks)
lenSSF = len(screenshotFiles)
lenSRFR = len(systemRamFreeReports)
new_row = row
for i in range(min(max(lenATR, lenRU, lenDF, lenCSR, lenDVR, lenLKN, lenSSF, lenSRFR), listLimit or max(lenATR, lenRU, lenDF, lenCSR, lenDVR, lenLKN, lenSSF, lenSRFR))):
new_row = row.copy()
if i < lenATR:
for key in ['date', 'activeTime', 'duration', 'minutes']:
new_row[f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = activeTimeRanges[i][key]
if i < lenRU:
for key in ['email', 'type']:
new_row[f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = recentUsers[i][key]
if i < lenDF:
for key in ['type', 'createTime']:
new_row[f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = deviceFiles[i][key]
if i < lenCSR:
new_row[f'cpuStatusReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}reportTime'] = cpuStatusReports[i]['reportTime']
for tempInfo in cpuStatusReports[i].get('cpuTemperatureInfo', []):
new_row[f'cpuStatusReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}cpuTemperatureInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{tempInfo["label"]}'] = tempInfo['temperature']
if 'cpuUtilizationPercentageInfo' in cpuStatusReports[i]:
new_row[f'cpuStatusReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}cpuUtilizationPercentageInfo'] = cpuStatusReports[i]['cpuUtilizationPercentageInfo']
if i < lenDVR:
j = 0
for volume in diskVolumeReports[i]['volumeInfo']:
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeId'] = volume['volumeId']
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}storageFree'] = volume['storageFree']
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}storageTotal'] = volume['storageTotal']
if showDVRstorageFreePercentage:
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}storageFreePercentage'] = volume['storageFreePercentage']
j += 1
if i < lenLKN:
for key in ['ipAddress', 'wanIpAddress']:
if key in lastKnownNetworks[i]:
new_row[f'lastKnownNetwork{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = lastKnownNetworks[i][key]
if i < lenSSF:
for key in ['name', 'type', 'downloadUrl', 'createTime']:
if key in screenshotFiles[i]:
new_row[f'screenshotFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = screenshotFiles[i][key]
if i < lenSRFR:
for key in ['reportTime', 'systemRamFreeInfo']:
new_row[f'systemRamFreeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = systemRamFreeReports[i][key]
csvPF.WriteRowTitles(new_row)
def _callbackPrintCrOS(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
_printCrOS(response)
else:
http_status, reason, message = checkGAPIError(exception)
if reason in [GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN]:
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.CROS_DEVICE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
cd = buildGAPIObject(API.DIRECTORY)
fieldsList = ['deviceId']
reverseLists = []
csvPF = CSVPrintFile(fieldsList, indexedTitles=CROS_INDEXED_TITLES)
FJQC = FormatJSONQuoteChar(csvPF)
projection = orderBy = sortOrder = None
ous = ['/']
includeChildOrgunits = True
queries = [None]
listLimit = 0
startDate = endDate = startTime = endTime = None
selectedLists = {}
queryTimes = {}
selectionAllowed = entityList is None
if selectionAllowed and (GC.Values[GC.PRINT_CROS_OUS] or GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]):
entityList = getCfgCrOSEntities()
selectionAllowed = False
allFields = noLists = oneRow = showDVRstorageFreePercentage = sortHeaders = False
activeTimeRangesOrder = 'ASCENDING'
showItemCountOnly = False
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif selectionAllowed and myarg == 'limittoou':
ous = [getOrgUnitItem()]
selectionAllowed = False
includeChildOrgunits = False
elif selectionAllowed and myarg in CROS_ENTITIES_MAP:
myarg = CROS_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_CROS_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN])
selectionAllowed = False
includeChildOrgunits = myarg in {Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS_AND_CHILDREN}
elif (selectionAllowed or queries == [None]) and myarg in {'query', 'queries'}:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif selectionAllowed and myarg == 'select':
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_CROS, crosAllowed=True, userAllowed=False)
selectionAllowed = False
elif myarg == 'orderby':
orderBy, sortOrder = getOrderBySortOrder(CROS_ORDERBY_CHOICE_MAP, 'DESCENDING', True)
elif myarg == 'onerow':
oneRow = True
elif myarg == 'nolists':
noLists = True
selectedLists = {}
elif myarg == 'listlimit':
listLimit = getInteger(minVal=0)
elif myarg in CROS_START_ARGUMENTS:
startDate, startTime = _getFilterDateTime()
elif myarg in CROS_END_ARGUMENTS:
endDate, endTime = _getFilterDateTime()
elif myarg == 'timerangeorder':
activeTimeRangesOrder = getChoice(SORTORDER_CHOICE_MAP, mapChoice=True)
elif myarg in PROJECTION_CHOICE_MAP:
projection = PROJECTION_CHOICE_MAP[myarg]
sortHeaders = True
if projection == 'FULL':
fieldsList = []
else:
fieldsList = CROS_BASIC_FIELDS_LIST[:]
elif myarg == 'allfields':
projection = 'FULL'
allFields = sortHeaders = True
fieldsList = []
elif myarg == 'sortheaders':
sortHeaders = getBoolean()
elif myarg in CROS_LIST_FIELDS_CHOICE_MAP:
selectedLists[CROS_LIST_FIELDS_CHOICE_MAP[myarg]] = True
elif myarg in CROS_FIELDS_CHOICE_MAP:
csvPF.AddField(myarg, CROS_FIELDS_CHOICE_MAP, fieldsList)
elif myarg == 'fields':
for field in _getFieldsList():
if field in CROS_FIELDS_CHOICE_MAP:
if field in CROS_LIST_FIELDS_CHOICE_MAP:
selectedLists[CROS_LIST_FIELDS_CHOICE_MAP[field]] = True
else:
csvPF.AddField(field, CROS_FIELDS_CHOICE_MAP, fieldsList)
else:
invalidChoiceExit(field, CROS_FIELDS_CHOICE_MAP, True)
elif myarg == 'reverselists':
for field in _getFieldsList():
if field in CROS_LIST_FIELDS_CHOICE_MAP:
reverseLists.append(CROS_LIST_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, CROS_LIST_FIELDS_CHOICE_MAP, True)
elif myarg == 'showdvrsfp':
showDVRstorageFreePercentage = True
elif myarg == 'showitemcountonly':
showItemCountOnly = True
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if selectedLists:
noLists = False
projection = 'FULL'
for selectList in selectedLists:
addFieldToFieldsList(selectList, CROS_FIELDS_CHOICE_MAP, fieldsList)
if fieldsList:
fieldsList.append('deviceId')
_, _, entityList = getEntityArgument(entityList)
if FJQC.formatJSON:
sortHeaders = False
csvPF.SetJSONTitles(['deviceId', 'JSON'])
substituteQueryTimes(queries, queryTimes)
if entityList is None:
sortRows = False
fields = getItemFieldsFromFieldsList('chromeosdevices', fieldsList)
for ou in ous:
ou = makeOrgUnitPathAbsolute(ou)
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if not includeChildOrgunits else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
for query in queries:
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, ou,
query=query, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
pageToken = None
totalItems = 0
tokenRetries = 0
while True:
try:
feed = callGAPI(cd.chromeosdevices(), 'list',
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageToken=pageToken,
customerId=GC.Values[GC.CUSTOMER_ID], query=query, projection=projection,
orgUnitPath=ou, includeChildOrgunits=includeChildOrgunits,
orderBy=orderBy, sortOrder=sortOrder, fields=fields, maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
tokenRetries = 0
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
if feed:
if not showItemCountOnly:
for cros in feed.get('chromeosdevices', []):
_printCrOS(cros)
del feed
if not pageToken:
_finalizeGAPIpagesResult(pageMessage)
printGotAccountEntities(totalItems)
break
except GAPI.invalidInput as e:
message = str(e)
# Invalid Input: xyz - Check for invalid pageToken!!
# 0123456789012345
if message[15:] == pageToken:
tokenRetries += 1
if tokenRetries <= 2:
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
time.sleep(tokenRetries*5)
continue
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
return
entityActionFailedWarning([Ent.CROS_DEVICE, None], invalidQuery(query) if query is not None else message)
return
except GAPI.invalidOrgunit as e:
entityActionFailedWarning([Ent.CROS_DEVICE, None], str(e))
return
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
if showItemCountOnly:
writeStdout(f'{totalItems}\n')
return
else:
if showItemCountOnly:
writeStdout(f'{len(entityList)}\n')
return
sortRows = True
if allFields or len(set(fieldsList)) > 1:
jcount = len(entityList)
fields = getFieldsFromFieldsList(fieldsList)
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]), ('deviceId', None), ('projection', projection), ('fields', fields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.chromeosdevices(), 'get')
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
bcount = 0
j = 0
for deviceId in entityList:
j += 1
svcparms = svcargs.copy()
svcparms['deviceId'] = deviceId
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, deviceId))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
bcount = 0
if bcount > 0:
dbatch.execute()
# The only field specified was deviceId, just list the CrOS devices
else:
for cros in entityList:
_printCrOS({'deviceId': cros})
if sortRows and orderBy:
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
if sortHeaders:
csvPF.SetSortTitles(['deviceId'])
csvPF.writeCSVfile('CrOS')
CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP = {
'activetimeranges': 'activeTimeRanges',
'devicefiles': 'deviceFiles',
'files': 'deviceFiles',
'recentusers': 'recentUsers',
'timeranges': 'activeTimeRanges',
'times': 'activeTimeRanges',
'users': 'recentUsers',
}
CROS_ACTIVITY_TIME_OBJECTS = {'createTime'}
# gam print crosactivity [todrive <ToDriveAttribute>*]
# [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
# [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
# (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
# gam print crosactivity [todrive <ToDriveAttribute>*] select <CrOSTypeEntity>
# gam <CrOSTypeEntity> print crosactivity [todrive <ToDriveAttribute>*]
# [orderby <CrOSOrderByFieldName> [ascending|descending]]
# [recentusers] [timeranges] [both] [devicefiles] [all] [oneuserperrow]
# [start <Date>] [end <Date>] [listlimit <Number>]
# [reverselists <CrOSActivityListFieldNameList>]
# [timerangeorder ascending|descending]
# [delimiter <Character>]
# [formatjson [quotechar <Character>]]
def doPrintCrOSActivity(entityList=None):
def _printCrOS(cros):
row = {}
for field in reverseLists:
if field in cros:
cros[field].reverse()
if 'orgUnitId' in cros:
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
if FJQC.formatJSON:
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_ACTIVITY_TIME_OBJECTS)):
csvPF.WriteRowNoFilter({'deviceId': cros['deviceId'],
'JSON': json.dumps(cleanJSON(cros, timeObjects=CROS_ACTIVITY_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
return
for attrib in cros:
if attrib not in {'recentUsers', 'activeTimeRanges', 'deviceFiles'}:
row[attrib] = cros[attrib]
for activeTimeRange in _filterActiveTimeRanges(cros, selectedLists.get('activeTimeRanges', False), listLimit, startDate, endDate, activeTimeRangesOrder):
new_row = row.copy()
for key in ['date', 'duration', 'minutes']:
new_row[f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = activeTimeRange[key]
csvPF.WriteRow(new_row)
recentUsers = _filterRecentUsers(cros, selectedLists.get('recentUsers', False), listLimit)
if recentUsers:
if not oneUserPerRow:
new_row = row.copy()
new_row[f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}email'] = delimiter.join([recentUser['email'] for recentUser in recentUsers])
csvPF.WriteRow(new_row)
else:
for recentUser in recentUsers:
new_row = row.copy()
new_row[f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}email'] = recentUser['email']
csvPF.WriteRow(new_row)
for deviceFile in _filterDeviceFiles(cros, selectedLists.get('deviceFiles', False), listLimit, startTime, endTime):
new_row = row.copy()
for key in ['type', 'createTime']:
new_row[f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = deviceFile[key]
csvPF.WriteRow(new_row)
def _callbackPrintCrOS(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
_printCrOS(response)
else:
http_status, reason, message = checkGAPIError(exception)
if reason in [GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN]:
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.CROS_DEVICE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
cd = buildGAPIObject(API.DIRECTORY)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
fieldsList = ['deviceId', 'annotatedAssetId', 'annotatedLocation', 'serialNumber', 'orgUnitId', 'orgUnitPath']
reverseLists = []
csvPF = CSVPrintFile(fieldsList)
FJQC = FormatJSONQuoteChar(csvPF)
projection = 'FULL'
orderBy = sortOrder = None
ous = [None]
queries = [None]
listLimit = 0
startDate = endDate = startTime = endTime = None
selectedLists = {}
queryTimes = {}
selectionAllowed = entityList is None
if selectionAllowed and (GC.Values[GC.PRINT_CROS_OUS] or GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]):
entityList = getCfgCrOSEntities()
selectionAllowed = False
oneUserPerRow = False
directlyInOU = True
activeTimeRangesOrder = 'ASCENDING'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif selectionAllowed and myarg == 'limittoou':
ous = [getOrgUnitItem()]
selectionAllowed = False
directlyInOU = True
elif selectionAllowed and myarg in CROS_ENTITIES_MAP:
myarg = CROS_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_CROS_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN])
selectionAllowed = False
directlyInOU = myarg in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OUS}
elif (selectionAllowed or queries == [None]) and myarg in {'query', 'queries'}:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif selectionAllowed and myarg == 'select':
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_CROS, crosAllowed=True, userAllowed=False)
selectionAllowed = False
elif myarg == 'oneuserperrow':
oneUserPerRow = True
elif myarg == 'listlimit':
listLimit = getInteger(minVal=0)
elif myarg in CROS_START_ARGUMENTS:
startDate, startTime = _getFilterDateTime()
elif myarg in CROS_END_ARGUMENTS:
endDate, endTime = _getFilterDateTime()
elif myarg == 'timerangeorder':
activeTimeRangesOrder = getChoice(SORTORDER_CHOICE_MAP, mapChoice=True)
elif myarg in CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP:
selectedLists[CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP[myarg]] = True
elif myarg == 'both':
selectedLists['activeTimeRanges'] = selectedLists['recentUsers'] = True
elif myarg == 'all':
selectedLists['activeTimeRanges'] = selectedLists['recentUsers'] = selectedLists['deviceFiles'] = True
elif myarg == 'reverselists':
for field in _getFieldsList():
if field in CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP:
reverseLists.append(CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP, True)
elif myarg == 'orderby':
orderBy, sortOrder = getOrderBySortOrder(CROS_ORDERBY_CHOICE_MAP, 'DESCENDING', True)
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if not selectedLists:
selectedLists['activeTimeRanges'] = selectedLists['recentUsers'] = True
if selectedLists.get('recentUsers', False):
fieldsList.append('recentUsers')
csvPF.AddTitles(f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}email')
if selectedLists.get('activeTimeRanges', False):
fieldsList.append('activeTimeRanges')
csvPF.AddTitles([f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}date',
f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}duration',
f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}minutes'])
if selectedLists.get('deviceFiles', False):
fieldsList.append('deviceFiles')
csvPF.AddTitles([f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}type',
f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}createTime'])
_, _, entityList = getEntityArgument(entityList)
if FJQC.formatJSON:
csvPF.SetJSONTitles(['deviceId', 'JSON'])
substituteQueryTimes(queries, queryTimes)
if entityList is None:
sortRows = False
fields = getItemFieldsFromFieldsList('chromeosdevices', fieldsList)
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
ouList = [ou]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([subou['orgUnitPath'] for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
for subou in ouList:
if subou is not None:
orgUnitPath = makeOrgUnitPathAbsolute(subou)
else:
orgUnitPath = subou
for query in queries:
if orgUnitPath is not None:
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, orgUnitPath, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
else:
printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
pageMessage = getPageMessage()
pageToken = None
totalItems = 0
tokenRetries = 0
while True:
try:
feed = callGAPI(cd.chromeosdevices(), 'list',
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageToken=pageToken,
customerId=GC.Values[GC.CUSTOMER_ID], query=query, projection=projection, orgUnitPath=orgUnitPath,
orderBy=orderBy, sortOrder=sortOrder, fields=fields, maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
tokenRetries = 0
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
if feed:
for cros in feed.get('chromeosdevices', []):
_printCrOS(cros)
del feed
if not pageToken:
_finalizeGAPIpagesResult(pageMessage)
printGotAccountEntities(totalItems)
break
except GAPI.invalidInput as e:
message = str(e)
# Invalid Input: xyz - Check for invalid pageToken!!
# 0123456789012345
if message[15:] == pageToken:
tokenRetries += 1
if tokenRetries <= 2:
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
time.sleep(tokenRetries*5)
continue
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
return
entityActionFailedWarning([Ent.CROS_DEVICE, None], invalidQuery(query) if query is not None else message)
return
except GAPI.invalidOrgunit as e:
entityActionFailedWarning([Ent.CROS_DEVICE, None], str(e))
return
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
else:
sortRows = True
jcount = len(entityList)
fields = getFieldsFromFieldsList(fieldsList)
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]), ('deviceId', None), ('projection', projection), ('fields', fields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.chromeosdevices(), 'get')
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
bcount = 0
j = 0
for deviceId in entityList:
j += 1
svcparms = svcargs.copy()
svcparms['deviceId'] = deviceId
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, deviceId))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
bcount = 0
if bcount > 0:
dbatch.execute()
if sortRows and orderBy:
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
csvPF.writeCSVfile('CrOS Activity')
# gam <CrOSTypeEntity> print [cros|croses|crosactivity]
def doPrintCrOSEntity(entityList):
if getChoice([Cmd.ARG_CROS, Cmd.ARG_CROSES, Cmd.ARG_CROSACTIVITY], defaultChoice=None) != Cmd.ARG_CROSACTIVITY:
if not Cmd.ArgumentsRemaining():
writeEntityNoHeaderCSVFile(Ent.CROS_DEVICE, entityList)
else:
doPrintCrOSDevices(entityList)
else:
doPrintCrOSActivity(entityList)
CROS_TELEMETRY_FIELDS_CHOICE_MAP = {
'audiostatusreport': 'audioStatusReport',
'batteryinfo': 'batteryInfo',
'batterystatusreport': 'batteryStatusReport',
'bootperformancereport': 'bootPerformanceReport',
'cpuinfo': 'cpuInfo',
'cpustatusreport': 'cpuStatusReport',
'customer': 'customer',
'deviceid': 'deviceId',
'graphicsinfo': 'graphicsInfo',
'graphicsstatusreport': 'graphicsStatusReport',
'memoryinfo': 'memoryInfo',
'memorystatusreport': 'memoryStatusReport',
'name': 'name',
'networkinfo': 'networkInfo',
'networkdiagnosticsreport': 'networkDiagnosticsReport',
'networkstatusreport': 'networkStatusReport',
'orgunitid': 'orgUnitId',
'osupdatestatus': 'osUpdateStatus',
'peripheralsreport': 'peripheralsReport',
'serialnumber': 'serialNumber',
'storageinfo': 'storageInfo',
'storagestatusreport': 'storageStatusReport',
'thunderboltinfo': 'thunderboltInfo',
}
CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP = {
'audiostatusreport': 'audioStatusReport',
'batteryinfo': 'batteryInfo',
'batterystatusreport': 'batteryStatusReport',
'bootperformancereport': 'bootPerformanceReport',
'cpuinfo': 'cpuInfo',
'cpustatusreport': 'cpuStatusReport',
'graphicsstatusreport': 'graphicsStatusReport',
'memorystatusreport': 'memoryStatusReport',
'networkdiagnosticsreport': 'networkDiagnosticsReport',
'networkstatusreport': 'networkStatusReport',
'osupdatestatus': 'osUpdateStatus',
'peripheralsreport': 'peripheralsReport',
'storagestatusreport': 'storageStatusReport',
'thunderboltinfo': 'thunderboltInfo',
}
CROS_TELEMETRY_SCALAR_FIELDS = ['deviceId', 'serialNumber', 'customer', 'name', 'orgUnitId', 'orgUnitPath']
CROS_TELEMETRY_SCALAR_FIELDS_SET = set(CROS_TELEMETRY_SCALAR_FIELDS)
CROS_TELEMETRY_LIST_FIELDS = list(CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP.values())
CROS_TELEMETRY_TIME_OBJECTS = {'reportTime', 'lastUpdateTime', 'lastUpdateCheckTime', 'lastRebootTime'}
# gam info crostelemetry <SerialNumber>
# <CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
# [start <Date>] [end <Date>] [listlimit <Number>]
# [reverselists <CrOSTelemetryListFieldNameList>]
# [formatjson [quotechar <Character>]]
# gam show crostelemetry
# [(ou|org|orgunit|ou_and_children <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
# <CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
# [start <Date>] [end <Date>] [listlimit <Number>]
# [reverselists <CrOSTelemetryListFieldNameList>]
# [formatjson [quotechar <Character>]]
# gam print crostelemetry [todrive <ToDriveAttribute>*]
# [(ou|org|orgunit|ou_and_children <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
# <CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
# [reverselists <CrOSTelemetryListFieldNameList>]
# [start <Date>] [end <Date>] [listlimit <Number>]
# [formatjson [quotechar <Character>]]
def doInfoPrintShowCrOSTelemetry():
def _cleanDevice(device):
for field in reverseLists:
if field in device:
device[field].reverse()
if listLimit or startTime or endTime:
for field in CROS_TELEMETRY_LIST_FIELDS:
if field in device:
listItems = device.pop(field)
device[field] = []
i = 0
for item in listItems:
if 'reportTime' in item:
timeValue, _ = iso8601.parse_date(item['reportTime'])
else:
timeValue = None
if (timeValue is None) or (((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime))):
device[field].append(item)
i += 1
if listLimit and i == listLimit:
break
storageInfo = device.get('storageInfo', {})
if 'totalDiskBytes' in storageInfo and 'availableDiskBytes' in storageInfo:
disk_avail = int(storageInfo['availableDiskBytes'])
disk_size = int(storageInfo['totalDiskBytes'])
if diskPercentOnly:
device['storageInfo'] = {}
device['storageInfo']['percentDiskFree'] = int((disk_avail / disk_size) * 100)
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
for cpuStatusReport in device.get('cpuStatusReport', []):
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
if 'temperatureCelsius' in tempInfo:
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
if showOrgUnitPath:
device['orgUnitPath'] = convertOrgUnitIDtoPath(cd, device['orgUnitId'])
def _printDevice(device):
_cleanDevice(device)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(device, timeObjects=CROS_TELEMETRY_TIME_OBJECTS))
else:
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(device, timeObjects=CROS_TELEMETRY_TIME_OBJECTS)):
csvPF.WriteRowNoFilter({'deviceId': device['deviceId'],
'JSON': json.dumps(cleanJSON(device, timeObjects=CROS_TELEMETRY_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
def _showDevice(device, i=0, count=0):
_cleanDevice(device)
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(device), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CROS_DEVICE, device['deviceId']], i, count)
Ind.Increment()
for up in CROS_TELEMETRY_SCALAR_FIELDS:
if up in device:
printKeyValueList([up, device[up]])
showJSON(None, device, skipObjects=CROS_TELEMETRY_SCALAR_FIELDS_SET, timeObjects=CROS_TELEMETRY_TIME_OBJECTS)
Ind.Decrement()
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
cd = None
parent = _getCustomersCustomerIdWithC()
fieldsList = []
reverseLists = []
action = Act.Get()
if action == Act.INFO:
sn = getString(Cmd.OB_SERIAL_NUMBER)
pfilters = [(f'serialNumber={sn}', f'serialNumber={sn}')]
Act.Set(Act.SHOW)
else:
pfilters = []
csvPF = CSVPrintFile(['deviceId'], CROS_TELEMETRY_SCALAR_FIELDS, CROS_TELEMETRY_LIST_FIELDS) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
diskPercentOnly = showOrgUnitPath = False
listLimit = 0
startTime = endTime = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'ou', 'org', 'orgunit', 'limittoou', 'ouandchildren', 'crossn', 'filter'}:
if pfilters:
Cmd.Backup()
usageErrorExit(Msg.ONLY_ONE_DEVICE_SELECTION_ALLOWED.format(pfilters[0][1]))
if myarg == 'crossn':
sn = getString(Cmd.OB_SERIAL_NUMBER)
pfilters = [(f'serialNumber={sn}', f'serialNumber={sn}')]
elif myarg == 'filter':
pf = getString(Cmd.OB_STRING)
pfilters = [(pf, pf)]
else:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
orgUnitPath, orgUnitId = getOrgUnitId(cd)
pfilters = [(f'orgUnitId={orgUnitId[3:]}', f'orgUnitPath={orgUnitPath}')]
if myarg == 'ouandchildren':
try:
subous = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitId,
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitId)
return
pfilters.extend([(f'orgUnitId={subou["orgUnitId"][3:]}', f'orgUnitPath={subou["orgUnitPath"]}') for subou in subous.get('organizationUnits', [])])
elif myarg == 'listlimit':
listLimit = getInteger()
elif myarg in CROS_START_ARGUMENTS:
_, startTime = _getFilterDateTime()
elif myarg in CROS_END_ARGUMENTS:
_, endTime = _getFilterDateTime()
elif myarg in CROS_TELEMETRY_FIELDS_CHOICE_MAP:
fieldsList.append(CROS_TELEMETRY_FIELDS_CHOICE_MAP[myarg])
elif myarg == 'fields':
for field in _getFieldsList():
if field in CROS_TELEMETRY_FIELDS_CHOICE_MAP:
fieldsList.append(CROS_TELEMETRY_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, CROS_TELEMETRY_FIELDS_CHOICE_MAP, True)
elif myarg == 'reverselists':
for field in _getFieldsList():
if field in CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP:
reverseLists.append(CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP, True)
elif myarg == 'showorgunitpath':
showOrgUnitPath = True
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
elif myarg == 'storagepercentonly':
diskPercentOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if fieldsList:
fieldsList.append('deviceId')
if showOrgUnitPath:
fieldsList.append('orgUnitId')
else:
fieldsList = list(CROS_TELEMETRY_FIELDS_CHOICE_MAP.values())
readMask = ','.join(set(fieldsList))
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(['deviceId', 'JSON'])
if not pfilters:
pfilters = [(None, 'All')]
for pfilter in pfilters:
printGettingAllAccountEntities(Ent.CROS_DEVICE, pfilter[1])
pageMessage = getPageMessage()
try:
devices = callGAPIpages(cm.customers().telemetry().devices(), 'list', 'devices',
pageMessage=pageMessage,
throwReasons=[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.INVALID_INPUT],
parent=parent, filter=pfilter[0],
readMask=readMask, pageSize=GC.Values[GC.DEVICE_MAX_RESULTS])
except (GAPI.invalidArgument, GAPI.invalidInput) as e:
message = str(e).replace('\n', ',')
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
return
except GAPI.permissionDenied as e:
accessErrorExitNonDirectory(API.CHROMEMANAGEMENT, str(e))
if csvPF:
for device in devices:
_printDevice(device)
else:
jcount = len(devices)
performActionNumItems(jcount, Ent.CROS_DEVICE)
j = 0
for device in devices:
j += 1
_showDevice(device, j, jcount)
if csvPF:
csvPF.writeCSVfile('CrOS Devices Telemetry')
# gam delete browser <DeviceID>
def doDeleteBrowsers():
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
deviceId = getString(Cmd.OB_DEVICE_ID)
checkForExtraneousArguments()
try:
callGAPI(cbcm.chromebrowsers(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=customerId, deviceId=deviceId)
entityActionPerformed([Ent.CHROME_BROWSER, deviceId])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId)
BROWSER_TIME_OBJECTS = {'firstRecordTime', 'lastActivityTime', 'lastPolicyFetchTime', 'lastRegistrationTime', 'lastStatusReportTime', 'safeBrowsingWarningsResetTime'}
def _showBrowser(browser, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(browser), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_BROWSER, browser['deviceId']], i, count)
Ind.Increment()
showJSON(None, browser, timeObjects=BROWSER_TIME_OBJECTS, dictObjectsKey={'machinePolicies': 'name'})
Ind.Decrement()
BROWSER_FIELDS_CHOICE_MAP = {
'annotatedassetid': 'annotatedAssetId',
'annotatedlocation': 'annotatedLocation',
'annotatednotes': 'annotatedNotes',
'annotateduser': 'annotatedUser',
'asset': 'annotatedAssetId',
'assetid': 'annotatedAssetId',
'browsers': 'browsers',
'browserversions': 'browserVersions',
'deviceid': 'deviceId',
'deviceidentifiershistory': 'deviceIdentifiersHistory',
'extensioncount': 'extensionCount',
'lastactivitytime': 'lastActivityTime',
'lastdeviceuser': 'lastDeviceUser',
'lastdeviceusers': 'lastDeviceUsers',
'lastpolicyfetchtime': 'lastPolicyFetchTime',
'lastregistrationtime': 'lastRegistrationTime',
'laststatusreporttime': 'lastStatusReportTime',
'location': 'annotatedLocation',
'machineextensionpolicies': 'machineExtensionPolicies',
'machinename': 'machineName',
'machinepolicies': 'machinePolicies',
'notes': 'annotatedNotes',
'org': 'orgUnitPath',
'orgunit': 'orgUnitPath',
'orgunitpath': 'orgUnitPath',
'osarchitecture': 'osArchitecture',
'osplatform': 'osPlatform',
'osplatformversion': 'osPlatformVersion',
'osversion': 'osVersion',
'ou': 'orgUnitPath',
'policycount': 'policyCount',
'safebrowsingclickthroughcount': 'safeBrowsingClickThroughCount',
'serialnumber': 'serialNumber',
'user': 'annotatedUser',
'virtualdeviceid': 'virtualDeviceId',
}
BROWSER_ANNOTATED_FIELDS_LIST = ['annotatedAssetId', 'annotatedLocation', 'annotatedNotes', 'annotatedUser', 'deviceId']
BROWSER_FULL_ACCESS_FIELDS = {'browsers', 'lastDeviceUsers', 'lastStatusReportTime', 'machinePolicies'}
# gam info browser <DeviceID>
# (basic|full|annotated |
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]) |
# (rawfields <BrowserFieldNameList>))
# [formatjson]
def doInfoBrowsers():
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
deviceId = getString(Cmd.OB_DEVICE_ID)
projection = 'BASIC'
fieldsList = []
rawFields = None
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'annotated':
projection = 'BASIC'
fieldsList = BROWSER_ANNOTATED_FIELDS_LIST
elif myarg in PROJECTION_CHOICE_MAP:
projection = PROJECTION_CHOICE_MAP[myarg]
fieldsList = []
elif getFieldsList(myarg, BROWSER_FIELDS_CHOICE_MAP, fieldsList, initialField='deviceId'):
pass
elif myarg == 'rawfields':
projection = 'FULL'
rawFields = _getRawFields('deviceId')
else:
FJQC.GetFormatJSON(myarg)
if projection == 'BASIC' and set(fieldsList).intersection(BROWSER_FULL_ACCESS_FIELDS):
projection = 'FULL'
fields = getFieldsFromFieldsList(fieldsList) if not rawFields else rawFields
try:
browser = callGAPI(cbcm.chromebrowsers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN],
customer=customerId, deviceId=deviceId, projection=projection, fields=fields)
_showBrowser(browser, FJQC)
except GAPI.invalidArgument as e:
entityActionFailedWarning([Ent.CHROME_BROWSER, deviceId], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId)
# gam move browsers ou|org|orgunit <OrgUnitPath>
# ((ids <DeviceIDList>) |
# (queries <QueryBrowserList> [querytime<String> <Time>]) |
# (browserou <OrgUnitItem>) | (browserous <OrgUnitList>) |
# <FileSelector> | <CSVFileSelector>)
# [batchsize <Integer>]
def doMoveBrowsers():
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
deviceIds = []
batch_size = GC.Values[GC.BATCH_SIZE]
orgUnitPath = ''
queries = []
queryTimes = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'ou', 'org', 'orgunit'}:
orgUnitPath = getOrgUnitItem()
elif myarg == 'ids':
deviceIds.extend(convertEntityToList(getString(Cmd.OB_DEVICE_ID_LIST, minLen=0)))
elif myarg == 'file':
deviceIds.extend(getEntitiesFromFile(False))
elif myarg in {'csv', 'csvfile'}:
deviceIds.extend(getEntitiesFromCSVFile(False))
elif myarg in {'query', 'queries'}:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg == 'browserou':
deviceIds.extend(getItemsToModify(Cmd.ENTITY_BROWSER_OU, getOrgUnitItem(pathOnly=True, absolutePath=True)))
elif myarg == 'browserous':
deviceIds.extend(getItemsToModify(Cmd.ENTITY_BROWSER_OUS, getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True)))
elif myarg == 'batchsize':
batch_size = getInteger(minVal=1, maxVal=600)
else:
unknownArgumentExit()
if not orgUnitPath:
missingArgumentExit('orgunit')
substituteQueryTimes(queries, queryTimes)
if queries:
deviceIds.extend(getItemsToModify(Cmd.ENTITY_BROWSER_QUERIES, queries))
body = {'org_unit_path': orgUnitPath}
bcount = 0
jcount = len(deviceIds)
j = 0
while bcount < jcount:
kcount = min(jcount-bcount, batch_size)
try:
body['resource_ids'] = deviceIds[bcount:bcount+kcount]
callGAPI(cbcm.chromebrowsers(), 'moveChromeBrowsersToOu',
mapNotFound=False,
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=customerId, body=body)
for deviceId in deviceIds:
j += 1
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_BROWSER, deviceId], j, jcount)
bcount += kcount
except GAPI.invalidOrgunit:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.INVALID_ORGUNIT)
break
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_BROWSER, f'IDs: {deviceIds[bcount]} - {deviceIds[bcount+kcount-1]}'], str(e))
bcount += kcount
UPDATE_BROWSER_ARGUMENT_TO_PROPERTY_MAP = {
'annotatedassetid': 'annotatedAssetId',
'annotatedlocation': 'annotatedLocation',
'annotatednotes': 'annotatedNotes',
'annotateduser': 'annotatedUser',
'asset': 'annotatedAssetId',
'assetid': 'annotatedAssetId',
'location': 'annotatedLocation',
'notes': 'annotatedNotes',
'updatenotes': 'annotatedNotes',
'user': 'annotatedUser',
}
BROWSER_DEVICEID_ANNOTATED_FIELDS = 'deviceId,annotatedAssetId,annotatedLocation,annotatedNotes,annotatedUser'
# gam update browser <BrowserEntity> <BrowserAttibute>+ [updatenotes <String>]
def doUpdateBrowsers():
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_BROWSER, browserAllowed=True, crosAllowed=False, userAllowed=False)
body = {}
updateNotes = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in UPDATE_BROWSER_ARGUMENT_TO_PROPERTY_MAP:
up = UPDATE_BROWSER_ARGUMENT_TO_PROPERTY_MAP[myarg]
if up == 'annotatedNotes':
body[up] = getStringWithCRsNLs()
updateNotes = body[up] if myarg == 'updatenotes' and body[up].find('#notes#') != -1 else None
else:
body[up] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
i = 0
count = len(entityList)
for deviceId in entityList:
i += 1
try:
browser = callGAPI(cbcm.chromebrowsers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=customerId, deviceId=deviceId,
projection='BASIC', fields=BROWSER_DEVICEID_ANNOTATED_FIELDS)
if updateNotes:
body['annotatedNotes'] = updateNotes.replace('#notes#', browser['annotatedNotes'])
browser.update(body)
callGAPI(cbcm.chromebrowsers(), 'update',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=customerId, deviceId=deviceId,
body=browser, projection='BASIC', fields="deviceId")
entityActionPerformed([Ent.CHROME_BROWSER, deviceId], i, count)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId, i, count)
def _getChromeProfileName():
profileName = getString(Cmd.OB_CHROMEPROFILE_ID)
if not profileName.startswith('customers'):
customerId = _getCustomerId()
profileName = f'customers/{customerId}/profiles/{profileName}'
return profileName
# gam delete chromeprofile <ChromeProfileName>
def doDeleteChromeProfile():
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
profileName = _getChromeProfileName()
checkForExtraneousArguments()
try:
callGAPI(cm.customers().profiles(), 'delete',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
name=profileName)
entityActionPerformed([Ent.CHROME_PROFILE, profileName])
except (GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.CHROME_PROFILE, profileName], str(e))
CHROMEPROFILE_TIME_OBJECTS = {
'firstEnrollmentTime',
'lastActivityTime',
'lastPolicyFetchTime',
'lastPolicySyncTime',
'lastStatusReportTime',
}
def _showChromeProfile(profile, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_PROFILE, profile['name']], i, count)
Ind.Increment()
showJSON(None, profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS)
Ind.Decrement()
CHROMEPROFILE_FIELDS_CHOICE_MAP = {
'affiliationstate': 'affiliationState',
'annotatedlocation': 'annotatedLocation',
'annotateduser': 'annotatedUser',
'attestationcredential': 'attestationCredential',
'browserchannel': 'browserChannel',
'browserversion': 'browserVersion',
'deviceinfo': 'deviceInfo',
'displayname': 'displayName',
'extensioncount': 'extensionCount',
'firstenrollmenttime': 'firstEnrollmentTime',
'identityprovider':'identityProvider',
'lastactivitytime': 'lastActivityTime',
'lastpolicyfetchtime': 'lastPolicyFetchTime',
'lastpolicysynctime': 'lastPolicySyncTime',
'laststatusreporttime': 'lastStatusReportTime',
'name': 'name',
'osplatformtype': 'osPlatformType',
'osplatformversion':'osPlatformVersion',
'osversion': 'osVersion',
'policycount': 'policyCount',
'profileid': 'profileId',
'profilepermanentid': 'profilePermanentId',
'reportingdata': 'reportingData',
'useremail': 'userEmail',
'userid': 'userId',
}
# gam info chromeprofile <ChromeProfileName>
# <ChromeProfileFieldName>* [fields <ChromeProfileFieldNameList>]
# [formatjson]
def doInfoChromeProfile():
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
profileName = _getChromeProfileName()
fieldsList = []
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getFieldsList(myarg, CHROMEPROFILE_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
profile = callGAPI(cm.customers().profiles(), 'get',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
name=profileName, fields=fields)
_showChromeProfile(profile, FJQC)
except (GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.CHROME_PROFILE, profileName], str(e))
CHROMEPROFILE_ORDERBY_CHOICE_MAP = {
'affiliationstate': 'affiliationState',
'browserchannel': 'browserChannel',
'browserversion': 'browserVersion',
'displayname': 'displayName',
'extensioncount': 'extensionCount',
'firstenrollmenttime': 'firstEnrollmentTime',
'identityprovider': 'identityProvider',
'lastactivitytime': 'lastActivityTime',
'lastpolicysynctime': 'lastPolicySyncTime',
'laststatusreporttime': 'lastStatusReportTime',
'osplatformtype': 'osPlatformType',
'osversion': 'osVersion',
'policycount': 'policyCount',
'profileid': 'profileId',
'useremail': 'userEmail',
}
# gam show chromeprofiles
# [filtertime.* <Time>] [filter <String>]
# [orderby <ChromeProfileOrderByFieldName> [ascending|descending]]
# <ChromeProfileFieldName>* [fields <ChromeProfileFieldNameList>]
# [formatjson]
# gam print chromeprofiles [todrive <ToDriveAttribute>*]
# [filtertime.* <Time>] [filter <String>]
# [orderby <ChromeProfileOrderByFieldName> [ascending|descending]]
# <ChromeProfileFieldName>* [fields <ChromeProfileFieldNameList>]
# [[formatjson [quotechar <Character>]]
def doPrintShowChromeProfiles():
def _printProfile(profile):
row = flattenJSON(profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': profile['name'], 'profileId': profile['profileId'],
'JSON': json.dumps(cleanJSON(profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
csvPF = CSVPrintFile(['name', 'profileId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(CHROMEPROFILE_ORDERBY_CHOICE_MAP)
sortHeaders = False
fieldsList = []
cbfilter = None
filterTimes = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getFieldsList(myarg, CHROMEPROFILE_FIELDS_CHOICE_MAP, fieldsList, initialField=['name', 'profileId']):
pass
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg.startswith('filtertime'):
filterTimes[myarg] = getTimeOrDeltaFromNow()
elif myarg in {'filter', 'filters'}:
cbfilter = getString(Cmd.OB_STRING)
elif myarg == 'sortheaders':
sortHeaders = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if filterTimes and filter is not None:
for filterTimeName, filterTimeValue in iter(filterTimes.items()):
cbfilter = cbfilter.replace(f'#{filterTimeName}#', filterTimeValue)
fields = getItemFieldsFromFieldsList('chromeBrowserProfiles', fieldsList)
customerId = _getCustomerId()
parent = f'customers/{customerId}'
printGettingAllAccountEntities(Ent.CHROME_PROFILE, cbfilter)
pageMessage = getPageMessage()
try:
feed = yieldGAPIpages(cm.customers().profiles(), 'list', 'chromeBrowserProfiles',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=parent, pageSize=200,
filter=cbfilter, orderBy=OBY.orderBy, fields=fields)
for profiles in feed:
if not csvPF:
jcount = len(profiles)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.CHROME_PROFILE)
Ind.Increment()
j = 0
for profile in profiles:
j += 1
_showChromeProfile(profile, FJQC, j, jcount)
Ind.Decrement()
else:
for profile in profiles:
_printProfile(profile)
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.CHROME_PROFILE, cbfilter], str(e))
if csvPF:
if sortHeaders:
csvPF.SetSortTitles(['name', 'profileId'])
csvPF.writeCSVfile('Chrome Profiles')
BROWSER_ORDERBY_CHOICE_MAP = {
'annotatedassetid': 'annotated_asset_id', 'asset': 'annotated_asset_id', 'assetid': 'annotated_asset_id',
'annotatedlocation': 'annotated_location', 'location': 'annotated_location',
'annotatednotes': 'notes', 'notes': 'notes',
'annotateduser': 'annotated_user', 'user': 'annotated_user',
'browserversionchannel': 'browser_version_channel',
'browserversionsortable': 'browser_version_sortable',
'deviceid': 'id', 'id': 'id',
'enrollmentdate': 'enrollment_date',
'extensioncount': 'extension_count',
'lastactivity': 'last_activity',
'lastsignedinuser': 'last_signed_in_user',
'lastsync': 'last_sync',
'machinename': 'machine_name',
'orgunit': 'org_unit', 'ou': 'org_unit', 'org': 'org_unit',
'osversion': 'os_version',
'osversionsortable': 'os_version_sortable',
'platformmajorversion': 'platform_major_version',
'policycount': 'policy_count',
# 'safebrowsingclickthrough': 'safe_browsing_clickthrough',
}
# gam show browsers
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
# [querytime<String> <Time>]
# [orderby <BrowserOrderByFieldName> [ascending|descending]]
# (basic|full|annotated |
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]) |
# (rawfields <BrowserFieldNameList>))
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]|(rawfields <BrowserFieldNameList>)
# [formatjson]
# gam print browsers [todrive <ToDriveAttribute>*]
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
# [querytime<String> <Time>]
# [orderby <BrowserOrderByFieldName> [ascending|descending]]
# (basic|full|annotated |
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]) |
# (rawfields <BrowserFieldNameList>))
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]|(rawfields <BrowserFieldNameList>)
# [sortheaders] [formatjson [quotechar <Character>]]
def doPrintShowBrowsers():
def _printBrowser(browser):
row = flattenJSON(browser, timeObjects=BROWSER_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'deviceId': browser['deviceId'],
'JSON': json.dumps(cleanJSON(browser, timeObjects=BROWSER_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
csvPF = CSVPrintFile(['deviceId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
rawFields = None
projection = 'BASIC'
orderBy = 'id'
sortOrder = 'ASCENDING'
entityList = orgUnitPath = None
queries = [None]
queryTimes = {}
sortHeaders = sortRows = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'query', 'queries'}:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg in {'ou', 'org', 'orgunit', 'browserou'}:
orgUnitPath = getOrgUnitItem(pathOnly=True, absolutePath=True)
elif myarg == 'select':
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_BROWSER, browserAllowed=True, crosAllowed=False, userAllowed=False)
elif myarg == 'orderby':
orderBy, sortOrder = getOrderBySortOrder(BROWSER_ORDERBY_CHOICE_MAP, 'DESCENDING', True)
elif myarg == 'annotated':
projection = 'BASIC'
fieldsList = BROWSER_ANNOTATED_FIELDS_LIST
elif (myarg == 'projection') or myarg in PROJECTION_CHOICE_MAP:
if myarg == 'projection':
projection = getChoice(PROJECTION_CHOICE_MAP, mapChoice=True)
else:
projection = PROJECTION_CHOICE_MAP[myarg]
fieldsList = []
elif myarg == 'allfields':
projection = 'FULL'
sortHeaders = True
fieldsList = []
elif myarg == 'sortheaders':
sortHeaders = True
elif getFieldsList(myarg, BROWSER_FIELDS_CHOICE_MAP, fieldsList, initialField='deviceId'):
pass
elif myarg == 'rawfields':
projection = 'FULL'
rawFields = _getRawFields('deviceId')
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if projection == 'BASIC' and set(fieldsList).intersection(BROWSER_FULL_ACCESS_FIELDS):
projection = 'FULL'
if FJQC.formatJSON:
sortHeaders = False
substituteQueryTimes(queries, queryTimes)
if entityList is None:
fields = getItemFieldsFromFieldsList('browsers', fieldsList) if not rawFields else f'nextPageToken,browsers({rawFields})'
for query in queries:
printGettingAllAccountEntities(Ent.CHROME_BROWSER, query)
pageMessage = getPageMessage()
try:
feed = yieldGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
pageMessage=pageMessage, messageAttribute='deviceId',
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customerId, orgUnitPath=orgUnitPath, query=query, projection=projection,
orderBy=orderBy, sortOrder=sortOrder, fields=fields)
for browsers in feed:
if not csvPF:
jcount = len(browsers)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.CHROME_BROWSER)
Ind.Increment()
j = 0
for browser in browsers:
j += 1
_showBrowser(browser, FJQC, j, jcount)
Ind.Decrement()
else:
for browser in browsers:
_printBrowser(browser)
except GAPI.invalidInput as e:
if query:
entityActionFailedWarning([Ent.CHROME_BROWSER, None], invalidQuery(query))
else:
entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e))
return
except (GAPI.invalidArgument, GAPI.invalidOrgunit, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e))
return
except (GAPI.badRequest, GAPI.resourceNotFound):
accessErrorExit(None)
else:
sortRows = True
jcount = len(entityList)
fields = getFieldsFromFieldsList(fieldsList) if not rawFields else rawFields
j = 0
for deviceId in entityList:
j += 1
try:
browser = callGAPI(cbcm.chromebrowsers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN],
customer=customerId, deviceId=deviceId, projection=projection, fields=fields)
_printBrowser(browser)
except GAPI.invalidArgument as e:
entityActionFailedWarning([Ent.CHROME_BROWSER, deviceId], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId)
if csvPF:
if sortRows and orderBy:
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
if sortHeaders:
csvPF.SetSortTitles(['deviceId'])
csvPF.writeCSVfile('Browsers')
BROWSER_TOKEN_TIME_OBJECTS = {'createTime', 'expireTime', 'revokeTime'}
def _showBrowserToken(browser, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(browser), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, browser['token']], i, count)
Ind.Increment()
showJSON(None, browser, timeObjects=BROWSER_TOKEN_TIME_OBJECTS)
Ind.Decrement()
# gam create browsertoken
# [ou|org|orgunit|browserou <OrgUnitPath>] [expire|expires <Time>]
# [formatjson]
def doCreateBrowserToken():
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
FJQC = FormatJSONQuoteChar()
body = {'token_type': 'CHROME_BROWSER'}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'ou', 'org', 'orgunit', 'browserou'}:
body['org_unit_path'] = getOrgUnitItem(pathOnly=True, absolutePath=True)
elif myarg in ['expire', 'expires']:
body['expire_time'] = getTimeOrDeltaFromNow()
else:
FJQC.GetFormatJSON(myarg)
try:
browser = callGAPI(cbcm.enrollmentTokens(), 'create',
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
customer=customerId, body=body)
if not FJQC.formatJSON:
entityActionPerformed([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, browser['token']])
Ind.Increment()
_showBrowserToken(browser, FJQC, 0, 0)
Ind.Decrement()
except (GAPI.invalidInput, GAPI.invalidOrgunit) as e:
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(None)
# gam revoke browsertoken <BrowserTokenPermanentID>
def doRevokeBrowserToken():
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
tokenPermanentId = getString(Cmd.OB_BROWSER_ENROLLEMNT_TOKEN_ID)
checkForExtraneousArguments()
try:
callGAPI(cbcm.enrollmentTokens(), 'revoke',
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
customer=customerId, tokenPermanentId=tokenPermanentId)
entityActionPerformed([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, tokenPermanentId])
except (GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.resourceNotFound, GAPI.invalidOrgunit) as e:
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, tokenPermanentId], str(e))
except GAPI.forbidden:
accessErrorExit(None)
BROWSER_TOKEN_FIELDS_CHOICE_MAP = {
'createtime': 'createTime',
'creatorid': 'creatorId',
'customerid': 'customerId',
'expiretime': 'expireTime',
'org': 'orgUnitPath',
'orgunit': 'orgUnitPath',
'orgunitpath': 'orgUnitPath',
'ou': 'orgUnitPath',
'revoketime': 'revokeTime',
'revokerid': 'revokerId',
'state': 'state',
'token': 'token',
'tokenpermanentid': 'tokenPermanentId',
}
# gam show browsertokens
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
# [querytime<String> <Time>]
# [orderby <BrowserTokenFieldName> [ascending|descending]]
# [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
# [formatjson]
# gam print browsertokens [todrive <ToDriveAttribute>*]
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
# [querytime<String> <Time>]
# [orderby <BrowserTokenFieldName> [ascending|descending]]
# [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
# [sortheaders] [formatjson [quotechar <Character>]]
def doPrintShowBrowserTokens():
def _printBrowserToken(browser):
row = flattenJSON(browser, timeObjects=BROWSER_TOKEN_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'token': browser['token'],
'JSON': json.dumps(cleanJSON(browser, timeObjects=BROWSER_TOKEN_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
cbcm = buildGAPIObject(API.CBCM)
customerId = _getCustomerIdNoC()
csvPF = CSVPrintFile(['token']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
orderBy = 'token'
sortOrder = 'ASCENDING'
orgUnitPath = None
queries = [None]
queryTimes = {}
sortHeaders = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'query', 'queries'}:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg in {'ou', 'org', 'orgunit', 'browserou'}:
orgUnitPath = getOrgUnitItem(pathOnly=True, absolutePath=True)
elif myarg == 'orderby':
orderBy, sortOrder = getOrderBySortOrder(BROWSER_TOKEN_FIELDS_CHOICE_MAP, 'DESCENDING', True)
elif myarg == 'allfields':
sortHeaders = True
fieldsList = []
elif myarg == 'sortheaders':
sortHeaders = True
elif getFieldsList(myarg, BROWSER_TOKEN_FIELDS_CHOICE_MAP, fieldsList, initialField='token'):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = getItemFieldsFromFieldsList('chromeEnrollmentTokens', fieldsList)
if FJQC.formatJSON:
sortHeaders = False
substituteQueryTimes(queries, queryTimes)
for query in queries:
printGettingAllAccountEntities(Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, query)
pageMessage = getPageMessage()
try:
browsers = callGAPIpages(cbcm.enrollmentTokens(), 'list', 'chromeEnrollmentTokens',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
customer=customerId, orgUnitPath=orgUnitPath, query=query,
fields=fields)
if not csvPF:
jcount = len(browsers)
performActionNumItems(jcount, Ent.CHROME_BROWSER_ENROLLMENT_TOKEN)
Ind.Increment()
j = 0
for browser in browsers:
j += 1
_showBrowserToken(browser, FJQC, j, jcount)
Ind.Decrement()
else:
for browser in browsers:
_printBrowserToken(browser)
except GAPI.invalidInput as e:
if query:
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], invalidQuery(query))
else:
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], str(e))
except GAPI.invalidOrgunit as e:
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(None)
if csvPF:
if orderBy:
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
if sortHeaders:
csvPF.SetSortTitles(['token'])
csvPF.writeCSVfile('Chrome Browser Enrollment Tokens')
def buildChatServiceObject(api=API.CHAT, user=None, i=0, count=0, entityTypeList=None, useAdminAccess=False):
if user is None:
_, chat = buildGAPIServiceObject(API.CHAT, user)
kvList = [Ent.CHAT_BOT, None]
else:
user, chat = buildGAPIServiceObject(api, user, i, count)
if not useAdminAccess:
kvList = [Ent.USER, user]
else:
kvList = [Ent.CHAT_ADMIN, f'{user}(asadmin)']
if entityTypeList is not None:
kvList.extend(entityTypeList)
return user, chat, kvList
def setupChatURL(chat):
return f'https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat?project={chat._http.credentials.project_id}'
def exitIfChatNotConfigured(chat, kvList, errMsg, i, count):
if (('No bot associated with this project.' in errMsg) or
('Invalid project number.' in errMsg) or
('Google Chat app not found.' in errMsg)):
systemErrorExit(API_ACCESS_DENIED_RC, Msg.TO_SET_UP_GOOGLE_CHAT.format(setupChatURL(chat)))
entityActionFailedWarning(kvList, errMsg, i, count)
def _getChatAdminAccess(adminAPI, userAPI):
if checkArgumentPresent(ADMIN_ACCESS_OPTIONS) or GC.Values[GC.USE_CHAT_ADMIN_ACCESS]:
return (True, adminAPI, {'useAdminAccess': True})
return (False, userAPI, {})
def _chkChatAdminAccess(count):
if count != 1:
usageErrorExit(Msg.CHAT_ADMIN_ACCESS_LIMITED_TO_ONE_USER.format(count))
def _cleanChatSpace(space):
space.pop('type', None)
space.pop('threaded', None)
def _cleanChatMessage(message):
message.pop('cards', None)
CHAT_TIME_OBJECTS = {'createTime', 'deleteTime', 'eventTime', 'lastActiveTime', 'lastUpdateTime'}
def _showChatItem(citem, entityType, FJQC, i=0, count=0):
if entityType == Ent.CHAT_SPACE:
_cleanChatSpace(citem)
elif entityType == Ent.CHAT_MESSAGE:
_cleanChatMessage(citem)
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(citem, timeObjects=CHAT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([entityType, citem['name']], i, count)
Ind.Increment()
showJSON(None, citem, timeObjects=CHAT_TIME_OBJECTS)
Ind.Decrement()
def _printChatItem(user, citem, parent, entityType, csvPF, FJQC, addCSVData=None):
if entityType == Ent.CHAT_SPACE:
_cleanChatSpace(citem)
baserow = {'User': user} if user is not None else {}
else:
if user is not None:
baserow = {'User': user, 'space.name': parent['name'], 'space.displayName': parent['displayName']}
else:
baserow = {'space.name': parent['name'], 'space.displayName': parent['displayName']}
if entityType == Ent.CHAT_MEMBER:
if addCSVData:
baserow.update(addCSVData)
elif entityType == Ent.CHAT_MESSAGE:
_cleanChatMessage(citem)
row = flattenJSON(citem, flattened=baserow.copy(), timeObjects=CHAT_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row.update({'name': citem['name'],
'JSON': json.dumps(cleanJSON(citem, timeObjects=CHAT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
csvPF.WriteRowNoFilter(row)
# gam setup chat
def doSetupChat():
checkForExtraneousArguments()
_, chat , _ = buildChatServiceObject()
writeStdout(Msg.TO_SET_UP_GOOGLE_CHAT.format(setupChatURL(chat)))
def getSpaceName(myarg):
if myarg == 'space':
chatSpace = getString(Cmd.OB_CHAT_SPACE)
if chatSpace.startswith('spaces/'):
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
def getChatSpaceParameters(myarg, body, typeChoicesMap, updateMask):
if myarg == 'displayname':
body['displayName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=128)
updateMask.add('displayName')
elif myarg == 'type':
body['spaceType'] = getChoice(typeChoicesMap, mapChoice=True)
updateMask.add('spaceType')
elif myarg == 'description':
body.setdefault('spaceDetails', {})
body['spaceDetails']['description'] = getString(Cmd.OB_STRING, minLen=0, maxLen=150)
updateMask.add('spaceDetails')
elif myarg in {'guidelines', 'rules'}:
body.setdefault('spaceDetails', {})
body['spaceDetails']['guidelines'] = getString(Cmd.OB_STRING, minLen=0, maxLen=5000)
updateMask.add('spaceDetails')
elif myarg == 'history':
body['spaceHistoryState'] = 'HISTORY_ON' if getBoolean() else 'HISTORY_OFF'
updateMask.add('spaceHistoryState')
elif myarg in {'audience', 'restricted'}:
body['accessSettings']= {'audience': None}
if myarg == 'audience':
body['accessSettings']['audience'] = getString(Cmd.OB_STRING, minLen=1, maxLen=100)
updateMask.add('accessSettings.audience')
else:
return False
return True
CHAT_MEMBER_ROLE_MAP = {
'member': 'ROLE_MEMBER',
'manager': 'ROLE_MANAGER'
}
CHAT_MEMBER_TYPE_MAP = {
'bot': 'BOT',
'human': 'HUMAN'
}
CHAT_SPACE_TYPE_MAP = {
'space': 'SPACE',
'groupchat': 'GROUP_CHAT',
'directmessage': 'DIRECT_MESSAGE',
}
CHAT_SPACE_PREDEFINED_PERMS_MAP = {
'announcement': 'ANNOUNCEMENT_SPACE',
'collaboration': 'COLLABORATION_SPACE',
}
CHAT_SPACE_MIN_MAX_MEMBERS = {
'SPACE': {'min': 0, 'max': 20},
'GROUP_CHAT': {'min': 2, 'max': 20},
'DIRECT_MESSAGE': {'min': 1, 'max': 1},
}
# gam <UserTypeEntity> create chatspace
# [type <ChatSpaceType>] [announcement|collaboration]
# [restricted|(audience <String>)]
# [externalusersallowed <Boolean>]
# [members <UserTypeEntity>]
# [displayname <String>]
# [description <String>] [guidelines|rules <String>]
# [history <Boolean>]
# [<ChatContent>]
# [formatjson|returnidonly]
def createChatSpace(users):
FJQC = FormatJSONQuoteChar()
body = {'space': {'spaceType': CHAT_SPACE_TYPE_MAP['space'], 'displayName': ''}, 'requestId': str(uuid.uuid4())}
members = []
tbody = {}
returnIdOnly = False
updateMask = set()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getChatSpaceParameters(myarg, body['space'], CHAT_SPACE_TYPE_MAP, updateMask):
pass
elif myarg in CHAT_SPACE_PREDEFINED_PERMS_MAP:
body['space']['predefinedPermissionSettings'] = CHAT_SPACE_PREDEFINED_PERMS_MAP[myarg]
elif myarg == 'externalusersallowed':
body['space']['externalUserAllowed'] = getBoolean()
elif myarg == 'members':
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
elif myarg == 'returnidonly':
returnIdOnly = True
elif myarg in SORF_TEXT_ARGUMENTS:
tbody['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
else:
FJQC.GetFormatJSON(myarg)
spaceType = body['space']['spaceType']
jcount = len(members)
if (jcount < CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['min'] or
jcount > CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['max']):
systemErrorExit(USAGE_ERROR_RC,
Msg.INVALID_NUMBER_OF_CHAT_SPACE_MEMBERS.format(Ent.Singular(Ent.CHAT_SPACE),
spaceType, jcount,
CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['min'],
CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['max']))
mtype = CHAT_MEMBER_TYPE_MAP['human']
if members:
body['memberships'] = []
for member in members:
name = normalizeEmailAddressOrUID(member)
body['memberships'].append({'member': {'name': f'users/{name}', 'type': mtype}})
if spaceType == 'SPACE':
if not body['space']['displayName']:
missingArgumentExit('displayname')
elif spaceType == 'GROUP_CHAT':
body['space'].pop('displayName', None)
body['space'].pop('predefinedPermissionSettings', None)
else: # DIRECT_MESSAGE
body['space'].pop('displayName', None)
body['space'].pop('spaceDetails', None)
body['space'].pop('predefinedPermissionSettings', None)
body['space']['singleUserBotDm'] = False
if tbody:
trimChatMessageIfRequired(tbody)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_SPACES, user, i, count,
[Ent.CHAT_SPACE, body['space'].get('displayName', spaceType)])
if not chat:
continue
try:
space = callGAPI(chat.spaces(), 'setup',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
body=body)
if not returnIdOnly:
kvList[-1] = space['name']
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
Ind.Increment()
_showChatItem(space, Ent.CHAT_SPACE, FJQC)
Ind.Decrement()
else:
writeStdout(f'{space["name"]}\n')
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
continue
if tbody:
parent = space['name']
_, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count,
[Ent.CHAT_SPACE, body['space'].get('displayName', parent)])
if not chat:
continue
try:
resp = callGAPI(chat.spaces().messages(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=parent, requestId=str(uuid.uuid4()), body=tbody)
if not returnIdOnly:
kvList.extend([Ent.CHAT_MESSAGE, resp['name']])
entityActionPerformed(kvList, i, count)
else:
writeStdout(f'{resp["name"]}\n')
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
CHAT_UPDATE_SPACE_TYPE_MAP = {
'space': 'SPACE',
}
CHAT_SPACE_ROLE_PERMISSIONS_MAP = {
'managers': 'managersAllowed',
'members': 'membersAllowed',
}
CHAT_UPDATE_SPACE_PERMISSIONS_MAP = {
'managemembersandgroups': 'manageMembersAndGroups',
'modifyspacedetails': 'modifySpaceDetails',
'togglehistory': 'toggleHistory',
'useatmentionall': 'useAtMentionAll',
'manageapps': 'manageApps',
'managewebhooks': 'manageWebhooks',
'replymessages': 'replyMessages',
}
# gam <UserTypeEntity> update chatspace <ChatSpace>
# [restricted|(audience <String>)]|
# ([displayname <String>]
# [type space]
# [description <String>] [guidelines|rules <String>]
# [history <Boolean>])
# managemembersandgroups managers|members
# modifyspacedetails managers|members
# togglehistory managers|members
# useatmentionall managers|members
# manageapps managers|members
# managewebhooks managers|members
# replymessages managers|members
# [formatjson]
def updateChatSpace(users):
FJQC = FormatJSONQuoteChar()
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_SPACES_ADMIN, API.CHAT_SPACES)
name = None
body = {}
updateMask = set()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
name = getSpaceName(myarg)
elif getChatSpaceParameters(myarg, body, CHAT_UPDATE_SPACE_TYPE_MAP, updateMask):
pass
elif myarg in CHAT_UPDATE_SPACE_PERMISSIONS_MAP:
body.setdefault('permissionSettings', {})
permissionSetting = CHAT_UPDATE_SPACE_PERMISSIONS_MAP[myarg]
role = getChoice(CHAT_SPACE_ROLE_PERMISSIONS_MAP, mapChoice=True)
body['permissionSettings'][permissionSetting] = {'managersAllowed': True}
if role == 'membersAllowed':
body['permissionSettings'][permissionSetting].update({'membersAllowed': True})
updateMask.add(f'permissionSettings.{permissionSetting}')
else:
FJQC.GetFormatJSON(myarg)
if not name:
missingArgumentExit('space')
if 'accessSettings.audience' in updateMask:
tempMask = updateMask-{'accessSettings.audience'}
if tempMask:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('restricted/audience', 'displayname,type,description,guidelines,history'))
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, name], useAdminAccess)
if not chat:
continue
try:
space = callGAPI(chat.spaces(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, updateMask=','.join(updateMask), body=body, **kwargsUAA)
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
Ind.Increment()
_showChatItem(space, Ent.CHAT_SPACE, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> delete chatspace <ChatSpace>
# gam <UserItem> delete chatspace asadmin <ChatSpace>
def deleteChatSpace(users):
name = None
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_SPACES_DELETE_ADMIN, API.CHAT_SPACES_DELETE)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
name = getSpaceName(myarg)
else:
unknownArgumentExit()
if not name:
missingArgumentExit('space')
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, name], useAdminAccess)
if not chat:
continue
try:
callGAPI(chat.spaces(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, **kwargsUAA)
entityActionPerformed(kvList, i, count)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
CHAT_SPACES_FIELDS_CHOICE_MAP = {
"accesssettings": "accessSettings",
"admininstalled": "adminInstalled",
"createtime": "createTime",
"displayname": "displayName",
"externaluserallowed": "externalUserAllowed",
"importmode": "importMode",
"lastactivetime": "lastActiveTime",
"membershipcount": "membershipCount",
"name": "name",
"permissionsettings": "permissionSettings",
"singleuserbotdm": "singleUserBotDm",
"spacedetails": "spaceDetails",
"spacehistorystate": "spaceHistoryState",
"spacethreadingstate": "spaceThreadingState",
"spacetype": "spaceType",
"spaceuri": "spaceUri",
"threaded": "spaceThreadingState",
"type": "spaceType",
}
# gam [<UserTypeEntity>] info chatspace <ChatSpace>
# [fields <ChatSpaceFieldNameList>]
# [formatjson]
# gam <UserItem> info chatspace asadmin <ChatSpace>
# [fields <ChatSpaceFieldNameList>]
# [formatjson]
def infoChatSpace(users, name=None):
FJQC = FormatJSONQuoteChar()
if name is None:
function = 'get'
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_SPACES_ADMIN, API.CHAT_SPACES)
else:
function = 'findDirectMessage'
useAdminAccess = None
kwargsUAA = {}
api = API.CHAT_SPACES
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if function == 'get' and (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
name = getSpaceName(myarg)
elif getFieldsList(myarg, CHAT_SPACES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
else:
FJQC.GetFormatJSON(myarg)
if function == 'get' and not name:
missingArgumentExit('space')
fields = getFieldsFromFieldsList(fieldsList)
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, name], useAdminAccess)
if not chat:
continue
try:
space = callGAPI(chat.spaces(), function,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, fields=fields, **kwargsUAA)
if not FJQC.formatJSON:
entityPerformAction(kvList, i, count)
Ind.Increment()
_showChatItem(space, Ent.CHAT_SPACE, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
def doInfoChatSpace():
infoChatSpace([None])
# gam [<UserTypeEntity>] info chatspacedm <UserItem>
# [fields <ChatSpaceFieldNameList>]
# [formatjson]
def infoChatSpaceDM(users):
cd = buildGAPIObject(API.DIRECTORY)
name = convertEmailAddressToUID(getEmailAddress(returnUIDprefix='uid:'), cd, 'user')
infoChatSpace(users, f'users/{name}')
def _getChatPageMessage(entityType, user, i, count, pfilter, useAdminAccess=False):
if user is not None:
printGettingAllEntityItemsForWhom(entityType, user if not useAdminAccess else f'{user}(asadmin)', i, count, pfilter)
return getPageMessageForWhom()
printGettingAllAccountEntities(entityType, pfilter)
return getPageMessage()
def _getChatSpaceListParms(myarg, kwargs):
if myarg in {'type', 'types'}:
for ctype in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
if ctype in CHAT_SPACE_TYPE_MAP:
kwargs.setdefault('filter', '')
if kwargs['filter']:
kwargs['filter'] += ' OR '
kwargs['filter'] += f'spaceType = "{CHAT_SPACE_TYPE_MAP[ctype]}"'
else:
invalidChoiceExit(ctype, CHAT_SPACE_TYPE_MAP, True)
else:
return False
return True
def _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
if myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'query':
queries[0] += ' AND '+getString(Cmd.OB_QUERY)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()
else:
return False
return True
CHAT_PAGE_SIZE = 1000
CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP = {
'createtime': 'createTime',
'lastactivetime': 'lastActiveTime',
'membershipcount': 'membershipCount.joined_direct_human_user_count'
}
# gam [<UserTypeEntity>] show chatspaces
# [types <ChatSpaceTypeList>]
# [fields <ChatSpaceFieldNameList>]
# [formatjson]
# gam [<UserTypeEntity>] print chatspaces [todrive <ToDriveAttribute>*]
# [types <ChatSpaceTypeList>]
# [fields <ChatSpaceFieldNameList>]
# [formatjson [quotechar <Character>]]
# gam <UserItem> show chatspaces asadmin
# [query <String>] [querytime<String> <Time>]
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
# [fields <ChatSpaceFieldNameList>]
# [formatjson]
# gam <UserItem> print chatspaces asadmin [todrive <ToDriveAttribute>*]
# [query <String>] [querytime<String> <Time>]
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
# [fields <ChatSpaceFieldNameList>]
# [formatjson [quotechar <Character>]]
def printShowChatSpaces(users):
csvPF = CSVPrintFile(['User', 'name'] if not isinstance(users, list) else ['name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP)
useAdminAccess, api, kwargsCS = _getChatAdminAccess(API.CHAT_SPACES_ADMIN, API.CHAT_SPACES)
fieldsList = []
queries = []
queryTimes = {}
pfilter = ''
if useAdminAccess:
function = 'search'
queries = ['customer = "customers/my_customer" AND spaceType = "SPACE"']
else:
function = 'list'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getFieldsList(myarg, CHAT_SPACES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
elif not useAdminAccess and _getChatSpaceListParms(myarg, kwargsCS):
pass
elif useAdminAccess and _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = getItemFieldsFromFieldsList('spaces', fieldsList)
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
kwargsCS['orderBy'] = OBY.orderBy
substituteQueryTimes(queries, queryTimes)
pfilter = kwargsCS['query'] = queries[0]
kwargsCS['useAdminAccess'] = True
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, None, useAdminAccess)
if not chat:
continue
try:
spaces = callGAPIpages(chat.spaces(), function, 'spaces',
pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, pfilter, useAdminAccess),
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargsCS)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
continue
jcount = len(spaces)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_SPACE, i, count)
Ind.Increment()
j = 0
for space in spaces:
j += 1
_showChatItem(space, Ent.CHAT_SPACE, FJQC, j, jcount)
Ind.Decrement()
else:
for space in spaces:
_printChatItem(user, space, None, Ent.CHAT_SPACE, csvPF, FJQC)
if csvPF:
csvPF.writeCSVfile('Chat Spaces')
def doPrintShowChatSpaces():
printShowChatSpaces([None])
def _getChatMemberEmail(cd, member):
if 'member' in member:
if member['member']['type'] == 'HUMAN':
_, memberUid = member['member']['name'].split('/')
member['member']['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{memberUid}', cd, None, emailTypes=['user'])
elif 'groupMember' in member:
_, memberUid = member['groupMember']['name'].split('/')
member['groupMember']['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{memberUid}', cd, None, emailTypes=['group'])
def normalizeUserMember(user, userList):
userList.append(normalizeEmailAddressOrUID(user))
def getUserMemberID(cd, user, userList):
userList.append(convertEmailAddressToUID(user, cd, emailType='user'))
def getGroupMemberID(cd, group, groupList):
groupList.append(convertEmailAddressToUID(group, cd, emailType='group'))
# gam <UserTypeEntity> create chatmember <ChatSpace>
# [type human|bot] [role member|manager]
# (user <UserItem>)* (members <UserTypeEntity>)*
# (group <GroupItem>)* (groups <GroupEntity>)*
# [formatjson|returnidonly]
# gam <UserItem> create chatmember asadmin <ChatSpace>
# [type human|bot] [role member|manager]
# (user <UserItem>)* (members <UserTypeEntity>)*
# (group <GroupItem>)* (groups <GroupEntity>)*
# [formatjson|returnidonly]
def createChatMember(users):
def addMembers(members, field, entityType, i, count):
jcount = len(members)
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
if jcount == 0:
return
kvList.extend([entityType, ''])
Ind.Increment()
j = 0
for body in members:
j += 1
kvList[-1] = body[field]['name']
try:
member = callGAPI(chat.spaces().members(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.INTERNAL_ERROR, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
parent=parent, body=body, **kwargsUAA)
if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
member = callGAPI(chat.spaces().members(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=member['name'], updateMask='role', body={'role': role}, **kwargsUAA)
if not returnIdOnly:
kvList[-1] = member['name']
_getChatMemberEmail(cd, member)
if not FJQC.formatJSON:
entityActionPerformed(kvList, j, jcount)
Ind.Increment()
_showChatItem(member, Ent.CHAT_MEMBER, FJQC)
Ind.Decrement()
else:
writeStdout(f'{member["name"]}\n')
except (GAPI.alreadyExists, GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning(kvList, str(e))
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
FJQC = FormatJSONQuoteChar()
parent = None
role = CHAT_MEMBER_ROLE_MAP['member']
mtype = CHAT_MEMBER_TYPE_MAP['human']
userList = []
groupList = []
returnIdOnly = False
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getSpaceName(myarg)
elif myarg == 'user':
normalizeUserMember(getEmailAddress(returnUIDprefix='uid:'), userList)
elif myarg in {'member', 'members'}:
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
for user in members:
normalizeUserMember(user, userList)
elif myarg == 'group':
getGroupMemberID(cd, getEmailAddress(returnUIDprefix='uid:'), groupList)
elif myarg == 'groups':
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
getGroupMemberID(cd, group, groupList)
elif myarg == 'role':
role = getChoice(CHAT_MEMBER_ROLE_MAP, mapChoice=True)
elif myarg == 'type':
mtype = getChoice(CHAT_MEMBER_TYPE_MAP, mapChoice=True)
elif myarg == 'returnidonly':
returnIdOnly = True
else:
FJQC.GetFormatJSON(myarg)
if not parent:
missingArgumentExit('space')
if not userList and not groupList:
missingArgumentExit('user|members|group|groups')
userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
userMembers = []
for user in userList:
userMembers.append({'member': {'name': f'users/{user}', 'type': mtype}})
groupMembers = []
for group in groupList:
groupMembers.append({'groupMember': {'name': f'groups/{group}'}})
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, parent], useAdminAccess)
if not chat:
continue
Ind.Increment()
if userMembers:
addMembers(userMembers, 'member', userEntityType, i, count)
if groupMembers:
addMembers(groupMembers, 'groupMember', Ent.CHAT_MEMBER_GROUP, i, count)
Ind.Decrement()
def _deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA):
j = 0
for name in memberNames:
j += 1
kvList[-1] = name
try:
callGAPI(chat.spaces().members(), 'delete',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, **kwargsUAA)
entityActionPerformed(kvList, j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
# gam <UserTypeEntity> delete chatmember <ChatSpace>
# ((user <UserItem>)|(members <UserTypeEntity>)|
# (group <GroupItem>)|(groups <GroupEntity>))+
# gam <UserItem> delete chatmember asadmin <ChatSpace>
# ((user <UserItem>)|(members <UserTypeEntity>)|
# (group <GroupItem>)|(groups <GroupEntity>))+
# gam <UserTypeEntity> remove chatmember
# members <ChatMemberList>
# gam <UserItem> remove chatmember asadmin
# members <ChatMemberList>
# gam <UserTypeEntity> update chatmember <ChatSpace>
# role member|manager
# ((user <UserItem>)|(members <UserTypeEntity>))+
# gam <UserTypeEntity> modify chatmember
# role member|manager
# members <ChatMemberList>
# gam <UserItem> update chatmember asadmin<ChatSpace>
# role member|manager
# ((user <UserItem>)|(members <UserTypeEntity>))+
# gam <UserItem> modify chatmember asadmin
# role member|manager
# members <ChatMemberList>
def deleteUpdateChatMember(users):
cd = buildGAPIObject(API.DIRECTORY)
FJQC = FormatJSONQuoteChar()
action = Act.Get()
deleteMode = action in {Act.DELETE, Act.REMOVE}
parent = None
body = {}
memberNames = []
userList = []
groupList = []
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if action in {Act.UPDATE, Act.MODIFY} and myarg == 'role':
body['role'] = getChoice(CHAT_MEMBER_ROLE_MAP, mapChoice=True)
continue
if action in {Act.REMOVE, Act.MODIFY}:
if myarg in {'member', 'members'}:
memberNames.extend(getString(Cmd.OB_CHAT_MEMBER).replace(',', ' ').split())
else:
unknownArgumentExit()
else: # {Act.DELETE, Act.UPDATE}
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getSpaceName(myarg)
elif myarg == 'user':
normalizeUserMember(getEmailAddress(returnUIDprefix='uid:'), userList)
elif myarg in {'member', 'members'}:
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
for user in members:
normalizeUserMember(user, userList)
elif deleteMode and myarg == 'group':
getGroupMemberID(cd, getEmailAddress(returnUIDprefix='uid:'), groupList)
elif deleteMode and myarg == 'groups':
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
getGroupMemberID(cd, group, groupList)
else:
unknownArgumentExit()
if not deleteMode and 'role' not in body:
missingArgumentExit('role')
if action in {Act.REMOVE, Act.MODIFY}:
if not memberNames:
missingArgumentExit('members')
else: # {Act.DELETE, Act.UPDATE}
if not parent:
missingArgumentExit('space')
if not userList and not groupList:
missingArgumentExit('user|members|group|groups')
for user in userList:
memberNames.append(f'{parent}/members/{user}')
for group in groupList:
memberNames.append(f'{parent}/members/group-{group}')
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, parent] if parent is not None else None, useAdminAccess)
if not chat:
continue
jcount = len(memberNames)
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MEMBER, i, count)
kvList.extend([Ent.CHAT_MEMBER, ''])
Ind.Increment()
if deleteMode:
_deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA)
else:
j = 0
for name in memberNames:
j += 1
kvList[-1] = name
try:
member = callGAPI(chat.spaces().members(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, updateMask='role', body=body, **kwargsUAA)
_getChatMemberEmail(cd, member)
Ind.Increment()
_showChatItem(member, Ent.CHAT_MEMBER, FJQC, j, jcount)
Ind.Decrement()
except GAPI.notFound as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
continue
Ind.Decrement()
CHAT_SYNC_PREVIEW_TITLES = ['space', 'member', 'role', 'action', 'message']
# gam <UserTypeEntity> sync chatmembers [asadmin] <ChatSpace>
# [role member|manager] [type human|bot]
# [addonly|removeonly]
# [preview [actioncsv]]
# (users <UserTypeEntity>)* (groups <GroupEntity>)*
def syncChatMembers(users):
def _previewAction(members, entityType, jcount, action):
Ind.Increment()
j = 0
for member in members:
j += 1
entityActionPerformed([Ent.CHAT_SPACE, parent, entityType, member, Ent.ROLE, role], j, jcount)
Ind.Decrement()
if csvPF:
for member in members:
csvPF.WriteRow({'space': parent, 'member': member, 'role': role, 'action': Act.PerformedName(action), 'message': Act.PREVIEW})
def addMembers(memberNames, members, entityType, i, count):
jcount = len(memberNames)
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
if jcount == 0:
return
if preview:
_previewAction(memberNames, entityType, jcount, Act.REMOVE)
return
kvList.extend([entityType, ''])
Ind.Increment()
j = 0
for memberName in memberNames:
j += 1
body = members[memberName]
kvList[-1] = memberName
try:
callGAPI(chat.spaces().members(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
parent=parent, body=body, **kwargsUAA)
if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
callGAPI(chat.spaces().members(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=memberName, updateMask='role', body={'role': role}, **kwargsUAA)
entityActionPerformed(kvList, j, jcount)
except (GAPI.alreadyExists, GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
Ind.Decrement()
del kvList[-2:]
def deleteMembers(memberNames, entityType, i, count):
jcount = len(memberNames)
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
if jcount == 0:
return
if preview:
_previewAction(memberNames, entityType, jcount, Act.ADD)
return
kvList.extend([entityType, ''])
Ind.Increment()
_deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA)
Ind.Decrement()
del kvList[-2:]
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
cd = buildGAPIObject(API.DIRECTORY)
parent = None
role = CHAT_MEMBER_ROLE_MAP['member']
mtype = CHAT_MEMBER_TYPE_MAP['human']
syncOperation = 'addremove'
kwargs = {}
preview = False
csvPF = None
userList = []
usersSpecified = False
groupList = []
groupsSpecified = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getSpaceName(myarg)
elif myarg == 'role':
role = getChoice(CHAT_MEMBER_ROLE_MAP, mapChoice=True)
elif myarg == 'type':
mtype = getChoice(CHAT_MEMBER_TYPE_MAP, mapChoice=True)
elif myarg in {'addonly', 'removeonly'}:
syncOperation = myarg
elif myarg == 'preview':
preview = True
elif myarg == 'actioncsv':
csvPF = CSVPrintFile(CHAT_SYNC_PREVIEW_TITLES)
elif myarg == 'users':
for user in getEntityList(Cmd.OB_USER_ENTITY):
getUserMemberID(cd, user, userList)
usersSpecified = True
elif myarg in {'member', 'members'}:
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
for user in members:
getUserMemberID(cd, user, userList)
usersSpecified = True
elif myarg == 'groups':
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
getGroupMemberID(cd, group, groupList)
groupsSpecified = True
else:
unknownArgumentExit()
if not parent:
missingArgumentExit('space')
userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
userMembers = {}
syncUsersSet = set()
for user in userList:
memberName = f'{parent}/members/{user}'
userMembers[memberName] = {'member': {'name': f'users/{user}', 'type': mtype}}
syncUsersSet.add(memberName)
groupMembers = {}
syncGroupsSet = set()
for group in groupList:
memberName = f'{parent}/members/group-{group}'
groupMembers[memberName] = {'groupMember': {'name': f'groups/{group}'}}
syncGroupsSet.add(memberName)
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parent}'
i, count, users = getEntityArgument(users)
if useAdminAccess:
kwargs['filter'] = 'member.type != "BOT"'
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, parent], useAdminAccess)
if not chat:
continue
currentUsersSet = set()
currentGroupsSet = set()
try:
members = callGAPIpages(chat.spaces().members(), 'list', 'memberships',
pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, i, count, qfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
parent=parent, showGroups=groupsSpecified, pageSize=CHAT_PAGE_SIZE, **kwargs, **kwargsUAA)
for member in members:
if 'member' in member:
if member['member']['type'] == mtype and member['role'] == role:
currentUsersSet.add(member['name'])
elif 'groupMember' in member:
currentGroupsSet.add(member['name'])
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
continue
if syncOperation != 'addonly':
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
if usersSpecified:
deleteMembers(currentUsersSet-syncUsersSet, userEntityType, i, count)
if groupsSpecified:
deleteMembers(currentGroupsSet-syncGroupsSet, Ent.CHAT_MEMBER_GROUP, i, count)
if syncOperation != 'removeonly':
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
if usersSpecified:
addMembers(syncUsersSet-currentUsersSet, userMembers, userEntityType, i, count)
if groupsSpecified:
addMembers(syncGroupsSet-currentGroupsSet, groupMembers, Ent.CHAT_MEMBER_GROUP, i, count)
if csvPF:
csvPF.writeCSVfile('Chat Member Updates')
CHAT_MEMBERS_FIELDS_CHOICE_MAP = {
"createtime": "createTime",
"deletetime": "deleteTime",
"groupmember": "groupMember",
"member": "member",
"name": "name",
"role": "role",
"state": "state",
}
# gam [<UserTypeEntity>] info chatmember members <ChatMemberList>
# [fields <ChatMemberFieldNameList>]
# [formatjson]
# gam <UserItem> info chatmember asadmin members <ChatMemberList>
# [fields <ChatMemberFieldNameList>]
# [formatjson]
def infoChatMember(users):
cd = buildGAPIObject(API.DIRECTORY)
FJQC = FormatJSONQuoteChar()
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
fieldsList = []
memberNames = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'member', 'members'}:
memberNames.extend(getString(Cmd.OB_CHAT_MEMBER).replace(',', ' ').split())
elif getFieldsList(myarg, CHAT_MEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
else:
FJQC.GetFormatJSON(myarg)
if not memberNames:
missingArgumentExit('members')
fields = getFieldsFromFieldsList(fieldsList)
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, None, useAdminAccess)
if not chat:
continue
jcount = len(memberNames)
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MEMBER, i, count)
kvList.extend([Ent.CHAT_MEMBER, ''])
j = 0
for name in memberNames:
j += 1
kvList[-1] = name
try:
member = callGAPI(chat.spaces().members(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, fields=fields, **kwargsUAA)
_getChatMemberEmail(cd, member)
Ind.Increment()
_showChatItem(member, Ent.CHAT_MEMBER, FJQC, j, jcount)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
continue
def doInfoChatMember():
infoChatMember([None])
# gam [<UserTypeEntity>] show chatmembers
# <ChatSpace>* [types <ChatSpaceTypeList>]
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
# [fields <ChatMemberFieldNameList>]
# [formatjson]
# gam [<UserTypeEntity>] print chatmembers [todrive <ToDriveAttribute>*]
# <ChatSpace>* [types <ChatSpaceTypeList>]
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
# [fields <ChatMemberFieldNameList>]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam <UserItem> show chatmembers asadmin
# <ChatSpace>* [query <String>] [querytime<String> <Time>]
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
# [fields <ChatMemberFieldNameList>]
# [formatjson]
# gam <UserItem> print chatmembers asadmin [todrive <ToDriveAttribute>*]
# <ChatSpace>* [query <String>] [querytime<String> <Time>]
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
# [fields <ChatMemberFieldNameList>]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
def printShowChatMembers(users):
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name'] if not isinstance(users, list) else ['space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP)
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
if useAdminAccess:
queries = ['customer = "customers/my_customer" AND spaceType = "SPACE"']
queryTimes = {}
fieldsList = []
pfilter = ''
kwargs = {}
kwargsCS = {}
parentList = []
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parentList.append({'name': getSpaceName(myarg), 'displayName': ''})
elif getFieldsList(myarg, CHAT_MEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
elif myarg == 'showinvited':
kwargs['showInvited'] = getBoolean()
elif myarg == 'showgroups':
kwargs['showGroups'] = getBoolean()
elif myarg =='filter':
pfilter = getString(Cmd.OB_STRING)
elif not useAdminAccess and _getChatSpaceListParms(myarg, kwargsCS):
pass
elif useAdminAccess and _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
pass
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if useAdminAccess:
if not parentList:
kwargsCS['orderBy'] = OBY.orderBy
substituteQueryTimes(queries, queryTimes)
kwargsCS['query'] = queries[0]
kwargsCS['useAdminAccess'] = True
else:
if not parentList and not kwargsCS:
kwargsCS['filter'] = 'spaceType = "SPACE" OR spaceType = "GROUP_CHAT" OR spaceType = "DIRECT_MESSAGE"'
if pfilter:
if 'member.type' not in pfilter:
kwargs['filter'] = 'member.type != "BOT" AND '+pfilter
else:
kwargs['filter'] = pfilter
else:
kwargs['filter'] = 'member.type != "BOT"'
fields = getItemFieldsFromFieldsList('memberships', fieldsList)
i, count, users = getEntityArgument(users)
if useAdminAccess:
_chkChatAdminAccess(count)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, None], useAdminAccess)
if not chat:
continue
if useAdminAccess:
_, chatsp, _ = buildChatServiceObject(API.CHAT_SPACES_ADMIN, user, i, count, None, useAdminAccess)
else:
_, chatsp, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, None, useAdminAccess)
if not chatsp:
continue
if kwargsCS:
if useAdminAccess:
try:
spaces = callGAPIpages(chatsp.spaces(), 'search', 'spaces',
pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, queries[0], True),
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=CHAT_PAGE_SIZE,
**kwargsCS)
for space in spaces:
if space['spaceType'] == 'SPACE' and 'membershipCount' in space:
parentList.append({'name': space['name'], 'displayName': space.get('displayName', 'None')})
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError,
GAPI.permissionDenied, GAPI.failedPrecondition) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
else:
try:
spaces = callGAPIpages(chatsp.spaces(), 'list', 'spaces',
pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, kwargsCS['filter']),
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=CHAT_PAGE_SIZE,
**kwargsCS)
for space in spaces:
# if 'membershipCount' in space:
# parentList.append({'name': space['name'], 'displayName': space.get('displayName', 'None')})
parentList.append({'name': space['name'], 'displayName': space.get('displayName', 'None')})
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
continue
jcount = len(parentList)
j = 0
for parent in parentList:
j += 1
parentName = parent['name']
kvList[-1] = parentName
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parentName}'
if 'filter' in kwargs:
qfilter += f', {kwargs["filter"]}'
try:
if not parent['displayName']:
space = callGAPI(chatsp.spaces(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=parentName, fields='displayName', **kwargsUAA)
parent['displayName'] = space.get('displayName', 'None')
members = callGAPIpages(chat.spaces().members(), 'list', 'memberships',
pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, j, jcount, qfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
parent=parentName, fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargs, **kwargsUAA)
for member in members:
_getChatMemberEmail(cd, member)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), j, jcount)
continue
if not csvPF:
kcount = len(members)
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, kcount, Ent.CHAT_MEMBER, j, jcount)
Ind.Increment()
k = 0
for member in members:
k += 1
member['space'] = {'name': parentName, 'displayName': parent['displayName']}
_showChatItem(member, Ent.CHAT_MEMBER, FJQC, k, kcount)
Ind.Decrement()
else:
for member in members:
_printChatItem(user, member, parent, Ent.CHAT_MEMBER, csvPF, FJQC, addCSVData)
if csvPF:
csvPF.writeCSVfile('Chat Members')
def doPrintShowChatMembers():
printShowChatMembers([None])
def _getChatSenderEmail(cd, sender):
if sender['type'] == 'HUMAN':
_, senderUid = sender['name'].split('/')
sender['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{senderUid}', cd, None, emailTypes=['user'])
def trimChatMessageIfRequired(body):
msgLen = len(body['text'])
if msgLen > 4096:
stderrWarningMsg(Msg.TRIMMED_MESSAGE_FROM_LENGTH_TO_MAXIMUM.format(msgLen, 4096))
body['text'] = body['text'][:4095]
CHAT_MESSAGE_REPLY_OPTION_MAP = {
'fail': 'REPLY_MESSAGE_OR_FAIL',
'fallbacktonew': 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD',
}
# gam [<UserTypeEntityu>] create chatmessage <ChatSpace>
# <ChatContent>
# [messageId <ChatMessageID>]
# [(thread <ChatThread>)|(threadkey <String>) [replyoption fail|fallbacktonew]]
# [returnidonly]
def createChatMessage(users):
messageId = messageReplyOption = parent = None
body = {}
returnIdOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parent = getSpaceName(myarg)
elif myarg == 'thread':
body.setdefault('thread', {})
body['thread']['name'] = getString(Cmd.OB_CHAT_THREAD)
Act.Set(Act.RESPOND)
elif myarg == 'threadkey':
body.setdefault('thread', {})
body['thread']['threadKey'] = getString(Cmd.OB_STRING)
Act.Set(Act.RESPOND)
elif myarg == 'replyoption':
messageReplyOption = getChoice(CHAT_MESSAGE_REPLY_OPTION_MAP, mapChoice=True)
elif myarg in SORF_TEXT_ARGUMENTS:
body['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
elif myarg == 'messageid':
messageId = getString(Cmd.OB_CHAT_MESSAGE_ID, minLen=1, maxLen=63)
elif myarg == 'returnidonly':
returnIdOnly = True
else:
unknownArgumentExit()
if not parent:
missingArgumentExit('space')
if 'text' not in body:
missingArgumentExit('text or textfile')
if 'thread' in body and messageReplyOption is None:
messageReplyOption = CHAT_MESSAGE_REPLY_OPTION_MAP['fail']
trimChatMessageIfRequired(body)
action = Act.Get()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_SPACE, parent])
if not chat:
continue
try:
resp = callGAPI(chat.spaces().messages(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
parent=parent, requestId=str(uuid.uuid4()),
messageReplyOption=messageReplyOption, messageId=messageId, body=body)
if not returnIdOnly:
kvList.extend([Ent.CHAT_MESSAGE, resp['name']])
if 'clientAssignedMessageId' in resp:
kvList.extend([Ent.CHAT_MESSAGE_ID, resp['clientAssignedMessageId']])
kvList.extend([Ent.CHAT_THREAD, resp['thread']['name']])
if (action == Act.RESPOND) and not resp.get('threadReply', False):
Act.Set(Act.CREATE)
entityActionPerformed(kvList, i, count)
Act.Set(action)
else:
writeStdout(f'{resp["name"]}\n')
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
def doCreateChatMessage():
createChatMessage([None])
# gam [<UserTypeMessage>] update chatmessage name <ChatMessage>
# <ChatContent>
def updateChatMessage(users):
name = None
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
name = getString(Cmd.OB_CHAT_MESSAGE)
elif myarg in SORF_TEXT_ARGUMENTS:
body['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
else:
unknownArgumentExit()
if not name:
missingArgumentExit('name')
if 'text' not in body:
missingArgumentExit('text or textfile')
trimChatMessageIfRequired(body)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_MESSAGE, name])
if not chat:
continue
try:
resp = callGAPI(chat.spaces().messages(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, updateMask='text', body=body)
kvList.extend([Ent.CHAT_THREAD, resp['thread']['name']])
entityActionPerformed(kvList, i, count)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
def doUpdateChatMessage():
updateChatMessage([None])
# gam [<UserTypeEntity>] delete chatmessage name <ChatMessage>
def deleteChatMessage(users):
name = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
name = getString(Cmd.OB_CHAT_MESSAGE)
else:
unknownArgumentExit()
if not name:
missingArgumentExit('name')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_MESSAGE, name])
if not chat:
continue
try:
callGAPI(chat.spaces().messages(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name)
entityActionPerformed(kvList, i, count)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
def doDeleteChatMessage():
deleteChatMessage([None])
CHAT_MESSAGES_FIELDS_CHOICE_MAP = {
"accessorywidgets": "accessoryWidgets",
"actionresponse": "actionResponse",
"annotations": "annotations",
"argumenttext": "argumentText",
"attachedgifs": "attachedGifs",
"attachment": "attachment",
"cards": "cards",
"cardsv2": "cardsV2",
"clientassignedmessageid": "clientAssignedMessageId",
"createtime": "createTime",
"deletetime": "deleteTime",
"deletionmetadata": "deletionMetadata",
"emojireactionsummaries": "emojiReactionSummaries",
"fallbacktext": "fallbackText",
"formattedtext": "formattedText",
"lastupdatetime": "lastUpdateTime",
"matchedurl": "matchedUrl",
"name": "name",
"privatemessageviewer": "privateMessageViewer",
"quotedmessagemetadata": "quotedMessageMetadata",
"sender": "sender",
"slashcommand": "slashCommand",
"space": "space",
"text": "text",
"thread": "thread",
"threadreply": "threadReply",
}
# gam [<UserTypeEntity>] info chatmessage name <ChatMessage>
# [fields <ChatMessageFieldNameList>]
# [formatjson]
def infoChatMessage(users):
cd = buildGAPIObject(API.DIRECTORY)
FJQC = FormatJSONQuoteChar()
fieldsList = []
name = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
name = getString(Cmd.OB_CHAT_MESSAGE)
elif getFieldsList(myarg, CHAT_MESSAGES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
else:
FJQC.GetFormatJSON(myarg)
if not name:
missingArgumentExit('name')
fields = getFieldsFromFieldsList(fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_MESSAGE, name])
if not chat:
continue
try:
message = callGAPI(chat.spaces().messages(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name, fields=fields)
_getChatSenderEmail(cd, message['sender'])
if not FJQC.formatJSON:
entityPerformAction(kvList, i, count)
Ind.Increment()
_showChatItem(message, Ent.CHAT_MESSAGE, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
def doInfoChatMessage():
infoChatMessage([None])
# gam <UserTypeEntity> show chatmessages
# <ChatSpace>+
# [showdeleted [<Boolean>]] [filter <String>]
# [fields <ChatMessageFieldNameList>]
# [formatjson]
# gam <UserTypeEntity> print chatmessages [todrive <ToDriveAttribute>*]
# <ChatSpace>+
# [showdeleted [<Boolean>]] [filter <String>]
# [fields <ChatMessageFieldNameList>]
# [formatjson [quotechar <Character>]]
def printShowChatMessages(users):
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name'] if not isinstance(users, list) else ['space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
pfilter = None
parentList = []
showDeleted = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parentList.append({'name': getSpaceName(myarg), 'displayName': ''})
elif getFieldsList(myarg, CHAT_MESSAGES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
elif myarg == 'showdeleted':
showDeleted = getBoolean()
elif myarg =='filter':
pfilter = getString(Cmd.OB_STRING)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not parentList:
missingArgumentExit('space')
fields = getItemFieldsFromFieldsList('messages', fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_SPACE, None])
if not chat:
continue
_, chatspg, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, None)
if not chatspg:
continue
jcount = len(parentList)
j = 0
for parent in parentList:
j += 1
parentName = parent['name']
kvList[-1] = parentName
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parentName}'
if pfilter:
qfilter += f', {pfilter}'
try:
if not parent['displayName']:
space = callGAPI(chatspg.spaces(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=parentName, fields='displayName')
parent['displayName'] = space.get('displayName', 'None')
messages = callGAPIpages(chat.spaces().messages(), 'list', 'messages',
pageMessage=_getChatPageMessage(Ent.CHAT_MESSAGE, user, i, count, qfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageSize=CHAT_PAGE_SIZE, parent=parentName, filter=pfilter, showDeleted=showDeleted,
fields=fields)
for message in messages:
if 'sender' in message:
_getChatSenderEmail(cd, message['sender'])
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
break
if not csvPF:
kcount = len(messages)
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, kcount, Ent.CHAT_MESSAGE, j, jcount)
Ind.Increment()
k = 0
for message in messages:
k += 1
if 'space' in message:
message['space']['displayName'] = parent['displayName']
_showChatItem(message, Ent.CHAT_MESSAGE, FJQC, k, kcount)
Ind.Decrement()
else:
for message in messages:
_printChatItem(user, message, parent, Ent.CHAT_MESSAGE, csvPF, FJQC)
if csvPF:
csvPF.writeCSVfile('Chat Messages')
# gam <UserTypeEntity> info chatevent name <ChatEvent>
# [formatjson]
def infoChatEvent(users):
FJQC = FormatJSONQuoteChar()
name = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
name = getString(Cmd.OB_CHAT_EVENT)
else:
FJQC.GetFormatJSON(myarg)
if not name:
missingArgumentExit('name')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_EVENTS, user, i, count, [Ent.CHAT_EVENT, name])
if not chat:
continue
try:
event = callGAPI(chat.spaces().spaceEvents(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=name)
if not FJQC.formatJSON:
entityPerformAction(kvList, i, count)
Ind.Increment()
_showChatItem(event, Ent.CHAT_EVENT, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
def doInfoChatEvent():
infoChatEvent([None])
# gam <UserTypeEntity> show chatevents
# <ChatSpace>+
# filter <String>
# [formatjson]
# gam <UserTypeEntity> print chatevents [todrive <ToDriveAttribute>*]
# <ChatSpace>+
# filter <String>
# [formatjson [quotechar <Character>]]
def printShowChatEvents(users):
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name'] if not isinstance(users, list) else ['space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
pfilter = None
parentList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
parentList.append({'name': getSpaceName(myarg), 'displayName': ''})
elif myarg =='filter':
pfilter = getString(Cmd.OB_STRING)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not parentList:
missingArgumentExit('space')
if not pfilter:
missingArgumentExit('filter')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_EVENTS, user, i, count, [Ent.CHAT_SPACE, None])
if not chat:
continue
_, chatspg, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, None)
if not chatspg:
continue
jcount = len(parentList)
j = 0
for parent in parentList:
j += 1
parentName = parent['name']
kvList[-1] = parentName
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parentName}, {pfilter}'
try:
if not parent['displayName']:
space = callGAPI(chatspg.spaces(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=parentName, fields='displayName')
parent['displayName'] = space.get('displayName', 'None')
events = callGAPIpages(chat.spaces().spaceEvents(), 'list', 'spaceEvents',
pageMessage=_getChatPageMessage(Ent.CHAT_EVENT, user, i, count, qfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageSize=CHAT_PAGE_SIZE, parent=parentName, filter=pfilter)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
break
if not csvPF:
kcount = len(events)
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, kcount, Ent.CHAT_EVENT, j, jcount)
Ind.Increment()
k = 0
for event in events:
k += 1
event['space'] = {'name': parentName, 'displayName': parent['displayName']}
_showChatItem(event, Ent.CHAT_EVENT, FJQC, k, kcount)
Ind.Decrement()
else:
for event in events:
_printChatItem(user, event, parent, Ent.CHAT_EVENT, csvPF, FJQC)
if csvPF:
csvPF.writeCSVfile('Chat Events')
def buildMeetServiceObject(api=API.MEET, user=None, i=0, count=0, entityTypeList=None):
if GC.Values[GC.MEET_V2_BETA]:
api = API.MEET_BETA
user, meet = buildGAPIServiceObject(api, user, i, count)
kvList = [Ent.USER, user]
if entityTypeList is not None:
kvList.extend(entityTypeList)
return user, meet, kvList
def _showMeetItem(mitem, FJQC=None, i=0, count=0):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(mitem), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.MEET_SPACE, mitem['name']], i, count)
Ind.Increment()
showJSON(None, mitem)
Ind.Decrement()
MEET_SPACE_OPTIONS_MAP = {
'accesstype': 'accessType',
'entrypointaccess': 'entryPointAccess',
'moderation': 'moderation',
'chatrestriction': 'chatRestriction',
'reactionrestriction': 'reactionRestriction',
'presentrestriction': 'presentRestriction',
'defaultjoinasviewer': 'defaultJoinAsViewerType',
'firstjoiner': 'firstJoinerType',
'autorecording': 'recordingConfig',
'autosmartnotes': 'smartNotesConfig',
'autotranscription': 'transcriptionConfig',
}
MEET_SPACE_ACCESSTYPE_CHOICES = {'open', 'trusted', 'restricted'}
MEET_SPACE_ENTRYPOINTACCESS_CHOICES_MAP = {
'all': 'ALL',
'creatorapponly': 'CREATOR_APP_ONLY'
}
MEET_SPACE_RESTRICTIONS_CHOICES_MAP = {
'hostsonly': 'HOSTS_ONLY',
'norestriction': 'NO_RESTRICTION'
}
#MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP = {
# 'hostsonly': 'HOSTS_ONLY',
# 'anyone': 'ANYONE'
# }
MEET_SPACE_ARTIFACT_SUB_OPTIONS = {
'recordingConfig': 'autoRecordingGeneration',
'smartNotesConfig': 'autoSmartNotesGeneration',
'transcriptionConfig': 'autoTranscriptionGeneration'
}
# [accesstype open|trusted|restricted]
# [entrypointaccess all|creatorapponly]
# [moderation <Boolean>]
# [chatrestriction hostsonly|norestriction]
# [reactionrestriction hostsonly|norestriction]
# [presentrestriction hostsonly|norestriction]
# [defaultjoinasviewer <Boolean>]
# [firstjoiner hostsonly|anyone]
# [autorecording <Boolean>]
# [autosmartnotes <Boolean>]
# [autotranscription <Boolean>]
def _getMeetSpaceParameters(myarg, body):
option = MEET_SPACE_OPTIONS_MAP.get(myarg, None)
if option is None:
return False
if option == 'accessType':
body['config'][option] = getChoice(MEET_SPACE_ACCESSTYPE_CHOICES).upper()
elif option == 'entryPointAccess':
body['config'][option] = getChoice(MEET_SPACE_ENTRYPOINTACCESS_CHOICES_MAP, mapChoice=True)
elif option == 'moderation':
body['config'][option] = 'ON' if getBoolean() else 'OFF'
elif option in {'chatRestriction', 'reactionRestriction', 'presentRestriction'}:
body['config'].setdefault('moderationRestrictions', {})
body['config']['moderationRestrictions'][option] = getChoice(MEET_SPACE_RESTRICTIONS_CHOICES_MAP, mapChoice=True)
elif option == 'defaultJoinAsViewerType':
body['config'][option] = 'ON' if getBoolean() else 'OFF'
# elif option == 'firstJoinerType':
# body['config'][option] = getChoice(MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP, mapChoice=True)
elif option in {'recordingConfig', 'transcriptionConfig', 'smartNotesConfig'}:
body['config'].setdefault('artifactConfig', {})
body['config']['artifactConfig'].setdefault(option, {})
body['config']['artifactConfig'][option][MEET_SPACE_ARTIFACT_SUB_OPTIONS[option]] = 'ON' if getBoolean() else 'OFF'
return True
# gam <UserTypeEntity> create meetspace
# <MeetSpaceOptions>*
# [formatjson|returnidonly]
def createMeetSpace(users):
FJQC = FormatJSONQuoteChar()
body = {'config': {'accessType': 'TRUSTED',
'entryPointAccess': 'ALL',
# 'moderation': 'OFF',
# 'moderationRestrictions': {},
# 'firstJoinerType': 'ANYONE',
}}
returnIdOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getMeetSpaceParameters(myarg, body):
pass
elif myarg == 'returnidonly':
returnIdOnly = True
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, None])
if not meet:
continue
try:
space = callGAPI(meet.spaces(), 'create',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
body=body)
if not returnIdOnly:
kvList[-1] = space['name']
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
Ind.Increment()
_showMeetItem(space, FJQC)
Ind.Decrement()
else:
writeStdout(f'{space["name"]}\n')
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.MEET_SPACE, None], str(e))
# gam <UserTypeEntity> update meetspace <MeetSpaceName>
# <MeetSpaceOptions>*
# [formatjson]
def updateMeetSpace(users):
FJQC = FormatJSONQuoteChar()
name = None
body = {'config': {}}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
name = getSpaceName(myarg)
elif _getMeetSpaceParameters(myarg, body):
pass
else:
FJQC.GetFormatJSON(myarg)
if not name:
missingArgumentExit('space')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, name])
if not meet:
continue
try:
space = callGAPI(meet.spaces(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name, updateMask='', body=body)
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
Ind.Increment()
_showMeetItem(space, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.MEET_SPACE, name], str(e))
# gam <UserTypeEntity> info meetspace <MeetSpaceName>
# [formatjson]
def infoMeetSpace(users):
FJQC = FormatJSONQuoteChar()
name = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
name = getSpaceName(myarg)
else:
FJQC.GetFormatJSON(myarg)
if not name:
missingArgumentExit('space')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, name])
if not meet:
continue
try:
space = callGAPI(meet.spaces(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
if not FJQC.formatJSON:
entityPerformAction(kvList, i, count)
Ind.Increment()
_showMeetItem(space, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit(kvList, str(e))
# gam <UserTypeEntity> end meetconference <MeetSpaceName>
def endMeetConference(users):
name = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
name = getSpaceName(myarg)
else:
unknownArgumentExit()
if not name:
missingArgumentExit('space')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, name, Ent.MEET_CONFERENCE, None])
if not meet:
continue
try:
callGAPI(meet.spaces(), 'endActiveConference',
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
entityActionPerformed(kvList, i,count)
except (GAPI.failedPrecondition, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.MEET_CONFERENCE, name], str(e))
def _getMeetPageMessage(entityType, user, i, count, pfilter):
printGettingAllEntityItemsForWhom(entityType, user, i, count, pfilter)
return getPageMessageForWhom()
MEET_CONFERENCE_TIME_OBJECTS = {'startTime', 'endTime', 'expireTime', 'earliestStartTime', 'latestEndTime'}
def _showMeetConfItem(citem, entityType, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(citem, timeObjects=MEET_CONFERENCE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([entityType, citem['name']], i, count)
Ind.Increment()
showJSON(None, citem, timeObjects=MEET_CONFERENCE_TIME_OBJECTS)
Ind.Decrement()
def _printMeetConfItem(user, citem, csvPF, FJQC):
baserow = {'User': user}
row = flattenJSON(citem, flattened=baserow.copy(), timeObjects=MEET_CONFERENCE_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row.update({'name': citem['name'],
'JSON': json.dumps(cleanJSON(citem, timeObjects=MEET_CONFERENCE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
csvPF.WriteRowNoFilter(row)
# gam <UserItem> show meetconferences
# [space <MeetSpaceName>] [code <String>]
# [andquery|orquery <String>] [querytime<String> <Time>]
# [formatjson]
# gam <UserItem> print meetconferences [todrive <ToDriveAttribute>*]
# [space <MeetSpaceName>] [code <String>]
# [andquery|orquery <String>] [querytime<String> <Time>]
# [formatjson [quotechar <Character>]]
def printShowMeetConferences(users):
def _updateQuery(conjunction, clause):
if queries[0]:
queries[0] += f' {conjunction} '
queries[0] += clause
csvPF = CSVPrintFile(['User', 'space', 'name', 'startTime', 'endTime', 'expireTime']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
queries = ['']
queryTimes = {}
pfilter = ''
kwargs = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
_updateQuery('AND', f'space.name = "{getSpaceName(myarg)}"')
elif myarg == 'code':
_updateQuery('AND', f'space.meeting_code = "{getString(Cmd.OB_STRING)}"')
elif myarg == 'andquery':
_updateQuery('AND', getString(Cmd.OB_QUERY))
elif myarg == 'orquery':
_updateQuery('OR', getString(Cmd.OB_QUERY))
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
substituteQueryTimes(queries, queryTimes)
if queries[0]:
pfilter = kwargs['filter'] = queries[0]
else:
pfilter = None
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_CONFERENCE, None])
if not meet:
continue
try:
confRecs = callGAPIpages(meet.conferenceRecords(), 'list', 'conferenceRecords',
pageMessage=_getMeetPageMessage(Ent.MEET_CONFERENCE, user, i, count, pfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
pageSize=100, **kwargs)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit(kvList, str(e))
continue
jcount = len(confRecs)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, jcount, Ent.MEET_CONFERENCE, i, count)
Ind.Increment()
j = 0
for confRec in confRecs:
j += 1
_showMeetConfItem(confRec, Ent.MEET_CONFERENCE, FJQC, j, jcount)
Ind.Decrement()
else:
for confRec in confRecs:
_printMeetConfItem(user, confRec, csvPF, FJQC)
if csvPF:
csvPF.writeCSVfile(Ent.Plural(Ent.MEET_CONFERENCE))
# gam <UserItem> show meetparticipants <MeetConferenceName>
# [query <String>] [querytime<String> <Time>]
# [formatjson]
# gam <UserItem> print meetparticipants <MeetConferenceName> [todrive <ToDriveAttribute>*]
# [query <String>] [querytime<String> <Time>]
# [formatjson [quotechar <Character>]]
# gam <UserItem> show meetrecordings <MeetConferenceName>
# [formatjson]
# gam <UserItem> print meetrecordings <MeetConferenceName> [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
# gam <UserItem> show meettranscripts <MeetConferenceName>
# [formatjson]
# gam <UserItem> print meettranscripts <MeetConferenceName> [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
def _printShowMeetItems(users, entityType):
def _updateQuery(conjunction, clause):
if queries[0]:
queries[0] += f' {conjunction} '
queries[0] += clause
csvPF = CSVPrintFile(['User', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
queries = ['']
queryTimes = {}
pfilter = ''
kwargs = {}
parent = getString(Cmd.OB_MEET_CONFERENCE_NAME)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif entityType == Ent.MEET_PARTICIPANT and myarg == 'andquery':
_updateQuery('AND', getString(Cmd.OB_QUERY))
elif entityType == Ent.MEET_PARTICIPANT and myarg == 'orquery':
_updateQuery('OR', getString(Cmd.OB_QUERY))
elif entityType == Ent.MEET_PARTICIPANT and myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
substituteQueryTimes(queries, queryTimes)
if queries[0]:
pfilter = kwargs['filter'] = queries[0]
else:
pfilter = None
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_CONFERENCE, parent])
if not meet:
continue
if entityType == Ent.MEET_PARTICIPANT:
service = meet.conferenceRecords().participants()
recType = 'participants'
pageSize = 250
elif entityType == Ent.MEET_RECORDING:
service = meet.conferenceRecords().recordings()
recType = 'recordings'
pageSize = 100
else:
service = meet.conferenceRecords().transcripts()
recType = 'transcripts'
pageSize = 100
try:
confRecs = callGAPIpages(service, 'list', recType,
pageMessage=_getMeetPageMessage(entityType, user, i, count, pfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=parent, pageSize=pageSize, **kwargs)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit(kvList, str(e))
continue
jcount = len(confRecs)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
Ind.Increment()
j = 0
for confRec in confRecs:
j += 1
_showMeetConfItem(confRec, entityType, FJQC, j, jcount)
Ind.Decrement()
else:
for confRec in confRecs:
_printMeetConfItem(user, confRec, csvPF, FJQC)
if csvPF:
csvPF.writeCSVfile(Ent.Plural(entityType))
def printShowMeetParticipants(users):
_printShowMeetItems(users, Ent.MEET_PARTICIPANT)
def printShowMeetRecordings(users):
_printShowMeetItems(users, Ent.MEET_RECORDING)
def printShowMeetTranscripts(users):
_printShowMeetItems(users, Ent.MEET_TRANSCRIPT)
def _getOrgunitsOrgUnitIdPath(cd, orgUnit):
if orgUnit.startswith('orgunits/'):
orgUnit = f'id:{orgUnit[9:]}'
orgUnitPath, orgUnitId = getOrgUnitId(cd, orgUnit)
return (orgUnitPath, f'orgunits/{orgUnitId[3:]}')
def _getChromePolicySchemaName():
name = getString(Cmd.OB_SCHEMA_NAME)
if not name.startswith('customers'):
name = f'customers/{GC.Values[GC.CUSTOMER_ID]}/policySchemas/{name}'
return name
def _getChromePolicySchema(cp, name, fields):
if not name.startswith('customers'):
name = f'customers/{GC.Values[GC.CUSTOMER_ID]}/policySchemas/{name}'
try:
return callGAPI(cp.customers().policySchemas(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
name=name, fields=fields)
except GAPI.notFound:
entityDoesNotExistExit(Ent.CHROME_POLICY_SCHEMA, name)
except (GAPI.badRequest, GAPI.forbidden):
accessErrorExit(None)
def commonprefix(m):
'''Given a list of strings m, return string which is prefix common to all'''
s1 = min(m)
loc = s1.find('ENUM_')
if loc > 0:
return s1[:loc+5]
s2 = max(m)
for i, c in enumerate(s1):
if c != s2[i]:
return s1[:i]
return s1
def simplifyChromeSchema(schema):
schema_name = schema['name'].split('/')[-1]
schema_dict = {'name': schema_name,
'description': schema.get('policyDescription', ''),
'settings': {}
}
fieldDescriptions = schema['fieldDescriptions']
for mtype in schema['definition']['messageType']:
for setting in mtype['field']:
setting_name = setting['name']
setting_dict = {'name': setting_name,
'constraints': None,
'descriptions': [],
'type': setting['type'],
}
if setting_dict['type'] == 'TYPE_STRING' and setting.get('label') == 'LABEL_REPEATED':
setting_dict['type'] = 'TYPE_LIST'
if setting_dict['type'] == 'TYPE_ENUM':
type_name = setting['typeName']
for an_enum in schema['definition']['enumType']:
if an_enum['name'] == type_name:
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
setting_dict['enum_prefix'] = commonprefix(setting_dict['enums'])
prefix_len = len(setting_dict['enum_prefix'])
setting_dict['enums'] = [enum[prefix_len:] for enum in setting_dict['enums'] if not enum.endswith('UNSPECIFIED')]
setting_dict['descriptions'] = ['']*len(setting_dict['enums'])
for i, an in enumerate(setting_dict['enums']):
for fdesc in fieldDescriptions:
if fdesc.get('field') == setting_name:
for d in fdesc.get('knownValueDescriptions', []):
if d['value'][prefix_len:] == an:
setting_dict['descriptions'][i] = d.get('description', '')
break
break
break
elif setting_dict['type'] == 'TYPE_MESSAGE':
continue
else:
setting_dict['enums'] = None
for fdesc in schema['fieldDescriptions']:
if fdesc['field'] == setting_name:
if 'knownValueDescriptions' in fdesc:
setting_dict['descriptions'] = fdesc['knownValueDescriptions']
elif 'description' in fdesc:
setting_dict['descriptions'] = [fdesc['description']]
schema_dict['settings'][setting_name.lower()] = setting_dict
return(schema_name, schema_dict)
def _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail):
if groupEmail:
Cmd.Backup()
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'group'))
targetName, targetResource = _getOrgunitsOrgUnitIdPath(cd, getString(Cmd.OB_ORGUNIT_PATH))
return (targetName, targetName, targetResource, Ent.ORGANIZATIONAL_UNIT, cp.customers().policies().orgunits())
def _getPolicyGroupTarget(cd, cp, myarg, orgUnit):
if orgUnit:
Cmd.Backup()
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'ou|org|orgunit'))
targetName = getEmailAddress(returnUIDprefix='uid:')
targetResource = f"groups/{convertEmailAddressToUID(targetName, cd, emailType='group')}"
return (targetName, targetName, targetResource, Ent.GROUP, cp.customers().policies().groups())
def checkPolicyArgs(targetResource, printer_id, app_id):
if not targetResource:
missingArgumentExit('ou|org|orgunit|group')
if printer_id and app_id:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('printerid', 'appid'))
def setPolicyKVList(baseList, printer_id, app_id):
kvList = baseList[:]
if app_id:
kvList.extend([Ent.APP_ID, app_id])
elif printer_id:
kvList.extend([Ent.PRINTER_ID, printer_id])
return kvList
def updatePolicyRequests(body, targetResource, printer_id, app_id):
for request in body['requests']:
request.setdefault('policyTargetKey', {})
request['policyTargetKey']['targetResource'] = targetResource
if app_id or printer_id:
request['policyTargetKey'].setdefault('additionalTargetKeys', {})
if app_id:
request['policyTargetKey']['additionalTargetKeys']['app_id'] = app_id
elif printer_id:
request['policyTargetKey']['additionalTargetKeys']['printer_id'] = printer_id
# gam delete chromepolicy
# (<SchemaName> [<JSONData>])+
# ((ou|org|orgunit <OrgUnitItem>)|(group <GroupItem>))
# [(printerid <PrinterID>)|(appid <AppID>)]
def doDeleteChromePolicy():
cp = buildGAPIObject(API.CHROMEPOLICY)
cd = buildGAPIObject(API.DIRECTORY)
customer = _getCustomersCustomerIdWithC()
app_id = groupEmail = orgUnit = printer_id = targetResource = None
body = {'requests': []}
schemaNameList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'ou', 'org', 'orgunit'}:
orgUnit, targetName, targetResource, entityType, service = _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail)
function = 'batchInherit'
elif myarg == 'group':
groupEmail, targetName, targetResource, entityType, service = _getPolicyGroupTarget(cd, cp, myarg, orgUnit)
function = 'batchDelete'
elif myarg == 'printerid':
printer_id = getString(Cmd.OB_PRINTER_ID)
elif myarg == 'appid':
app_id = getString(Cmd.OB_APP_ID)
else:
schema = _getChromePolicySchema(cp, Cmd.Previous(), 'name')
schemaName = schema['name'].split('/')[-1]
schemaNameList.append(schemaName)
body['requests'].append({'policySchema': schemaName})
if checkArgumentPresent('json'):
jsonData = getJSON(['direct', 'name', 'orgUnitPath', 'parentOrgUnitPath', 'group'])
if 'additionalTargetKeys' in jsonData:
body['requests'][-1].setdefault('policyTargetKey', {'additionalTargetKeys': {}})
for atk in jsonData['additionalTargetKeys']:
body['requests'][-1]['policyTargetKey']['additionalTargetKeys'][atk['name']] = atk['value']
checkPolicyArgs(targetResource, printer_id, app_id)
count = len(body['requests'])
if count != 1:
entityPerformActionNumItems([entityType, targetName], count, Ent.CHROME_POLICY)
if count == 0:
return
kvList = setPolicyKVList([entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)], printer_id, app_id)
updatePolicyRequests(body, targetResource, printer_id, app_id)
try:
callGAPI(service, function,
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, body=body)
entityActionPerformed(kvList)
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable, GAPI.quotaExceeded) as e:
entityActionFailedWarning(kvList, str(e))
CHROME_SCHEMA_TYPE_MESSAGE = {
'chrome.users.AutoUpdateCheckPeriodNew':
{'autoupdatecheckperiodminutesnew':
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
'chrome.users.AutoUpdateCheckPeriodNewV2':
{'autoupdatecheckperiodminutesnew':
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
'chrome.users.BrowserSwitcherDelayDuration':
{'browserswitcherdelayduration':
{'casedField': 'browserSwitcherDelayDuration',
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1}},
'chrome.users.CloudReportingUploadFrequencyV2':
{'cloudreportinguploadfrequency':
{'casedField': 'cloudReportingUploadFrequency',
'type': 'count', 'minVal': 3, 'maxVal': 24, 'scale': 1}},
'chrome.users.FetchKeepaliveDurationSecondsOnShutdown':
{'fetchkeepalivedurationsecondsonshutdown':
{'casedField': 'fetchKeepaliveDurationSecondsOnShutdown',
'type': 'duration', 'minVal': 0, 'maxVal': 5, 'scale': 1}},
'chrome.users.MaxInvalidationFetchDelay':
{'maxinvalidationfetchdelay':
{'casedField': 'maxInvalidationFetchDelay',
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1, 'default': 10}},
'chrome.users.PrintingMaxSheetsAllowed':
{'printingmaxsheetsallowednullable':
{'casedField': 'printingMaxSheetsAllowedNullable',
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1}},
'chrome.users.PrintJobHistoryExpirationPeriodNew':
{'printjobhistoryexpirationperioddaysnew':
{'casedField': 'printJobHistoryExpirationPeriodDaysNew',
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 86400}},
'chrome.users.RelaunchNotificationWithDurationV2':
{'relaunchnotificationperiodduration':
{'casedField': 'relaunchNotificationPeriodDuration',
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 3600}},
'chrome.users.SecurityTokenSessionSettings':
{'securitytokensessionnotificationseconds':
{'casedField': 'securityTokenSessionNotificationSeconds',
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1}},
'chrome.users.SessionLength':
{'sessiondurationlimit':
{'casedField': 'sessionDurationLimit',
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60}},
'chrome.users.UpdatesSuppressed':
{'updatessuppresseddurationmin':
{'casedField': 'updatesSuppressedDurationMin',
'type': 'count', 'minVal': 1, 'maxVal': 1440, 'scale': 1},
'updatessuppressedstarttime':
{'casedField': 'updatesSuppressedStartTime',
'type': 'timeOfDay'}},
'chrome.devices.managedguest.Avatar':
{'useravatarimage':
{'casedField': 'userAvatarImage',
'type': 'downloadUri'}},
'chrome.devices.managedguest.Wallpaper':
{'wallpaperimage':
{'casedField': 'wallpaperImage',
'type': 'downloadUri'}},
'chrome.devices.SignInWallpaperImage':
{'devicewallpaperimage':
{'casedField': 'deviceWallpaperImage',
'type': 'downloadUri'}},
'chrome.users.Avatar':
{'useravatarimage':
{'casedField': 'userAvatarImage',
'type': 'downloadUri'}},
'chrome.users.Wallpaper':
{'wallpaperimage':
{'casedField': 'wallpaperImage',
'type': 'downloadUri'}},
}
CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN = re.compile(r'^([a-z]+)-(\d+)$')
CHROME_TARGET_VERSION_PATTERN = re.compile(r'^(\d{1,4}\.){1,4}$')
# gam update chromepolicy [convertcrnl]
# (<SchemaName> ((<Field> <Value>)+ | <JSONData>))+
# ((ou|orgunit <OrgUnitItem>)|(group <GroupItem>))
# [(printerid <PrinterID>)|(appid <AppID>)]
def doUpdateChromePolicy():
def getSpecialVtypeValue(vtype, value):
if vtype == 'duration':
return {vtype: f'{value}s'}
if vtype in {'value', 'downloadUri'}:
return {vtype: value}
if vtype == 'count':
return value
#if vtype == timeOfDay:
hours, minutes = value.split(':')
return {vtype: {'hours': hours, 'minutes': minutes}}
cp = buildGAPIObject(API.CHROMEPOLICY)
cd = buildGAPIObject(API.DIRECTORY)
cv = None
customer = _getCustomersCustomerIdWithC()
app_id = channelMap = groupEmail = orgUnit = printer_id = targetResource = None
body = {'requests': []}
schemaNameList = []
convertCRsNLs = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'ou', 'org', 'orgunit'}:
orgUnit, targetName, targetResource, entityType, service = _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail)
elif myarg == 'group':
groupEmail, targetName, targetResource, entityType, service = _getPolicyGroupTarget(cd, cp, myarg, orgUnit)
elif myarg == 'printerid':
printer_id = getString(Cmd.OB_PRINTER_ID)
elif myarg == 'appid':
app_id = getString(Cmd.OB_APP_ID)
elif myarg == 'convertcrnl':
convertCRsNLs = True
else:
schemaName, schema = simplifyChromeSchema(_getChromePolicySchema(cp, Cmd.Previous(), '*'))
body['requests'].append({'policyValue': {'policySchema': schemaName, 'value': {}},
'updateMask': ''})
schemaNameList.append(schemaName)
while Cmd.ArgumentsRemaining():
field = getArgumentEmptyAllowed()
# Allow an empty field/value pair which makes processing an input CSV file with schemas with different numbers of fields easy
if not field:
if Cmd.ArgumentsRemaining():
Cmd.Advance()
continue
if field in {'ou', 'org', 'orgunit', 'group', 'printerid', 'appid'} or '.' in field:
if field != 'appid' or not schemaName.startswith('chrome.devices.kiosk'):
Cmd.Backup()
break # field is actually a new policy name or orgunit
# JSON
if field == 'json':
jsonData = getJSON(['direct', 'name', 'orgUnitPath', 'parentOrgUnitPath', 'group'])
schemaNameAppId = schemaName
if 'additionalTargetKeys' in jsonData:
body['requests'][-1].setdefault('policyTargetKey', {'additionalTargetKeys': {}})
for atk in jsonData['additionalTargetKeys']:
body['requests'][-1]['policyTargetKey']['additionalTargetKeys'][atk['name']] = atk['value']
if atk['name'] == 'app_id':
schemaNameAppId += f"({atk['value']})"
schemaNameList[-1] = schemaNameAppId
for field in jsonData.get('fields', []):
casedField = field['name']
lowerField = casedField.lower()
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
tmschema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(lowerField)
if tmschema:
body['requests'][-1]['policyValue']['value'][casedField] = getSpecialVtypeValue(tmschema['type'], field['value'])
body['requests'][-1]['updateMask'] += f'{casedField},'
continue
vtype = schema['settings'].get(lowerField, {}).get('type')
value = field['value']
if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']:
value = int(value)
elif vtype == 'TYPE_BOOL':
pass
elif vtype == 'TYPE_ENUM':
prefix = schema['settings'][lowerField]['enum_prefix']
if not value.startswith(prefix):
value = f"{prefix}{value}"
elif vtype == 'TYPE_LIST':
value = value.split(',') if value else []
if myarg == 'chrome.users.chromebrowserupdates' and casedField == 'targetVersionPrefixSetting':
mg = CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN.match(value)
if mg:
channel = mg.group(1).lower().replace('_', '')
if channelMap is None:
cv, channelMap = getPlatformChannelMap(cv, Ent.CHROME_CHANNEL)
if channel not in channelMap:
invalidChoiceExit(value, channelMap, True)
cv, status, milestone = getRelativeMilestone(cv, channelMap[channel], int(mg.group(2)))
if not status:
Cmd.Backup()
invalidArgumentExit(f'{milestone} for {casedField}: {value}')
value = f'{milestone}.'
elif value and not CHROME_TARGET_VERSION_PATTERN.match(value):
Cmd.Backup()
invalidArgumentExit(f'{Msg.CHROME_TARGET_VERSION_FORMAT} for {casedField}: {value}')
body['requests'][-1]['policyValue']['value'][casedField] = value
body['requests'][-1]['updateMask'] += f'{casedField},'
break
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
tmschema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(field)
if tmschema:
casedField = tmschema['casedField']
vtype = tmschema['type']
if vtype == 'downloadUri':
value = getString(Cmd.OB_STRING)
elif vtype != 'timeOfDay':
if 'default' not in tmschema:
value = getInteger(minVal=tmschema['minVal'], maxVal=tmschema['maxVal'])*tmschema['scale']
else:
value = getIntegerEmptyAllowed(minVal=tmschema['minVal'], maxVal=tmschema['maxVal'], default=tmschema['default'])*tmschema['scale']
else:
value = getHHMM()
body['requests'][-1]['policyValue']['value'][casedField] = getSpecialVtypeValue(vtype, value)
body['requests'][-1]['updateMask'] += f'{casedField},'
continue
if field not in schema['settings']:
Cmd.Backup()
missingChoiceExit(schema['settings'])
casedField = schema['settings'][field]['name']
vtype = schema['settings'][field]['type']
value = getString(Cmd.OB_STRING, minLen=0 if vtype in {'TYPE_STRING', 'TYPE_LIST'} else 1)
if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']:
if not value.isnumeric():
Cmd.Backup()
invalidArgumentExit(integerLimits(None, None))
value = int(value)
elif vtype == 'TYPE_BOOL':
value = value.lower()
if value in TRUE_VALUES:
value = True
elif value in FALSE_VALUES:
value = False
else:
invalidChoiceExit(value, TRUE_FALSE, True)
elif vtype == 'TYPE_ENUM':
value = value.upper()
prefix = schema['settings'][field]['enum_prefix']
enum_values = schema['settings'][field]['enums']
if value in enum_values:
value = f'{prefix}{value}'
elif value.replace(prefix, '') in enum_values:
pass
else:
invalidChoiceExit(value, enum_values, True)
elif vtype == 'TYPE_LIST':
value = value.split(',') if value else []
elif vtype == 'TYPE_STRING' and convertCRsNLs:
value = unescapeCRsNLs(value)
if myarg == 'chrome.users.chromebrowserupdates' and casedField == 'targetVersionPrefixSetting':
mg = CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN.match(value)
if mg:
channel = mg.group(1).lower().replace('_', '')
if channelMap is None:
cv, channelMap = getPlatformChannelMap(cv, Ent.CHROME_CHANNEL)
if channel not in channelMap:
invalidChoiceExit(value, channelMap, True)
cv, status, milestone = getRelativeMilestone(cv, channelMap[channel], int(mg.group(2)))
if not status:
Cmd.Backup()
invalidArgumentExit(f'{milestone} for {casedField}: {value}')
value = f'{milestone}.'
elif value and not CHROME_TARGET_VERSION_PATTERN.match(value):
Cmd.Backup()
invalidArgumentExit(Msg.CHROME_TARGET_VERSION_FORMAT)
body['requests'][-1]['policyValue']['value'][casedField] = value
body['requests'][-1]['updateMask'] += f'{casedField},'
checkPolicyArgs(targetResource, printer_id, app_id)
count = len(body['requests'])
if count > 0 and not body['requests'][-1]['updateMask']:
body['requests'].pop()
kvList = setPolicyKVList([entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)], printer_id, app_id)
if count != 1:
entityPerformActionNumItems(kvList, count, Ent.CHROME_POLICY)
if count == 0:
return
updatePolicyRequests(body, targetResource, printer_id, app_id)
try:
callGAPI(service, 'batchModify',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, body=body)
entityActionPerformed(kvList)
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable, GAPI.quotaExceeded) as e:
entityActionFailedWarning(kvList, str(e))
CHROME_POLICY_INDEXED_TITLES = ['fields']
CHROME_POLICY_SHOW_ALL = -1
CHROME_POLICY_SHOW_DIRECT = True
CHROME_POLICY_SHOW_INHERITED = False
CHROME_POLICY_SHOW_CHOICE_MAP = {
'all': CHROME_POLICY_SHOW_ALL,
'direct': CHROME_POLICY_SHOW_DIRECT,
'inherited': CHROME_POLICY_SHOW_INHERITED
}
# gam show chromepolicies
# ((ou|orgunit <OrgUnitItem>)|(group <GroupItem>))
# [(printerid <PrinterID>)|(appid <AppID>)]
# (filter <StringList>)* (namespace <NamespaceList>)*
# [show all|direct|inherited]
# [formatjson]
# gam print chromepolicies [todrive <ToDriveAttribute>*]
# ((ou|orgunit <OrgUnitItem>)|(group <GroupItem>))
# [(printerid <PrinterID>)|(appid <AppID>)]
# (filter <StringList>)* (namespace <NamespaceList>)*
# [show all|direct|inherited]
# [[formatjson [quotechar <Character>]]
def doPrintShowChromePolicies():
def normalizedPolicy(policy):
norm = {'name': policy['value']['policySchema']}
if app_id:
norm['appId'] = app_id
elif printer_id:
norm['printerId'] = printer_id
if entityType == Ent.ORGANIZATIONAL_UNIT:
orgUnitId = policy.get('targetKey', {}).get('targetResource')
norm['orgUnitPath'] = convertOrgUnitIDtoPath(cd, orgUnitId) if orgUnitId else UNKNOWN
parentOrgUnitId = policy.get('sourceKey', {}).get('targetResource')
norm['parentOrgUnitPath'] = convertOrgUnitIDtoPath(cd, parentOrgUnitId) if parentOrgUnitId else UNKNOWN
norm['direct'] = orgUnitId == parentOrgUnitId
else:
groupId = policy.get('targetKey', {}).get('targetResource')
if groupId is not None:
groupId = groupId.split('/')[1]
norm['group'] = getGroupEmailFromID(groupId, cd)
if norm['group'] is None:
norm['group'] = groupId
else:
norm['group'] = UNKNOWN
norm['additionalTargetKeys'] = []
for setting, value in sorted(policy.get('targetKey', {}).get('additionalTargetKeys', {}).items()):
norm['additionalTargetKeys'].append({'name': setting, 'value': value})
norm['fields'] = []
name = policy['value']['policySchema']
values = policy.get('value', {}).get('value', {})
if name in {'chrome.devices.managedguest.apps.ManagedConfiguration',
'chrome.devices.kiosk.apps.ManagedConfiguration',
'chrome.users.apps.ManagedConfiguration'} and 'managedConfiguration' in values:
values['managedConfiguration'] = json.dumps(values['managedConfiguration'], ensure_ascii=False).replace('\\n', '').replace('\\"', '"')[1:-1]
for setting, value in values.items():
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name, {}).get(setting.lower())
if schema and setting == schema['casedField']:
vtype = schema['type']
if vtype in {'duration', 'value'}:
value = value.get(vtype, '')
if value:
if value.endswith('s'):
value = value[:-1]
value = int(value) // schema['scale']
elif vtype == 'count':
pass
elif vtype == 'downloadUri':
value = value.get(vtype, '')
else: #timeOfDay
hours = value.get(vtype, {}).get('hours', 0)
minutes = value.get(vtype, {}).get('minutes', 0)
value = f'{hours:02}:{minutes:02}'
elif isinstance(value, str) and value.find('_ENUM_') != -1:
value = value.split('_ENUM_')[-1]
elif isinstance(value, list):
if len(value) > 0:
if not isinstance(value[0], dict):
value = ','.join(value)
else:
value = json.dumps(value, ensure_ascii=False)
else:
value = ''
norm['fields'].append({'name': setting, 'value': value})
return norm
def _showPolicy(policy, j, jcount):
policy = normalizedPolicy(policy)
if (entityType == Ent.GROUP) or showPolicies in (CHROME_POLICY_SHOW_ALL, policy['direct']):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(policy), ensure_ascii=False, sort_keys=True))
return
printKeyValueListWithCount([policy['name']], j, jcount)
Ind.Increment()
showJSON(None, policy, sortDictKeys=False)
Ind.Decrement()
def _printPolicy(policy):
policy = normalizedPolicy(policy)
if (entityType == Ent.GROUP) or showPolicies in (CHROME_POLICY_SHOW_ALL, policy['direct']):
row = flattenJSON(policy)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(row):
if entityType == Ent.ORGANIZATIONAL_UNIT:
csvPF.WriteRowNoFilter({'name': policy['name'],
'orgUnitPath': policy['orgUnitPath'],
'parentOrgUnitPath': policy['parentOrgUnitPath'],
'direct': policy['direct'],
'JSON': json.dumps(cleanJSON(policy),
ensure_ascii=False, sort_keys=True)})
else:
csvPF.WriteRowNoFilter({'name': policy['name'],
'group': policy['group'],
'JSON': json.dumps(cleanJSON(policy),
ensure_ascii=False, sort_keys=True)})
cp = buildGAPIObject(API.CHROMEPOLICY)
cd = buildGAPIObject(API.DIRECTORY)
customer = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['name'], indexedTitles=CHROME_POLICY_INDEXED_TITLES) if Act.csvFormat() else None
if csvPF:
csvPF.SetNoEscapeChar(True)
FJQC = FormatJSONQuoteChar(csvPF)
app_id = groupEmail = orgUnit = printer_id = targetResource = None
showPolicies = CHROME_POLICY_SHOW_ALL
psFilters = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'ou', 'org', 'orgunit'}:
orgUnit, targetName, targetResource, entityType, _ = _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail)
elif myarg == 'group':
groupEmail, targetName, targetResource, entityType, _ = _getPolicyGroupTarget(cd, cp, myarg, orgUnit)
elif myarg == 'printerid':
printer_id = getString(Cmd.OB_PRINTER_ID)
elif myarg == 'appid':
app_id = getString(Cmd.OB_APP_ID)
elif myarg == 'filter':
for psFilter in getString(Cmd.OB_STRING).replace(',', ' ').split():
psFilters.append(psFilter)
elif myarg == 'namespace':
for psFilter in getString(Cmd.OB_STRING).replace(',', ' ').split():
if psFilter.endswith('.*'):
psFilters.append(psFilter)
else:
psFilters.append(f'{psFilter}.*')
elif myarg == 'show':
showPolicies = getChoice(CHROME_POLICY_SHOW_CHOICE_MAP, mapChoice=True)
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
checkPolicyArgs(targetResource, printer_id, app_id)
body = {'policyTargetKey': {'targetResource': targetResource}}
if app_id:
body['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
if not psFilters:
psFilters = ['chrome.users.apps.*',
'chrome.devices.kiosk.apps.*',
'chrome.devices.managedguest.apps.*',
]
elif printer_id:
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
if not psFilters:
psFilters = ['chrome.printers.*']
elif not psFilters:
if entityType == Ent.ORGANIZATIONAL_UNIT:
psFilters = ['chrome.users.*',
'chrome.users.apps.*',
'chrome.users.appsconfig.*',
'chrome.devices.*',
'chrome.devices.kiosk.*',
'chrome.devices.kiosk.apps.*',
'chrome.devices.managedguest.*',
'chrome.devices.managedguest.apps.*',
'chrome.networks.cellular.*',
'chrome.networks.certificates.*',
'chrome.networks.ethernet.*',
'chrome.networks.globalsettings.*',
'chrome.networks.vpn.*',
'chrome.networks.wifi.*',
'chrome.printers.*',
'chrome.printservers.*',
]
else:
psFilters = ['chrome.users.*',
'chrome.users.apps.*',
'chrome.users.appsconfig.*',
'chrome.printers.*',
'chrome.printservers.*',
]
if csvPF:
if entityType == Ent.ORGANIZATIONAL_UNIT:
csvPF.AddTitles(['orgUnitPath', 'parentOrgUnitPath', 'direct'])
else:
csvPF.AddTitles(['group'])
csvPF.SetSortAllTitles()
if not FJQC.formatJSON:
if app_id:
csvPF.AddSortTitles(['appId'])
elif printer_id:
csvPF.AddSortTitles(['printerId'])
else:
csvPF.SetJSONTitles(csvPF.titlesList+['JSON'])
policies = []
for psFilter in psFilters:
body['policySchemaFilter'] = psFilter
body['pageToken'] = None
printGettingAllEntityItemsForWhom(Ent.CHROME_POLICY, targetName, query=body['policySchemaFilter'])
try:
policies.extend(callGAPIpages(cp.customers().policies(), 'resolve', 'resolvedPolicies',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
customer=customer, body=body, pageArgsInBody=True))
except (GAPI.notFound, GAPI.permissionDenied, GAPI.quotaExceeded) as e:
entityActionFailedWarning([entityType, targetName], str(e))
continue
except (GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CHROME_POLICY, body['policySchemaFilter']], str(e))
continue
# sort policies first by app/printer id then by schema name
policies = sorted(policies,
key=lambda k: (list(k.get('targetKey', {}).get('additionalTargetKeys', {}).values()),
k.get('value', {}).get('policySchema', '')))
if not csvPF:
jcount = len(policies)
if not FJQC.formatJSON:
kvList = setPolicyKVList([entityType, targetName], printer_id, app_id)
entityPerformActionModifierNumItems(kvList, Msg.MAXIMUM_OF, jcount, Ent.CHROME_POLICY)
Ind.Increment()
j = 0
for policy in policies:
j += 1
_showPolicy(policy, j, jcount)
Ind.Decrement()
else:
for policy in policies:
_printPolicy(policy)
if csvPF:
csvPF.writeCSVfile(f'Chrome Policies - {targetName}')
CHROME_IMAGE_SCHEMAS_MAP = {
'chrome.devices.managedguest.avatar': {'name': 'chrome.devices.managedguest.Avatar', 'field': 'userAvatarImage'},
'chrome.devices.managedguest.wallpaper': {'name': 'chrome.devices.managedguest.Wallpaper', 'field': 'wallpaperImage'},
'chrome.devices.signinwallpaperimage': {'name': 'chrome.devices.SignInWallpaperImage', 'field': 'deviceWallpaperImage'},
'chrome.users.avatar': {'name': 'chrome.users.Avatar', 'field': 'userAvatarImage'},
'chrome.users.wallpaper': {'name': 'chrome.users.Wallpaper', 'field': 'wallpaperImage'},
}
# gam create chromepolicyimage <ChromePolicyImageSchemaName> <FileName>
def doCreateChromePolicyImage():
cp = buildGAPIObject(API.CHROMEPOLICY)
parent = _getCustomersCustomerIdWithC()
schema = getChoice(CHROME_IMAGE_SCHEMAS_MAP, mapChoice=True)
parameters = {DFA_URL: None}
parameters[DFA_LOCALFILEPATH] = os.path.expanduser(getString(Cmd.OB_FILE_NAME))
try:
f = open(parameters[DFA_LOCALFILEPATH], 'rb')
f.close()
except IOError as e:
Cmd.Backup()
usageErrorExit(f'{parameters[DFA_LOCALFILEPATH]}: {str(e)}')
parameters[DFA_LOCALFILENAME] = os.path.basename(parameters[DFA_LOCALFILEPATH])
parameters[DFA_LOCALMIMETYPE] = mimetypes.guess_type(parameters[DFA_LOCALFILEPATH])[0]
if parameters[DFA_LOCALMIMETYPE] is None:
parameters[DFA_LOCALMIMETYPE] = 'image/jpeg'
checkForExtraneousArguments()
media_body = getMediaBody(parameters)
try:
result = callGAPI(cp.media(), 'upload',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN],
customer=parent, media_body=media_body,
body={'policyField': f"{schema['name']}.{schema['field']}"})
entityActionPerformed([Ent.CHROME_POLICY_IMAGE, f"{schema['name']} {schema['field']} {result['downloadUri']}"])
except (GAPI.invalidArgument, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CHROME_POLICY_IMAGE, f"{schema['name']}", Ent.FILE, f"{parameters[DFA_LOCALFILEPATH]}"], str(e))
def _showChromePolicySchema(schema, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(schema), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_POLICY_SCHEMA, schema['name']], i, count)
Ind.Increment()
showJSON(None, schema)
Ind.Decrement()
CHROME_POLICY_SCHEMA_FIELDS_CHOICE_MAP = {
'accessrestrictions': 'accessRestrictions',
'additionaltargetkeynames': 'additionalTargetKeyNames',
'categorytitle': 'categoryTitle',
'definition': 'definition',
'fielddescriptions': 'fieldDescriptions',
'name': 'name',
'notices': 'notices',
'policyapilifecycle': 'policyApiLifecycle',
'policydescription': 'policyDescription',
'schemaname': 'schemaName',
'supporturi': 'supportUri',
'validtargetresources': 'validTargetResources',
}
# gam info chromeschema <SchemaName>
# <ChromePolicySchemaFieldName>* [fields <ChromePolicySchemaFieldNameList>]
# [formatjson]
def doInfoChromePolicySchemas():
cp = buildGAPIObject(API.CHROMEPOLICY)
FJQC = FormatJSONQuoteChar()
fieldsList = []
name = _getChromePolicySchemaName()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getFieldsList(myarg, CHROME_POLICY_SCHEMA_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
schema = callGAPI(cp.customers().policySchemas(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
name=name, fields=fields)
_showChromePolicySchema(schema, FJQC, 0, 0)
except GAPI.notFound:
entityUnknownWarning(Ent.CHROME_POLICY_SCHEMA, name)
except (GAPI.badRequest, GAPI.forbidden):
accessErrorExit(None)
# gam show chromeschemas
# [filter <String>]
# <ChromePolicySchemaFieldName>* [fields <ChromePolicySchemaFieldNameList>]
# [formatjson]
# gam print chromschemas [todrive <ToDriveAttribute>*]
# [filter <String>]
# <ChromePolicySchemaFieldName>* [fields <ChromePolicySchemaFieldNameList>]
# [[formatjson [quotechar <Character>]]
def doPrintShowChromeSchemas():
def _printChromePolicySchema(schema):
row = flattenJSON(schema)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(row):
row = {'name': schema['name']}
if 'policyDescription' in schema:
row['policyDescription'] = schema['policyDescription']
if 'policyApiLifecycle' in schema:
row['policyApiLifecycleStage'] = schema['policyApiLifecycle'].get('policyApiLifecycleStage', '')
row['JSON'] = json.dumps(cleanJSON(schema), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
if checkArgumentPresent('std'):
doShowChromeSchemasStd()
return
cp = buildGAPIObject(API.CHROMEPOLICY)
parent = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['name', 'schemaName', 'policyDescription',
'policyApiLifecycle.policyApiLifecycleStage',
'policyApiLifecycle.description',
'policyApiLifecycle.endSupport.year',
'policyApiLifecycle.endSupport.month',
'policyApiLifecycle.endSupport.day',
'policyApiLifecycle.deprecatedInFavorOf',
'policyApiLifecycle.deprecatedInFavorOf.0'],
'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
pfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'filter':
pfilter = getString(Cmd.OB_STRING)
elif getFieldsList(myarg, CHROME_POLICY_SCHEMA_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and FJQC.formatJSON:
jsonTitles = ['name']
if not fieldsList or 'policyDescription' in fieldsList:
jsonTitles.append('policyDescription')
if not fieldsList or 'policyApiLifecycle' in fieldsList:
jsonTitles.append('policyApiLifecycleStage')
jsonTitles.append('JSON')
csvPF.SetJSONTitles(jsonTitles)
fields = getItemFieldsFromFieldsList('policySchemas', fieldsList)
printGettingAllAccountEntities(Ent.CHROME_POLICY_SCHEMA, pfilter)
pageMessage = getPageMessage()
try:
schemas = callGAPIpages(cp.customers().policySchemas(), 'list', 'policySchemas',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
parent=parent, filter=pfilter, fields=fields)
if not csvPF:
jcount = len(schemas)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.CHROME_POLICY_SCHEMA)
Ind.Increment()
j = 0
for schema in schemas:
j += 1
_showChromePolicySchema(schema, FJQC, j, jcount)
Ind.Decrement()
else:
for schema in schemas:
_printChromePolicySchema(schema)
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.CHROME_POLICY, None], invalidQuery(pfilter) if pfilter else str(e))
except (GAPI.badRequest, GAPI.forbidden):
accessErrorExit(None)
if csvPF:
csvPF.writeCSVfile('Chrome Policy Schemas')
# gam show chromeschemas std [filter <String>]
def doShowChromeSchemasStd():
cp = buildGAPIObject(API.CHROMEPOLICY)
sfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'filter':
sfilter = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
parent = _getCustomersCustomerIdWithC()
result = callGAPIpages(cp.customers().policySchemas(), 'list', 'policySchemas',
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
parent=parent, filter=sfilter)
schemas = {}
for schema in result:
schema_name, schema_dict = simplifyChromeSchema(schema)
schemas[schema_name.lower()] = schema_dict
for _, value in sorted(iter(schemas.items())):
printKeyValueList([f'{value.get("name")}', f'{value.get("description")}'])
Ind.Increment()
for val in value['settings'].values():
vtype = val.get('type')
printKeyValueList([f'{val.get("name")}', f'{vtype}'])
Ind.Increment()
if vtype == 'TYPE_ENUM':
enums = val.get('enums', [])
descriptions = val.get('descriptions', [])
for i in range(len(val.get('enums', []))):
printKeyValueList([f'{enums[i]}', f'{descriptions[i]}'])
elif vtype == 'TYPE_BOOL':
pvs = val.get('descriptions')
for pvi in pvs:
if isinstance(pvi, dict):
pvalue = pvi.get('value')
pdescription = pvi.get('description')
printKeyValueList([f'{pvalue}', f'{pdescription}'])
elif isinstance(pvi, list):
printKeyValueList([f'{pvi[0]}'])
else:
description = val.get('descriptions')
if len(description) > 0:
printKeyValueList([f'{description[0]}'])
Ind.Decrement()
Ind.Decrement()
printBlankLine()
# gam create chromenetwork
# <OrgUnitItem> <String> <JSONData>
def doCreateChromeNetwork():
cp = buildGAPIObject(API.CHROMEPOLICY)
cd = buildGAPIObject(API.DIRECTORY)
customer = _getCustomersCustomerIdWithC()
body = {}
orgUnitPath, body['targetResource'] = _getOrgunitsOrgUnitIdPath(cd, getString(Cmd.OB_ORGUNIT_PATH))
body['name'] = getString(Cmd.OB_STRING)
body.update(getJSON(['direct', 'name', 'orgUnitPath', 'parentOrgUnitPath', 'group']))
checkForExtraneousArguments()
kvList = [Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_NETWORK_NAME, body['name']]
try:
result = callGAPI(cp.customers().policies().networks(), 'defineNetwork',
bailOnInternalError=True,
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, body=body)
entityActionPerformed(kvList+[Ent.CHROME_NETWORK_ID, result['networkId']])
except (GAPI.alreadyExists, GAPI.permissionDenied, GAPI.invalidArgument,
GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvList, str(e))
# gam delete chromenetwork
# <OrgUnitItem> <NetworkID>
def doDeleteChromeNetwork():
cp = buildGAPIObject(API.CHROMEPOLICY)
cd = buildGAPIObject(API.DIRECTORY)
customer = _getCustomersCustomerIdWithC()
body = {}
orgUnitPath, body['targetResource'] = _getOrgunitsOrgUnitIdPath(cd, getString(Cmd.OB_ORGUNIT_PATH))
body['networkId'] = getString(Cmd.OB_NETWORK_ID)
checkForExtraneousArguments()
kvList = [Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_NETWORK_ID, body['networkId']]
try:
callGAPI(cp.customers().policies().networks(), 'removeNetwork',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, body=body)
entityActionPerformed(kvList)
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvList, str(e))
# Device command utilities
def buildGAPICIDeviceServiceObject():
if not GC.Values[GC.ENABLE_DASA]:
_, ci = buildGAPIServiceObject(API.CLOUDIDENTITY_DEVICES, _getAdminEmail(), displayError=True)
else:
ci = buildGAPIObject(API.CLOUDIDENTITY_DEVICES)
if not ci:
sys.exit(GM.Globals[GM.SYSEXITRC])
return ci
def getUpdateDeleteCIDeviceOptions(entityType, count, action, doit, actionChoices):
kwargs = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if not action and myarg == 'action':
action = getChoice(actionChoices)
elif action == 'wipe' and myarg == 'removeresetlock':
kwargs = {'body': {'removeResetLock': True}}
elif myarg == 'doit':
doit = True
else:
unknownArgumentExit()
if not action:
actionNotPerformedNumItemsWarning(count, entityType, Msg.NO_ACTION_SPECIFIED)
sys.exit(GM.Globals[GM.SYSEXITRC])
if not doit:
actionNotPerformedNumItemsWarning(count, entityType, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION)
sys.exit(GM.Globals[GM.SYSEXITRC])
return action, kwargs
def getCIDeviceEntity():
ci = buildGAPICIDeviceServiceObject()
customer = _getCustomersCustomerIdNoC()
if checkArgumentPresent('devicesn'):
query = f'serial:{getString(Cmd.OB_SERIAL_NUMBER)}'
elif checkArgumentPresent('query'):
query = getString(Cmd.OB_QUERY)
else:
name = getString(Cmd.OB_DEVICE_ENTITY)
if name[:6].lower() == 'query:':
query = name[6:]
else:
if name.lower() in {'id', 'name'}:
name = getString(Cmd.OB_DEVICE_ID)
if not name.startswith('devices/'):
name = f'devices/{name}'
return ([{'name': name}], ci, customer, True)
printGettingAllAccountEntities(Ent.DEVICE, query)
pageMessage = getPageMessage()
try:
devices = callGAPIpages(ci.devices(), 'list', 'devices',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageMessage=pageMessage,
customer=customer, filter=query,
fields='nextPageToken,devices(name)', pageSize=100)
return (devices, ci, customer, False)
except (GAPI.invalid, GAPI.invalidArgument):
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.DEVICE, None], str(e))
return ([], ci, customer, False)
DEVICE_USERNAME_PATTERN = re.compile(r'^(devices/.+)/(deviceUsers/.+)$')
DEVICE_USERNAME_FORMAT_REQUIRED = 'devices/<String>/deviceUsers/<String>'
def getCIDeviceUserEntity():
ci = buildGAPICIDeviceServiceObject()
customer = _getCustomersCustomerIdNoC()
if checkArgumentPresent('query'):
query = getString(Cmd.OB_QUERY)
else:
name = getString(Cmd.OB_DEVICE_USER_ENTITY)
if name[:6].lower() == 'query:':
query = name[6:]
else:
if name.lower() in {'id', 'name'}:
name = getString(Cmd.OB_DEVICE_USER_ID)
if DEVICE_USERNAME_PATTERN.match(name):
return ([{'name': name}], ci, customer, True)
Cmd.Backup()
invalidArgumentExit(DEVICE_USERNAME_FORMAT_REQUIRED)
printGettingAllAccountEntities(Ent.DEVICE_USER, query)
pageMessage = getPageMessage()
try:
deviceUsers = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
throwReasons=[GAPI.INVALID, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageMessage=pageMessage,
customer=customer, filter=query, parent='devices/-',
fields='nextPageToken,deviceUsers(name)', pageSize=20)
return (deviceUsers, ci, customer, False)
except GAPI.invalid:
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.DEVICE_USER, None], str(e))
return ([], ci, customer, False)
def _makeDeviceId(name, device):
deviceId = f'{name}'
for field in ['deviceType', 'serialNumber', 'assetTag']:
if field in device:
deviceId += f', {field}: {device[field]}'
return deviceId
DEVICE_TYPE_MAP = {
'android': 'ANDROID',
'chromeos': 'CHROME_OS',
'googlesync': 'GOOGLE_SYNC',
'ios': 'IOS',
'linux': 'LINUX',
'macos': 'MAC_OS',
'windows': 'WINDOWS'
}
DEVICE_TIME_OBJECTS = {'createTime', 'firstSyncTime', 'lastSyncTime', 'lastUpdateTime', 'securityPatchTime'}
# gam create device serialnumber <String> devicetype <DeviceType> [assettag <String>]
def doCreateCIDevice():
ci = buildGAPICIDeviceServiceObject()
customer = _getCustomersCustomerIdNoC()
body = {'deviceType': '', 'serialNumber': ''}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'serialnumber':
body['serialNumber'] = getString(Cmd.OB_STRING)
elif myarg == 'devicetype':
body['deviceType'] = getChoice(DEVICE_TYPE_MAP, mapChoice=True)
elif myarg in {'assettag', 'assteid'}:
body['assetTag'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
if not body['serialNumber']:
missingArgumentExit('serialnumber')
if not body['deviceType']:
missingArgumentExit('devicetype')
try:
result = callGAPI(ci.devices(), 'create',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
customer=customer, body=body)
if 'response' in result:
entityActionPerformed([Ent.COMPANY_DEVICE, _makeDeviceId(f'{result["response"]["name"]}', body)])
else:
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', body)], result['error']['message'])
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.alreadyExists) as e:
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', body)], str(e))
DEVICE_ACTION_CHOICES = {'cancelwipe', 'wipe'}
def _performCIDeviceAction(action):
entityList, ci, customer, doit = getCIDeviceEntity()
count = len(entityList)
action, kwargs = getUpdateDeleteCIDeviceOptions(Ent.DEVICE, count, action, doit, DEVICE_ACTION_CHOICES)
if action == 'delete':
kwargs['customer'] = customer
else:
kwargs.setdefault('body', {})
kwargs['body']['customer'] = customer
i = 0
for device in entityList:
i += 1
name = device['name']
try:
result = callGAPI(ci.devices(), action,
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, **kwargs)
if result['done']:
if 'error' not in result:
entityActionPerformed([Ent.DEVICE, name], i, count)
else:
entityActionFailedWarning([Ent.DEVICE, name], result['error']['message'], i, count)
else:
entityActionPerformedMessage([Ent.DEVICE, name], Msg.ACTION_IN_PROGRESS.format(action), i, count)
except GAPI.notFound:
entityUnknownWarning(Ent.DEVICE, f'{name}', i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.DEVICE, f'{name}'], str(e), i, count)
# gam delete device <DeviceEntity> [doit]
def doDeleteCIDevice():
_performCIDeviceAction('delete')
# gam cancelwipe device <DeviceEntity> [doit]
def doCancelWipeCIDevice():
_performCIDeviceAction('cancelWipe')
# gam wipe device <DeviceEntity> [removeresetlock] [doit]
def doWipeCIDevice():
_performCIDeviceAction('wipe')
# gam update device <DeviceEntity> action <DeviceAction> [removeresetlock] [doit]
def doUpdateCIDevice():
_performCIDeviceAction(None)
DEVICE_MISSING_ACTION_MAP = {
'delete': 'delete',
'wipe': 'wipe',
'donothing': 'none',
'none': 'none',
}
# gam sync devices
# <CSVFileSelector>
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
# (devicetype_column <String>)|(static_devicetype <DeviceType>)
# (serialnumber_column <String>)
# [assettag_column <String>]
# [unassigned_missing_action delete|wipe|none|donothing]
# [assigned_missing_action delete|wipe|none|donothing]
# [preview]
def doSyncCIDevices():
ci = buildGAPICIDeviceServiceObject()
customer = _getCustomersCustomerIdNoC()
queryTimes = {}
queries = [None]
filename = None
serialNumberColumn = 'serialNumber'
deviceTypeColumn = 'deviceType'
assetTagColumn = None
staticDeviceType = None
fieldsList = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
unassignedMissingAction = 'delete'
assignedMissingAction = 'none'
preview = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ['filter', 'filters', 'query', 'queries']:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg in {'csv', 'csvfile'}:
filename = getString(Cmd.OB_STRING)
elif myarg == 'serialnumbercolumn':
serialNumberColumn = getString(Cmd.OB_STRING)
elif myarg == 'devicetypecolumn':
deviceTypeColumn = getString(Cmd.OB_STRING)
elif myarg in {'assettagcolumn', 'assetidcolumn'}:
assetTagColumn = getString(Cmd.OB_STRING)
fieldsList.append('assetTag')
elif myarg == 'staticdevicetype':
staticDeviceType = getChoice(DEVICE_TYPE_MAP, mapChoice=True)
elif myarg == 'unassignedmissingaction':
unassignedMissingAction = getChoice(DEVICE_MISSING_ACTION_MAP, mapChoice=True)
elif myarg == 'assignedmissingaction':
assignedMissingAction = getChoice(DEVICE_MISSING_ACTION_MAP, mapChoice=True)
elif myarg == 'preview':
preview = True
else:
unknownArgumentExit()
if not filename:
missingArgumentExit('csvfile')
f, csvFile, fieldnames = openCSVFileReader(filename)
if serialNumberColumn not in fieldnames:
csvFieldErrorExit(serialNumberColumn, fieldnames)
if not staticDeviceType and deviceTypeColumn not in fieldnames:
csvFieldErrorExit(deviceTypeColumn, fieldnames)
if assetTagColumn and assetTagColumn not in fieldnames:
csvFieldErrorExit(assetTagColumn, fieldnames)
localDevices = {}
for row in csvFile:
# upper() is very important to comparison since Google
# always return uppercase serials
localDevice = {'serialNumber': row[serialNumberColumn].strip().upper()}
if staticDeviceType:
localDevice['deviceType'] = staticDeviceType
else:
dt = row[deviceTypeColumn].strip()
localDevice['deviceType'] = DEVICE_TYPE_MAP.get(dt.lower().replace('_', '').replace('-', ''), dt.upper())
sndt = f"{localDevice['serialNumber']}-{localDevice['deviceType']}"
if assetTagColumn:
localDevice['assetTag'] = row[assetTagColumn].strip()
sndt += f"-{localDevice['assetTag']}"
localDevices[sndt] = localDevice
closeFile(f)
fields = f'nextPageToken,devices({",".join(fieldsList)})'
remoteDevices = {}
remoteDeviceMap = {}
substituteQueryTimes(queries, queryTimes)
for query in queries:
printGettingAllAccountEntities(Ent.COMPANY_DEVICE, query)
pageMessage = getPageMessage()
try:
result = callGAPIpages(ci.devices(), 'list', 'devices',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageMessage=pageMessage,
customer=customer, filter=query, view='COMPANY_INVENTORY',
fields=fields, pageSize=100)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.COMPANY_DEVICE, None], str(e))
return
for remoteDevice in result:
sn = remoteDevice['serialNumber']
last_sync = remoteDevice.pop('lastSyncTime', NEVER_TIME_NOMS)
name = remoteDevice.pop('name')
sndt = f"{remoteDevice['serialNumber']}-{remoteDevice['deviceType']}"
if assetTagColumn:
if 'assetTag' not in remoteDevice:
remoteDevice['assetTag'] = ''
sndt += f"-{remoteDevice['assetTag']}"
remoteDevices[sndt] = remoteDevice
remoteDeviceMap[sndt] = {'name': name}
if last_sync == NEVER_TIME_NOMS:
remoteDeviceMap[sndt]['unassigned'] = True
devicesToAdd = []
for sndt, device in iter(localDevices.items()):
if sndt not in remoteDevices:
devicesToAdd.append(device)
missingDevices = []
for sndt, device in iter(remoteDevices.items()):
if sndt not in localDevices:
missingDevices.append(device)
Act.Set([Act.CREATE, Act.CREATE_PREVIEW][preview])
count = len(devicesToAdd)
performActionNumItems(count, Ent.COMPANY_DEVICE)
i = 0
for device in devicesToAdd:
i += 1
try:
if not preview:
result = callGAPI(ci.devices(), 'create',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
customer=customer, body=device)
if 'error' in result:
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', device)], result['error']['message'], i, count)
continue
deviceId = _makeDeviceId(f'{result["response"]["name"]}', device)
else:
deviceId = _makeDeviceId('/devices/???', device)
entityActionPerformed([Ent.COMPANY_DEVICE, deviceId], i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.alreadyExists) as e:
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', device)], str(e), i, count)
Act.Set([Act.PROCESS, Act.PROCESS_PREVIEW][preview])
count = len(missingDevices)
performActionNumItems(count, Ent.COMPANY_DEVICE)
i = 0
for device in missingDevices:
i += 1
sn = device['serialNumber']
sndt = f"{sn}-{device['deviceType']}"
if assetTagColumn:
sndt += f"-{device['assetTag']}"
name = remoteDeviceMap[sndt]['name']
deviceId = _makeDeviceId(f'{name}', device)
unassigned = remoteDeviceMap[sndt].get('unassigned')
action = unassignedMissingAction if unassigned else assignedMissingAction
if action == 'none':
Act.Set([Act.NOACTION, Act.NOACTION_PREVIEW][preview])
entityActionPerformed([Ent.COMPANY_DEVICE, deviceId], i, count)
continue
if action == 'delete':
Act.Set([Act.DELETE, Act.DELETE_PREVIEW][preview])
kwargs = {'customer': customer}
else:
Act.Set([Act.WIPE, Act.WIPE_PREVIEW][preview])
kwargs = {'body': {'customer': customer}}
try:
if not preview:
result = callGAPI(ci.devices(), action,
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, **kwargs)
else:
result = {'done': True}
if result['done']:
if 'error' not in result:
entityActionPerformed([Ent.COMPANY_DEVICE, deviceId], i, count)
else:
entityActionFailedWarning([Ent.COMPANY_DEVICE, deviceId], result['error']['message'], i, count)
else:
entityActionPerformedMessage([Ent.COMPANY_DEVICE, deviceId], Msg.ACTION_IN_PROGRESS.format(action), i, count)
except GAPI.notFound:
entityUnknownWarning(Ent.COMPANY_DEVICE, deviceId, i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.COMPANY_DEVICE, deviceId], str(e), i, count)
DEVICE_FIELDS_CHOICE_MAP = {
'androidspecificattributes': 'androidSpecificAttributes',
'assettag': 'assetTag',
'basebandversion': 'basebandVersion',
'bootloaderversion': 'bootloaderVersion',
'brand': 'brand',
'buildnumber': 'buildNumber',
'compromisedstate': 'compromisedState',
'createtime': 'createTime',
'deviceid': 'deviceId',
'devicetype': 'deviceType',
'enableddeveloperoptions': 'enabledDeveloperOptions',
'enabledusbdebugging': 'enabledUsbDebugging',
'encryptionstate': 'encryptionState',
'endpointverificationspecificattributes': 'endpointVerificationSpecificAttributes',
'hostname': 'hostname',
'imei': 'imei',
'kernelversion': 'kernelVersion',
'lastsynctime': 'lastSyncTime',
'managementstate': 'managementState',
'manufacturer': 'manufacturer',
'meid': 'meid',
'model': 'model',
'name': 'name',
'networkoperator': 'networkOperator',
'osversion': 'osVersion',
'otheraccounts': 'otherAccounts',
'ownertype': 'ownerType',
'releaseversion': 'releaseVersion',
'securitypatchtime': 'securityPatchTime',
'serialnumber': 'serialNumber',
'unifieddeviceid': 'unifiedDeviceId',
'wifimacaddresses': 'wifiMacAddresses'
}
DEVICEUSER_FIELDS_CHOICE_MAP = {
'compromisedstate': 'compromisedState',
'createtime': 'createTime',
'firstsynctime': 'firstSyncTime',
'languagecode': 'languageCode',
'lastsynctime': 'lastSyncTime',
'managementstate': 'managementState',
'name': 'name',
'passwordstate': 'passwordState',
'useragent': 'userAgent',
'useremail': 'userEmail',
}
# gam info device <DeviceEntity>
# <DeviceFieldName>* [fields <DevieFieldNameList>] [userfields <DeviceUserFieldNameList>]
# [nodeviceusers]
# [formatjson]
def doInfoCIDevice():
entityList, ci, customer, _ = getCIDeviceEntity()
FJQC = FormatJSONQuoteChar()
fieldsList = []
userFieldsList = []
getDeviceUsers = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getFieldsList(myarg, DEVICE_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
pass
elif getFieldsList(myarg, DEVICEUSER_FIELDS_CHOICE_MAP, userFieldsList, initialField='name', fieldsArg='userfields'):
pass
elif myarg == 'nodeviceusers':
getDeviceUsers = False
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
userFields = getFieldsFromFieldsList(userFieldsList)
i = 0
count = len(entityList)
for device in entityList:
i += 1
name = device['name']
try:
device = callGAPI(ci.devices(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name, customer=customer, fields=fields)
if getDeviceUsers:
device_users = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
throwReasons=[GAPI.INVALID, GAPI.PERMISSION_DENIED],
parent=name, customer=customer, fields=userFields)
for device_user in device_users:
device_user['client_states'] = callGAPIpages(ci.devices().deviceUsers().clientStates(), 'list', 'clientStates',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=device_user['name'], customer=customer)
else:
device_users = []
if FJQC.formatJSON:
if getDeviceUsers:
device['users'] = device_users
printLine(json.dumps(cleanJSON(device, timeObjects=DEVICE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
else:
printEntity([Ent.DEVICE, device.pop('name')])
Ind.Increment()
showJSON(None, device, timeObjects=DEVICE_TIME_OBJECTS)
count = len(device_users)
i = 0
for device_user in device_users:
i += 1
printEntity([Ent.DEVICE_USER, device_user.pop('name')], i, count)
Ind.Increment()
showJSON(None, device_user, timeObjects=DEVICE_TIME_OBJECTS)
Ind.Decrement()
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.DEVICE, f'{name}')
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.DEVICE, f'{name}'], str(e))
DEVICE_VIEW_CHOICE_MAP = {
'all': (None, Ent.DEVICE),
'company': ('COMPANY_INVENTORY', Ent.COMPANY_DEVICE),
'personal': ('USER_ASSIGNED_DEVICES', Ent.PERSONAL_DEVICE),
'nocompanydevices': ('USER_ASSIGNED_DEVICES', Ent.PERSONAL_DEVICE),
'nopersonaldevices': ('COMPANY_INVENTORY', Ent.COMPANY_DEVICE)
}
DEVICE_ORDERBY_CHOICE_MAP = {
'createtime': 'create_time',
'devicetype': 'device_type',
'lastsynctime': 'last_sync_time',
'model': 'model',
'osversion': 'os_version',
'serialnumber': 'serial_number'
}
# gam print devices [todrive <ToDriveAttribute>*]
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
# <DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>]
# [orderby <DeviceOrderByFieldName> [ascending|descending]]
# [all|company|personal|nocompanydevices|nopersonaldevices]
# [nodeviceusers]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintCIDevices():
ci = buildGAPICIDeviceServiceObject()
customer = _getCustomersCustomerIdNoC()
parent = 'devices/-'
csvPF = CSVPrintFile(['name'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(DEVICE_ORDERBY_CHOICE_MAP)
fieldsList = []
userFieldsList = []
queryTimes = {}
queries = [None]
view, entityType = DEVICE_VIEW_CHOICE_MAP['all']
getDeviceUsers = True
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ['filter', 'filters', 'query', 'queries']:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg in DEVICE_VIEW_CHOICE_MAP:
view, entityType = DEVICE_VIEW_CHOICE_MAP[myarg]
elif myarg == 'nodeviceusers':
getDeviceUsers = False
elif getFieldsList(myarg, DEVICE_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
pass
elif getFieldsList(myarg, DEVICEUSER_FIELDS_CHOICE_MAP, userFieldsList, initialField='name', fieldsArg='userfields'):
pass
elif myarg == 'sortheaders':
pass
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = getItemFieldsFromFieldsList('devices', fieldsList)
userFields = getItemFieldsFromFieldsList('deviceUsers', userFieldsList)
substituteQueryTimes(queries, queryTimes)
itemCount = 0
for query in queries:
printGettingAllAccountEntities(entityType, query)
pageMessage = getPageMessage()
try:
devices = callGAPIpages(ci.devices(), 'list', 'devices',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, filter=query,
orderBy=OBY.orderBy, view=view, fields=fields, pageSize=100)
if showItemCountOnly:
itemCount += len(devices)
continue
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([entityType, None], str(e))
continue
if getDeviceUsers:
ci._http.credentials.refresh(transportCreateRequest())
deviceDict = {}
for device in devices:
deviceDict[device['name']] = device
printGettingAllAccountEntities(Ent.DEVICE_USER, query)
pageMessage = getPageMessage()
try:
deviceUsers = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, filter=query, parent=parent,
orderBy=OBY.orderBy, fields=userFields, pageSize=20)
for deviceUser in deviceUsers:
mg = DEVICE_USERNAME_PATTERN.match(deviceUser['name'])
if mg:
deviceName = mg.group(1)
if deviceName in deviceDict:
deviceDict[deviceName].setdefault('users', [])
deviceDict[deviceName]['users'].append(deviceUser)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([entityType, None], str(e))
for device in devices:
row = flattenJSON(device, timeObjects=DEVICE_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': device['name'],
'JSON': json.dumps(cleanJSON(device, timeObjects=DEVICE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
csvPF.writeCSVfile('Devices')
DEVICE_USER_ACTION_CHOICES = {'approve', 'block', 'cancelwipe', 'wipe'}
def _performCIDeviceUserAction(action):
entityList, ci, customer, doit = getCIDeviceUserEntity()
count = len(entityList)
action, kwargs = getUpdateDeleteCIDeviceOptions(Ent.DEVICE_USER, count, action, doit, DEVICE_USER_ACTION_CHOICES)
if action == 'delete':
kwargs['customer'] = customer
else:
kwargs.setdefault('body', {})
kwargs['body']['customer'] = customer
i = 0
for deviceUser in entityList:
i += 1
name = deviceUser['name']
try:
result = callGAPI(ci.devices().deviceUsers(), action,
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, **kwargs)
if result['done']:
if 'error' not in result:
entityActionPerformed([Ent.DEVICE_USER, name], i, count)
else:
entityActionFailedWarning([Ent.DEVICE_USER, name], result['error']['message'], i, count)
else:
entityActionPerformedMessage([Ent.DEVICE_USER, name], Msg.ACTION_IN_PROGRESS.format(action), i, count)
Ind.Increment()
showJSON(None, result, timeObjects=DEVICE_TIME_OBJECTS)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.DEVICE_USER, f'{name}', i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.DEVICE_USER, f'{name}'], str(e), i, count)
# gam approve deviceuser <DeviceUserEntity> [doit]
def doApproveCIDeviceUser():
_performCIDeviceUserAction('approve')
# gam block deviceuser <DeviceUserEntity> [doit]
def doBlockCIDeviceUser():
_performCIDeviceUserAction('block')
# gam delete deviceuser <DeviceUserEntity> [doit]
def doDeleteCIDeviceUser():
_performCIDeviceUserAction('delete')
# gam cancelwipe deviceuser <DeviceUserEntity> [doit]
def doCancelWipeCIDeviceUser():
_performCIDeviceUserAction('cancelWipe')
# gam wipe deviceuser <DeviceUserEntity> [doit]
def doWipeCIDeviceUser():
_performCIDeviceUserAction('wipe')
# gam update deviceuser <DeviceUserEntity> action <DeviceUserAction> [doit]
def doUpdateCIDeviceUser():
_performCIDeviceUserAction(None)
# gam info deviceuser <DeviceUserEntity>
# <DeviceUserFieldName>* [fields <DevieUserFieldNameList>]
# [formatjson]
def doInfoCIDeviceUser():
entityList, ci, customer, _ = getCIDeviceUserEntity()
FJQC = FormatJSONQuoteChar()
userFieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getFieldsList(myarg, DEVICE_FIELDS_CHOICE_MAP, userFieldsList, initialField='name'):
pass
else:
FJQC.GetFormatJSON(myarg)
userFields = getFieldsFromFieldsList(userFieldsList)
i = 0
count = len(entityList)
for deviceUser in entityList:
i += 1
name = deviceUser['name']
try:
deviceUser = callGAPI(ci.devices().deviceUsers(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name, customer=customer, fields=userFields)
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(deviceUser, timeObjects=DEVICE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
else:
printEntity([Ent.DEVICE_USER, deviceUser.pop('name')], i, count)
Ind.Increment()
showJSON(None, deviceUser, timeObjects=DEVICE_TIME_OBJECTS)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.DEVICE_USER, f'{name}', i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.DEVICE_USER, f'{name}'], str(e), i, count)
# gam print deviceusers [todrive <ToDriveAttribute>*]
# [select <DeviceID>]
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
# <DeviceUserFieldName>* [fields <DevieUserFieldNameList>]
# [orderby <DeviceOrderByFieldName> [ascending|descending]]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintCIDeviceUsers():
ci = buildGAPICIDeviceServiceObject()
customer = _getCustomersCustomerIdNoC()
csvPF = CSVPrintFile(['name'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(DEVICE_ORDERBY_CHOICE_MAP)
userFieldsList = []
queryTimes = {}
queries = [None]
parent = 'devices/-'
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'select':
parent = getString(Cmd.OB_DEVICE_ID)
if not parent.startswith('devices/'):
parent = f'devices/{parent}'
elif myarg in ['filter', 'filters', 'query', 'queries']:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg == 'orderby':
OBY.GetChoice()
elif getFieldsList(myarg, DEVICEUSER_FIELDS_CHOICE_MAP, userFieldsList, initialField='name'):
pass
elif myarg == 'sortheaders':
pass
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
userFields = getItemFieldsFromFieldsList('deviceUsers', userFieldsList)
substituteQueryTimes(queries, queryTimes)
itemCount = 0
for query in queries:
printGettingAllAccountEntities(Ent.DEVICE_USER, query)
pageMessage = getPageMessage()
try:
deviceUsers = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customer, filter=query,
orderBy=OBY.orderBy, parent=parent, fields=userFields, pageSize=20)
if showItemCountOnly:
itemCount += len(deviceUsers)
continue
for deviceUser in deviceUsers:
row = flattenJSON(deviceUser, timeObjects=DEVICE_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': deviceUser['name'],
'JSON': json.dumps(cleanJSON(deviceUser, timeObjects=DEVICE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
except (GAPI.invalid, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.DEVICE_USER, None], str(e))
break
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
csvPF.writeCSVfile('Device Users')
DEVICE_USER_COMPLIANCE_STATE_CHOICE_MAP = {
'compliant': 'COMPLIANT',
'noncompliant': 'NON_COMPLIANT'
}
DEVICE_USER_HEALTH_SCORE_CHOICE_MAP = {
'verypoor': 'VERY_POOR',
'poor': 'POOR',
'neutral': 'NEUTRAL',
'good': 'GOOD',
'verygood': 'VERY_GOOD'
}
DEVICE_USER_MANAGED_STATE_CHOICE_MAP = {
'clear': None,
'managed': 'MANAGED',
'unmanaged': 'UNMANAGED'
}
DEVICE_USER_CUSTOM_VALUE_TYPE_CHOICE_MAP = {
'bool': 'boolValue',
'boolean': 'boolValue',
'number': 'numberValue',
'string': 'stringValue'
}
# gam info deviceuserstate <DeviceUserEntity> [clientid <String>]
def doInfoCIDeviceUserState():
setTrueCustomerId()
entityList, ci, customer, _ = getCIDeviceUserEntity()
customerID = customer[10:]
client_id = f'{customerID}-gam'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'clientid':
client_id = f'{customerID}-{getString(Cmd.OB_STRING)}'
else:
unknownArgumentExit()
count = len(entityList)
i = 0
for deviceUser in entityList:
i += 1
deviceUser = deviceUser['name']
name = f'{deviceUser}/clientStates/{client_id}'
try:
result = callGAPI(ci.devices().deviceUsers().clientStates(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, customer=customer)
printEntity([Ent.DEVICE_USER_CLIENT_STATE, name], i, count)
Ind.Increment()
result.pop('name', None)
showJSON(None, result, timeObjects=DEVICE_TIME_OBJECTS)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.DEVICE_USER, deviceUser, i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.DEVICE_USER, deviceUser], str(e), i, count)
# gam update deviceuserstate <DeviceUserEntity> [clientid <String>]
# [customid <String>] [assettags clear|<AssetTagList>]
# [compliantstate|compliancestate compliant|noncompliant] [managedstate clear|managed|unmanaged]
# [healthscore verypoor|poor|neutral|good|verygood] [scorereason clear|<String>]
# (customvalue (bool|boolean <Boolean>)|(number <Integer>)(string <String>))*
def doUpdateCIDeviceUserState():
setTrueCustomerId()
entityList, ci, customer, _ = getCIDeviceUserEntity()
customerID = customer[10:]
client_id = f'{customerID}-gam'
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'clientid':
client_id = f'{customerID}-{getString(Cmd.OB_STRING)}'
elif myarg in ['assettag', 'assettags']:
body['assetTags'] = convertEntityToList(getString(Cmd.OB_STRING, minLen=0), shlexSplit=True)
if not body['assetTags'] or body['assetTags'] == ['clear']:
# TODO: This doesn't work to clear existing values; figure out why.
body['assetTags'] = [None]
elif myarg in ['compliantstate', 'compliancestate']:
body['complianceState'] = getChoice(DEVICE_USER_COMPLIANCE_STATE_CHOICE_MAP, mapChoice=True)
elif myarg == 'healthscore':
body['healthScore'] = getChoice(DEVICE_USER_HEALTH_SCORE_CHOICE_MAP, mapChoice=True)
elif myarg in ['scorereason']:
body['scoreReason'] = getString(Cmd.OB_STRING)
if body['scoreReason'] == 'clear':
body['scoreReason'] = None
elif myarg == 'customid':
body['customId'] = getString(Cmd.OB_STRING)
elif myarg == 'customvalue':
valueType = getChoice(DEVICE_USER_CUSTOM_VALUE_TYPE_CHOICE_MAP, mapChoice=True)
key = getString(Cmd.OB_STRING)
if valueType == 'boolValue':
value = getBoolean()
elif valueType == 'numberValue':
value = getInteger()
else: # stringValue
value = getString(Cmd.OB_STRING)
body.setdefault('keyValuePairs', {})
body['keyValuePairs'][key] = {valueType: value}
elif myarg == 'managedstate':
body['managed'] = getChoice(DEVICE_USER_MANAGED_STATE_CHOICE_MAP, mapChoice=True)
else:
unknownArgumentExit()
count = len(entityList)
i = 0
for deviceUser in entityList:
i += 1
deviceUser = deviceUser['name']
name = f'{deviceUser}/clientStates/{client_id}'
try:
result = callGAPI(ci.devices().deviceUsers().clientStates(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=name, customer=customer, updateMask=','.join(body.keys()), body=body)
if result['done']:
if 'error' not in result:
entityActionPerformed([Ent.DEVICE_USER_CLIENT_STATE, name], i, count)
result = result['response']
result.pop('name')
else:
entityActionFailedWarning([Ent.DEVICE_USER_CLIENT_STATE, name], result['error']['message'], i, count)
else:
entityActionPerformedMessage([Ent.DEVICE_USER_CLIENT_STATE, name], Msg.ACTION_IN_PROGRESS.format('update Client State'), i, count)
Ind.Increment()
showJSON(None, result, timeObjects=DEVICE_TIME_OBJECTS)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.DEVICE_USER, deviceUser)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.DEVICE_USER, deviceUser], str(e))
def isolatePrinterID(name):
''' converts long name into simple ID'''
return name.split('/')[-1]
def _getPrinterID():
cd = buildGAPIObject(API.PRINTERS)
customer = _getCustomersCustomerIdWithC()
printerId = getString(Cmd.OB_PRINTER_ID)
pattern = re.compile(rf'^{customer}/chrome/printers/(.+)$')
mg = pattern.match(printerId)
if mg:
return (printerId, mg.group(1), cd)
return (f'{customer}/chrome/printers/{printerId}', printerId, cd)
def _getPrinterEntity():
cd = buildGAPIObject(API.PRINTERS)
customer = _getCustomersCustomerIdWithC()
printerId = getString(Cmd.OB_PRINTER_ID)
entitySelector = printerId.lower()
if entitySelector == Cmd.ENTITY_SELECTOR_FILE:
printerIds = getEntitiesFromFile(False)
elif entitySelector == Cmd.ENTITY_SELECTOR_CSVFILE:
printerIds = getEntitiesFromCSVFile(False)
else:
printerIds = printerId.replace(',', ' ').split()
pattern = re.compile(rf'^{customer}/chrome/printers/(.+)$')
for i, printerId in enumerate(printerIds):
mg = pattern.match(printerId)
if mg:
printerIds[i] = mg.group(1)
return (customer, printerIds, cd)
CREATE_PRINTER_JSON_SKIP_FIELDS = ['id', 'name', 'createTime', 'orgUnitPath', 'auxiliaryMessages']
UPDATE_PRINTER_JSON_SKIP_FIELDS = ['id', 'name', 'createTime', 'orgUnitId', 'orgUnitPath', 'auxiliaryMessages']
def _getPrinterAttributes(cd, jsonDeleteFields):
'''get printer attributes for create/update commands'''
body = {}
showDetails = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'description':
body['description'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'displayname':
body['displayName'] = getString(Cmd.OB_STRING)
elif myarg == 'makeandmodel':
body['makeAndModel'] = getString(Cmd.OB_STRING)
elif myarg in {'ou', 'org', 'orgunit', 'orgunitid'}:
_, body['orgUnitId'] = getOrgUnitId(cd)
body['orgUnitId'] = body['orgUnitId'][3:]
elif myarg == 'uri':
body['uri'] = getString(Cmd.OB_STRING)
elif myarg in {'driverless', 'usedriverlessconfig'}:
body['useDriverlessConfig'] = getBoolean()
elif myarg == 'nodetails':
showDetails = False
elif myarg == 'json':
body.update(getJSON(jsonDeleteFields))
else:
unknownArgumentExit()
if body.get('makeAndModel'):
body.pop('useDriverlessConfig', None)
return (body, showDetails)
PRINTER_FIELDS_CHOICE_MAP = {
'auxiliarymessages': 'auxiliaryMessages',
'createtime': 'createTime',
'description': 'description',
'displayname': 'displayName',
'id': 'id',
'makeandmodel': 'makeAndModel',
'name': 'name',
'org': 'orgUnitId',
'orgunit': 'orgUnitId',
'orgunitid': 'orgUnitId',
'ou': 'orgUnitId',
'uri': 'uri',
'usedriverlessconfig': 'useDriverlessConfig',
}
PRINTER_TIME_OBJECTS = {'createTime'}
def _checkPrinterInheritance(cd, printer, orgUnitId, showInherited):
if 'orgUnitId' in printer:
if not showInherited:
if orgUnitId is not None and printer['orgUnitId'] != orgUnitId:
return False
elif orgUnitId is not None and printer['orgUnitId'] != orgUnitId:
printer['inherited'] = True
printer['parentOrgUnitId'] = printer['orgUnitId']
printer['parentOrgUnitPath'] = convertOrgUnitIDtoPath(cd, f'id:{printer["parentOrgUnitId"]}')
printer['orgUnitId'] = orgUnitId
else:
printer['inherited'] = False
printer['parentOrgUnitId'] = printer['parentOrgUnitPath'] = ''
printer['orgUnitPath'] = convertOrgUnitIDtoPath(cd, f'id:{printer["orgUnitId"]}')
return True
def _showPrinter(cd, printer, FJQC, orgUnitId=None, showInherited=False, i=0, count=0):
if not _checkPrinterInheritance(cd, printer, orgUnitId, showInherited):
return False
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(printer, timeObjects=PRINTER_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.PRINTER, printer['id']], i, count)
Ind.Increment()
showJSON(None, printer, timeObjects=PRINTER_TIME_OBJECTS)
Ind.Decrement()
# gam create printer <PrinterAttribute>+ [nodetails]
def doCreatePrinter():
cd = buildGAPIObject(API.DIRECTORY)
parent = _getCustomersCustomerIdWithC()
body, showDetails = _getPrinterAttributes(cd, CREATE_PRINTER_JSON_SKIP_FIELDS)
if not body.get('orgUnitId'):
missingArgumentExit('orgunit')
try:
printer = callGAPI(cd.customers().chrome().printers(), 'create',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=parent, body=body)
entityActionPerformed([Ent.PRINTER, printer['id']])
if showDetails:
_showPrinter(cd, printer, None)
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PRINTER, None], str(e))
# gam update printer <PrinterID> <PrinterAttribute>+ [nodetails]
def doUpdatePrinter():
name, printerId, cd = _getPrinterID()
body, showDetails = _getPrinterAttributes(cd, UPDATE_PRINTER_JSON_SKIP_FIELDS)
updateMask = ','.join(list(body.keys()))
# note clearMask seems unnecessary. Updating field to '' clears it.
try:
printer = callGAPI(cd.customers().chrome().printers(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name, updateMask=updateMask, body=body)
entityActionPerformed([Ent.PRINTER, printerId])
if showDetails:
_showPrinter(cd, printer, None)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PRINTER, printerId], str(e))
# gam delete printer
# <PrinterIDList>|
# <FileSelector>|
# <CSVFileSelector>
def doDeletePrinter():
parent, printerIds, cd = _getPrinterEntity()
# max 50 per API call
batch_size = 50
for chunk in range(0, len(printerIds), batch_size):
body = {'printerIds': printerIds[chunk:chunk + batch_size]}
result = callGAPI(cd.customers().chrome().printers(), 'batchDeletePrinters',
parent=parent, body=body)
for printerId in result.get('printerIds', []):
entityActionPerformed([Ent.PRINTER, printerId])
for failure in result.get('failedPrinters', []):
if 'printerIds' in failure:
entityActionFailedWarning([Ent.PRINTER, failure['printerIds']], failure.get('errorMessage', 'Unknown printer'))
else:
entityActionFailedWarning([Ent.PRINTER, failure['printer']['id']], failure['errorMessage'])
# gam info printer <PrinterID>
# [fields <PrinterFieldNameList>] [formatjson]
def doInfoPrinter():
name, printerId, cd = _getPrinterID()
FJQC = FormatJSONQuoteChar()
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getFieldsList(myarg, PRINTER_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
printer = callGAPI(cd.customers().chrome().printers(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.PERMISSION_DENIED],
name=name, fields=fields)
_showPrinter(cd, printer, FJQC)
except GAPI.notFound:
entityUnknownWarning(Ent.PRINTER, f'{printerId}')
except (GAPI.invalid, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.DEVICE, f'{printerId}'], str(e))
ORGUNIT_ENTITIES_MAP = {
'org': Cmd.ENTITY_OU,
'organdchildren': Cmd.ENTITY_OU_AND_CHILDREN,
'orgs': Cmd.ENTITY_OUS,
'orgsandchildren': Cmd.ENTITY_OUS_AND_CHILDREN,
'ou': Cmd.ENTITY_OU,
'ouandchildren': Cmd.ENTITY_OU_AND_CHILDREN,
'ous': Cmd.ENTITY_OUS,
'ousandchildren': Cmd.ENTITY_OUS_AND_CHILDREN,
'orgunit': Cmd.ENTITY_OU,
'orgunitandchildren': Cmd.ENTITY_OU_AND_CHILDREN,
'orgunits': Cmd.ENTITY_OUS,
'orgunitsandchildren': Cmd.ENTITY_OUS_AND_CHILDREN,
}
# gam show printers
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [filter <String>] [showinherited [<Boolean>]
# [fields <PrinterFieldNameList>] [formatjson]
# gam print printers [todrive <ToDriveAttribute>*]
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [filter <String>] [showinherited [<Boolean>]
# [fields <PrinterFieldNameList>] [[formatjson [quotechar <Character>]]
def doPrintShowPrinters():
def _printPrinter(printer):
if not _checkPrinterInheritance(cd, printer, orgUnitId, showInherited):
return False
row = flattenJSON(printer, timeObjects=PRINTER_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'id': printer['id'],
'JSON': json.dumps(cleanJSON(printer, timeObjects=PRINTER_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
cd = buildGAPIObject(API.DIRECTORY)
parent = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['id']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
ous = [None]
directlyInOU = True
pfilter = None
showInherited = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORGUNIT_ENTITIES_MAP:
myarg = ORGUNIT_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
elif getFieldsList(myarg, PRINTER_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
elif myarg == 'filter':
pfilter = getString(Cmd.OB_STRING)
elif myarg == 'showinherited':
showInherited = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if fieldsList and (not directlyInOU or showInherited):
fieldsList.append('orgUnitId')
fields = getItemFieldsFromFieldsList('printers', fieldsList)
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
_, orgUnitId = getOrgUnitId(cd, ou)
ouList = [(ou, orgUnitId[3:])]
else:
ouList = [('/', None)]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
for subou in ouList:
orgUnitPath = subou[0]
orgUnitId = subou[1]
if orgUnitId is not None:
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
printGettingAllEntityItemsForWhom(Ent.PRINTER, orgUnitPath, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
pageMessage = getPageMessageForWhom()
else:
printGettingAllAccountEntities(Ent.PRINTER, pfilter)
pageMessage = getPageMessage()
try:
printers = callGAPIpages(cd.customers().chrome().printers(), 'list', 'printers',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.PERMISSION_DENIED],
parent=parent, orgUnitId=orgUnitId, filter=pfilter, fields=fields)
except (GAPI.invalid, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PRINTER, None], str(e))
return
if not csvPF:
jcount = len(printers)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.PRINTER)
Ind.Increment()
j = 0
for printer in printers:
j += 1
_showPrinter(cd, printer, FJQC, orgUnitId, showInherited, j, jcount)
Ind.Decrement()
else:
for printer in printers:
_printPrinter(printer)
if csvPF:
csvPF.writeCSVfile('Printers')
# gam print printermodels [todrive <ToDriveAttribute>*]
# [filter <String>]
# [[formatjson [quotechar <Character>]]
# gam show printermodels
# [filter <String>]
# [formatjson]
def doPrintShowPrinterModels():
def _showPrinterModel(model, FJQC, i, count):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(model), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.PRINTER_MODEL, model['manufacturer']], i, count)
Ind.Increment()
for field in ['displayName', 'makeAndModel']:
printKeyValueList([field, model[field]])
Ind.Decrement()
def _printPrinterModel(model):
row = flattenJSON(model)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(model),
ensure_ascii=False, sort_keys=True)})
cd = buildGAPIObject(API.DIRECTORY)
parent = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile() if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
pfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'filter':
pfilter = getString(Cmd.OB_STRING)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not FJQC.formatJSON:
csvPF.SetTitles(['manufacturer', 'displayName', 'makeAndModel'])
csvPF.SetSortAllTitles()
printGettingAllAccountEntities(Ent.PRINTER_MODEL, pfilter)
pageMessage = getPageMessage()
try:
models = callGAPIpages(cd.customers().chrome().printers(), 'listPrinterModels', 'printerModels',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
pageMessage=pageMessage,
parent=parent, pageSize=10000, filter=pfilter)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.PRINTER_MODEL, None], str(e))
return
if not csvPF:
jcount = len(models)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.PRINTER_MODEL)
Ind.Increment()
j = 0
for model in models:
j += 1
_showPrinterModel(model, FJQC, j, jcount)
Ind.Decrement()
else:
for model in models:
_printPrinterModel(model)
if csvPF:
csvPF.writeCSVfile('Printer Models')
CHROME_APPS_TIME_OBJECTS = {'firstPublishTime', 'latestPublishTime'}
CHROME_APPS_TYPE_CHOICES = ['android', 'chrome', 'web']
# gam info chromeapp android|chrome|web <AppID> [formatjson]
def doInfoChromeApp():
cm = buildGAPIObject(API.CHROMEMANAGEMENT_APPDETAILS)
app_type = getChoice(CHROME_APPS_TYPE_CHOICES)
app_id = getString(Cmd.OB_APP_ID)
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
FJQC.GetFormatJSON(myarg)
if app_type == 'chrome':
service = cm.customers().apps().chrome()
elif app_type == 'android':
service = cm.customers().apps().android()
else:
service = cm.customers().apps().web()
try:
appDetails = callGAPI(service, 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
name=f'customers/{GC.Values[GC.CUSTOMER_ID]}/apps/{app_type}/{app_id}')
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(appDetails), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_APP, app_id])
Ind.Increment()
showJSON(None, appDetails, timeObjects=CHROME_APPS_TIME_OBJECTS)
Ind.Decrement()
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_APP, f'{app_type}/{app_id}')
def _getPrintChromeGetting(subou, pfilter, entityType):
orgUnitPath = subou[0]
orgUnitId = subou[1]
query = pfilter
if orgUnitId is not None:
if query:
query += ' AND '
else:
query = ''
query += f'orgUnitPath={orgUnitPath}'
printGettingAllAccountEntities(entityType, query)
return (orgUnitPath, orgUnitId)
CHROME_APPS_ORDERBY_CHOICE_MAP = {
'appname': 'app_name',
'apptype': 'appType',
'installtype': 'install_type',
'numberofpermissions': 'number_of_permissions',
'totalinstallcount': 'total_install_count',
}
CHROME_APPS_TITLES = [
'displayName',
'browserDeviceCount', 'osUserCount',
'appType', 'description',
'appInstallType', 'appSource',
'disabled', 'homepageUri', 'permissions'
]
# gam print chromeapps [todrive <ToDriveAttribute>*]
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [filter <String>]
# [orderby appname|apptype|installtype|numberofpermissions|totalinstallcount]
# [formatjson [quotechar <Character>]] [delimiter <Character>]
# gam show chromeapps
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [filter <String>]
# [orderby appname|apptype|installtype|numberofpermissions|totalinstallcount]
# [formatjson]
def doPrintShowChromeApps():
def _printApp(app):
if showOrgUnit:
app['orgUnitPath'] = orgUnitPath
row = flattenJSON(app, simpleLists=['permissions'], delimiter=delimiter)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'appId': app['appId'],
'JSON': json.dumps(cleanJSON(app), ensure_ascii=False, sort_keys=True)})
def _showApp(app, i=0, count=0):
if showOrgUnit:
app['orgUnitPath'] = orgUnitPath
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(app), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_APP, app['appId']], i, count)
Ind.Increment()
showJSON(None, app)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
customerId = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['appId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
ous = [None]
directlyInOU = True
showOrgUnit = False
orderBy = pfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORGUNIT_ENTITIES_MAP:
myarg = ORGUNIT_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
elif myarg == 'filter':
pfilter = getString(Cmd.OB_STRING)
elif myarg == 'orderby':
orderBy = getChoice(CHROME_APPS_ORDERBY_CHOICE_MAP, mapChoice=True)
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if ous[0] is not None:
showOrgUnit = True
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(CHROME_APPS_TITLES)
if showOrgUnit:
csvPF.AddTitle('orgUnitPath')
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
_, orgUnitId = getOrgUnitId(cd, ou)
ouList = [(ou, orgUnitId[3:])]
else:
ouList = [('/', None)]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
for subou in ouList:
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_APP)
pageMessage = getPageMessage()
try:
apps = callGAPIpages(cm.customers().reports(), 'countInstalledApps', 'installedApps',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageMessage=pageMessage,
customer=customerId, orgUnitId=orgUnitId, filter=pfilter, orderBy=orderBy)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CHROME_APP, None], str(e))
return
jcount = len(apps)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CHROME_APP)
Ind.Increment()
j = 0
for app in apps:
j += 1
_showApp(app, j, jcount)
Ind.Decrement()
else:
for app in apps:
_printApp(app)
if csvPF:
csvPF.writeCSVfile('Chrome Installed Applications')
CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP = {
'extension': 'EXTENSION',
'app': 'APP',
'theme': 'THEME',
'hostedapp': 'HOSTED_APP',
'androidapp': 'ANDROID_APP',
}
CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP = {
'deviceid': 'deviceId',
'machine': 'machine',
}
CHROME_APP_DEVICES_TITLES = ['appType', 'deviceId', 'machine']
# gam print chromeappdevices [todrive <ToDriveAttribute>*]
# appid <AppID> apptype extension|app|theme|hostedapp|androidapp
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [start <Date>] [end <Date>]
# [orderby deviceid|machine]
# [formatjson [quotechar <Character>]]
# gam show chromeappdevices
# appid <AppID> apptype extension|app|theme|hostedapp|androidapp
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [start <Date>] [end <Date>]
# [orderby deviceid|machine]
# [formatjson]
def doPrintShowChromeAppDevices():
def _printDevice(device):
device['appId'] = appId
device['appType'] = appType
if showOrgUnit:
device['orgUnitPath'] = orgUnitPath
row = flattenJSON(device)
if not FJQC.formatJSON:
csvPF.WriteRow(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'appId': device['appId'],
'JSON': json.dumps(cleanJSON(device), ensure_ascii=False, sort_keys=True)})
def _showDevice(device, i=0, count=0):
device['appId'] = appId
device['appType'] = appType
if showOrgUnit:
device['orgUnitPath'] = orgUnitPath
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(device), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_APP, device['appId']], i, count)
Ind.Increment()
showJSON(None, device)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
customerId = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['appId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
ous = [None]
directlyInOU = True
showOrgUnit = False
appId = appType = orderBy = None
startDate = endDate = pfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORGUNIT_ENTITIES_MAP:
myarg = ORGUNIT_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
elif myarg == 'appid':
appId = getString(Cmd.OB_APP_ID)
elif myarg == 'apptype':
appType = getChoice(CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP, mapChoice=True)
elif myarg in CROS_START_ARGUMENTS:
startDate, _ = _getFilterDateTime()
startDate = startDate.strftime(YYYYMMDD_FORMAT)
elif myarg in CROS_END_ARGUMENTS:
endDate, _ = _getFilterDateTime()
endDate = endDate.strftime(YYYYMMDD_FORMAT)
elif myarg == 'orderby':
orderBy = getChoice(CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP, mapChoice=True)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if appId is None:
missingArgumentExit('appid')
if appType is None:
missingArgumentExit('apptype')
if endDate:
pfilter = f'last_active_date<={endDate}'
if startDate:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
if ous[0] is not None:
showOrgUnit = True
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(CHROME_APP_DEVICES_TITLES)
if showOrgUnit:
csvPF.AddTitle('orgUnitPath')
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
_, orgUnitId = getOrgUnitId(cd, ou)
ouList = [(ou, orgUnitId[3:])]
else:
ouList = [('/', None)]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
for subou in ouList:
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_APP_DEVICE)
pageMessage = getPageMessage()
try:
devices = callGAPIpages(cm.customers().reports(), 'findInstalledAppDevices', 'devices',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageMessage=pageMessage,
appId=appId, appType=appType,
customer=customerId, orgUnitId=orgUnitId, filter=pfilter, orderBy=orderBy)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CHROME_APP_DEVICE, None], str(e))
return
jcount = len(devices)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CHROME_APP_DEVICE)
Ind.Increment()
j = 0
for device in devices:
j += 1
_showDevice(device, j, jcount)
Ind.Decrement()
else:
for device in devices:
_printDevice(device)
if csvPF:
csvPF.writeCSVfile('Chrome Installed Application Devices')
CHROME_AUE_TITLES = ['aueMonth', 'aueYear', 'expired']
# gam print chromeaues [todrive <ToDriveAttribute>*]
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [minauedate <Date>] [maxauedate <Date>]
# [formatjson [quotechar <Character>]]
# gam show chromeaues
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [minauedate <Date>] [maxauedate <Date>]
# [formatjson]
def doPrintShowChromeAues():
def _printAue(aue):
if showOrgUnit:
aue['orgUnitPath'] = orgUnitPath
row = flattenJSON(aue)
if not FJQC.formatJSON:
csvPF.WriteRow(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'model': aue['model'], 'count': aue['count'],
'JSON': json.dumps(cleanJSON(aue), ensure_ascii=False, sort_keys=True)})
def _showAue(aue, i=0, count=0):
if showOrgUnit:
aue['orgUnitPath'] = orgUnitPath
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(aue), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_MODEL, aue['model']], i, count)
Ind.Increment()
showJSON(None, aue)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
customerId = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['model', 'count']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
ous = [None]
directlyInOU = True
showOrgUnit = False
minAueDate = maxAueDate = pfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORGUNIT_ENTITIES_MAP:
myarg = ORGUNIT_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
elif myarg == 'minauedate':
minAueDate, _ = _getFilterDateTime()
minAueDate = minAueDate.strftime(YYYYMMDD_FORMAT)
elif myarg == 'maxauedate':
maxAueDate, _ = _getFilterDateTime()
maxAueDate = maxAueDate.strftime(YYYYMMDD_FORMAT)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if minAueDate:
pfilter = f'minAueDate={minAueDate}'
if maxAueDate:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += f'maxAueDate>={maxAueDate}'
if ous[0] is not None:
showOrgUnit = True
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(CHROME_AUE_TITLES)
if showOrgUnit:
csvPF.AddTitle('orgUnitPath')
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
_, orgUnitId = getOrgUnitId(cd, ou)
ouList = [(ou, orgUnitId[3:])]
else:
ouList = [('/', None)]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
for subou in ouList:
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_MODEL)
try:
aues = callGAPI(cm.customers().reports(), 'countChromeDevicesReachingAutoExpirationDate',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customerId, orgUnitId=orgUnitId, minAueDate=minAueDate, maxAueDate=maxAueDate).get('deviceAueCountReports', [])
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CHROME_MODEL, None], str(e))
return
jcount = len(aues)
if not csvPF:
Ind.Increment()
j = 0
for aue in sorted(aues, key=lambda k: k.get('model', UNKNOWN)):
j += 1
_showAue(aue, j, jcount)
Ind.Decrement()
else:
for aue in sorted(aues, key=lambda k: k.get('model', UNKNOWN)):
_printAue(aue)
if csvPF:
csvPF.writeCSVfile('Chrome AUEs')
CHROME_NEEDSATTN_TITLES = ['noRecentPolicySyncCount', 'noRecentUserActivityCount', 'pendingUpdate',
'osVersionNotCompliantCount', 'unsupportedPolicyCount']
# gam print chromeneedsattn [todrive <ToDriveAttribute>*]
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [formatjson [quotechar <Character>]]
# gam show chromeneedsattn
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [formatjson]
def doPrintShowChromeNeedsAttn():
def _printNeedsAttn(needsattn):
if showOrgUnit:
needsattn['orgUnitPath'] = orgUnitPath
row = flattenJSON(needsattn)
if not FJQC.formatJSON:
csvPF.WriteRow(row)
elif csvPF.CheckRowTitles(row):
row = {'orgUnitPath': orgUnitPath} if showOrgUnit else {}
row['JSON'] = json.dumps(cleanJSON(needsattn), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
def _showNeedsAttn(needsattn, i=0, count=0):
if showOrgUnit:
needsattn['orgUnitPath'] = orgUnitPath
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(needsattn), ensure_ascii=False, sort_keys=True))
return
if showOrgUnit:
printEntity([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], i, count)
Ind.Increment()
showJSON(None, needsattn)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
customerId = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile([]) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
ous = [None]
directlyInOU = True
showOrgUnit = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORGUNIT_ENTITIES_MAP:
myarg = ORGUNIT_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if ous[0] is not None:
showOrgUnit = True
if csvPF:
if not FJQC.formatJSON:
csvPF.AddTitles(CHROME_NEEDSATTN_TITLES)
if showOrgUnit:
csvPF.AddTitle('orgUnitPath')
elif showOrgUnit:
csvPF.SetJSONTitles(['orgUnitPath', 'JSON'])
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
_, orgUnitId = getOrgUnitId(cd, ou)
ouList = [(ou, orgUnitId[3:])]
else:
ouList = [('/', None)]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
count = len(ouList)
i = 0
for subou in ouList:
i += 1
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, None, Ent.CHROME_DEVICE)
try:
result = callGAPI(cm.customers().reports(), 'countChromeDevicesThatNeedAttention',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=customerId, orgUnitId=orgUnitId, readMask=','.join(CHROME_NEEDSATTN_TITLES))
for field in CHROME_NEEDSATTN_TITLES:
result.setdefault(field, 0)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitId], str(e))
return
if not csvPF:
_showNeedsAttn(result, i, count)
else:
_printNeedsAttn(result)
if csvPF:
csvPF.writeCSVfile('Chrome Devices Needing Attention')
CHROME_VERSIONS_TITLES = ['channel', 'system', 'deviceOsVersion']
# gam print chromeversions [todrive <ToDriveAttribute>*]
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [start <Date>] [end <Date>]
# [recentfirst [<Boolean>]]
# [formatjson [quotechar <Character>]]
# gam show chromeversions
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
# [start <Date>] [end <Date>]
# [recentfirst [<Boolean>]]
# [formatjson]
def doPrintShowChromeVersions():
def _getVersionKey(v):
if 'version' not in v:
return (0, 0, 0, 0)
k = v['version'].split('.')
for i, x in enumerate(k):
k[i] = int(x)
return tuple(k)
def _printVersion(version):
if showOrgUnit:
version['orgUnitPath'] = orgUnitPath
if 'version' not in version:
version['version'] = UNKNOWN
row = flattenJSON(version)
if not FJQC.formatJSON:
csvPF.WriteRow(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'version': version['version'], 'count': version['count'],
'JSON': json.dumps(cleanJSON(version), ensure_ascii=False, sort_keys=True)})
def _showVersion(version, i=0, count=0):
if showOrgUnit:
version['orgUnitPath'] = orgUnitPath
if 'version' not in version:
version['version'] = UNKNOWN
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(version), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHROME_VERSION, version['version']], i, count)
Ind.Increment()
showJSON(None, version)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
customerId = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['version', 'count']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
ous = [None]
directlyInOU = True
reverse = showOrgUnit = False
startDate = endDate = pfilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ORGUNIT_ENTITIES_MAP:
myarg = ORGUNIT_ENTITIES_MAP[myarg]
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
elif myarg in CROS_START_ARGUMENTS:
startDate, _ = _getFilterDateTime()
startDate = startDate.strftime(YYYYMMDD_FORMAT)
elif myarg in CROS_END_ARGUMENTS:
endDate, _ = _getFilterDateTime()
endDate = endDate.strftime(YYYYMMDD_FORMAT)
elif myarg == 'recentfirst':
reverse = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if endDate:
pfilter = f'last_active_date<={endDate}'
if startDate:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += f'last_active_date>={startDate}'
if ous[0] is not None:
showOrgUnit = True
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(CHROME_VERSIONS_TITLES)
if showOrgUnit:
csvPF.AddTitle('orgUnitPath')
for ou in ous:
if ou is not None:
ou = makeOrgUnitPathAbsolute(ou)
_, orgUnitId = getOrgUnitId(cd, ou)
ouList = [(ou, orgUnitId[3:])]
else:
ouList = [('/', None)]
if not directlyInOU:
try:
orgs = callGAPI(cd.orgunits(), 'list',
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
return
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
for subou in ouList:
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_VERSION)
pageMessage = getPageMessage()
try:
versions = callGAPIpages(cm.customers().reports(), 'countChromeVersions', 'browserVersions',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageMessage=pageMessage,
customer=customerId, orgUnitId=orgUnitId, filter=pfilter)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CHROME_VERSION, None], str(e))
return
jcount = len(versions)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CHROME_VERSION)
Ind.Increment()
j = 0
for version in sorted(versions, key=_getVersionKey, reverse=reverse):
j += 1
_showVersion(version, j, jcount)
Ind.Decrement()
else:
for version in sorted(versions, key=_getVersionKey, reverse=reverse):
_printVersion(version)
if csvPF:
csvPF.writeCSVfile('Chrome Versions')
def getPlatformChannelMap(cv, entityType):
if cv is None:
cv = buildGAPIObjectNoAuthentication(API.CHROMEVERSIONHISTORY)
if entityType == Ent.CHROME_PLATFORM:
svc = cv.platforms()
parent = 'chrome'
field = 'platformType'
else: # elif entityType == Ent.CHROME_CHANNEL:
svc = cv.platforms().channels()
parent = 'chrome/platforms/all'
field = 'channelType'
try:
pcitems = callGAPIpages(svc, 'list', CHROME_VERSIONHISTORY_ITEMS[entityType],
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=parent, fields=f'nextPageToken,{CHROME_VERSIONHISTORY_ITEMS[entityType]}')
pcMap = {'all': 'all'}
for pcitem in pcitems:
pcType = pcitem[field].lower()
pcMap[pcType.replace('_', '')] = pcType
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([entityType, None], str(e))
pcMap = {}
return (cv, pcMap)
def getRelativeMilestone(cv, channel, minus):
''' takes a channel and minus_versions like stable and -1. returns current given milestone number '''
if cv is None:
cv = buildGAPIObjectNoAuthentication(API.CHROMEVERSIONHISTORY)
try:
releases = callGAPIpages(cv.platforms().channels().versions().releases(), 'list', 'releases',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=f'chrome/platforms/all/channels/{channel}/versions/all',
fields='nextPageToken,releases(version)')
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.CHROME_RELEASE, None], str(e))
return (cv, False, str(e))
milestones = []
# Note that milestones are usually sequential but some numbers
# may be skipped. For example, there was no Chrome 82 stable.
# Thus we need to do more than find the latest version and subtract.
for release in releases:
if 'version' in release:
milestone = int(release['version'].split('.')[0])
if milestone not in milestones:
milestones.append(int(milestone))
milestones.sort(reverse=True)
try:
return (cv, True, str(milestones[minus]))
except IndexError:
return (cv, False, f'{channel}-{0}:{channel}-{len(milestones)-1}')
CHROME_HISTORY_ENTITY_CHOICE_MAP = {
'platforms': Ent.CHROME_PLATFORM,
'channels': Ent.CHROME_CHANNEL,
'versions': Ent.CHROME_VERSION,
'releases': Ent.CHROME_RELEASE,
}
CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP = {
Ent.CHROME_VERSION: {
'channel': 'channel',
'name': 'name',
'platform': 'platform',
'version': 'version'
},
Ent.CHROME_RELEASE: {
'channel': 'channel',
'endtime': 'endtime',
'fraction': 'fraction',
'name': 'name',
'platform': 'platform',
'starttime': 'starttime',
'version': 'version'
}
}
CHROME_VERSIONHISTORY_TITLES = {
Ent.CHROME_PLATFORM: ['platform'],
Ent.CHROME_CHANNEL: ['channel', 'platform'],
Ent.CHROME_VERSION: ['version', 'channel', 'platform',
'major_version', 'minor_version', 'build', 'patch'],
Ent.CHROME_RELEASE: ['version', 'channel', 'platform',
'major_version', 'minor_version', 'build', 'patch',
'fraction', 'fractionGroup', 'serving.startTime',
'serving.endTime', 'pinnable']
}
CHROME_VERSIONHISTORY_ITEMS = {
Ent.CHROME_PLATFORM: 'platforms',
Ent.CHROME_CHANNEL: 'channels',
Ent.CHROME_VERSION: 'versions',
Ent.CHROME_RELEASE: 'releases'
}
CHROME_VERSIONHISTORY_TIMEOBJECTS = {
Ent.CHROME_PLATFORM: None,
Ent.CHROME_CHANNEL: None,
Ent.CHROME_VERSION: None,
Ent.CHROME_RELEASE: ['startTime', 'endTime']
}
# gam print chromehistory platforms [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
# gam show chromehistory platforms
# [formatjson]
# gam print chromehistory channels [todrive <ToDriveAttribute>*]
# [platform <ChromePlatformType>]
# [formatjson [quotechar <Character>]]
# gam show chromehistory channels
# [platform <ChromePlatformType>]
# [formatjson]
# gam print chromehistory versions [todrive <ToDriveAttribute>*]
# [platform <ChromePlatformType>] [channel <ChromeChannelType>]
# (orderby <ChromeVersionsOrderByFieldName> [ascending|descending])*
# [filter <String>]
# [formatjson [quotechar <Character>]]
# gam show chromehistory versions
# [platform <ChromePlatformType>] [channel <ChromeChannelType>]
# (orderby <ChromeVersionsOrderByFieldName> [ascending|descending])*
# [filter <String>]
# [formatjson]
# gam print chromehistory releases [todrive <ToDriveAttribute>*]
# [platform <ChromePlatformType>] [channel <ChromeChannelType>] [version <String>]
# (orderby <ChromeReleasessOrderByFieldName> [ascending|descending])*
# [filter <String>]
# [formatjson [quotechar <Character>]]
# gam show chromehistory releases
# [platform <ChromePlatformType>] [channel <ChromeChannelType>] [version <String>]
# (orderby <ChromeReleasessOrderByFieldName> [ascending|descending])*
# [filter <String>]
# [formatjson]
def doPrintShowChromeHistory():
def addDetailFields(citem):
for key in list(citem):
if key.endswith('Type'):
citem[key[:-4]] = citem.pop(key)
if 'channel' in citem:
citem['channel'] = citem['channel'].lower()
else:
channel_match = re.search(r"\/channels\/([^/]*)", citem['name'])
if channel_match:
try:
citem['channel'] = channel_match.group(1)
except IndexError:
pass
if 'platform' in citem:
citem['platform'] = citem['platform'].lower()
else:
platform_match = re.search(r"\/platforms\/([^/]*)", citem['name'])
if platform_match:
try:
citem['platform'] = platform_match.group(1)
except IndexError:
pass
if citem.get('version', '').count('.') == 3:
citem['major_version'], citem['minor_version'], citem['build'], citem['patch'] = citem['version'].split('.')
citem.pop('name')
def _printItem(citem):
addDetailFields(citem)
row = flattenJSON(citem, timeObjects=CHROME_VERSIONHISTORY_TIMEOBJECTS[entityType])
if not FJQC.formatJSON:
csvPF.WriteRow(row)
elif csvPF.CheckRowTitles(row):
keyField = CHROME_VERSIONHISTORY_TITLES[entityType][0]
csvPF.WriteRowNoFilter({keyField: citem[keyField],
'JSON': json.dumps(cleanJSON(citem), ensure_ascii=False, sort_keys=True)})
def _showItem(citem, i=0, count=0):
addDetailFields(citem)
keyField = CHROME_VERSIONHISTORY_TITLES[entityType][0]
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(citem), ensure_ascii=False, sort_keys=True))
return
printEntity([entityType, citem[keyField]], i, count)
Ind.Increment()
citem = flattenJSON(citem, timeObjects=CHROME_VERSIONHISTORY_TIMEOBJECTS[entityType])
for field in CHROME_VERSIONHISTORY_TITLES[entityType]:
if field in citem:
printKeyValueList([field, citem[field]])
Ind.Decrement()
cv = buildGAPIObjectNoAuthentication(API.CHROMEVERSIONHISTORY)
entityType = getChoice(CHROME_HISTORY_ENTITY_CHOICE_MAP, mapChoice=True)
csvPF = CSVPrintFile(CHROME_VERSIONHISTORY_TITLES[entityType][0:1]) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
platformMap = None
channelMap = None
cplatform = 'all'
channel = 'all'
version = 'all'
kwargs = {}
if entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE}:
OBY = OrderBy(CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP[entityType])
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif entityType != Ent.CHROME_PLATFORM and myarg == 'platform':
if platformMap is None:
_, platformMap = getPlatformChannelMap(cv, Ent.CHROME_PLATFORM)
cplatform = getChoice(platformMap, mapChoice=True)
elif entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE} and myarg == 'channel':
if channelMap is None:
_, channelMap = getPlatformChannelMap(cv, Ent.CHROME_CHANNEL)
channel = getChoice(channelMap, mapChoice=True)
elif entityType == Ent.CHROME_RELEASE and myarg == 'version':
version = getString(Cmd.OB_CHROME_VERSION)
elif entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE} and myarg == 'orderby':
OBY.GetChoice()
elif entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE} and myarg == 'filter':
kwargs['filter'] = getString(Cmd.OB_STRING)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(CHROME_VERSIONHISTORY_TITLES[entityType][1:])
if entityType == Ent.CHROME_PLATFORM:
svc = cv.platforms()
parent = 'chrome'
elif entityType == Ent.CHROME_CHANNEL:
svc = cv.platforms().channels()
parent = f'chrome/platforms/{cplatform}'
elif entityType == Ent.CHROME_VERSION:
svc = cv.platforms().channels().versions()
parent = f'chrome/platforms/{cplatform}/channels/{channel}'
kwargs['orderBy'] = OBY.orderBy
else: #elif entityType == Ent.CHROME_RELEASE
svc = cv.platforms().channels().versions().releases()
parent = f'chrome/platforms/{cplatform}/channels/{channel}/versions/{version}'
kwargs['orderBy'] = OBY.orderBy
printGettingAllAccountEntities(entityType)
pageMessage = getPageMessage()
try:
citems = callGAPIpages(svc, 'list', CHROME_VERSIONHISTORY_ITEMS[entityType],
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
pageMessage=pageMessage,
parent=parent, fields=f'nextPageToken,{CHROME_VERSIONHISTORY_ITEMS[entityType]}',
**kwargs)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([entityType, None], str(e))
return
if not csvPF:
jcount = len(citems)
if not FJQC.formatJSON:
performActionNumItems(jcount, entityType)
j = 0
for citem in citems:
j += 1
_showItem(citem, j, jcount)
else:
for citem in citems:
_printItem(citem)
if csvPF:
csvPF.writeCSVfile(Ent.Plural(entityType))
# gam print chromesnvalidity [todrive <ToDriveAttribute>*]
# cros_sn <SerialNumberEntity> [listlimit <Number>]
# [delimiter <Character>]
def doPrintChromeSnValidity():
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['serialNumber', 'exactMatches', 'exactMatchDeviceIds', 'prefixMatches', 'prefixMatchDeviceIds'])
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
serialNumberList = []
listLimit = 0
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'crossn':
serialNumberList = getEntityList(Cmd.OB_STRING_LIST)
elif myarg == 'listlimit':
listLimit = getInteger(minVal=0)
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
unknownArgumentExit()
if not serialNumberList:
actionNotPerformedNumItemsWarning(0, Ent.CROS_SERIAL_NUMBER, Msg.NO_SERIAL_NUMBERS_SPECIFIED)
return
for serialNumber in serialNumberList:
query = f'id:{serialNumber}'
printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
try:
devices = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customerId=GC.Values[GC.CUSTOMER_ID],
query=query, fields='nextPageToken,chromeosdevices(deviceId,serialNumber)',
orderBy='serialNumber', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
except (GAPI.invalidInput, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
exactMatches = prefixMatches = 0
exactMatchDeviceIds = []
prefixMatchDeviceIds = []
serialNumberLower = serialNumber.lower()
for device in devices:
if device['serialNumber'].lower() == serialNumberLower:
exactMatches += 1
if not listLimit or exactMatches <= listLimit:
exactMatchDeviceIds.append(device['deviceId'])
else:
prefixMatches += 1
if not listLimit or prefixMatches <= listLimit:
prefixMatchDeviceIds.append(device['deviceId'])
row = {'serialNumber': serialNumber,
'exactMatches': exactMatches,
'exactMatchDeviceIds': delimiter.join(exactMatchDeviceIds),
'prefixMatches': prefixMatches,
'prefixMatchDeviceIds': delimiter.join(prefixMatchDeviceIds)}
csvPF.WriteRow(row)
if csvPF:
csvPF.writeCSVfile('Chrome Serial Number Validity')
# Mobile command utilities
MOBILE_ACTION_CHOICE_MAP = {
'accountwipe': 'admin_account_wipe',
'adminaccountwipe': 'admin_account_wipe',
'wipeaccount': 'admin_account_wipe',
'adminremotewipe': 'admin_remote_wipe',
'wipe': 'admin_remote_wipe',
'approve': 'approve',
'block': 'block',
'cancelremotewipethenactivate': 'cancel_remote_wipe_then_activate',
'cancelremotewipethenblock': 'cancel_remote_wipe_then_block',
}
def getMobileDeviceEntity():
cd = buildGAPIObject(API.DIRECTORY)
if checkArgumentPresent('query'):
query = getString(Cmd.OB_QUERY)
else:
resourceId = getString(Cmd.OB_MOBILE_DEVICE_ENTITY)
if resourceId[:6].lower() == 'query:':
query = resourceId[6:]
else:
Cmd.Backup()
query = None
if not query:
return ([{'resourceId': device, 'email': []} for device in getEntityList(Cmd.OB_MOBILE_ENTITY)], cd, True)
try:
printGettingAllAccountEntities(Ent.MOBILE_DEVICE, query)
devices = callGAPIpages(cd.mobiledevices(), 'list', 'mobiledevices',
pageMessage=getPageMessage(),
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], query=query,
fields='nextPageToken,mobiledevices(resourceId,email)')
except GAPI.invalidInput:
Cmd.Backup()
usageErrorExit(Msg.INVALID_QUERY)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
return ([{'resourceId': device['resourceId'], 'email': device.get('email', [])} for device in devices], cd, False)
def _getUpdateDeleteMobileOptions(myarg, options):
if myarg in {'matchusers', 'ifusers'}:
_, matchUsers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
options['matchUsers'] = {normalizeEmailAddressOrUID(user) for user in matchUsers}
elif myarg == 'doit':
options['doit'] = True
else:
unknownArgumentExit()
def _getMobileDeviceUser(mobileDevice, options):
if options['matchUsers']:
if mobileDevice['email']:
for deviceUser in mobileDevice['email']:
if deviceUser.lower() in options['matchUsers']:
return (deviceUser, True)
return (mobileDevice['email'][0], False)
return (UNKNOWN, False)
if mobileDevice['email']:
return (mobileDevice['email'][0], True)
return (UNKNOWN, True)
# gam update mobile|mobiles <MobileDeviceEntity> action <MobileAction>
# [doit] [matchusers <UserTypeEntity>]
def doUpdateMobileDevices():
entityList, cd, doit = getMobileDeviceEntity()
body = {}
options = {'doit': doit, 'matchUsers': set()}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'action':
body['action'] = getChoice(MOBILE_ACTION_CHOICE_MAP, mapChoice=True)
else:
_getUpdateDeleteMobileOptions(myarg, options)
if not body:
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, None], Msg.NO_ACTION_SPECIFIED)
return
i = 0
count = len(entityList)
for device in entityList:
i += 1
resourceId = device['resourceId']
deviceUser, status = _getMobileDeviceUser(device, options)
if not status:
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USER_NOT_IN_MATCHUSERS, i, count)
elif not options['doit']:
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
else:
try:
callGAPI(cd.mobiledevices(), 'action',
bailOnInternalError=True,
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.RESOURCE_ID_NOT_FOUND,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], resourceId=resourceId, body=body)
printEntityKVList([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser],
[Msg.ACTION_APPLIED, body['action']], i, count)
except GAPI.internalError:
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.resourceIdNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], str(e), i, count)
# gam delete mobile|mobiles <MobileDeviceEntity>
# [doit] [matchusers <UserTypeEntity>]
def doDeleteMobileDevices():
entityList, cd, doit = getMobileDeviceEntity()
options = {'doit': doit, 'matchUsers': set()}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
_getUpdateDeleteMobileOptions(myarg, options)
i = 0
count = len(entityList)
for device in entityList:
i += 1
resourceId = device['resourceId']
deviceUser, status = _getMobileDeviceUser(device, options)
if not status:
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USER_NOT_IN_MATCHUSERS, i, count)
elif not options['doit']:
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
else:
try:
callGAPI(cd.mobiledevices(), 'delete',
bailOnInternalError=True,
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.RESOURCE_ID_NOT_FOUND, GAPI.BAD_REQUEST,
GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], resourceId=resourceId)
entityActionPerformed([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], i, count)
except GAPI.internalError:
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.resourceIdNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], str(e), i, count)
MOBILE_FIELDS_CHOICE_MAP = {
'adbstatus': 'adbStatus',
'applications': 'applications',
'basebandversion': 'basebandVersion',
'bootloaderversion': 'bootloaderVersion',
'brand': 'brand',
'buildnumber': 'buildNumber',
'defaultlanguage': 'defaultLanguage',
'developeroptionsstatus': 'developerOptionsStatus',
'devicecompromisedstatus': 'deviceCompromisedStatus',
'deviceid': 'deviceId',
'devicepasswordstatus': 'devicePasswordStatus',
'email': 'email',
'encryptionstatus': 'encryptionStatus',
'firstsync': 'firstSync',
'hardware': 'hardware',
'hardwareid': 'hardwareId',
'imei': 'imei',
'kernelversion': 'kernelVersion',
'lastsync': 'lastSync',
'managedaccountisonownerprofile': 'managedAccountIsOnOwnerProfile',
'manufacturer': 'manufacturer',
'meid': 'meid',
'model': 'model',
'name': 'name',
'networkoperator': 'networkOperator',
'os': 'os',
'otheraccountsinfo': 'otherAccountsInfo',
'privilege': 'privilege',
'releaseversion': 'releaseVersion',
'resourceid': 'resourceId',
'securitypatchlevel': 'securityPatchLevel',
'serialnumber': 'serialNumber',
'status': 'status',
'supportsworkprofile': 'supportsWorkProfile',
'type': 'type',
'unknownsourcesstatus': 'unknownSourcesStatus',
'useragent': 'userAgent',
'wifimacaddress': 'wifiMacAddress',
}
MOBILE_TIME_OBJECTS = {'firstSync', 'lastSync'}
def _initMobileFieldsParameters():
return {'fieldsList': [], 'projection': None}
def _getMobileFieldsArguments(myarg, parameters):
if myarg == 'allfields':
parameters['projection'] = 'FULL'
parameters['fieldsList'] = []
elif myarg in PROJECTION_CHOICE_MAP:
parameters['projection'] = PROJECTION_CHOICE_MAP[myarg]
parameters['fieldsList'] = []
elif getFieldsList(myarg, MOBILE_FIELDS_CHOICE_MAP, parameters['fieldsList'], initialField='resourceId'):
pass
else:
return False
return True
# gam info mobile|mobiles <MobileDeviceEntity>
# [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>] [formatjson]
def doInfoMobileDevices():
entityList, cd, _ = getMobileDeviceEntity()
parameters = _initMobileFieldsParameters()
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getMobileFieldsArguments(myarg, parameters):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(parameters['fieldsList'])
i = 0
count = len(entityList)
for device in entityList:
i += 1
resourceId = device['resourceId']
try:
mobile = callGAPI(cd.mobiledevices(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.RESOURCE_ID_NOT_FOUND,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], resourceId=resourceId, projection=parameters['projection'], fields=fields)
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(mobile, timeObjects=MOBILE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
else:
printEntity([Ent.MOBILE_DEVICE, resourceId], i, count)
Ind.Increment()
attrib = 'deviceId'
if attrib in mobile:
mobile[attrib] = mobile[attrib].encode('unicode-escape').decode(UTF8)
attrib = 'securityPatchLevel'
if attrib in mobile and int(mobile[attrib]):
mobile[attrib] = formatLocalTimestamp(mobile[attrib])
showJSON(None, mobile, timeObjects=MOBILE_TIME_OBJECTS)
Ind.Decrement()
except GAPI.internalError:
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.resourceIdNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], str(e), i, count)
MOBILE_ORDERBY_CHOICE_MAP = {
'deviceid': 'deviceId',
'email': 'email',
'lastsync': 'lastSync',
'model': 'model',
'name': 'name',
'os': 'os',
'status': 'status',
'type': 'type',
}
# gam print mobile [todrive <ToDriveAttribute>*]
# [(query <QueryMobile>)|(queries <QueryMobileList>) [querytime<String> <Time>]]
# [orderby <MobileOrderByFieldName> [ascending|descending]]
# [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>]
# [delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintMobileDevices():
def _appDetails(app):
appDetails = []
for field in ['displayName', 'packageName', 'versionName']:
appDetails.append(app.get(field, '<None>'))
appDetails.append(str(app.get('versionCode', '<None>')))
permissions = app.get('permission', [])
if permissions:
appDetails.append('/'.join(permissions))
else:
appDetails.append('<None>')
return '-'.join(appDetails)
def _printMobile(mobile):
if FJQC.formatJSON:
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(mobile, listLimit=listLimit, timeObjects=MOBILE_TIME_OBJECTS)):
csvPF.WriteRowNoFilter({'resourceId': mobile['resourceId'],
'JSON': json.dumps(cleanJSON(mobile, listLimit=listLimit, timeObjects=MOBILE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
return
row = {}
for attrib in mobile:
if attrib in DEFAULT_SKIP_OBJECTS:
pass
elif attrib in {'name', 'email', 'otherAccountsInfo'}:
if listLimit > 0:
row[attrib] = delimiter.join(mobile[attrib][0:listLimit])
elif listLimit == 0:
row[attrib] = delimiter.join(mobile[attrib])
elif attrib == 'deviceId':
row[attrib] = mobile[attrib].encode('unicode-escape').decode(UTF8)
elif attrib in MOBILE_TIME_OBJECTS:
row[attrib] = formatLocalTime(mobile[attrib])
elif attrib == 'securityPatchLevel' and int(mobile[attrib]):
row[attrib] = formatLocalTimestamp(mobile[attrib])
elif attrib != 'applications':
row[attrib] = mobile[attrib]
attrib = 'applications'
if not oneAppPerRow or attrib not in mobile or appsLimit < 0:
if attrib in mobile and appsLimit >= 0:
applications = []
j = 0
for app in mobile[attrib]:
j += 1
if appsLimit and (j > appsLimit):
break
applications.append(_appDetails(app))
row[attrib] = delimiter.join(applications)
csvPF.WriteRowTitles(row)
else:
j = 0
for app in mobile[attrib]:
j += 1
if appsLimit and (j > appsLimit):
break
appRow = row.copy()
appRow[attrib] = _appDetails(app)
csvPF.WriteRowTitles(appRow)
cd = buildGAPIObject(API.DIRECTORY)
parameters = _initMobileFieldsParameters()
csvPF = CSVPrintFile('resourceId')
FJQC = FormatJSONQuoteChar(csvPF)
orderBy = sortOrder = None
oneAppPerRow = False
queryTimes = {}
queries = [None]
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
listLimit = 1
appsLimit = -1
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'query', 'queries'}:
queries = getQueries(myarg)
elif myarg.startswith('querytime'):
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
elif myarg == 'orderby':
orderBy, sortOrder = getOrderBySortOrder(MOBILE_ORDERBY_CHOICE_MAP)
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'listlimit':
listLimit = getInteger(minVal=-1)
elif myarg == 'appslimit':
appsLimit = getInteger(minVal=-1)
elif myarg == 'oneappperrow':
oneAppPerRow = True
elif _getMobileFieldsArguments(myarg, parameters):
pass
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not FJQC.formatJSON:
csvPF.SetSortTitles(['resourceId', 'deviceId', 'serialNumber', 'name', 'email', 'status'])
if appsLimit >= 0:
parameters['projection'] = 'FULL'
fields = getItemFieldsFromFieldsList('mobiledevices', parameters['fieldsList'])
substituteQueryTimes(queries, queryTimes)
itemCount = 0
for query in queries:
printGettingAllAccountEntities(Ent.MOBILE_DEVICE, query)
pageMessage = getPageMessage()
totalItems = 0
try:
feed = yieldGAPIpages(cd.mobiledevices(), 'list', 'mobiledevices',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], query=query, projection=parameters['projection'],
orderBy=orderBy, sortOrder=sortOrder, fields=fields, maxResults=GC.Values[GC.MOBILE_MAX_RESULTS])
for mobiles in feed:
totalItems += len(mobiles)
if showItemCountOnly:
itemCount += len(mobiles)
continue
for mobile in mobiles:
_printMobile(mobile)
printGotAccountEntities(totalItems)
except GAPI.invalidInput:
entityActionFailedWarning([Ent.MOBILE_DEVICE, None], invalidQuery(query))
return
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
csvPF.writeCSVfile('Mobile')
GROUP_DISCOVER_CHOICES = {
'allmemberscandiscover': 'ALL_MEMBERS_CAN_DISCOVER',
'allindomaincandiscover': 'ALL_IN_DOMAIN_CAN_DISCOVER',
'anyonecandiscover': 'ANYONE_CAN_DISCOVER',
}
GROUP_ASSIST_CONTENT_CHOICES = {
'allmembers': 'ALL_MEMBERS',
'ownersandmanagers': 'OWNERS_AND_MANAGERS',
'managersonly': 'MANAGERS_ONLY',
'ownersonly': 'OWNERS_ONLY',
'none': 'NONE',
}
GROUP_MODERATE_CONTENT_CHOICES = {
'allmembers': 'ALL_MEMBERS',
'ownersandmanagers': 'OWNERS_AND_MANAGERS',
'ownersonly': 'OWNERS_ONLY',
'none': 'NONE',
}
GROUP_MODERATE_MEMBERS_CHOICES = {
'allmembers': 'ALL_MEMBERS',
'ownersandmanagers': 'OWNERS_AND_MANAGERS',
'ownersonly': 'OWNERS_ONLY',
'none': 'NONE',
}
GROUP_DEPRECATED_ATTRIBUTES = {
'allowgooglecommunication': ['allowGoogleCommunication', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'favoriterepliesontop': ['favoriteRepliesOnTop', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'maxmessagebytes': ['maxMessageBytes', {GC.VAR_TYPE: GC.TYPE_INTEGER, GC.VAR_LIMITS: (ONE_KILO_BYTES, ONE_MEGA_BYTES)}],
'messagedisplayfont': ['messageDisplayFont', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'defaultfont': 'DEFAULT_FONT', 'fixedwidthfont': 'FIXED_WIDTH_FONT'}}],
'whocanaddreferences': ['whoCanAddReferences', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmarkfavoritereplyonowntopic': ['whoCanMarkFavoriteReplyOnOwnTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
}
GROUP_DISCOVER_ATTRIBUTES = {
'showingroupdirectory': ['showInGroupDirectory', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
}
GROUP_ASSIST_CONTENT_ATTRIBUTES = {
'whocanassigntopics': ['whoCanAssignTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanenterfreeformtags': ['whoCanEnterFreeFormTags', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanhideabuse': ['whoCanHideAbuse', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmaketopicssticky': ['whoCanMakeTopicsSticky', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmarkduplicate': ['whoCanMarkDuplicate', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmarkfavoritereplyonanytopic': ['whoCanMarkFavoriteReplyOnAnyTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmarknoresponseneeded': ['whoCanMarkNoResponseNeeded', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmodifytagsandcategories': ['whoCanModifyTagsAndCategories', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocantaketopics': ['whoCanTakeTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanunassigntopic': ['whoCanUnassignTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanunmarkfavoritereplyonanytopic': ['whoCanUnmarkFavoriteReplyOnAnyTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
}
GROUP_MODERATE_CONTENT_ATTRIBUTES = {
'whocanapprovemessages': ['whoCanApproveMessages', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocandeleteanypost': ['whoCanDeleteAnyPost', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocandeletetopics': ['whoCanDeleteTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocanlocktopics': ['whoCanLockTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocanmovetopicsin': ['whoCanMoveTopicsIn', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocanmovetopicsout': ['whoCanMoveTopicsOut', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocanpostannouncements': ['whoCanPostAnnouncements', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
}
GROUP_MODERATE_MEMBERS_ATTRIBUTES = {
'whocanadd': ['whoCanAdd', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'allmanagerscanadd': 'ALL_MANAGERS_CAN_ADD', 'allownerscanadd': 'ALL_OWNERS_CAN_ADD',
'allmemberscanadd': 'ALL_MEMBERS_CAN_ADD', 'nonecanadd': 'NONE_CAN_ADD'}}],
'whocanapprovemembers': ['whoCanApproveMembers', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'allownerscanapprove': 'ALL_OWNERS_CAN_APPROVE', 'allmanagerscanapprove': 'ALL_MANAGERS_CAN_APPROVE',
'allmemberscanapprove': 'ALL_MEMBERS_CAN_APPROVE', 'nonecanapprove': 'NONE_CAN_APPROVE'}}],
'whocanbanusers': ['whoCanBanUsers', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_MEMBERS_CHOICES}],
'whocaninvite': ['whoCanInvite', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'allmemberscaninvite': 'ALL_MEMBERS_CAN_INVITE', 'allmanagerscaninvite': 'ALL_MANAGERS_CAN_INVITE',
'allownerscaninvite': 'ALL_OWNERS_CAN_INVITE', 'nonecaninvite': 'NONE_CAN_INVITE'}}],
'whocanmodifymembers': ['whoCanModifyMembers', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_MEMBERS_CHOICES}],
}
GROUP_BASIC_ATTRIBUTES = {
'description': ['description', {GC.VAR_TYPE: GC.TYPE_STRING}],
'name': ['name', {GC.VAR_TYPE: GC.TYPE_STRING}],
'displayname': ['name', {GC.VAR_TYPE: GC.TYPE_STRING}],
}
GROUP_SETTINGS_ATTRIBUTES = {
'allowexternalmembers': ['allowExternalMembers', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'allowwebposting': ['allowWebPosting', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'archiveonly': ['archiveOnly', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'customfootertext': ['customFooterText', {GC.VAR_TYPE: GC.TYPE_STRING}],
'customreplyto': ['customReplyTo', {GC.VAR_TYPE: GC.TYPE_EMAIL_OPTIONAL}],
'customrolesenabledforsettingstobemerged': ['customRolesEnabledForSettingsToBeMerged', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'defaultmessagedenynotificationtext': ['defaultMessageDenyNotificationText', {GC.VAR_TYPE: GC.TYPE_STRING}],
'defaultsender': ['defaultSender', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'self': 'DEFAULT_SELF', 'defaultself': 'DEFAULT_SELF', 'group': 'GROUP'}}],
'enablecollaborativeinbox': ['enableCollaborativeInbox', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'includecustomfooter': ['includeCustomFooter', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'includeinglobaladdresslist': ['includeInGlobalAddressList', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'isarchived': ['isArchived', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'memberscanpostasthegroup': ['membersCanPostAsTheGroup', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'messagemoderationlevel': ['messageModerationLevel', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'moderateallmessages': 'MODERATE_ALL_MESSAGES', 'moderatenonmembers': 'MODERATE_NON_MEMBERS',
'moderatenewmembers': 'MODERATE_NEW_MEMBERS', 'moderatenone': 'MODERATE_NONE'}}],
'primarylanguage': ['primaryLanguage', {GC.VAR_TYPE: GC.TYPE_LANGUAGE}],
'replyto': ['replyTo', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'replytocustom': 'REPLY_TO_CUSTOM', 'replytosender': 'REPLY_TO_SENDER', 'replytolist': 'REPLY_TO_LIST',
'replytoowner': 'REPLY_TO_OWNER', 'replytoignore': 'REPLY_TO_IGNORE', 'replytomanagers': 'REPLY_TO_MANAGERS'}}],
'sendmessagedenynotification': ['sendMessageDenyNotification', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'spammoderationlevel': ['spamModerationLevel', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'allow': 'ALLOW', 'moderate': 'MODERATE', 'silentlymoderate': 'SILENTLY_MODERATE', 'reject': 'REJECT'}}],
'whocancontactowner': ['whoCanContactOwner', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'anyonecancontact': 'ANYONE_CAN_CONTACT', 'allindomaincancontact': 'ALL_IN_DOMAIN_CAN_CONTACT',
'allmemberscancontact': 'ALL_MEMBERS_CAN_CONTACT', 'allmanagerscancontact': 'ALL_MANAGERS_CAN_CONTACT',
'allownerscancontact': 'ALL_OWNERS_CAN_CONTACT'}}],
'whocanjoin': ['whoCanJoin', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'anyonecanjoin': 'ANYONE_CAN_JOIN', 'allindomaincanjoin': 'ALL_IN_DOMAIN_CAN_JOIN',
'invitedcanjoin': 'INVITED_CAN_JOIN', 'canrequesttojoin': 'CAN_REQUEST_TO_JOIN'}}],
'whocanleavegroup': ['whoCanLeaveGroup', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'allmanagerscanleave': 'ALL_MANAGERS_CAN_LEAVE', 'allownerscanleave': 'ALL_OWNERS_CAN_LEAVE',
'allmemberscanleave': 'ALL_MEMBERS_CAN_LEAVE', 'nonecanleave': 'NONE_CAN_LEAVE'}}],
'whocanpostmessage': ['whoCanPostMessage', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'nonecanpost': 'NONE_CAN_POST', 'allmanagerscanpost': 'ALL_MANAGERS_CAN_POST',
'allmemberscanpost': 'ALL_MEMBERS_CAN_POST', 'allownerscanpost': 'ALL_OWNERS_CAN_POST',
'allindomaincanpost': 'ALL_IN_DOMAIN_CAN_POST', 'anyonecanpost': 'ANYONE_CAN_POST'}}],
'whocanviewgroup': ['whoCanViewGroup', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'anyonecanview': 'ANYONE_CAN_VIEW', 'allindomaincanview': 'ALL_IN_DOMAIN_CAN_VIEW',
'allmemberscanview': 'ALL_MEMBERS_CAN_VIEW', 'allmanagerscanview': 'ALL_MANAGERS_CAN_VIEW',
'allownerscanview': 'ALL_OWNERS_CAN_VIEW'}}],
'whocanviewmembership': ['whoCanViewMembership', {GC.VAR_TYPE: GC.TYPE_CHOICE,
'choices': {'allindomaincanview': 'ALL_IN_DOMAIN_CAN_VIEW', 'allmemberscanview': 'ALL_MEMBERS_CAN_VIEW',
'allmanagerscanview': 'ALL_MANAGERS_CAN_VIEW', 'allownerscanview': 'ALL_OWNERS_CAN_VIEW'}}],
}
GROUP_ALIAS_ATTRIBUTES = {
'collaborative': ['enableCollaborativeInbox', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
'gal': ['includeInGlobalAddressList', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
}
GROUP_MERGED_ATTRIBUTES = {
'whocandiscovergroup': ['whoCanDiscoverGroup', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_DISCOVER_CHOICES}],
'whocanassistcontent': ['whoCanAssistContent', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
'whocanmoderatecontent': ['whoCanModerateContent', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
'whocanmoderatemembers': ['whoCanModerateMembers', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_MEMBERS_CHOICES}],
}
GROUP_MERGED_ATTRIBUTES_PRINT_ORDER = ['whoCanDiscoverGroup', 'whoCanAssistContent', 'whoCanModerateContent', 'whoCanModerateMembers']
GROUP_MERGED_TO_COMPONENT_MAP = {
'whoCanDiscoverGroup': GROUP_DISCOVER_ATTRIBUTES,
'whoCanAssistContent': GROUP_ASSIST_CONTENT_ATTRIBUTES,
'whoCanModerateContent': GROUP_MODERATE_CONTENT_ATTRIBUTES,
'whoCanModerateMembers': GROUP_MODERATE_MEMBERS_ATTRIBUTES,
}
GROUP_ATTRIBUTES_SET = set(list(GROUP_BASIC_ATTRIBUTES)+list(GROUP_SETTINGS_ATTRIBUTES)+list(GROUP_ALIAS_ATTRIBUTES)+
list(GROUP_ASSIST_CONTENT_ATTRIBUTES)+list(GROUP_MODERATE_CONTENT_ATTRIBUTES)+list(GROUP_MODERATE_MEMBERS_ATTRIBUTES)+
list(GROUP_MERGED_ATTRIBUTES)+list(GROUP_DEPRECATED_ATTRIBUTES))
GROUP_FIELDS_WITH_CRS_NLS = {'customFooterText', 'defaultMessageDenyNotificationText', 'description'}
def getGroupAttrProperties(myarg):
attrProperties = GROUP_BASIC_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_SETTINGS_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_ALIAS_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_DISCOVER_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_ASSIST_CONTENT_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_MODERATE_CONTENT_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_MODERATE_MEMBERS_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_MERGED_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
attrProperties = GROUP_DEPRECATED_ATTRIBUTES.get(myarg)
if attrProperties is not None:
return attrProperties
return None
def getGroupAttrValue(argument, gs_body):
if argument == 'copyfrom':
gs_body[argument] = getEmailAddress()
return
attrProperties = getGroupAttrProperties(argument)
if attrProperties is None:
unknownArgumentExit()
attrName = attrProperties[0]
attribute = attrProperties[1]
attrType = attribute[GC.VAR_TYPE]
if attrType == GC.TYPE_BOOLEAN:
gs_body[attrName] = str(getBoolean()).lower()
elif attrType == GC.TYPE_STRING:
if attrName in GROUP_FIELDS_WITH_CRS_NLS:
gs_body[attrName] = getStringWithCRsNLs()
else:
gs_body[attrName] = getString(Cmd.OB_STRING, minLen=0)
elif attrType == GC.TYPE_CHOICE:
gs_body[attrName] = getChoice(attribute['choices'], mapChoice=True)
elif attrType in [GC.TYPE_EMAIL, GC.TYPE_EMAIL_OPTIONAL]:
gs_body[attrName] = getEmailAddress(noUid=True, optional=attrType == GC.TYPE_EMAIL_OPTIONAL)
if attrType == GC.TYPE_EMAIL_OPTIONAL and gs_body[attrName] is None:
gs_body[attrName] = ''
elif attrType == GC.TYPE_LANGUAGE:
gs_body[attrName] = getLanguageCode(LANGUAGE_CODES_MAP)
else: # GC.TYPE_INTEGER
minVal, maxVal = attribute[GC.VAR_LIMITS]
if attrName == 'maxMessageBytes':
gs_body[attrName] = getMaxMessageBytes(minVal, maxVal)
else:
gs_body[attrName] = getInteger(minVal, maxVal)
def GroupIsAbuseOrPostmaster(emailAddr):
return emailAddr.startswith('abuse@') or emailAddr.startswith('postmaster@')
def mapGroupEmailForSettings(emailAddr):
return emailAddr.replace('/', '%2F')
def getSettingsFromGroup(cd, group, gs, gs_body):
if gs_body:
copySettingsFromGroup = gs_body.pop('copyfrom', None)
if copySettingsFromGroup:
try:
if copySettingsFromGroup.find('@') == -1: # group settings API won't take uid so we make sure cd API is used so that we can grab real email.
copySettingsFromGroup = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=copySettingsFromGroup, fields='email')['email']
settings = callGAPI(gs.groups(), 'get',
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(copySettingsFromGroup), fields='*')
if settings is not None:
for field in ['email', 'name', 'description']:
settings.pop(field, None)
settings.update(gs_body)
return settings
entityItemValueListActionNotPerformedWarning([Ent.GROUP, group], [Ent.COPYFROM_GROUP, copySettingsFromGroup], Msg.API_ERROR_SETTINGS)
return None
except GAPI.notFound:
entityItemValueListActionNotPerformedWarning([Ent.GROUP, group], [Ent.COPYFROM_GROUP, copySettingsFromGroup], Msg.DOES_NOT_EXIST)
return None
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.backendError, GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityItemValueListActionNotPerformedWarning([Ent.GROUP, group], [Ent.COPYFROM_GROUP, copySettingsFromGroup], str(e))
return None
return gs_body
def checkReplyToCustom(group, settings, i=0, count=0):
if settings.get('replyTo') != 'REPLY_TO_CUSTOM' or settings.get('customReplyTo', ''):
return True
entityActionNotPerformedWarning([Ent.GROUP, group], Msg.REPLY_TO_CUSTOM_REQUIRES_EMAIL_ADDRESS, i, count)
return False
GROUP_CIGROUP_ENTITYTYPE_MAP = {False: Ent.GROUP, True: Ent.CLOUD_IDENTITY_GROUP}
GROUP_CIGROUP_FIELDS_MAP = {'name': 'displayName', 'displayname': 'displayName', 'description': 'description'}
GROUP_JSON_SKIP_FIELDS = ['email', 'adminCreated', 'directMembersCount', 'members', 'aliases', 'nonEditableAliases']
GROUP_ACCESS_TYPE_CHOICE_MAP = {
'public': {
'whoCanJoin': 'ALL_IN_DOMAIN_CAN_JOIN',
'whoCanPostMessage': 'ALL_IN_DOMAIN_CAN_POST',
'whoCanViewGroup': 'ALL_IN_DOMAIN_CAN_VIEW',
'whoCanViewMembership': 'ALL_IN_DOMAIN_CAN_VIEW',
},
'team': {
'whoCanJoin': 'CAN_REQUEST_TO_JOIN',
'whoCanPostMessage': 'ALL_IN_DOMAIN_CAN_POST',
'whoCanViewGroup': 'ALL_IN_DOMAIN_CAN_VIEW',
'whoCanViewMembership': 'ALL_IN_DOMAIN_CAN_VIEW',
},
'announcementonly': {
'whoCanJoin': 'ALL_IN_DOMAIN_CAN_JOIN',
'whoCanPostMessage': 'ALL_MANAGERS_CAN_POST',
'whoCanViewGroup': 'ALL_IN_DOMAIN_CAN_VIEW',
'whoCanViewMembership': 'ALL_MANAGERS_CAN_VIEW',
},
'restricted': {
'whoCanJoin': 'CAN_REQUEST_TO_JOIN',
'whoCanPostMessage': 'ALL_MEMBERS_CAN_POST',
'whoCanViewGroup': 'ALL_MEMBERS_CAN_VIEW',
'whoCanViewMembership': 'ALL_MEMBERS_CAN_VIEW',
}
}
# gam create group <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>*
# [verifynotinvitable]
def doCreateGroup(ciGroupsAPI=False):
cd = buildGAPIObject(API.DIRECTORY)
verifyNotInvitable = getBeforeUpdate = False
groupEmail = getEmailAddress(noUid=True)
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
if not ciGroupsAPI:
ci = None
body = {'email': groupEmail}
else:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
initialGroupConfig = 'EMPTY'
setTrueCustomerId(cd)
parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}'
body = {'groupKey': {'id': groupEmail},
'parent': parent,
'labels': {CIGROUP_DISCUSSION_FORUM_LABEL: ''},
}
gs_body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'getbeforeupdate':
getBeforeUpdate = True
elif myarg == 'json':
gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS))
elif myarg == 'accesstype':
gs_body.update(getChoice(GROUP_ACCESS_TYPE_CHOICE_MAP, mapChoice=True))
elif ciGroupsAPI and myarg in ['alias', 'aliases']:
body.setdefault('additionalGroupKeys', [])
for alias in convertEntityToList(getString(Cmd.OB_CIGROUP_ALIAS_LIST), shlexSplit=True):
body['additionalGroupKeys'].append({'id': alias})
elif ciGroupsAPI and myarg == 'dynamic':
body.setdefault('dynamicGroupMetadata', {'queries': []})
body['dynamicGroupMetadata']['queries'].append({'resourceType': 'USER',
'query': getString(Cmd.OB_QUERY)})
elif ciGroupsAPI and myarg == 'makeowner':
initialGroupConfig = 'WITH_INITIAL_OWNER'
elif ciGroupsAPI and myarg in {'security', 'makesecuritygroup'}:
body['labels'][CIGROUP_SECURITY_LABEL] = ''
elif myarg == 'verifynotinvitable':
verifyNotInvitable = True
else:
getGroupAttrValue(myarg, gs_body)
if verifyNotInvitable:
isInvitableUser, _ = _getIsInvitableUser(None, groupEmail)
if isInvitableUser:
entityActionNotPerformedWarning([Ent.GROUP, groupEmail], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
return
if ciGroupsAPI:
for k, v in iter(GROUP_CIGROUP_FIELDS_MAP.items()):
if k in gs_body:
body[v] = gs_body.pop(k)
body.setdefault('displayName', groupEmail)
if gs_body:
gs_body.setdefault('name', body.get('displayName', groupEmail))
gs = buildGAPIObject(API.GROUPSSETTINGS)
gs_body = getSettingsFromGroup(cd, groupEmail, gs, gs_body)
if not gs_body or not checkReplyToCustom(groupEmail, gs_body):
return
if not getBeforeUpdate:
settings = gs_body
try:
if not ciGroupsAPI:
callGAPI(cd.groups(), 'insert',
throwReasons=GAPI.GROUP_CREATE_THROW_REASONS,
body=body, fields='')
else:
callGAPI(ci.groups(), 'create',
throwReasons=GAPI.CIGROUP_CREATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
initialGroupConfig=initialGroupConfig, body=body, fields='')
if gs_body and not GroupIsAbuseOrPostmaster(groupEmail):
if getBeforeUpdate:
settings = callGAPI(gs.groups(), 'get',
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND],
groupUniqueId=mapGroupEmailForSettings(groupEmail), fields='*')
settings.update(gs_body)
callGAPI(gs.groups(), 'update',
bailOnInvalidError='messageModerationLevel' in settings,
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
groupUniqueId=mapGroupEmailForSettings(groupEmail), body=settings, fields='')
entityActionPerformed([entityType, groupEmail])
except (GAPI.alreadyExists, GAPI.duplicate):
duplicateAliasGroupUserWarning(cd, [entityType, groupEmail])
except GAPI.notFound:
entityActionFailedWarning([entityType, groupEmail], Msg.DOES_NOT_EXIST)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.invalidArgument, GAPI.failedPrecondition,
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([entityType, groupEmail], str(e))
except GAPI.required:
entityActionFailedWarning([entityType, groupEmail], Msg.INVALID_JSON_SETTING)
# [addonly|removeonly]
def getSyncOperation():
return getChoice(['addonly', 'removeonly'], defaultChoice='addremove')
UPDATE_GROUP_SUBCMDS = ['add', 'create', 'delete', 'remove', 'clear', 'sync', 'update']
GROUP_PREVIEW_TITLES = ['group', 'email', 'role', 'action', 'message']
# gam update groups <GroupEntity> [email <EmailAddress>]
# [copyfrom <GroupItem>] <GroupAttribute>*
# [security|makesecuritygroup]
# [admincreated <Boolean>]
# [verifynotinvitable]
# gam update groups <GroupEntity> create [<GroupRole>]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [delivery <DeliverySetting>]
# [preview] [actioncsv]
# <UserTypeEntity>
# gam update groups <GroupEntity> delete|remove [<GroupRole>]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [preview] [actioncsv]
# <UserTypeEntity>
# gam update groups <GroupEntity> sync [<GroupRole>|ignorerole]
# [usersonly|groupsonly] [addonly|removeonly]
# [notsuspended|suspended] [notarchived|archived]
# [removedomainnostatusmembers]
# [delivery <DeliverySetting>] [preview] [actioncsv]
# (additionalmembers <EmailAddressEntity>)*
# <UserTypeEntity>
# gam update groups <GroupEntity> update [<GroupRole>]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [delivery <DeliverySetting>] [preview] [actioncsv]
# [createifnotfound]
# <UserTypeEntity>
# gam update groups <GroupEntity> clear [member] [manager] [owner]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [emailclearpattern|emailretainpattern <REMatchPattern>]
# [removedomainnostatusmembers]
# [preview] [actioncsv]
def doUpdateGroups():
def _getPreviewActionCSV():
preview = checkArgumentPresent('preview')
if checkArgumentPresent('actioncsv'):
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
else:
csvPF = None
return (preview, csvPF)
def _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count):
roleLower = role.lower()
if roleLower in GROUP_ROLES_MAP:
return (GROUP_ROLES_MAP[roleLower], groupMemberLists[origGroup][role])
entityActionNotPerformedWarning([entityType, group, Ent.ROLE, role], Msg.INVALID_ROLE.format(','.join(sorted(GROUP_ROLES_MAP))), i, count)
return (None, None)
def _getRoleGroupMemberType(defaultRole=Ent.ROLE_MEMBER, allowIgnoreRole=False):
if not allowIgnoreRole or not checkArgumentPresent(['ignorerole']):
role = getChoice(GROUP_ROLES_MAP, defaultChoice=defaultRole, mapChoice=True)
else:
role = Ent.ROLE_ALL
groupMemberType = getChoice({'usersonly': Ent.TYPE_USER, 'groupsonly': Ent.TYPE_GROUP}, defaultChoice='ALL', mapChoice=True)
return (role, groupMemberType)
def _getMemberEmailStatus(member):
if member['type'] == Ent.TYPE_CUSTOMER:
return (member['id'], member.get('status', 'UNKNOWN'))
email = member['email'].lower()
if not removeDomainNoStatusMembers or 'status' in member:
return (email, member.get('status', 'UNKNOWN'))
_, domain = splitEmailAddress(email)
if domain != GC.Values[GC.DOMAIN]:
return (email, 'UNKNOWN')
return (email, 'NONE')
def _executeBatch(dbatch, batchParms):
dbatch.execute()
if batchParms['wait'] > 0:
time.sleep(batchParms['wait'])
# Convert foo@googlemail.com to foo@gmail.com; eliminate periods in name for foo.bar@gmail.com
def _cleanConsumerAddress(emailAddress, mapCleanToOriginal):
atLoc = emailAddress.find('@')
if atLoc > 0:
if emailAddress[atLoc+1:] in {'gmail.com', 'googlemail.com'}:
cleanEmailAddress = emailAddress[:atLoc].replace('.', '')+'@gmail.com'
if cleanEmailAddress != emailAddress:
mapCleanToOriginal[cleanEmailAddress] = emailAddress
return cleanEmailAddress
return emailAddress
def _previewAction(group, members, role, jcount, action):
Ind.Increment()
j = 0
for member in members:
j += 1
entityActionPerformed([entityType, group, role, member], j, jcount)
Ind.Decrement()
if csvPF:
for member in members:
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.PerformedName(action), 'message': Act.PREVIEW})
def _showSuccess(group, member, role, delivery_settings, j, jcount, optMsg=None):
kvList = []
if role is not None and role != 'None':
kvList.append(f'{Ent.Singular(Ent.ROLE)}: {role}')
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
kvList.append(f'{Ent.Singular(Ent.DELIVERY)}: {delivery_settings}')
if optMsg:
kvList.append(optMsg)
entityActionPerformedMessage([entityType, group, Ent.MEMBER, member], ', '.join(kvList), j, jcount)
if csvPF:
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Performed(), 'message': Act.SUCCESS})
def _showFailure(group, member, role, errMsg, j, jcount):
entityActionFailedWarning([entityType, group, Ent.MEMBER, member, Ent.ROLE, role], errMsg, j, jcount)
if csvPF:
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Failed(), 'message': errMsg})
def _addMember(group, i, count, role, delivery_settings, member, j, jcount):
body = {'role': role}
if member.find('@') != -1:
body['email'] = member
else:
body['id'] = member
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
body['delivery_settings'] = delivery_settings
try:
callGAPI(cd.members(), 'insert',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.DUPLICATE, GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND,
GAPI.INVALID_MEMBER, GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED,
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, body=body, fields='')
_showSuccess(group, member, role, delivery_settings, j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(entityType, group, i, count)
except (GAPI.duplicate, GAPI.memberNotFound, GAPI.resourceNotFound,
GAPI.invalidMember, GAPI.cyclicMembershipsNotAllowed, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
_showFailure(group, member, role, str(e), j, jcount)
except GAPI.conflict:
_showSuccess(group, member, role, delivery_settings, j, jcount, Msg.ACTION_MAY_BE_DELAYED)
def _handleDuplicateAdd(group, i, count, role, delivery_settings, member, j, jcount):
try:
result = callGAPI(cd.members(), 'get',
throwReasons=[GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
groupKey=group, memberKey=member, fields='role')
_showFailure(group, member, role, Msg.DUPLICATE_ALREADY_A_ROLE.format(Ent.Singular(result['role'])), j, jcount)
return
except (GAPI.memberNotFound, GAPI.resourceNotFound):
pass
printEntityKVList([entityType, group, Ent.MEMBER, member], [Msg.MEMBERSHIP_IS_PENDING_WILL_DELETE_ADD_TO_ACCEPT], j, jcount)
try:
callGAPI(cd.members(), 'delete',
throwReasons=[GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
groupKey=group, memberKey=member)
except (GAPI.memberNotFound, GAPI.resourceNotFound):
_showFailure(group, member, role, Msg.DUPLICATE, j, jcount)
return
_addMember(group, i, count, role, delivery_settings, member, j, jcount)
_ADD_MEMBER_REASON_TO_MESSAGE_MAP = {GAPI.DUPLICATE: Msg.DUPLICATE,
GAPI.CONDITION_NOT_MET: Msg.DUPLICATE,
GAPI.MEMBER_NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.RESOURCE_NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.INVALID_MEMBER: Msg.INVALID_MEMBER,
GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED: Msg.WOULD_MAKE_MEMBERSHIP_CYCLE}
def _callbackAddGroupMembers(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], ri[RI_OPTION], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason in GAPI.MEMBERS_THROW_REASONS and reason != GAPI.SERVICE_NOT_AVAILABLE:
entityUnknownWarning(entityType, ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
elif reason == GAPI.CONFLICT:
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], ri[RI_OPTION], int(ri[RI_J]), int(ri[RI_JCOUNT]), Msg.ACTION_MAY_BE_DELAYED)
elif reason in [GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET]:
_handleDuplicateAdd(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
elif reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS:
errMsg = getHTTPError(_ADD_MEMBER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
_showFailure(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
if addBatchParms['adjust']:
addBatchParms['adjust'] = False
addBatchParms['wait'] += 0.25
writeStderr(f'{WARNING_PREFIX}{Msg.INTER_BATCH_WAIT_INCREASED.format(addBatchParms["wait"])}\n')
time.sleep(0.1)
_addMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
def _batchAddGroupMembers(group, i, count, addMembers, role, delivery_settings):
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
jcount = len(addMembers)
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
if jcount == 0:
return
if preview:
_previewAction(group, addMembers, role, jcount, Act.ADD)
return
if addBatchParms['size'] == 1 or jcount <= addBatchParms['size']:
Ind.Increment()
j = 0
for member in addMembers:
j += 1
_addMember(group, i, count, role, delivery_settings, member, j, jcount)
Ind.Decrement()
return
body = {'role': role}
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
body['delivery_settings'] = delivery_settings
svcargs = dict([('groupKey', group), ('body', body), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.members(), 'insert')
dbatch = cd.new_batch_http_request(callback=_callbackAddGroupMembers)
bcount = 0
Ind.Increment()
j = 0
for member in addMembers:
j += 1
svcparms = svcargs.copy()
if member.find('@') != -1:
svcparms['body']['email'] = member
svcparms['body'].pop('id', None)
else:
svcparms['body']['id'] = member
svcparms['body'].pop('email', None)
dbatch.add(method(**svcparms), request_id=batchRequestID(group, i, count, j, jcount, member, role, delivery_settings))
bcount += 1
if bcount >= addBatchParms['size']:
addBatchParms['adjust'] = True
_executeBatch(dbatch, addBatchParms)
dbatch = cd.new_batch_http_request(callback=_callbackAddGroupMembers)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
def _removeMember(group, i, count, role, member, j, jcount):
try:
callGAPI(cd.members(), 'delete',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER,
GAPI.RESOURCE_NOT_FOUND,
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, memberKey=member)
_showSuccess(group, member, role, DELIVERY_SETTINGS_UNDEFINED, j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(entityType, group, i, count)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.resourceNotFound, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
_showFailure(group, member, role, str(e), j, jcount)
except GAPI.conflict:
_showSuccess(group, member, role, DELIVERY_SETTINGS_UNDEFINED, j, jcount, Msg.ACTION_MAY_BE_DELAYED)
_REMOVE_MEMBER_REASON_TO_MESSAGE_MAP = {GAPI.MEMBER_NOT_FOUND: f'{Msg.NOT_A} {Ent.Singular(Ent.MEMBER)}',
GAPI.CONDITION_NOT_MET: f'{Msg.NOT_A} {Ent.Singular(Ent.MEMBER)}',
GAPI.INVALID_MEMBER: Msg.DOES_NOT_EXIST}
def _callbackRemoveGroupMembers(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], DELIVERY_SETTINGS_UNDEFINED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason in GAPI.MEMBERS_THROW_REASONS and reason != GAPI.SERVICE_NOT_AVAILABLE:
entityUnknownWarning(entityType, ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
elif reason == GAPI.CONFLICT:
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], DELIVERY_SETTINGS_UNDEFINED, int(ri[RI_J]), int(ri[RI_JCOUNT]), Msg.ACTION_MAY_BE_DELAYED)
elif reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS+[GAPI.RESOURCE_NOT_FOUND]:
errMsg = getHTTPError(_REMOVE_MEMBER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
_showFailure(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
if remBatchParms['adjust']:
remBatchParms['adjust'] = False
remBatchParms['wait'] += 0.25
writeStderr(f'{WARNING_PREFIX}{Msg.INTER_BATCH_WAIT_INCREASED.format(remBatchParms["wait"])}\n')
time.sleep(0.1)
_removeMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
def _batchRemoveGroupMembers(group, i, count, removeMembers, role, qualifier=''):
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
jcount = len(removeMembers)
if not qualifier:
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
else:
entityPerformActionNumItemsModifier([entityType, group], jcount, role, qualifier, i, count)
if jcount == 0:
return
if preview:
_previewAction(group, removeMembers, role, jcount, Act.REMOVE)
return
if remBatchParms['size'] == 1 or jcount <= remBatchParms['size']:
Ind.Increment()
j = 0
for member in removeMembers:
j += 1
_removeMember(group, i, count, role, member, j, jcount)
Ind.Decrement()
return
svcargs = dict([('groupKey', group), ('memberKey', None)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.members(), 'delete')
dbatch = cd.new_batch_http_request(callback=_callbackRemoveGroupMembers)
bcount = 0
Ind.Increment()
j = 0
for member in removeMembers:
j += 1
svcparms = svcargs.copy()
svcparms['memberKey'] = member
dbatch.add(method(**svcparms), request_id=batchRequestID(group, i, count, j, jcount, svcparms['memberKey'], role))
bcount += 1
if bcount >= remBatchParms['size']:
remBatchParms['adjust'] = True
_executeBatch(dbatch, remBatchParms)
dbatch = cd.new_batch_http_request(callback=_callbackRemoveGroupMembers)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
_UPDATE_MEMBER_REASON_TO_MESSAGE_MAP = {GAPI.MEMBER_NOT_FOUND: f'{Msg.NOT_A} {Ent.Singular(Ent.MEMBER)}',
GAPI.INVALID_MEMBER: Msg.DOES_NOT_EXIST,
GAPI.RESOURCE_NOT_FOUND: Msg.DOES_NOT_EXIST}
def _getUpdateBody(role, delivery_settings):
body = {}
if role is not None:
body['role'] = role
else:
if delivery_settings == DELIVERY_SETTINGS_UNDEFINED:
# Backwards compatability; if neither role or delivery is specified, role = MEMBER
role = Ent.ROLE_MEMBER
body['role'] = role
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
body['delivery_settings'] = delivery_settings
return (body, role)
def _updateMember(group, i, count, role, delivery_settings, member, j, jcount):
body, role = _getUpdateBody(role, delivery_settings)
try:
callGAPI(cd.members(), 'patch',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.RESOURCE_NOT_FOUND],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, memberKey=member, body=body, fields='')
_showSuccess(group, member, role, delivery_settings, j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(entityType, group, i, count)
except GAPI.memberNotFound as e:
if createIfNotFound:
Act.Set(Act.ADD)
_addMember(group, i, count, role, delivery_settings, member, j, jcount)
Act.Set(Act.UPDATE)
else:
_showFailure(group, member, role, str(e), j, jcount)
except (GAPI.invalidMember, GAPI.resourceNotFound, GAPI.serviceNotAvailable) as e:
_showFailure(group, member, role, str(e), j, jcount)
def _callbackUpdateGroupMembers(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], ri[RI_OPTION], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason == GAPI.MEMBER_NOT_FOUND and createIfNotFound:
Act.Set(Act.ADD)
_addMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
Act.Set(Act.UPDATE)
elif reason in GAPI.MEMBERS_THROW_REASONS and reason != GAPI.SERVICE_NOT_AVAILABLE:
entityUnknownWarning(entityType, ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
elif reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS:
errMsg = getHTTPError(_UPDATE_MEMBER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
_showFailure(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
if addBatchParms['adjust']:
addBatchParms['adjust'] = False
addBatchParms['wait'] += 0.25
writeStderr(f'{WARNING_PREFIX}{Msg.INTER_BATCH_WAIT_INCREASED.format(addBatchParms["wait"])}\n')
time.sleep(0.1)
_updateMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
def _batchUpdateGroupMembers(group, i, count, updateMembers, role, delivery_settings):
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][preview])
jcount = len(updateMembers)
entityPerformActionNumItems([entityType, group], jcount, Ent.MEMBER, i, count)
if jcount == 0:
return
if preview:
_previewAction(group, updateMembers, role or Ent.ROLE_USER, jcount, Act.UPDATE)
return
if updBatchParms['size'] == 1 or jcount <= updBatchParms['size']:
Ind.Increment()
j = 0
for member in updateMembers:
j += 1
_updateMember(group, i, count, role, delivery_settings, member, j, jcount)
Ind.Decrement()
return
body, role = _getUpdateBody(role, delivery_settings)
svcargs = dict([('groupKey', group), ('memberKey', None), ('body', body), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.members(), 'patch')
dbatch = cd.new_batch_http_request(callback=_callbackUpdateGroupMembers)
bcount = 0
Ind.Increment()
j = 0
for member in updateMembers:
j += 1
svcparms = svcargs.copy()
svcparms['memberKey'] = member
dbatch.add(method(**svcparms), request_id=batchRequestID(group, i, count, j, jcount, svcparms['memberKey'], role, delivery_settings))
bcount += 1
if bcount >= updBatchParms['size']:
updBatchParms['adjust'] = True
_executeBatch(dbatch, updBatchParms)
dbatch = cd.new_batch_http_request(callback=_callbackUpdateGroupMembers)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
ci = None
entityType = Ent.GROUP
csvPF = None
getBeforeUpdate = preview = False
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
CL_subCommand = getChoice(UPDATE_GROUP_SUBCMDS, defaultChoice=None)
addBatchParms = {'size': GC.Values[GC.BATCH_SIZE], 'wait': GC.Values[GC.INTER_BATCH_WAIT], 'adjust': True}
remBatchParms = {'size': GC.Values[GC.BATCH_SIZE], 'wait': GC.Values[GC.INTER_BATCH_WAIT], 'adjust': True}
updBatchParms = {'size': GC.Values[GC.BATCH_SIZE], 'wait': GC.Values[GC.INTER_BATCH_WAIT], 'adjust': True}
if not CL_subCommand:
body = {}
gs_body = {}
ci_body = {}
verifyNotInvitable = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'email':
body['email'] = getEmailAddress(noUid=True)
elif myarg == 'admincreated':
body['adminCreated'] = getBoolean()
elif myarg == 'getbeforeupdate':
getBeforeUpdate = True
elif myarg in {'security', 'makesecuritygroup'}:
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
CIGROUP_SECURITY_LABEL: ''}
elif myarg == 'json':
gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS))
elif myarg == 'accesstype':
gs_body.update(getChoice(GROUP_ACCESS_TYPE_CHOICE_MAP, mapChoice=True))
elif myarg == 'verifynotinvitable':
verifyNotInvitable = True
else:
getGroupAttrValue(myarg, gs_body)
if 'email' in body and verifyNotInvitable:
isInvitableUser, _ = _getIsInvitableUser(None, body['email'])
if isInvitableUser:
entityActionNotPerformedWarning([Ent.GROUP, body['email']], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
return
if ci_body:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
if 'email' in body:
ci_body['groupKey'] = {'id': body.pop('email')}
if gs_body:
gs = buildGAPIObject(API.GROUPSSETTINGS)
gs_body = getSettingsFromGroup(cd, ','.join(entityList), gs, gs_body)
if ci_body:
for k, v in iter(GROUP_CIGROUP_FIELDS_MAP.items()):
if k in gs_body:
ci_body[v] = gs_body.pop(k)
if gs_body:
if not getBeforeUpdate:
settings = gs_body
elif not ci_body:
return
elif not body and not ci_body:
return
Act.Set(Act.UPDATE)
i = 0
count = len(entityList)
for group in entityList:
i += 1
ci, _, group = convertGroupCloudIDToEmail(ci, group, i, count)
if gs_body and not GroupIsAbuseOrPostmaster(group):
try:
if group.find('@') == -1: # group settings API won't take uid so we make sure cd API is used so that we can grab real email.
group = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=group, fields='email')['email']
if getBeforeUpdate:
settings = callGAPI(gs.groups(), 'get',
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(group), fields='*')
settings.update(gs_body)
if not checkReplyToCustom(group, settings, i, count):
continue
except GAPI.notFound:
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
continue
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.backendError, GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([entityType, group], str(e), i, count)
continue
if body:
try:
group = callGAPI(cd.groups(), 'update',
throwReasons=GAPI.GROUP_UPDATE_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=group, body=body, fields='email')['email']
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput,
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.forbidden) as e:
entityActionFailedWarning([entityType, group], str(e), i, count)
continue
if gs_body and not GroupIsAbuseOrPostmaster(group):
try:
callGAPI(gs.groups(), 'update',
bailOnInvalidError='messageModerationLevel' in settings,
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(group), body=settings, fields='')
except GAPI.notFound:
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
continue
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([entityType, group], str(e), i, count)
continue
except GAPI.required:
entityActionFailedWarning([entityType, group], Msg.INVALID_JSON_SETTING, i, count)
continue
if ci_body:
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
if not name:
continue
try:
callGAPI(ci.groups(), 'patch',
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
continue
entityActionPerformed([entityType, group], i, count)
elif CL_subCommand in {'create', 'add'}:
baseRole, groupMemberType = _getRoleGroupMemberType()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
delivery_settings = getDeliverySettings()
preview, csvPF = _getPreviewActionCSV()
_, addMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = addMembers if isinstance(addMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
roleList = [baseRole]
if groupMemberLists:
if not subkeyRoleField:
addMembers = groupMemberLists[group]
else:
roleList = groupMemberLists[group]
origGroup = group
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
if not group:
continue
for role in roleList:
if groupMemberLists and subkeyRoleField:
role, addMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
if role is None:
continue
_batchAddGroupMembers(group, i, count,
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True) for member in addMembers],
role, delivery_settings)
elif CL_subCommand in {'delete', 'remove'}:
baseRole, groupMemberType = _getRoleGroupMemberType()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
preview, csvPF = _getPreviewActionCSV()
_, removeMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = removeMembers if isinstance(removeMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
roleList = [baseRole]
if groupMemberLists:
if not subkeyRoleField:
removeMembers = groupMemberLists[group]
else:
roleList = groupMemberLists[group]
origGroup = group
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
if not group:
continue
for role in roleList:
if groupMemberLists and subkeyRoleField:
role, removeMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
if role is None:
continue
_batchRemoveGroupMembers(group, i, count,
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True) for member in removeMembers],
role)
elif CL_subCommand == 'sync':
baseRole, groupMemberType = _getRoleGroupMemberType(allowIgnoreRole=True)
ignoreRole = baseRole == Ent.ROLE_ALL
syncOperation = getSyncOperation()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
removeDomainNoStatusMembers = checkArgumentPresent('removedomainnostatusmembers')
delivery_settings = getDeliverySettings()
preview, csvPF = _getPreviewActionCSV()
additionalMembers = {}
while checkArgumentPresent('additionalmembers'):
additionalRole = getChoice(GROUP_ROLES_MAP, defaultChoice=baseRole, mapChoice=True)
additionalMembers.setdefault(additionalRole, [])
additionalMembers[additionalRole].extend(getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY))
_, syncMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = syncMembers if isinstance(syncMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
syncMembersSets = {}
syncMembersMaps = {}
currentMembersSets = {}
currentMembersMaps = {}
domainNoStatusMembersSets = {}
if groupMemberLists is None:
syncMembersSets[baseRole] = set()
syncMembersMaps[baseRole] = {}
for member in syncMembers:
syncMembersSets[baseRole].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True), syncMembersMaps[baseRole]))
for member in additionalMembers.get(baseRole, []):
syncMembersSets[baseRole].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True), syncMembersMaps[baseRole]))
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
origGroup = group
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
if not group:
continue
if groupMemberLists is None:
roleList = [baseRole]
else:
if not subkeyRoleField:
roleList = [baseRole]
else:
roleList = groupMemberLists[origGroup]
for role in roleList:
role = role.upper()
syncMembersSets[role] = set()
syncMembersMaps[role] = {}
rolesSet = set()
for role in roleList:
origRole = role
role = role.upper()
if groupMemberLists is None:
rolesSet.add(role)
else:
if not subkeyRoleField:
rolesSet.add(role)
syncMembers = groupMemberLists[origGroup]
else:
role, syncMembers = _validateSubkeyRoleGetMembers(group, origRole, origGroup, groupMemberLists, i, count)
if role is None:
continue
rolesSet.add(role)
for member in syncMembers:
syncMembersSets[role].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True), syncMembersMaps[role]))
for member in additionalMembers.get(role, []):
syncMembersSets[role].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True), syncMembersMaps[role]))
if not rolesSet:
continue
memberRoles = ','.join(sorted(rolesSet))
printGettingAllEntityItemsForWhom(memberRoles, group, entityType=entityType)
try:
result = callGAPIpages(cd.members(), 'list', 'members',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group,
roles=None if Ent.ROLE_MEMBER in rolesSet or ignoreRole else memberRoles,
fields='nextPageToken,members(email,id,type,status,role)',
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
entityUnknownWarning(entityType, group, i, count)
continue
for role in rolesSet:
currentMembersSets[role] = set()
currentMembersMaps[role] = {}
domainNoStatusMembersSets[role] = set()
for member in result:
role = member.get('role', Ent.ROLE_MEMBER) if not ignoreRole else Ent.ROLE_ALL
email, memberStatus = _getMemberEmailStatus(member)
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
if not removeDomainNoStatusMembers or memberStatus != 'NONE':
if _checkMemberStatusIsSuspendedIsArchived(memberStatus, isSuspended, isArchived):
currentMembersSets[role].add(_cleanConsumerAddress(email, currentMembersMaps[role]))
else:
domainNoStatusMembersSets[role].add(member['id'])
del result
if syncOperation != 'addonly':
for role in rolesSet:
if domainNoStatusMembersSets[role]:
_batchRemoveGroupMembers(group, i, count,
domainNoStatusMembersSets[role],
role)
_batchRemoveGroupMembers(group, i, count,
[currentMembersMaps[role].get(emailAddress, emailAddress) for emailAddress in currentMembersSets[role]-syncMembersSets[role]],
role)
if syncOperation != 'removeonly':
for role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER, Ent.ROLE_ALL]:
if role in rolesSet:
_batchAddGroupMembers(group, i, count,
[syncMembersMaps[role].get(emailAddress, emailAddress) for emailAddress in syncMembersSets[role]-currentMembersSets[role]],
role if role != Ent.ROLE_ALL else Ent.ROLE_MEMBER, delivery_settings)
elif CL_subCommand == 'update':
baseRole, groupMemberType = _getRoleGroupMemberType(defaultRole=None)
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
delivery_settings = getDeliverySettings()
preview, csvPF = _getPreviewActionCSV()
createIfNotFound = checkArgumentPresent('createifnotfound')
_, updateMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = updateMembers if isinstance(updateMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
roleList = [baseRole]
if groupMemberLists:
if not subkeyRoleField:
updateMembers = groupMemberLists[group]
else:
roleList = groupMemberLists[group]
origGroup = group
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
if not group:
continue
for role in roleList:
if groupMemberLists and subkeyRoleField:
role, updateMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
if role is None:
continue
_batchUpdateGroupMembers(group, i, count,
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any', checkForCustomerId=True) for member in updateMembers],
role, delivery_settings)
else: #clear
rolesSet = set()
groupMemberType = 'ALL'
isSuspended = isArchived = None
removeDomainNoStatusMembers = False
emailMatchPattern = None
clearMatch = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[myarg])
elif myarg == 'usersonly':
groupMemberType = Ent.TYPE_USER
elif myarg == 'groupsonly':
groupMemberType = Ent.TYPE_GROUP
elif myarg in SUSPENDED_ARGUMENTS:
isSuspended = _getIsSuspended(myarg)
elif myarg in ARCHIVED_ARGUMENTS:
isArchived = _getIsArchived(myarg)
elif myarg == 'removedomainnostatusmembers':
removeDomainNoStatusMembers = True
elif myarg in {'emailclearpattern', 'emailretainpattern'}:
emailMatchPattern = getREPattern(re.IGNORECASE)
clearMatch = myarg == 'emailclearpattern'
elif myarg == 'preview':
preview = True
elif myarg == 'actioncsv':
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
else:
unknownArgumentExit()
if isSuspended is not None and isArchived is not None:
if isSuspended == isArchived:
qualifier = '(Suspended) (Archived)' if isSuspended else '(Non-Suspended) (Non-Archived)'
else:
qualifier = '(Suspended)' if isSuspended else '(Archived)'
elif isSuspended is not None:
qualifier = '(Suspended)' if isSuspended else '(Non-Suspended)'
elif isArchived is not None:
qualifier = '(Archived)' if isArchived else '(Non-Archived)'
else:
qualifier = ''
Act.Set(Act.REMOVE)
if not rolesSet:
rolesSet.add(Ent.ROLE_MEMBER)
memberRoles = ','.join(sorted(rolesSet))
i = 0
count = len(entityList)
for group in entityList:
i += 1
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
if not group:
continue
printGettingAllEntityItemsForWhom(memberRoles, group, entityType=entityType)
try:
result = callGAPIpages(cd.members(), 'list', 'members',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, roles=None if Ent.ROLE_MEMBER in rolesSet else memberRoles,
fields='nextPageToken,members(email,id,type,status,role)',
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
entityUnknownWarning(entityType, group, i, count)
continue
removeMembers = {}
for role in rolesSet:
removeMembers[role] = set()
for member in result:
role = member.get('role', Ent.ROLE_MEMBER)
email, memberStatus = _getMemberEmailStatus(member)
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
if not removeDomainNoStatusMembers:
if _checkMemberStatusIsSuspendedIsArchived(memberStatus, isSuspended, isArchived):
if emailMatchPattern is None:
removeMembers[role].add(email if memberStatus != 'UNKNOWN' else member['id'])
elif member['type'] == Ent.TYPE_CUSTOMER:
pass
elif emailMatchPattern.match(email):
if clearMatch:
removeMembers[role].add(email if memberStatus != 'UNKNOWN' else member['id'])
else:
if not clearMatch:
removeMembers[role].add(email if memberStatus != 'UNKNOWN' else member['id'])
elif memberStatus == 'NONE':
removeMembers[role].add(member['id'])
del result
for role in rolesSet:
_batchRemoveGroupMembers(group, i, count, removeMembers[role], role, qualifier)
if csvPF:
csvPF.writeCSVfile('Group Updates')
def verifyGroupPrimaryEmail(cd, group, i, count):
try:
result = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
groupKey=group, fields='id,email')
if (result['email'].lower() == group) or (result['id'] == group):
return True
entityActionNotPerformedWarning([Ent.GROUP, group], Msg.NOT_A_PRIMARY_EMAIL_ADDRESS, i, count)
return False
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
pass
entityUnknownWarning(Ent.GROUP, group, i, count)
return False
# gam delete groups <GroupEntity> [noactionifalias]
def doDeleteGroups(ciGroupsAPI=False):
if not ciGroupsAPI:
cd = buildGAPIObject(API.DIRECTORY)
ci = None
else:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
if not ciGroupsAPI:
noActionIfAlias = checkArgumentPresent('noactionifalias')
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
try:
if not ciGroupsAPI:
ci, _, groupKey = convertGroupCloudIDToEmail(ci, group, i, count)
if noActionIfAlias and not verifyGroupPrimaryEmail(cd, groupKey, i, count):
continue
callGAPI(cd.groups(), 'delete',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
groupKey=groupKey)
else:
_, groupKey, group = convertGroupEmailToCloudID(ci, group, i, count)
if not groupKey:
continue
callGAPI(ci.groups(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
name=groupKey)
entityActionPerformed([entityType, groupKey], i, count)
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.invalid):
entityUnknownWarning(entityType, groupKey, i, count)
def getGroupRoles(myarg, rolesSet):
if myarg in {'role', 'roles'}:
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
if role in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[role])
else:
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
elif myarg in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[myarg])
else:
return False
return True
GROUP_MEMBER_TYPES_MAP = {
'customer': Ent.TYPE_CUSTOMER,
'group': Ent.TYPE_GROUP,
'user': Ent.TYPE_USER,
}
ALL_GROUP_MEMBER_TYPES = {Ent.TYPE_CUSTOMER, Ent.TYPE_GROUP, Ent.TYPE_USER}
def getGroupMemberTypes(myarg, typesSet):
if myarg in {'type', 'types'}:
for gtype in getString(Cmd.OB_GROUP_TYPE_LIST).lower().replace(',', ' ').split():
if gtype in GROUP_MEMBER_TYPES_MAP:
typesSet.add(GROUP_MEMBER_TYPES_MAP[gtype])
else:
invalidChoiceExit(gtype, GROUP_MEMBER_TYPES_MAP, True)
else:
return False
return True
MEMBEROPTION_MEMBERNAMES = 0
MEMBEROPTION_NODUPLICATES = 1
MEMBEROPTION_RECURSIVE = 2
MEMBEROPTION_GETDELIVERYSETTINGS = 3
MEMBEROPTION_ISARCHIVED = 4
MEMBEROPTION_ISSUSPENDED = 5
MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP = 6
MEMBEROPTION_MATCHPATTERN = 7
MEMBEROPTION_DISPLAYMATCH = 8
def initMemberOptions():
return [False, False, False, False, None, None, False, None, True]
def getMemberMatchOptions(myarg, memberOptions):
if myarg in {'memberemaildisplaypattern', 'memberemailskippattern'}:
memberOptions[MEMBEROPTION_MATCHPATTERN] = getREPattern(re.IGNORECASE)
memberOptions[MEMBEROPTION_DISPLAYMATCH] = myarg == 'memberemaildisplaypattern'
else:
return False
return True
def checkMemberMatch(member, memberOptions):
if not memberOptions[MEMBEROPTION_MATCHPATTERN]:
return True
if member['type'] != Ent.TYPE_CUSTOMER:
if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member['email']):
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member['id']):
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
def checkCIMemberMatch(member, memberOptions):
if not memberOptions[MEMBEROPTION_MATCHPATTERN]:
return True
if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member.get('preferredMemberKey', {}).get('id', '')):
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
def initIPSGMGroupMemberDisplayOptions():
return {Ent.ROLE_MEMBER: {'show': False},
Ent.ROLE_MANAGER: {'show': False},
Ent.ROLE_OWNER: {'show': False},
'categories': [],
'internal': False, 'external': False, 'showCategory': False,
'internalDomains': set([GC.Values[GC.DOMAIN]])}
def getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
def setMemberDisplayOptionsRole():
for role in rolesSet:
memberDisplayOptions[role]['show'] = True
if myarg in {'role', 'roles'}:
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
if role in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[role])
else:
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
setMemberDisplayOptionsRole()
elif myarg in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[myarg])
setMemberDisplayOptionsRole()
elif myarg == 'members':
role = Ent.ROLE_MEMBER
rolesSet.add(role)
memberDisplayOptions[role]['show'] = True
elif myarg == 'managers':
role = Ent.ROLE_MANAGER
rolesSet.add(role)
memberDisplayOptions[role]['show'] = True
elif myarg == 'owners':
role = Ent.ROLE_OWNER
rolesSet.add(role)
memberDisplayOptions[role]['show'] = True
elif myarg == 'internal':
memberDisplayOptions['internal'] = memberDisplayOptions['showCategory'] = True
elif myarg == 'external':
memberDisplayOptions['external'] = memberDisplayOptions['showCategory'] = True
elif myarg == 'internaldomains':
memberDisplayOptions['internalDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').lower().split())
else:
return False
return True
def initPGGroupMemberDisplayOptions():
return {Ent.ROLE_MEMBER: {'show': False, 'countOnly': False},
Ent.ROLE_MANAGER: {'show': False, 'countOnly': False},
Ent.ROLE_OWNER: {'show': False, 'countOnly': False},
'totalCount': False, 'categories': [],
'internal': False, 'external': False, 'showCategory': False,
'internalDomains': set([GC.Values[GC.DOMAIN]])}
def getPGGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
if myarg == 'memberscount':
role = Ent.ROLE_MEMBER
rolesSet.add(role)
memberDisplayOptions[role]['show'] = True
memberDisplayOptions[role]['countOnly'] = True
elif myarg == 'managerscount':
role = Ent.ROLE_MANAGER
rolesSet.add(role)
memberDisplayOptions[role]['show'] = True
memberDisplayOptions[role]['countOnly'] = True
elif myarg == 'ownerscount':
role = Ent.ROLE_OWNER
rolesSet.add(role)
memberDisplayOptions[role]['show'] = True
memberDisplayOptions[role]['countOnly'] = True
elif myarg == 'totalcount':
memberDisplayOptions['totalCount'] = True
elif myarg == 'countsonly':
for role in Ent.ROLE_LIST:
memberDisplayOptions[role]['countOnly'] = True
else:
return getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions)
return True
GROUP_FIELDS_CHOICE_MAP = {
'admincreated': 'adminCreated',
'aliases': ['aliases', 'nonEditableAliases'],
'description': 'description',
'directmemberscount': 'directMembersCount',
'email': 'email',
'id': 'id',
'name': 'name',
}
GROUP_INFO_PRINT_ORDER = ['id', 'name', 'description', 'directMembersCount', 'adminCreated']
INFO_GROUP_OPTIONS = {'nousers', 'groups'}
CIGROUP_FIELDS_CHOICE_MAP = {
'additionalgroupkeys': 'additionalGroupKeys',
'createtime': 'createTime',
'description': 'description',
'displayname': 'displayName',
'dynamicgroupmetadata': 'dynamicGroupMetadata',
'email': 'groupKey',
'groupkey': 'groupKey',
'id': 'name',
'labels': 'labels',
'name': 'displayName',
'parent': 'parent',
'updatetime': 'updateTime',
}
CIGROUP_FULL_FIELDS = {'additionalGroupKeys', 'createTime', 'dynamicGroupMetadata', 'parent', 'updateTime'}
CIGROUP_FIELDS_WITH_CRS_NLS = {'description'}
CIGROUP_INFO_ORDER = ['id', 'name', 'description', 'createTime', 'updateTime',
'groupKey', 'additionalGroupKeys', 'labels', 'parent', 'dynamicGroupMetadata',
'SecuritySettings']
CIGROUP_PRINT_ORDER = ['id', 'name', 'description', 'createTime', 'updateTime',
'groupKey', 'additionalGroupKeys', 'parent', 'dynamicGroupMetadata',
'memberRestrictionQuery', 'memberRestrictionEvaluation']
CIGROUP_TIME_OBJECTS = {'createTime', 'updateTime', 'statusTime'}
def mapCIGroupFieldNames(group):
if 'groupKey' in group:
group['email'] = group['groupKey'].pop('id')
if not group['groupKey']:
group.pop('groupKey')
if 'name' in group:
group['id'] = group.pop('name')
if 'displayName' in group:
group['name'] = group.pop('displayName')
def _showCIGroup(group, groupEmail, i=0, count=0):
printEntity([Ent.CLOUD_IDENTITY_GROUP, groupEmail], i, count)
mapCIGroupFieldNames(group)
Ind.Increment()
for key in CIGROUP_INFO_ORDER:
if key not in group:
continue
value = group[key]
if key == 'labels':
for k, v in iter(value.items()):
if v == '':
value[k] = True
if isinstance(value, (list, dict)):
showJSON(key, value, timeObjects=CIGROUP_TIME_OBJECTS)
elif key not in CIGROUP_FIELDS_WITH_CRS_NLS:
if key not in CIGROUP_TIME_OBJECTS:
printKeyValueList([key, value])
else:
printKeyValueList([key, formatLocalTime(value)])
else:
printKeyValueWithCRsNLs(key, value)
Ind.Decrement()
def infoGroups(entityList):
def initGroupFieldsLists():
if not groupFieldsLists['cd']:
groupFieldsLists['cd'] = ['email']
if not groupFieldsLists['ci']:
groupFieldsLists['ci'] = []
if not groupFieldsLists['gs']:
groupFieldsLists['gs'] = []
cd = buildGAPIObject(API.DIRECTORY)
ci = None
getAliases = getUsers = True
getCloudIdentity = getGroups = getSettings = False
showDeprecatedAttributes = True
FJQC = FormatJSONQuoteChar()
groups = []
members = []
groupFieldsLists = {'cd': None, 'ci': None, 'gs': None}
isSuspended = isArchived = None
rolesSet = set()
typesSet = set()
memberOptions = initMemberOptions()
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'quick':
getAliases = getUsers = False
elif myarg == 'nousers':
getUsers = False
elif myarg == 'nodeprecated':
showDeprecatedAttributes = not getBoolean()
elif myarg in SUSPENDED_ARGUMENTS:
isSuspended = _getIsSuspended(myarg)
elif myarg in ARCHIVED_ARGUMENTS:
isArchived = _getIsArchived(myarg)
elif myarg == 'noaliases':
getAliases = False
elif myarg == 'groups':
getGroups = True
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
getUsers = True
elif getGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif myarg == 'basic':
initGroupFieldsLists()
for field in GROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
elif myarg in {'ciallfields', 'allcifields'}:
if not groupFieldsLists['ci']:
groupFieldsLists['ci'] = []
for field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
elif myarg in GROUP_FIELDS_CHOICE_MAP:
initGroupFieldsLists()
addFieldToFieldsList(myarg, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
elif myarg in GROUP_ATTRIBUTES_SET:
initGroupFieldsLists()
attrProperties = getGroupAttrProperties(myarg)
groupFieldsLists['gs'].extend([attrProperties[0]])
elif myarg == 'fields':
initGroupFieldsLists()
for field in _getFieldsList():
if field in GROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
else:
attrProperties = getGroupAttrProperties(field)
if attrProperties is None:
invalidChoiceExit(field, list(GROUP_FIELDS_CHOICE_MAP)+list(GROUP_ATTRIBUTES_SET), True)
groupFieldsLists['gs'].extend([attrProperties[0]])
elif myarg == 'cifields':
initGroupFieldsLists()
for field in _getFieldsList():
if field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
else:
invalidChoiceExit(field, list(CIGROUP_FIELDS_CHOICE_MAP), True)
# Ignore info user arguments that may have come from whatis
elif myarg in INFO_USER_OPTIONS:
if myarg == 'schemas':
getString(Cmd.OB_SCHEMA_NAME_LIST)
else:
FJQC.GetFormatJSON(myarg)
if isSuspended is not None and isArchived is not None:
if isSuspended == isArchived:
entityType = Ent.MEMBER_SUSPENDED_ARCHIVED if isSuspended else Ent.MEMBER_NOT_SUSPENDED_NOT_ARCHIVED
else:
entityType = Ent.MEMBER_SUSPENDED if isSuspended else Ent.MEMBER_ARCHIVED
elif isSuspended is not None:
entityType = Ent.MEMBER_SUSPENDED if isSuspended else Ent.MEMBER_NOT_SUSPENDED
elif isArchived is not None:
entityType = Ent.MEMBER_ARCHIVED if isArchived else Ent.MEMBER_NOT_ARCHIVED
else:
entityType = Ent.MEMBER
if not typesSet:
typesSet = ALL_GROUP_MEMBER_TYPES
cdfields = getFieldsFromFieldsList(groupFieldsLists['cd'])
memberRoles = ','.join(sorted(rolesSet)) if rolesSet else None
if groupFieldsLists['gs'] is None:
getSettings = True
gsfields = None
elif groupFieldsLists['gs']:
getSettings = True
gsfields = getFieldsFromFieldsList(groupFieldsLists['gs'])
else:
gsfields = None
if getSettings:
gs = buildGAPIObject(API.GROUPSSETTINGS)
if groupFieldsLists['ci']:
getCloudIdentity = True
cifields = getFieldsFromFieldsList(groupFieldsLists['ci'])
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
else:
cifields = None
checkCategory = memberDisplayOptions['showCategory']
i = 0
count = len(entityList)
for group in entityList:
i += 1
ci, _, group = convertGroupCloudIDToEmail(ci, group, i, count)
try:
basic_info = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=group, fields=cdfields)
group = basic_info['email']
if getCloudIdentity:
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
if not name:
continue
cigInfo = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields=cifields)
else:
cigInfo = {}
settings = {}
if getSettings and not GroupIsAbuseOrPostmaster(group):
# Use email address retrieved from cd since GS API doesn't support uid
settings = callGAPI(gs.groups(), 'get',
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(group), fields=gsfields)
if getGroups:
groups = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=group, orderBy='email', fields='nextPageToken,groups(name,email)')
if getUsers:
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,id,role,status,type)')
result = callGAPIpages(cd.members(), 'list', 'members',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
members = []
for member in result:
if ((member['type'] in typesSet) and
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived) and
checkMemberMatch(member, memberOptions) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
members.append(member)
if FJQC.formatJSON:
basic_info.update(settings)
if cigInfo:
basic_info['cloudIdentity'] = cigInfo
if getGroups:
basic_info['groups'] = groups
if getUsers:
basic_info['members'] = members
printLine(json.dumps(cleanJSON(basic_info), ensure_ascii=False, sort_keys=True))
continue
printEntity([Ent.GROUP, group], i, count)
Ind.Increment()
if cigInfo:
_showCIGroup(cigInfo, None)
printEntity([Ent.GROUP_SETTINGS, None])
Ind.Increment()
for key in GROUP_INFO_PRINT_ORDER:
if key not in basic_info:
continue
value = basic_info[key]
if isinstance(value, list):
printKeyValueList([key, None])
Ind.Increment()
for val in value:
printKeyValueList([val])
Ind.Decrement()
elif key not in GROUP_FIELDS_WITH_CRS_NLS:
printKeyValueList([key, value])
else:
printKeyValueWithCRsNLs(key, value)
if settings:
for key, attr in sorted(iter(GROUP_SETTINGS_ATTRIBUTES.items())):
key = attr[0]
if key in settings:
if key not in GROUP_FIELDS_WITH_CRS_NLS:
printKeyValueList([key, settings[key]])
else:
printKeyValueWithCRsNLs(key, settings[key])
for key in GROUP_MERGED_ATTRIBUTES_PRINT_ORDER:
if key in settings:
printKeyValueList([key, settings[key]])
Ind.Increment()
showTitle = False
else:
showTitle = True
if showDeprecatedAttributes:
for subkey, subattr in sorted(iter(GROUP_MERGED_TO_COMPONENT_MAP[key].items())):
subkey = subattr[0]
if subkey in settings:
if showTitle:
printKeyValueList([key, ''])
Ind.Increment()
showTitle = False
printKeyValueList([subkey, settings[subkey]])
if not showTitle:
Ind.Decrement()
if showDeprecatedAttributes:
showTitle = True
for subkey, attr in sorted(iter(GROUP_DEPRECATED_ATTRIBUTES.items())):
subkey = attr[0]
if subkey in settings:
if showTitle:
printKeyValueList(['Deprecated', ''])
Ind.Increment()
showTitle = False
if subkey != 'maxMessageBytes':
printKeyValueList([subkey, settings[subkey]])
else:
printKeyValueList([subkey, formatMaxMessageBytes(settings[subkey], ONE_KILO_BYTES, ONE_MEGA_BYTES)])
if not showTitle:
Ind.Decrement()
Ind.Decrement()
if getAliases:
for up in ['aliases', 'nonEditableAliases']:
aliases = basic_info.get(up, [])
if aliases:
printEntitiesCount([Ent.NONEDITABLE_ALIAS, Ent.EMAIL_ALIAS][up == 'aliases'], aliases)
Ind.Increment()
for alias in aliases:
printKeyValueList(['alias', alias])
Ind.Decrement()
if getGroups:
printEntitiesCount(Ent.GROUP, groups)
Ind.Increment()
for groupm in groups:
printKeyValueList([groupm['name'], groupm['email']])
Ind.Decrement()
if getUsers:
printEntitiesCount(entityType, members)
Ind.Increment()
for member in members:
memberDetails = [member.get('role', Ent.ROLE_MEMBER).lower(), f'{member.get("email", member["id"])} ({member["type"].lower()})']
if checkCategory:
memberDetails[1] += f' ({member["category"]})'
printKeyValueList(memberDetails)
Ind.Decrement()
printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.GROUP)), len(members)])
Ind.Decrement()
except GAPI.notFound:
entityActionFailedWarning([Ent.GROUP, group], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.groupNotFound, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
GAPI.invalid, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden, GAPI.badRequest,
GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([Ent.GROUP, group], str(e), i, count)
# gam info groups <GroupEntity>
# [nousers] [quick] [noaliases] [groups]
# [basic] <GroupFieldName>* [fields <GroupFieldNameList>] [nodeprecated]
# [ciallfields|(cifields <CIGroupFieldNameList>)]
# [roles <GroupRoleList>] [members] [managers] [owners]
# [internal] [internaldomains <DomainList>] [external]
# [notsuspended|suspended] [notarchived|archived]
# [types <GroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [formatjson]
def doInfoGroups():
infoGroups(getEntityList(Cmd.OB_GROUP_ENTITY))
def groupFilters(kwargs, query):
queryTitle = ''
if kwargs.get('domain'):
queryTitle += f'domain={kwargs["domain"]}, '
if query is not None:
queryTitle += f'query="{query}", '
if queryTitle:
return query, queryTitle[:-2]
return query, queryTitle
def getGroupFilters(myarg, kwargsDict, showOwnedBy):
if getUserGroupDomainQueryFilters(myarg, kwargsDict):
pass
elif myarg in {'member', 'showownedby'}:
emailAddressOrUID = getEmailAddress()
if emailAddressOrUID != GC.Values[GC.CUSTOMER_ID].lower():
kwargsDict['queries'] = [f'memberKey={emailAddressOrUID}']
key = 'email' if emailAddressOrUID.find('@') != -1 else 'id'
else:
kwargsDict['queries'] = [f'memberKey={GC.Values[GC.CUSTOMER_ID]}']
key = 'id'
if myarg == 'showownedby':
showOwnedBy['key'] = key
showOwnedBy['value'] = emailAddressOrUID
else:
return False
return True
def checkGroupShowOwnedBy(showOwnedBy, members):
for member in members:
if (member.get('role', Ent.ROLE_MEMBER) == Ent.ROLE_OWNER) and (member.get(showOwnedBy['key'], '').lower() == showOwnedBy['value']):
return True
return False
def getGroupMatchPatterns(myarg, matchPatterns, ciGroupsAPI):
if myarg == 'emailmatchpattern':
matchPatterns['email'] = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE)}
elif myarg == 'namematchpattern':
matchPatterns['name' if not ciGroupsAPI else 'displayName'] = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE|re.UNICODE)}
elif myarg == 'descriptionmatchpattern':
matchPatterns['description'] = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE|re.UNICODE)}
elif not ciGroupsAPI and myarg == 'admincreatedmatch':
matchPatterns['adminCreated'] = getBoolean(None)
else:
return False
return True
def updateFieldsForGroupMatchPatterns(matchPatterns, fieldsList, csvPF=None):
for field in ['name', 'description', 'adminCreated']:
if field in matchPatterns:
if csvPF is not None:
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, fieldsList)
else:
fieldsList.append(field)
def clearUnneededGroupMatchPatterns(matchPatterns):
for field in ['name', 'displayName', 'description', 'adminCreated']:
matchPatterns.pop(field, None)
def checkGroupMatchPatterns(groupEmail, group, matchPatterns):
for field, match in iter(matchPatterns.items()):
if field == 'email':
if not match['not']:
if not match['pattern'].match(groupEmail):
return False
else:
if match['pattern'].match(groupEmail):
return False
elif field == 'adminCreated':
if match != group[field]:
return False
else: # field in {'name', 'displayName', 'description'}:
if not match['not']:
if not match['pattern'].match(group[field]):
return False
else:
if match['pattern'].match(group[field]):
return False
return True
MEMBERS_TITLES = {
'combined': {
'total': ['TotalCount', ''],
Ent.ROLE_MEMBER: ['MembersCount', 'Members'],
Ent.ROLE_MANAGER: ['ManagersCount', 'Managers'],
Ent.ROLE_OWNER: ['OwnersCount', 'Owners']
},
'internal': {
'total': ['TotalInternalCount', ''],
Ent.ROLE_MEMBER: ['InternalMembersCount', 'InternalMembers'],
Ent.ROLE_MANAGER: ['InternalManagersCount', 'InternalManagers'],
Ent.ROLE_OWNER: ['InternalOwnersCount', 'InternalOwners']
},
'external': {
'total': ['TotalExternalCount', ''],
Ent.ROLE_MEMBER: ['ExternalMembersCount', 'ExternalMembers'],
Ent.ROLE_MANAGER: ['ExternalManagersCount', 'ExternalManagers'],
Ent.ROLE_OWNER: ['ExternalOwnersCount', 'ExternalOwners']
}
}
def setMemberDisplayTitles(memberDisplayOptions, csvPF):
if memberDisplayOptions['totalCount']:
csvPF.AddTitles(MEMBERS_TITLES['combined']['total'][0])
if not memberDisplayOptions['internal'] and not memberDisplayOptions['external']:
memberDisplayOptions['categories'].append('combined')
else:
if memberDisplayOptions['internal']:
memberDisplayOptions['categories'].append('internal')
if memberDisplayOptions['external']:
memberDisplayOptions['categories'].append('external')
for category in memberDisplayOptions['categories']:
if memberDisplayOptions['totalCount'] and category != 'combined':
csvPF.AddTitles(MEMBERS_TITLES[category]['total'][0])
for role in Ent.ROLE_LIST:
if memberDisplayOptions[role]['show']:
csvPF.AddTitles(MEMBERS_TITLES[category][role][0])
if not memberDisplayOptions[role]['countOnly']:
csvPF.AddTitles(MEMBERS_TITLES[category][role][1])
def setMemberDisplaySortTitles(memberDisplayOptions, sortTitles):
if memberDisplayOptions['totalCount']:
sortTitles.append(MEMBERS_TITLES['total'][0])
for category in memberDisplayOptions['categories']:
if memberDisplayOptions['totalCount'] and category != 'combined':
sortTitles.append(MEMBERS_TITLES[category]['total'][0])
for role in Ent.ROLE_LIST:
if memberDisplayOptions[role]['show']:
sortTitles.append(MEMBERS_TITLES[category][role][0])
if not memberDisplayOptions[role]['countOnly']:
sortTitles.append(MEMBERS_TITLES[category][role][1])
def addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
isSuspended, isArchived, ciGroupsAPI):
membersInfo = {
'combined': {'totalTitle': 'TotalCount',
Ent.ROLE_MEMBER: {'titles': ['MembersCount', 'Members'],
'count': 0, 'email': []},
Ent.ROLE_MANAGER: {'titles': ['ManagersCount', 'Managers'],
'count': 0, 'email': []},
Ent.ROLE_OWNER: {'titles': ['OwnersCount', 'Owners'],
'count': 0, 'email': []}},
'internal': {'totalTitle': 'TotalInternalCount',
Ent.ROLE_MEMBER: {'titles': ['InternalMembersCount', 'InternalMembers'],
'count': 0, 'email': []},
Ent.ROLE_MANAGER: {'titles': ['InternalManagersCount', 'InternalManagers'],
'count': 0, 'email': []},
Ent.ROLE_OWNER: {'titles': ['InternalOwnersCount', 'InternalOwners'],
'count': 0, 'email': []}},
'external': {'totalTitle': 'TotalExternalCount',
Ent.ROLE_MEMBER: {'titles': ['ExternalMembersCount', 'ExternalMembers'],
'count': 0, 'email': []},
Ent.ROLE_MANAGER: {'titles': ['ExternalManagersCount', 'ExternalManagers'],
'count': 0, 'email': []},
Ent.ROLE_OWNER: {'titles': ['ExternalOwnersCount', 'ExternalOwners'],
'count': 0, 'email': []}}}
checkMatch = checkMemberMatch if not ciGroupsAPI else checkCIMemberMatch
for member in groupMembers:
if not ciGroupsAPI:
member_email = member.get('email', member.get('id', None))
else:
member_email = member.get('preferredMemberKey', {}).get('id', member['name'])
if not member_email:
writeStderr(f' Not sure what to do with: {member}\n')
continue
if not memberDisplayOptions['showCategory']:
category = 'combined'
else:
if member_email.find('@') > 0:
_, domain = member_email.lower().split('@', 1)
category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external'
else:
category = 'internal'
if not memberDisplayOptions[category]:
continue
if ((member['type'] in typesSet) and
(ciGroupsAPI or _checkMemberIsSuspendedIsArchived(member, isSuspended, isArchived)) and
checkMatch(member, memberOptions)):
role = member.get('role', Ent.ROLE_MEMBER)
if role not in {Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER}:
role = Ent.ROLE_MEMBER
if memberDisplayOptions[role]['show']:
membersInfo[category][role]['count'] += 1
if not memberDisplayOptions[role]['countOnly']:
membersInfo[category][role]['email'].append(member_email)
totalCount = 0
for category in memberDisplayOptions['categories']:
categoryCount = 0
for role in Ent.ROLE_LIST:
if memberDisplayOptions[role]['show']:
categoryCount += membersInfo[category][role]['count']
row[membersInfo[category][role]['titles'][0]] = membersInfo[category][role]['count']
if not memberDisplayOptions[role]['countOnly']:
row[membersInfo[category][role]['titles'][1]] = delimiter.join(membersInfo[category][role]['email'])
if memberDisplayOptions['totalCount'] and category != 'combined':
row[membersInfo[category]['totalTitle']] = categoryCount
totalCount += categoryCount
if memberDisplayOptions['totalCount']:
row['TotalCount'] = totalCount
PRINT_GROUPS_JSON_TITLES = ['email', 'JSON']
# gam print groups [todrive <ToDriveAttribute>*]
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
# (group|group_ns|group_susp <GroupItem>)|
# (select <GroupEntity>)]
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
# [descriptionmatchpattern [not] <REMatchPattern>] (matchsetting [not] <GroupAttribute>)*
# [admincreatedmatch <Boolean>]
# [maxresults <Number>]
# [allfields|([basic] [settings] <GroupFieldName>* [fields <GroupFieldNameList>])]
# [ciallfields|(cifields <CIGroupFieldNameList>)]
# [nodeprecated]
# [roles <GroupRoleList>]
# [members|memberscount] [managers|managerscount] [owners|ownerscount] [totalcount] [countsonly]
# [internal] [internaldomains <DomainList>] [external]
# [includederivedmembership]
# [notsuspended|suspended] [notarchived|archived]
# [types <GroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [convertcrnl] [delimiter <Character>] [sortheaders]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintGroups():
def _printGroupRow(groupEntity, groupSettings, groupMembers):
nonlocal itemCount
row = {}
if matchSettings:
if not isinstance(groupSettings, dict):
return
for key, match in iter(matchSettings.items()):
gvalue = groupSettings.get(key)
if match['notvalues'] and gvalue in match['notvalues']:
return
if match['values'] and gvalue not in match['values']:
return
if showOwnedBy and not checkGroupShowOwnedBy(showOwnedBy, groupMembers):
return
if showItemCountOnly:
itemCount += 1
return
if deprecatedAttributesSet and isinstance(groupSettings, dict):
deprecatedKeys = []
for key in groupSettings:
if key in deprecatedAttributesSet:
deprecatedKeys.append(key)
for key in deprecatedKeys:
groupSettings.pop(key)
if FJQC.formatJSON:
row['email'] = groupEntity['email']
row['JSON'] = json.dumps(cleanJSON(groupEntity), ensure_ascii=False, sort_keys=True)
if rolesSet and groupMembers is not None:
row['JSON-members'] = json.dumps(groupMembers, ensure_ascii=False, sort_keys=True)
if isinstance(groupSettings, dict):
row['JSON-settings'] = json.dumps(cleanJSON(groupSettings), ensure_ascii=False, sort_keys=True)
groupCloudIdentity = ciGroups.get(row['email'], {})
if groupCloudIdentity:
row['JSON-cloudIdentity'] = json.dumps(cleanJSON(groupCloudIdentity, timeObjects=CIGROUP_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
return
for field in groupFieldsLists['cd']:
if field in groupEntity:
if isinstance(groupEntity[field], list):
row[field] = delimiter.join(groupEntity[field])
elif convertCRNL and field in GROUP_FIELDS_WITH_CRS_NLS:
row[field] = escapeCRsNLs(groupEntity[field])
else:
row[field] = groupEntity[field]
if rolesSet and groupMembers is not None:
addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
isSuspended, isArchived, False)
if isinstance(groupSettings, dict):
for key, value in iter(groupSettings.items()):
if key not in {'kind', 'etag', 'email', 'name', 'description'}:
if value is None:
value = ''
csvPF.AddTitles(key)
if convertCRNL and key in GROUP_FIELDS_WITH_CRS_NLS:
row[key] = escapeCRsNLs(value)
else:
row[key] = value
groupCloudEntity = ciGroups.get(row['email'], {})
if groupCloudEntity:
for k, v in iter(groupCloudEntity.pop('labels', {}).items()):
if v == '':
groupCloudEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = True
else:
groupCloudEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
for key, value in sorted(iter(flattenJSON({'cloudIdentity': groupCloudEntity}, flattened={}, timeObjects=CIGROUP_TIME_OBJECTS).items())):
csvPF.AddTitles(key)
row[key] = value
csvPF.WriteRow(row)
def _callbackProcessGroupBasic(request_id, response, exception):
ri = request_id.splitlines()
i = int(ri[RI_I])
if exception is not None:
http_status, reason, message = checkGAPIError(exception)
if reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.GROUP_GET_RETRY_REASONS:
if reason in GAPI.GROUP_GET_THROW_REASONS:
entityUnknownWarning(Ent.GROUP, ri[RI_ENTITY], i, int(ri[RI_COUNT]))
else:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP, None], errMsg, i, int(ri[RI_COUNT]))
return
waitOnFailure(1, 10, reason, message)
try:
response = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=ri[RI_ENTITY], fields=cdfields)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError) as e:
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP, None], str(e), i, int(ri[RI_COUNT]))
return
entityList.append(response)
def _callbackProcessGroupMembers(request_id, response, exception):
ri = request_id.splitlines()
i = int(ri[RI_I])
totalItems = 0
items = 'members'
pageMessage = getPageMessageForWhom(forWhom=ri[RI_ENTITY], showFirstLastItems=True)
if exception is not None:
http_status, reason, message = checkGAPIError(exception)
if reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], ri[RI_ROLE], None], errMsg, i, int(ri[RI_COUNT]))
groupData[i]['required'] -= 1
return
waitOnFailure(1, 10, reason, message)
try:
response = callGAPI(cd.members(), 'list',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
includeDerivedMembership=memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP],
groupKey=ri[RI_ENTITY], roles=ri[RI_ROLE], fields='nextPageToken,members(email,id,role,type,status)',
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], ri[RI_ROLE], None], str(e), i, int(ri[RI_COUNT]))
groupData[i]['required'] -= 1
return
while True:
pageToken, totalItems = _processGAPIpagesResult(response, items, groupData[i][items], totalItems, pageMessage, 'email', ri[RI_ROLE])
if not pageToken:
_finalizeGAPIpagesResult(pageMessage)
break
try:
response = callGAPI(cd.members(), 'list',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
pageToken=pageToken,
includeDerivedMembership=memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP],
groupKey=ri[RI_ENTITY], roles=ri[RI_ROLE], fields='nextPageToken,members(email,id,role,type,status)',
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], ri[RI_ROLE], None], str(e), i, int(ri[RI_COUNT]))
break
groupData[i]['required'] -= 1
def _callbackProcessGroupSettings(request_id, response, exception):
ri = request_id.splitlines()
i = int(ri[RI_I])
if exception is not None:
http_status, reason, message = checkGAPIError(exception)
if reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.GROUP_SETTINGS_RETRY_REASONS:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP_SETTINGS, None], errMsg, i, int(ri[RI_COUNT]))
groupData[i]['required'] -= 1
return
waitOnFailure(1, 10, reason, message)
try:
response = callGAPI(gs.groups(), 'get',
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(ri[RI_ENTITY]), fields=gsfields)
except GAPI.notFound:
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP_SETTINGS, None], Msg.DOES_NOT_EXIST, i, int(ri[RI_COUNT]))
response = {}
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.backendError, GAPI.invalid, GAPI.invalidParameter, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP_SETTINGS, None], str(e), i, int(ri[RI_COUNT]))
response = {}
groupData[i]['settings'] = response
groupData[i]['required'] -= 1
def _writeCompleteRows():
complete = [k for k in groupData if groupData[k]['required'] == 0]
for k in complete:
_printGroupRow(groupData[k]['entity'], groupData[k]['settings'], groupData[k]['members'])
del groupData[k]
cd = buildGAPIObject(API.DIRECTORY)
ci = None
kwargsDict = initUserGroupDomainQueryFilters()
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
getCloudIdentity = getSettings = showCIgroupKey = sortHeaders = False
memberDisplayOptions = initPGGroupMemberDisplayOptions()
maxResults = None
groupFieldsLists = {'cd': ['email'], 'ci': [], 'gs': []}
csvPF = CSVPrintFile(groupFieldsLists['cd'])
FJQC = FormatJSONQuoteChar(csvPF)
rolesSet = set()
typesSet = set()
memberOptions = initMemberOptions()
entitySelection = isSuspended = isArchived = None
showOwnedBy = {}
matchPatterns = {}
matchSettings = {}
deprecatedAttributesSet = set()
ciGroups = {}
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getGroupFilters(myarg, kwargsDict, showOwnedBy):
pass
elif getGroupMatchPatterns(myarg, matchPatterns, False):
pass
elif myarg in {'group', 'groupns', 'groupsusp'}:
entitySelection = [getString(Cmd.OB_EMAIL_ADDRESS)]
if myarg == 'groupns':
isSuspended = False
elif myarg == 'groupsusp':
isSuspended = True
elif myarg == 'select':
entitySelection = getEntityList(Cmd.OB_GROUP_ENTITY)
elif myarg in SUSPENDED_ARGUMENTS:
isSuspended = _getIsSuspended(myarg)
elif myarg in ARCHIVED_ARGUMENTS:
isArchived = _getIsArchived(myarg)
elif myarg == 'maxresults':
maxResults = getInteger(minVal=1, maxVal=200)
elif myarg == 'nodeprecated':
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_DISCOVER_ATTRIBUTES.values())])
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_ASSIST_CONTENT_ATTRIBUTES.values())])
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_MODERATE_CONTENT_ATTRIBUTES.values())])
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_MODERATE_MEMBERS_ATTRIBUTES.values())])
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_DEPRECATED_ATTRIBUTES.values())])
elif myarg in {'convertcrnl', 'converttextnl', 'convertfooternl'}:
convertCRNL = True
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'basic':
sortHeaders = True
for field in GROUP_FIELDS_CHOICE_MAP:
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
elif myarg in {'ciallfields', 'allcifields'}:
sortHeaders = True
groupFieldsLists['ci'] = []
for field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
elif myarg == 'settings':
getSettings = sortHeaders = True
elif myarg == 'allfields':
getSettings = sortHeaders = True
groupFieldsLists['cd'] = []
groupFieldsLists['gs'] = []
for field in GROUP_FIELDS_CHOICE_MAP:
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
elif myarg == 'sortheaders':
sortHeaders = getBoolean()
elif myarg in GROUP_FIELDS_CHOICE_MAP:
csvPF.AddField(myarg, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
elif myarg in GROUP_ATTRIBUTES_SET:
attrProperties = getGroupAttrProperties(myarg)
csvPF.AddField(myarg, {myarg: attrProperties[0]}, groupFieldsLists['gs'])
elif myarg == 'fields':
for field in _getFieldsList():
if field in GROUP_FIELDS_CHOICE_MAP:
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
else:
attrProperties = getGroupAttrProperties(field)
if attrProperties is None:
invalidChoiceExit(field, list(GROUP_FIELDS_CHOICE_MAP)+list(GROUP_ATTRIBUTES_SET), True)
csvPF.AddField(field, {field: attrProperties[0]}, groupFieldsLists['gs'])
elif myarg == 'cifields':
for field in _getFieldsList():
if field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
else:
invalidChoiceExit(field, list(CIGROUP_FIELDS_CHOICE_MAP), True)
elif myarg == 'matchsetting':
valueList = getChoice({'not': 'notvalues'}, mapChoice=True, defaultChoice='values')
matchBody = {}
getGroupAttrValue(getString(Cmd.OB_FIELD_NAME).lower(), matchBody)
for key, value in iter(matchBody.items()):
matchSettings.setdefault(key, {'notvalues': [], 'values': []})
matchSettings[key][valueList].append(value)
elif getPGGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
pass
elif getGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif myarg == 'includederivedmembership':
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if not typesSet:
typesSet = ALL_GROUP_MEMBER_TYPES
updateFieldsForGroupMatchPatterns(matchPatterns, groupFieldsLists['cd'], csvPF)
if groupFieldsLists['cd']:
cdfields = ','.join(set(groupFieldsLists['cd']))
cdfieldsnp = f'nextPageToken,groups({cdfields})'
else:
cdfields = cdfieldsnp = None
if matchSettings:
groupFieldsLists['gs'].extend(list(matchSettings))
if groupFieldsLists['gs']:
getSettings = True
gsfields = ','.join(set(groupFieldsLists['gs']))
else:
gsfields = None
if getSettings:
gs = buildGAPIObject(API.GROUPSSETTINGS)
if groupFieldsLists['ci']:
setTrueCustomerId(cd)
getCloudIdentity = True
showCIgroupKey = 'groupKey' in groupFieldsLists['ci']
if not showCIgroupKey:
groupFieldsLists['ci'].append('groupKey(id)')
cifields = ','.join(set(groupFieldsLists['ci']))
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
getRolesSet = rolesSet.copy()
if showOwnedBy:
getRolesSet.add(Ent.ROLE_OWNER)
getRoles = ','.join(sorted(getRolesSet))
showDetails = getRoles or getSettings or getCloudIdentity
if rolesSet:
setMemberDisplayTitles(memberDisplayOptions, csvPF)
if FJQC.formatJSON:
sortHeaders = False
csvPF.SetJSONTitles(PRINT_GROUPS_JSON_TITLES)
if rolesSet:
csvPF.AddJSONTitle('JSON-members')
if getSettings:
csvPF.AddJSONTitle('JSON-settings')
if getCloudIdentity:
csvPF.AddJSONTitle('JSON-cloudIdentity')
if entitySelection is None:
entityList = []
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
kwargs = kwargsQuery[0]
query = kwargsQuery[1]
query, pquery = groupFilters(kwargs, query)
printGettingAllAccountEntities(Ent.GROUP, pquery)
try:
entityList.extend(callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', query=query, fields=cdfieldsnp, maxResults=maxResults, **kwargs))
except (GAPI.invalidMember, GAPI.invalidInput) as e:
if not invalidMember(query):
entityActionFailedExit([Ent.GROUP, None], str(e))
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
if kwargs.get('domain'):
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
else:
accessErrorExit(cd)
else:
svcargs = dict([('groupKey', None), ('fields', cdfields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
cdmethod = getattr(cd.groups(), 'get')
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupBasic)
cdbcount = 0
entityList = []
i = 0
count = len(entitySelection)
for groupEntity in entitySelection:
i += 1
groupEmail = normalizeEmailAddressOrUID(groupEntity)
svcparms = svcargs.copy()
svcparms['groupKey'] = groupEmail
printGettingEntityItem(Ent.GROUP, groupEmail, i, count)
cdbatch.add(cdmethod(**svcparms), request_id=batchRequestID(groupEmail, i, count, 0, 0, None))
cdbcount += 1
if cdbcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(cdbatch)
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupBasic)
cdbcount = 0
if cdbcount > 0:
cdbatch.execute()
required = 0
if getRoles:
required += 1
svcargs = dict([('groupKey', None), ('roles', getRoles), ('fields', 'nextPageToken,members(email,id,role,type,status)'),
('includeDerivedMembership', memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP]),
('maxResults', GC.Values[GC.MEMBER_MAX_RESULTS])]+GM.Globals[GM.EXTRA_ARGS_LIST])
if getSettings:
required += 1
svcargsgs = dict([('groupUniqueId', None), ('fields', gsfields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
cdmethod = getattr(cd.members(), 'list')
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupMembers)
cdbcount = 0
if getSettings:
gsmethod = getattr(gs.groups(), 'get')
gsbatch = gs.new_batch_http_request(callback=_callbackProcessGroupSettings)
gsbcount = 0
groupData = {}
itemCount = 0
i = 0
count = len(entityList)
for groupEntity in entityList:
i += 1
groupEmail = groupEntity['email']
if not checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
continue
if not showDetails:
_printGroupRow(groupEntity, None, None)
continue
if getCloudIdentity:
_, name, groupEmail = convertGroupEmailToCloudID(ci, groupEmail, i, count)
printGettingEntityItemForWhom(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
if name:
try:
ciGroup = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields=cifields)
key = ciGroup['groupKey']['id']
if not showCIgroupKey:
ciGroup.pop('groupKey')
ciGroups[key] = ciGroup
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.GROUP, groupEmail, Ent.CLOUD_IDENTITY_GROUP, None], str(e), i, count)
groupData[i] = {'entity': groupEntity, 'cloudIdentity': {}, 'settings': getSettings, 'members': [], 'required': required}
if getRoles:
printGettingEntityItemForWhom(getRoles, groupEmail, i, count)
svcparms = svcargs.copy()
svcparms['groupKey'] = groupEmail
cdbatch.add(cdmethod(**svcparms), request_id=batchRequestID(groupEmail, i, count, 0, 0, None, getRoles))
cdbcount += 1
if cdbcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(cdbatch)
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupMembers)
cdbcount = 0
_writeCompleteRows()
if getSettings:
if not GroupIsAbuseOrPostmaster(groupEmail):
printGettingEntityItemForWhom(Ent.GROUP_SETTINGS, groupEmail, i, count)
svcparmsgs = svcargsgs.copy()
svcparmsgs['groupUniqueId'] = mapGroupEmailForSettings(groupEmail)
gsbatch.add(gsmethod(**svcparmsgs), request_id=batchRequestID(groupEmail, i, count, 0, 0, None))
gsbcount += 1
if gsbcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(gsbatch)
gsbatch = gs.new_batch_http_request(callback=_callbackProcessGroupSettings)
gsbcount = 0
_writeCompleteRows()
else:
groupData[i]['settings'] = False
groupData[i]['required'] -= 1
if cdbcount > 0:
cdbatch.execute()
if getSettings and gsbcount > 0:
gsbatch.execute()
_writeCompleteRows()
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
if sortHeaders:
sortTitles = ['email']+GROUP_INFO_PRINT_ORDER+['aliases', 'nonEditableAliases']
if getSettings:
sortTitles += sorted([attr[0] for attr in iter(GROUP_SETTINGS_ATTRIBUTES.values())])
for key in GROUP_MERGED_ATTRIBUTES_PRINT_ORDER:
sortTitles.append(key)
if not deprecatedAttributesSet:
sortTitles += sorted([attr[0] for attr in iter(GROUP_MERGED_TO_COMPONENT_MAP[key].values())])
if not deprecatedAttributesSet:
sortTitles += sorted([attr[0] for attr in iter(GROUP_DEPRECATED_ATTRIBUTES.values())])
if rolesSet:
setMemberDisplaySortTitles(memberDisplayOptions, sortTitles)
csvPF.SetSortTitles(sortTitles)
csvPF.writeCSVfile('Groups')
def mapCIGroupMemberFieldNames(member):
member['email'] = member['preferredMemberKey'].pop('id')
if not member['preferredMemberKey']:
member.pop('preferredMemberKey')
if 'name' in member:
member['id'] = member.pop('name')
INFO_GROUPMEMBERS_FIELDS = ['role', 'type', 'status', 'delivery_settings']
# gam <UserTypeEntity> info member <GroupEntity>
def infoGroupMembers(entityList, ciGroupsAPI=False):
if not ciGroupsAPI:
cd = buildGAPIObject(API.DIRECTORY)
ci = None
fieldsList = INFO_GROUPMEMBERS_FIELDS
fields = ','.join(fieldsList)
else:
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
groups = getEntityList(Cmd.OB_GROUP_ENTITY)
checkForExtraneousArguments()
groupsLists = groups if isinstance(groups, dict) else None
i, count, entityList = getEntityArgument(entityList)
for user in entityList:
i += 1
memberKey = normalizeEmailAddressOrUID(user)
if groupsLists:
groups = groupsLists[user]
jcount = len(groups)
entityPerformActionNumItems([Ent.MEMBER, memberKey], jcount, entityType, i, count)
Ind.Increment()
j = 0
for group in groups:
j += 1
if not ciGroupsAPI:
try:
ci, _, groupKey = convertGroupCloudIDToEmail(ci, group, i, count)
result = callGAPI(cd.members(), 'get',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND], retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=groupKey, memberKey=memberKey, fields=fields)
result.setdefault('role', Ent.ROLE_MEMBER)
printEntity([entityType, groupKey], j, jcount)
Ind.Increment()
for field in INFO_GROUPMEMBERS_FIELDS:
printKeyValueList([field, result[field]])
Ind.Decrement()
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([entityType, group], str(e), j, jcount)
except GAPI.memberNotFound:
entityActionFailedWarning([entityType, group, Ent.MEMBER, memberKey], Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.MEMBER)), j, jcount)
else:
_, groupKey, group = convertGroupEmailToCloudID(ci, group, j, jcount)
if not groupKey:
continue
try:
memberName = callGAPI(ci.groups().memberships(), 'lookup',
throwReasons=GAPI.GROUP_GET_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
parent=groupKey, memberKey_id=memberKey, fields='name').get('name')
result = callGAPI(ci.groups().memberships(), 'get',
name=memberName)
printEntity([entityType, group], j, jcount)
Ind.Increment()
getCIGroupMemberRoleFixType(result)
kvList = ['role', result['role']]
if 'expireTime' in result:
kvList.extend(['expireTime', formatLocalTime(result['expireTime'])])
printKeyValueList(kvList)
printKeyValueList(['type', result['type']])
for field in ['createTime', 'updateTime']:
printKeyValueList([field, formatLocalTime(result[field])])
if 'deliverySetting' in result:
printKeyValueList(['deliverySetting', result['deliverySetting']])
Ind.Decrement()
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden) as e:
entityActionFailedWarning([entityType, groupKey], str(e), j, jcount)
except (GAPI.notFound, GAPI.memberNotFound, GAPI.permissionDenied):
entityActionFailedWarning([entityType, groupKey, Ent.MEMBER, memberKey], Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.MEMBER)), j, jcount)
Ind.Decrement()
# gam info member <UserTypeEntity> <GroupEntity>
def doInfoGroupMembers():
infoGroupMembers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1], False)
def getGroupMembersEntityList(cd, entityList, matchPatterns, fieldsList, kwargsDict):
if entityList is None:
updateFieldsForGroupMatchPatterns(matchPatterns, fieldsList)
entityList = []
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
kwargs = kwargsQuery[0]
query = kwargsQuery[1]
query, pquery = groupFilters(kwargs, query)
printGettingAllAccountEntities(Ent.GROUP, pquery)
try:
entityList.extend(callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', query=query, fields=f'nextPageToken,groups({",".join(set(fieldsList))})', **kwargs))
except (GAPI.invalidMember, GAPI.invalidInput) as e:
if not invalidMember(query):
entityActionFailedExit([Ent.GROUP, None], str(e))
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
if kwargs.get('domain'):
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
else:
accessErrorExit(cd)
else:
clearUnneededGroupMatchPatterns(matchPatterns)
return entityList
def getGroupMembers(cd, groupEmail, memberRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level, typesSet):
def _getMemberDeliverySettings(member):
if 'delivery_settings' not in member:
try:
member['delivery_settings'] = callGAPI(cd.members(), 'get',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=groupEmail, memberKey=member['id'], fields='delivery_settings')['delivery_settings']
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
pass
except GAPI.memberNotFound:
pass
else:
memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS] = False
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, i, count)
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,id,role,status,type,delivery_settings)')
try:
groupMembers = callGAPIpages(cd.members(), 'list', 'members',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
includeDerivedMembership=memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP],
groupKey=groupEmail, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.GROUP, groupEmail, i, count)
return
checkCategory = memberDisplayOptions['showCategory']
if not memberOptions[MEMBEROPTION_RECURSIVE]:
if memberOptions[MEMBEROPTION_NODUPLICATES]:
for member in groupMembers:
if (_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions)) and
member['id'] not in membersSet):
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
_getMemberDeliverySettings(member)
membersSet.add(member['id'])
if member['type'] in typesSet and checkMemberMatch(member, memberOptions):
membersList.append(member)
else:
for member in groupMembers:
if (_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
_getMemberDeliverySettings(member)
if member['type'] in typesSet and checkMemberMatch(member, memberOptions):
membersList.append(member)
elif memberOptions[MEMBEROPTION_NODUPLICATES]:
groupMemberList = []
for member in groupMembers:
if member['type'] != Ent.TYPE_GROUP:
if ((member['type'] in typesSet and
checkMemberMatch(member, memberOptions) and
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions)) and
member['id'] not in membersSet)):
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
_getMemberDeliverySettings(member)
membersSet.add(member['id'])
member['level'] = level
member['subgroup'] = groupEmail
membersList.append(member)
else:
if member['id'] not in membersSet:
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
_getMemberDeliverySettings(member)
membersSet.add(member['id'])
if (member['type'] in typesSet and
checkMemberMatch(member, memberOptions) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
member['level'] = level
member['subgroup'] = groupEmail
membersList.append(member)
groupMemberList.append(member['email'])
for member in groupMemberList:
getGroupMembers(cd, member, memberRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level+1, typesSet)
else:
for member in groupMembers:
if member['type'] != Ent.TYPE_GROUP:
if ((member['type'] in typesSet) and
checkMemberMatch(member, memberOptions) and
_checkMemberRoleIsSuspendedIsArchived(member, validRoles,
memberOptions[MEMBEROPTION_ISSUSPENDED],
memberOptions[MEMBEROPTION_ISARCHIVED]) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
_getMemberDeliverySettings(member)
member['level'] = level
member['subgroup'] = groupEmail
membersList.append(member)
else:
if (member['type'] in typesSet and
checkMemberMatch(member, memberOptions) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
member['level'] = level
member['subgroup'] = groupEmail
membersList.append(member)
getGroupMembers(cd, member['email'], memberRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level+1, typesSet)
GROUPMEMBERS_FIELDS_CHOICE_MAP = {
'delivery': 'delivery_settings',
'deliverysettings': 'delivery_settings',
'email': 'email',
'group': 'group',
'groupemail': 'group',
'id': 'id',
'name': 'name',
'role': 'role',
'status': 'status',
'type': 'type',
'useremail': 'email',
}
GROUPMEMBERS_DEFAULT_FIELDS = ['group', 'type', 'role', 'id', 'status', 'email']
# gam print group-members [todrive <ToDriveAttribute>*]
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
# (group|group_ns|group_susp <GroupItem>)|
# (select <GroupEntity>)]
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
# [descriptionmatchpattern [not] <REMatchPattern>]
# [roles <GroupRoleList>] [members] [managers] [owners]
# [internal] [internaldomains <DomainList>] [external]
# [notsuspended|suspended] [notarchived|archived]
# [types <GroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [membernames] [showdeliverysettings]
# <MembersFieldName>* [fields <MembersFieldNameList>]
# [userfields <UserFieldNameList>]
# [allschemas|(schemas|custom|customschemas <SchemaNameList>)]
# [(recursive [noduplicates])|includederivedmembership] [nogroupemail]
# [peoplelookup|(peoplelookupuser <EmailAddress>)]
# [unknownname <String>] [cachememberinfo [Boolean]]
# [formatjson [quotechar <Character>]]
def doPrintGroupMembers():
def getNameFromPeople(memberId):
try:
info = callGAPI(people.people(), 'get',
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName=f'people/{memberId}', personFields='names')
if 'names' in info:
for sourceType in ['PROFILE', 'CONTACT']:
for name in info['names']:
if name['metadata']['source']['type'] == sourceType:
return name['displayName']
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.forbidden, GAPI.permissionDenied, GAPI.failedPrecondition):
pass
return unknownName
cd = buildGAPIObject(API.DIRECTORY)
ci = None
people = None
memberOptions = initMemberOptions()
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
groupColumn = True
customerKey = GC.Values[GC.CUSTOMER_ID]
kwargsDict = initUserGroupDomainQueryFilters()
subTitle = f'{Msg.ALL} {Ent.Plural(Ent.GROUP)}'
fieldsList = []
csvPF = CSVPrintFile('group')
FJQC = FormatJSONQuoteChar(csvPF)
entityList = None
showOwnedBy = {}
cdfieldsList = ['email']
userFieldsList = []
schemaParms = _initSchemaParms('basic')
rolesSet = set()
typesSet = set()
matchPatterns = {}
showDeliverySettings = False
cacheMemberInfo = False
memberInfo = {}
memberNames = {}
unknownName = UNKNOWN
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getGroupFilters(myarg, kwargsDict, showOwnedBy):
pass
elif getGroupMatchPatterns(myarg, matchPatterns, False):
pass
elif myarg in {'group', 'groupns', 'groupsusp'}:
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
subTitle = f'{Ent.Singular(Ent.GROUP)}={entityList[0]}'
if myarg == 'groupns':
memberOptions[MEMBEROPTION_ISSUSPENDED] = False
elif myarg == 'groupsusp':
memberOptions[MEMBEROPTION_ISSUSPENDED] = True
elif myarg == 'select':
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
subTitle = f'{Msg.SELECTED} {Ent.Plural(Ent.GROUP)}'
elif myarg in SUSPENDED_ARGUMENTS:
memberOptions[MEMBEROPTION_ISSUSPENDED] = _getIsSuspended(myarg)
elif myarg in ARCHIVED_ARGUMENTS:
memberOptions[MEMBEROPTION_ISARCHIVED] = _getIsArchived(myarg)
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
pass
elif getGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif csvPF.GetFieldsListTitles(myarg, GROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='email'):
pass
elif myarg == 'membernames':
memberOptions[MEMBEROPTION_MEMBERNAMES] = True
elif myarg == 'showdeliverysettings':
showDeliverySettings = True
elif myarg == 'userfields':
for field in _getFieldsList():
if field in USER_FIELDS_CHOICE_MAP:
csvPF.AddField(field, USER_FIELDS_CHOICE_MAP, userFieldsList)
else:
invalidChoiceExit(field, USER_FIELDS_CHOICE_MAP, True)
elif myarg in {'allschemas', 'custom', 'schemas', 'customschemas'}:
if myarg == 'allschemas':
schemaParms = _initSchemaParms('full')
else:
_getSchemaNameList(schemaParms)
userFieldsList.append('customSchemas')
elif myarg == 'noduplicates':
memberOptions[MEMBEROPTION_NODUPLICATES] = True
elif myarg == 'recursive':
memberOptions[MEMBEROPTION_RECURSIVE] = True
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = False
elif myarg == 'includederivedmembership':
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
memberOptions[MEMBEROPTION_RECURSIVE] = False
elif myarg == 'nogroupemail':
groupColumn = False
elif myarg == 'peoplelookup':
people = buildGAPIObject(API.PEOPLE)
elif myarg == 'peoplelookupuser':
_, people = buildGAPIServiceObject(API.PEOPLE, getEmailAddress())
if not people:
return
elif myarg == 'unknownname':
unknownName = getString(Cmd.OB_STRING)
elif myarg == 'cachememberinfo':
cacheMemberInfo = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if not typesSet:
typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_GROUP_MEMBER_TYPES
entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargsDict)
if not fieldsList:
for field in GROUPMEMBERS_DEFAULT_FIELDS:
csvPF.AddField(field, {field: field}, fieldsList)
if showDeliverySettings:
field = 'delivery_settings'
csvPF.AddField(field, {field: field}, fieldsList)
elif 'name'in fieldsList:
memberOptions[MEMBEROPTION_MEMBERNAMES] = True
fieldsList.remove('name')
if 'group' in fieldsList:
fieldsList.remove('group')
if not groupColumn:
csvPF.RemoveTitles(['group'])
if memberDisplayOptions['showCategory']:
csvPF.AddTitles('category')
if userFieldsList:
if not memberOptions[MEMBEROPTION_MEMBERNAMES] and 'name.fullName' in userFieldsList:
memberOptions[MEMBEROPTION_MEMBERNAMES] = True
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
if 'name.fullName' not in userFieldsList:
userFieldsList.append('name.fullName')
csvPF.AddTitles('name')
csvPF.RemoveTitles([f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}fullName'])
if FJQC.formatJSON:
if groupColumn:
csvPF.SetJSONTitles(['group', 'JSON'])
else:
csvPF.SetJSONTitles(['JSON'])
memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS] = 'delivery_settings' in fieldsList
userFields = getFieldsFromFieldsList(userFieldsList)
if not rolesSet:
rolesSet = ALL_GROUP_ROLES
getRolesSet = rolesSet.copy()
if showOwnedBy:
getRolesSet.add(Ent.ROLE_OWNER)
getRoles = ','.join(sorted(getRolesSet))
level = 0
setCustomerMemberEmail = 'email' in fieldsList
i = 0
count = len(entityList)
for group in entityList:
i += 1
if isinstance(group, dict):
groupEmail = group['email']
else:
groupEmail = convertUIDtoEmailAddress(group, cd, 'group', ciGroupsAPI=True)
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, groupEmail, i, count)
if not checkGroupMatchPatterns(groupEmail, group, matchPatterns):
continue
membersList = []
membersSet = set()
getGroupMembers(cd, groupEmail, getRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level, typesSet)
if showOwnedBy and not checkGroupShowOwnedBy(showOwnedBy, membersList):
continue
for member in membersList:
if member['role'] not in rolesSet:
continue
memberId = member['id']
row = {}
if groupColumn:
row['group'] = groupEmail
if memberOptions[MEMBEROPTION_RECURSIVE]:
row['level'] = member['level']
row['subgroup'] = member['subgroup']
for title in fieldsList:
row[title] = member.get(title, '')
if setCustomerMemberEmail and (memberId == customerKey):
row['email'] = memberId
if memberDisplayOptions['showCategory']:
row['category'] = member['category']
memberType = member.get('type')
if userFieldsList:
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
row['name'] = unknownName
if memberType == Ent.TYPE_USER:
try:
if not cacheMemberInfo or memberId not in memberInfo:
mbinfo = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE, GAPI.FAILED_PRECONDITION],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=memberId, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
fields=userFields)
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
mname = mbinfo['name'].pop('fullName', unknownName)
row['name'] = mname
if not mbinfo['name']:
mbinfo.pop('name')
if cacheMemberInfo:
memberNames[memberId] = mname
if cacheMemberInfo:
memberInfo[memberId] = mbinfo
else:
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
row['name'] = memberNames[memberId]
mbinfo = memberInfo.get(memberId, {})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(mbinfo, flattened=row))
else:
row.update(mbinfo)
fjrow = {}
if groupColumn:
fjrow['group'] = groupEmail
fjrow['JSON'] = json.dumps(cleanJSON(row, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(fjrow)
continue
except GAPI.userNotFound:
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
if people:
if memberId not in memberNames:
memberNames[memberId] = getNameFromPeople(memberId)
else:
memberNames[memberId] = unknownName
row['name'] = memberNames[memberId]
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.serviceNotAvailable, GAPI.failedPrecondition):
if memberOptions[MEMBEROPTION_MEMBERNAMES] and cacheMemberInfo:
memberNames[memberId] = unknownName
elif memberType == Ent.TYPE_GROUP:
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
try:
if not cacheMemberInfo or memberId not in memberNames:
row['name'] = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE, GAPI.FAILED_PRECONDITION],
retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=memberId, fields='name')['name']
if cacheMemberInfo:
memberNames[memberId] = row['name']
else:
row['name'] = memberNames[memberId]
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
GAPI.invalid, GAPI.systemError, GAPI.serviceNotAvailable, GAPI.failedPrecondition):
if memberOptions[MEMBEROPTION_MEMBERNAMES] and cacheMemberInfo:
memberNames[memberId] = unknownName
elif memberType == Ent.TYPE_CUSTOMER:
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
try:
if not cacheMemberInfo or memberId not in memberNames:
row['name'] = callGAPI(cd.customers(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerKey=memberId, fields='customerDomain')['customerDomain']
if cacheMemberInfo:
memberNames[memberId] = row['name']
else:
row['name'] = memberNames[memberId]
except (GAPI.badRequest, GAPI.invalidInput, GAPI.resourceNotFound, GAPI.forbidden):
if memberOptions[MEMBEROPTION_MEMBERNAMES] and cacheMemberInfo:
memberNames[memberId] = unknownName
if not FJQC.formatJSON:
csvPF.WriteRow(row)
else:
fjrow = {}
if groupColumn:
fjrow['group'] = groupEmail
fjrow['JSON'] = json.dumps(cleanJSON(row, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(fjrow)
if not FJQC.formatJSON:
sortTitles = GROUPMEMBERS_DEFAULT_FIELDS
if memberDisplayOptions['showCategory']:
sortTitles.append('category')
csvPF.SetSortTitles(sortTitles)
csvPF.SortTitles()
csvPF.SetSortTitles([])
if memberOptions[MEMBEROPTION_RECURSIVE]:
csvPF.MoveTitlesToEnd(['level', 'subgroup'])
csvPF.writeCSVfile(f'Group Members ({subTitle})')
# gam show group-members
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
# (group|group_ns|group_susp <GroupItem>)|
# (select <GroupEntity>)]
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
# [descriptionmatchpattern [not] <REMatchPattern>]
# [roles <GroupRoleList>] [members] [managers] [owners] [depth <Number>]
# [internal] [internaldomains <DomainList>] [external]
# [notsuspended|suspended] [notarchived|archived]
# [types <GroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [includederivedmembership]
def doShowGroupMembers():
def _roleOrder(key):
return {Ent.ROLE_OWNER: 0, Ent.ROLE_MANAGER: 1, Ent.ROLE_MEMBER: 2}.get(key, 3)
def _typeOrder(key):
return {Ent.TYPE_CUSTOMER: 0, Ent.TYPE_USER: 1, Ent.TYPE_GROUP: 2, Ent.TYPE_EXTERNAL: 3}.get(key, 4)
def _statusOrder(key):
return {'ACTIVE': 0, 'SUSPENDED': 1, 'UNKNOWN': 2}.get(key, 3)
def _showGroup(groupEmail, depth):
try:
membersList = callGAPIpages(cd.members(), 'list', 'members',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
includeDerivedMembership=includeDerivedMembership,
groupKey=groupEmail, fields='nextPageToken,members(email,id,role,status,type)', maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
if showOwnedBy and not checkGroupShowOwnedBy(showOwnedBy, membersList):
return
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
if depth == 0:
entityUnknownWarning(Ent.GROUP, groupEmail, i, count)
return
if depth == 0:
printEntity([Ent.GROUP, groupEmail], i, count)
if depth == 0 or Ent.TYPE_GROUP in typesSet:
Ind.Increment()
for member in sorted(membersList, key=lambda k: (_roleOrder(k.get('role', Ent.ROLE_MEMBER)), _typeOrder(k['type']), _statusOrder(k.get('status', '')))):
if (_checkMemberIsSuspendedIsArchived(member, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
if (member.get('role', Ent.ROLE_MEMBER) in rolesSet and
member['type'] in typesSet and
checkMemberMatch(member, memberOptions)):
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member.get("email", member["id"])}'
if checkCategory:
memberDetails += f', {member["category"]}'
memberDetails += f' , {member.get("status", "")}'
printKeyValueList([memberDetails])
if not includeDerivedMembership and (member['type'] == Ent.TYPE_GROUP) and (maxdepth == -1 or depth < maxdepth):
_showGroup(member['email'], depth+1)
if depth == 0 or Ent.TYPE_GROUP in typesSet:
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
ci = None
kwargsDict = initUserGroupDomainQueryFilters()
entityList = None
showOwnedBy = {}
cdfieldsList = ['email']
rolesSet = set()
typesSet = set()
memberOptions = initMemberOptions()
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
matchPatterns = {}
maxdepth = -1
includeDerivedMembership = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getGroupFilters(myarg, kwargsDict, showOwnedBy):
pass
elif getGroupMatchPatterns(myarg, matchPatterns, False):
pass
elif myarg in {'group', 'groupns', 'groupsusp'}:
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
if myarg == 'groupns':
memberOptions[MEMBEROPTION_ISSUSPENDED] = False
elif myarg == 'groupsusp':
memberOptions[MEMBEROPTION_ISSUSPENDED] = True
elif myarg == 'select':
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
elif myarg in SUSPENDED_ARGUMENTS:
memberOptions[MEMBEROPTION_ISSUSPENDED] = _getIsSuspended(myarg)
elif myarg in ARCHIVED_ARGUMENTS:
memberOptions[MEMBEROPTION_ISARCHIVED] = _getIsArchived(myarg)
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
pass
elif getGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif myarg == 'depth':
maxdepth = getInteger(minVal=-1)
elif myarg == 'includederivedmembership':
includeDerivedMembership = True
else:
unknownArgumentExit()
if not rolesSet:
rolesSet = ALL_GROUP_ROLES
if not typesSet:
typesSet = ALL_GROUP_MEMBER_TYPES
checkCategory = memberDisplayOptions['showCategory']
entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargsDict)
i = 0
count = len(entityList)
for group in entityList:
i += 1
if isinstance(group, dict):
groupEmail = group['email']
else:
groupEmail = convertUIDtoEmailAddress(group, cd, 'group', ciGroupsAPI=True)
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, groupEmail, i, count)
if checkGroupMatchPatterns(groupEmail, group, matchPatterns):
_showGroup(groupEmail, 0)
def getGroupParents(cd, groupParents, groupEmail, groupName, kwargs):
groupParents[groupEmail] = {'name': groupName, 'parents': []}
_setUserGroupArgs(groupEmail, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
for parentGroup in entityList:
groupParents[groupEmail]['parents'].append(parentGroup['email'])
if parentGroup['email'] not in groupParents:
getGroupParents(cd, groupParents, parentGroup['email'], parentGroup['name'], kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, groupEmail)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
def showGroupParents(groupParents, groupEmail, role, i, count):
kvList = [groupEmail, f'{groupParents[groupEmail]["name"]}']
if role:
kvList.extend([Ent.Singular(Ent.ROLE), role])
printKeyValueListWithCount(kvList, i, count)
Ind.Increment()
for parentEmail in groupParents[groupEmail]['parents']:
showGroupParents(groupParents, parentEmail, None, 0, 0)
Ind.Decrement()
def addJsonGroupParents(groupParents, userGroup, groupEmail):
userGroup.setdefault('parents', [])
for parentEmail in groupParents[groupEmail]['parents']:
userGroup['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name'], 'parents': []})
addJsonGroupParents(groupParents, userGroup['parents'][-1], parentEmail)
def printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList):
if groupParents[groupEmail]['parents']:
for parentEmail in groupParents[groupEmail]['parents']:
row['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name']})
printGroupParents(groupParents, parentEmail, row, csvPF, delimiter, showParentsAsList)
del row['parents'][-1]
else:
if not showParentsAsList:
csvPF.WriteRowTitles(flattenJSON(row))
else:
crow = row.copy()
if 'Role' in row:
crow['Role'] = row['Role']
parents = crow.pop('parents')
crow['ParentsCount'] = len(parents)
crow['Parents'] = delimiter.join([parent['email'] for parent in parents])
crow['ParentsName'] = delimiter.join([parent['name'] for parent in parents])
csvPF.WriteRow(flattenJSON(crow))
# gam print grouptree <GroupEntity> [todrive <ToDriveAttribute>*]
# [showparentsaslist [<Boolean>]] [delimiter <Character>]
# [formatjson [quotechar <Character>]]
# gam show grouptree <GroupEntity>
# [formatjson]
def doPrintShowGroupTree():
cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile(['Group', 'Name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showParentsAsList = False
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'delimiter':
delimiter = getCharacter()
elif csvPF and myarg == 'showparentsaslist':
showParentsAsList = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and not FJQC.formatJSON:
if not showParentsAsList:
csvPF.SetIndexedTitles(['parents'])
else:
csvPF.AddTitles(['ParentsCount', 'Parents', 'ParentsName'])
groupParents = {}
i = 0
count = len(entityList)
if not csvPF and not FJQC.formatJSON:
performActionNumItems(count, Ent.GROUP_TREE)
for group in entityList:
i += 1
groupEmail = normalizeEmailAddressOrUID(group)
if groupEmail not in groupParents:
try:
groupName = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=groupEmail, fields='name')['name']
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
GAPI.invalid, GAPI.systemError) as e:
entityActionFailedWarning([Ent.GROUP, groupEmail], str(e), i, count)
continue
getGroupParents(cd, groupParents, groupEmail, groupName, kwargs)
if not FJQC.formatJSON:
if not csvPF:
showGroupParents(groupParents, groupEmail, None, i, count)
else:
row = {'Group': groupEmail, 'Name': groupParents[groupEmail]['name'], 'parents': []}
printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList)
else:
groupInfo = {'email': groupEmail, 'name': groupParents[groupEmail]['name'], 'parents': []}
addJsonGroupParents(groupParents, groupInfo, groupEmail)
if not csvPF:
printLine(json.dumps(cleanJSON(groupInfo), ensure_ascii=False, sort_keys=True))
else:
row = flattenJSON(groupInfo)
if csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'Group': groupEmail, 'Name': groupParents[groupEmail]['name'],
'JSON': json.dumps(cleanJSON(groupInfo),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Group Tree')
# gam create cigroup <EmailAddress>
# [copyfrom <GroupItem>] <GroupAttribute>
# [makeowner] [alias|aliases <CIGroupAliasList>]
# [security|makesecuritygroup]
# [dynamic <QueryDynamicGroup>]
def doCreateCIGroup():
doCreateGroup(ciGroupsAPI=True)
# gam update cigroups <GroupEntity> [email <EmailAddress>]
# [copyfrom <GroupItem>] <GroupAttribute>*
# [security|makesecuritygroup|dynamicsecurity|makedynamicsecuritygroup]
# [dynamic <QueryDynamicGroup>]
# [memberrestrictions <QueryMemberRestrictions>]
# [locked|unlocked]
# gam update cigroups <GroupEntity> add|create [<GroupRole>]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [expire|expires <Time>] [preview] [actioncsv]
# <UserTypeEntity>
# gam update cigroups <GroupEntity> delete|remove [<GroupRole>]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [preview] [actioncsv]
# <UserTypeEntity>
# gam update cigroups <GroupEntity> sync [<GroupRole>|ignorerole]
# [usersonly|groupsonly] [addonly|removeonly]
# [notsuspended|suspended] [notarchived|archived]
# [expire|expires <Time>] [preview] [actioncsv]
# <UserTypeEntity>
# gam update cigroups <GroupEntity> update [<GroupRole>]
# [usersonly|groupsonly]
# [notsuspended|suspended] [notarchived|archived]
# [expire|expires <Time>] [preview] [actioncsv]
# <UserTypeEntity>
# gam update cigroups <GroupEntity> clear [member] [manager] [owner]
# [usersonly|groupsonly]
# [emailclearpattern|emailretainpattern <REMatchPattern>]
# [preview] [actioncsv]
def doUpdateCIGroups():
def _getExpireTime(role):
if role == Ent.ROLE_MEMBER and checkArgumentPresent(['expire', 'expires']):
return getTimeOrDeltaFromNow()
return None
def _getPreviewActionCSV():
preview = checkArgumentPresent('preview')
if checkArgumentPresent('actioncsv'):
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
else:
csvPF = None
return (preview, csvPF)
def _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count):
roleLower = role.lower()
if roleLower in GROUP_ROLES_MAP:
return (GROUP_ROLES_MAP[roleLower], groupMemberLists[origGroup][role])
entityActionNotPerformedWarning([entityType, group, Ent.ROLE, role], Msg.INVALID_ROLE.format(','.join(sorted(GROUP_ROLES_MAP))), i, count)
return (None, None)
def _getRoleGroupMemberType(defaultRole=Ent.ROLE_MEMBER, allowIgnoreRole=False):
if not allowIgnoreRole or not checkArgumentPresent(['ignorerole']):
role = getChoice(GROUP_ROLES_MAP, defaultChoice=defaultRole, mapChoice=True)
else:
role = Ent.ROLE_ALL
groupMemberType = getChoice({'usersonly': Ent.TYPE_USER, 'groupsonly': Ent.TYPE_GROUP}, defaultChoice='ALL', mapChoice=True)
return (role, groupMemberType)
def _getMemberEmail(member):
if member['type'] == Ent.TYPE_CUSTOMER:
return member['id']
return member.get('preferredMemberKey', {}).get('id', '')
def checkDynamicGroup(ci, group, i, count):
try:
result = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=group, fields='labels')
if CIGROUP_DYNAMIC_LABEL in result.get('labels', {}):
entityActionNotPerformedWarning([entityType, group], Msg.DYNAMIC_GROUP_MEMBERSHIP_CANNOT_BE_MODIFIED, i, count)
return True
return False
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
return True
# Convert foo@googlemail.com to foo@gmail.com; eliminate periods in name for foo.bar@gmail.com
def _cleanConsumerAddress(emailAddress, mapCleanToOriginal):
atLoc = emailAddress.find('@')
if atLoc > 0:
if emailAddress[atLoc+1:] in {'gmail.com', 'googlemail.com'}:
cleanEmailAddress = emailAddress[:atLoc].replace('.', '')+'@gmail.com'
if cleanEmailAddress != emailAddress:
mapCleanToOriginal[cleanEmailAddress] = emailAddress
return cleanEmailAddress
return emailAddress
def _previewAction(group, members, role, jcount, action):
Ind.Increment()
j = 0
for member in members:
j += 1
entityActionPerformed([entityType, group, role, member], j, jcount)
Ind.Decrement()
if csvPF:
for member in members:
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.PerformedName(action), 'message': Act.PREVIEW})
def _showSuccess(group, member, role, expireTime, j, jcount, optMsg=None):
kvList = []
if role is not None and role != 'None':
kvList.append(f'{Ent.Singular(Ent.ROLE)}: {role}')
if expireTime:
kvList.extend(['expireTime', formatLocalTime(expireTime)])
if optMsg:
kvList.append(optMsg)
entityActionPerformedMessage([entityType, group, Ent.MEMBER, member], ', '.join(kvList), j, jcount)
if csvPF:
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Performed(), 'message': Act.SUCCESS})
def _showFailure(group, member, role, errMsg, j, jcount):
entityActionFailedWarning([entityType, group, Ent.MEMBER, member], errMsg, j, jcount)
if csvPF:
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Failed(), 'message': errMsg})
def _batchAddGroupMembers(group, i, count, addMembers, role, expireTime):
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
jcount = len(addMembers)
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
if jcount == 0:
return
if preview:
_previewAction(group, addMembers, role, jcount, Act.ADD)
return
Ind.Increment()
j = 0
for member in addMembers:
j += 1
body = {'preferredMemberKey': {'id': member}, 'roles': [{'name': Ent.ROLE_MEMBER}]}
if role != Ent.ROLE_MEMBER:
body['roles'].append({'name': role})
elif expireTime not in {None, NEVER_TIME}:
body['roles'][0]['expiryDetail'] = {'expireTime': expireTime}
try:
callGAPI(ci.groups().memberships(), 'create',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.DUPLICATE, GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND,
GAPI.INVALID_MEMBER, GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED,
GAPI.CONDITION_NOT_MET, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED,
GAPI.ALREADY_EXISTS, GAPI.CONFLICT],
parent=group, body=body, fields='')
_showSuccess(group, member, role, expireTime, j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(entityType, group, i, count)
except (GAPI.duplicate, GAPI.memberNotFound, GAPI.resourceNotFound,
GAPI.invalidMember, GAPI.cyclicMembershipsNotAllowed, GAPI.conditionNotMet,
GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.alreadyExists) as e:
_showFailure(group, member, role, str(e), j, jcount)
except GAPI.conflict:
_showSuccess(group, member, role, expireTime, j, jcount, Msg.ACTION_MAY_BE_DELAYED)
Ind.Decrement()
def _batchRemoveGroupMembers(group, i, count, removeMembers, role):
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
jcount = len(removeMembers)
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
if jcount == 0:
return
if preview:
_previewAction(group, removeMembers, role, jcount, Act.REMOVE)
return
Ind.Increment()
j = 0
for member in removeMembers:
j += 1
memberEmail = member['email']
try:
callGAPI(ci.groups().memberships(), 'delete',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
name=member['name'])
_showSuccess(group, memberEmail, role, None, j, jcount)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
_showFailure(group, memberEmail, role, str(e), j, jcount)
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
cib = None
entityType = Ent.CLOUD_IDENTITY_GROUP
csvPF = None
getBeforeUpdate = preview = False
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
CL_subCommand = getChoice(UPDATE_GROUP_SUBCMDS, defaultChoice=None)
lockGroup = None
if not CL_subCommand:
gs_body = {}
ci_body = {}
se_body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'email':
ci_body['groupKey'] = {'id': getEmailAddress(noUid=True)}
elif myarg == 'getbeforeupdate':
getBeforeUpdate = True
elif myarg == 'dynamic':
ci_body.setdefault('dynamicGroupMetadata', {'queries': []})
ci_body['dynamicGroupMetadata']['queries'].append({'resourceType': 'USER',
'query': getString(Cmd.OB_QUERY)})
elif myarg in {'security', 'makesecuritygroup'}:
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
CIGROUP_SECURITY_LABEL: ''}
elif myarg in {'dynamicsecurity', 'makedynamicsecuritygroup'}:
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
CIGROUP_DYNAMIC_LABEL: '',
CIGROUP_SECURITY_LABEL: ''}
elif myarg in {'lockedsecurity', 'makelockedsecuritygroup'}:
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
CIGROUP_LOCKED_LABEL: '',
CIGROUP_SECURITY_LABEL: ''}
elif myarg in ['memberrestriction', 'memberrestrictions']:
query = getString(Cmd.OB_QUERY, minLen=0)
member_types = {'USER': '1', 'SERVICE_ACCOUNT': '2', 'GROUP': '3',}
for key, val in iter(member_types.items()):
query = query.replace(key, val)
se_body['memberRestriction'] = {'query': query}
elif myarg == 'locked':
lockGroup = True
elif myarg == 'unlocked':
lockGroup = False
elif myarg == 'json':
gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS))
elif myarg == 'accesstype':
gs_body.update(getChoice(GROUP_ACCESS_TYPE_CHOICE_MAP, mapChoice=True))
else:
getGroupAttrValue(myarg, gs_body)
if gs_body:
gs = buildGAPIObject(API.GROUPSSETTINGS)
gs_body = getSettingsFromGroup(cd, ','.join(entityList), gs, gs_body)
for k, v in iter(GROUP_CIGROUP_FIELDS_MAP.items()):
if k in gs_body:
ci_body[v] = gs_body.pop(k)
if gs_body:
if not getBeforeUpdate:
settings = gs_body
elif not ci_body:
return
elif not ci_body and not se_body and lockGroup is None:
return
Act.Set(Act.UPDATE)
i = 0
count = len(entityList)
for group in entityList:
i += 1
ci, _, group = convertGroupCloudIDToEmail(ci, group, i, count)
if gs_body and not GroupIsAbuseOrPostmaster(group):
try:
if group.find('@') == -1: # group settings API won't take uid so we make sure cd API is used so that we can grab real email.
group = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=group, fields='email')['email']
if getBeforeUpdate:
settings = callGAPI(gs.groups(), 'get',
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(group), fields='*')
settings.update(gs_body)
if not checkReplyToCustom(group, settings, i, count):
continue
except GAPI.notFound:
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
continue
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.backendError, GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([entityType, group], str(e), i, count)
continue
if gs_body and not GroupIsAbuseOrPostmaster(group):
try:
callGAPI(gs.groups(), 'update',
bailOnInvalidError='messageModerationLevel' in settings,
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
groupUniqueId=mapGroupEmailForSettings(group), body=settings, fields='')
except GAPI.notFound:
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
continue
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([entityType, group], str(e), i, count)
continue
except GAPI.required:
entityActionFailedWarning([entityType, group], Msg.INVALID_JSON_SETTING, i, count)
continue
if ci_body or se_body or lockGroup is not None:
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
if not name:
continue
cipl = ci
twoUpdates = False
if 'labels' in ci_body or lockGroup is not None:
try:
cigInfo = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields='labels')
except (GAPI.notFound, GAPI.groupNotFound, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
GAPI.invalid, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden, GAPI.badRequest,
GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
continue
# If a group currently isn't a security group or locked, and we want to add security and locked,
# we have to do two commands to meet a beta requirement
ci_body.setdefault('labels', {})
if ((CIGROUP_SECURITY_LABEL not in cigInfo['labels']) and
(CIGROUP_LOCKED_LABEL not in cigInfo['labels']) and
((CIGROUP_SECURITY_LABEL in ci_body['labels']) and
((CIGROUP_LOCKED_LABEL in ci_body['labels']) or lockGroup))):
twoUpdates = True
ci_body['labels'].update(cigInfo['labels'])
if lockGroup is not None:
if lockGroup:
if CIGROUP_LOCKED_LABEL not in ci_body['labels']:
ci_body['labels'][CIGROUP_LOCKED_LABEL] = ''
else:
if CIGROUP_LOCKED_LABEL in ci_body['labels']:
ci_body['labels'].pop(CIGROUP_LOCKED_LABEL)
if CIGROUP_LOCKED_LABEL in ci_body['labels']:
if cib is None:
cib = buildGAPIObject(API.CLOUDIDENTITY_GROUPS_BETA)
cipl = cib
if ci_body:
try:
if twoUpdates:
ci_body['labels'].pop(CIGROUP_LOCKED_LABEL)
callGAPI(cipl.groups(), 'patch',
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS,
retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
ci_body['labels'][CIGROUP_LOCKED_LABEL] = ''
callGAPI(cipl.groups(), 'patch',
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS,
retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
continue
if se_body:
# It seems like a bug that API requires /securitySettings appended to name.
# We'll see if Google servers change this at some point.
try:
callGAPI(ci.groups(), 'updateSecuritySettings',
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=f'{name}/securitySettings', updateMask='member_restriction.query', body=se_body)
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
continue
entityActionPerformed([entityType, group], i, count)
elif CL_subCommand in {'create', 'add'}:
baseRole, groupMemberType = _getRoleGroupMemberType()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
expireTime = _getExpireTime(baseRole)
preview, csvPF = _getPreviewActionCSV()
_, addMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = addMembers if isinstance(addMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
roleList = [baseRole]
if groupMemberLists:
if not subkeyRoleField:
addMembers = groupMemberLists[group]
else:
roleList = groupMemberLists[group]
origGroup = group
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
if not group or checkDynamicGroup(ci, parent, i, count):
continue
for role in roleList:
if groupMemberLists and subkeyRoleField:
role, addMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
if role is None:
continue
_batchAddGroupMembers(parent, i, count,
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True, ciGroupsAPI=True) for member in addMembers],
role, expireTime)
elif CL_subCommand in {'delete', 'remove'}:
baseRole, groupMemberType = _getRoleGroupMemberType()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
preview, csvPF = _getPreviewActionCSV()
_, removeMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = removeMembers if isinstance(removeMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
roleList = [baseRole]
if groupMemberLists:
if not subkeyRoleField:
removeMembers = groupMemberLists[group]
else:
roleList = groupMemberLists[group]
origGroup = group
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
if not group or checkDynamicGroup(ci, parent, i, count):
continue
for role in roleList:
if groupMemberLists and subkeyRoleField:
role, removeMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
if role is None:
continue
Act.Set([Act.DELETE, Act.DELETE_PREVIEW][preview])
jcount = len(removeMembers)
entityPerformActionNumItems([entityType, group], jcount, Ent.MEMBER, i, count)
if jcount == 0:
continue
if preview:
_previewAction(group, removeMembers, role or Ent.ROLE_USER, jcount, Act.DELETE)
continue
Ind.Increment()
j = 0
for member in removeMembers:
j += 1
memberEmail = convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True, ciGroupsAPI=True)
try:
memberName = callGAPI(ci.groups().memberships(), 'lookup',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
parent=parent, memberKey_id=memberEmail, fields='name').get('name')
callGAPI(ci.groups().memberships(), 'delete',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
name=memberName)
_showSuccess(group, memberEmail, role, None, j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden,
GAPI.notFound, GAPI.memberNotFound, GAPI.permissionDenied, GAPI.invalidMember, GAPI.failedPrecondition) as e:
_showFailure(group, memberEmail, role, str(e), j, jcount)
Ind.Decrement()
elif CL_subCommand == 'sync':
baseRole, groupMemberType = _getRoleGroupMemberType(allowIgnoreRole=True)
ignoreRole = baseRole == Ent.ROLE_ALL
syncOperation = getSyncOperation()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
expireTime = _getExpireTime(baseRole)
preview, csvPF = _getPreviewActionCSV()
_, syncMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = syncMembers if isinstance(syncMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
syncMembersSets = {}
syncMembersMaps = {}
currentMembersSets = {}
currentMembersMaps = {}
if groupMemberLists is None:
syncMembersSets[baseRole] = set()
syncMembersMaps[baseRole] = {}
for member in syncMembers:
syncMembersSets[baseRole].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True, ciGroupsAPI=True), syncMembersMaps[baseRole]))
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
origGroup = group
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
if not group or checkDynamicGroup(ci, parent, i, count):
continue
if groupMemberLists is None:
roleList = [baseRole]
else:
if not subkeyRoleField:
roleList = [baseRole]
else:
roleList = groupMemberLists[origGroup]
for role in roleList:
role = role.upper()
syncMembersSets[role] = set()
syncMembersMaps[role] = {}
rolesSet = set()
for role in roleList:
origRole = role
role = role.upper()
if groupMemberLists is None:
rolesSet.add(role)
else:
if not subkeyRoleField:
rolesSet.add(role)
syncMembers = groupMemberLists[origGroup]
else:
role, syncMembers = _validateSubkeyRoleGetMembers(group, origRole, origGroup, groupMemberLists, i, count)
if role is None:
continue
rolesSet.add(role)
for member in syncMembers:
syncMembersSets[role].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True), syncMembersMaps[role]))
if not rolesSet:
continue
memberRoles = ','.join(sorted(rolesSet))
printGettingAllEntityItemsForWhom(memberRoles, group, entityType=entityType)
try:
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
parent=parent, view='FULL',
fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)',
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, group)
continue
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e))
continue
currentMembersNames = {}
for role in rolesSet:
currentMembersSets[role] = set()
currentMembersMaps[role] = {}
for member in result:
getCIGroupMemberRoleFixType(member)
role = member['role'] if not ignoreRole else Ent.ROLE_ALL
email = member.get('preferredMemberKey', {}).get('id', '')
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
cleanAddress = _cleanConsumerAddress(email, currentMembersMaps[role])
currentMembersSets[role].add(cleanAddress)
currentMembersNames[cleanAddress] = member['name']
del result
if syncOperation != 'addonly':
for role in rolesSet:
_batchRemoveGroupMembers(parent, i, count,
[{'name': currentMembersNames[emailAddress],
'email': currentMembersMaps[role].get(emailAddress, emailAddress)} for emailAddress in currentMembersSets[role]-syncMembersSets[role]],
role)
if syncOperation != 'removeonly':
for role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER, Ent.ROLE_ALL]:
if role in rolesSet:
_batchAddGroupMembers(parent, i, count,
[syncMembersMaps[role].get(emailAddress, emailAddress) for emailAddress in syncMembersSets[role]-currentMembersSets[role]],
role if role != Ent.ROLE_ALL else Ent.ROLE_MEMBER, expireTime)
elif CL_subCommand == 'update':
baseRole, groupMemberType = _getRoleGroupMemberType()
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
expireTime = _getExpireTime(baseRole)
preview, csvPF = _getPreviewActionCSV()
_, updateMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
groupMemberLists = updateMembers if isinstance(updateMembers, dict) else None
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
checkForExtraneousArguments()
i = 0
count = len(entityList)
for group in entityList:
i += 1
roleList = [baseRole]
if groupMemberLists:
if not subkeyRoleField:
updateMembers = groupMemberLists[group]
else:
roleList = groupMemberLists[group]
origGroup = group
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
if not group or checkDynamicGroup(ci, parent, i, count):
continue
for role in roleList:
if groupMemberLists and subkeyRoleField:
role, updateMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
if role is None:
continue
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][preview])
jcount = len(updateMembers)
entityPerformActionNumItems([entityType, group], jcount, Ent.MEMBER, i, count)
if jcount == 0:
continue
if preview:
_previewAction(group, updateMembers, role, jcount, Act.UPDATE)
continue
Ind.Increment()
j = 0
for member in updateMembers:
j += 1
memberEmail = convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
checkForCustomerId=True, ciGroupsAPI=True)
try:
memberName = callGAPI(ci.groups().memberships(), 'lookup',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
parent=parent, memberKey_id=memberEmail, fields='name').get('name')
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden,
GAPI.notFound, GAPI.memberNotFound, GAPI.permissionDenied, GAPI.invalidMember) as e:
_showFailure(group, memberEmail, role, str(e), j, jcount)
continue
preUpdateRoles = []
addRoles = []
removeRoles = []
postUpdateRoles = []
memberRoles = callGAPI(ci.groups().memberships(), 'get',
name=memberName, fields='name,preferredMemberKey,roles,type')
getCIGroupMemberRoleFixType(memberRoles)
current_roles = [crole['name'] for crole in memberRoles['roles']]
# When upgrading role, strip any expiryDetail from member before role changes
if role != Ent.ROLE_MEMBER:
if 'expireTime' in memberRoles:
preUpdateRoles.append({'fieldMask': 'expiryDetail.expireTime',
'membershipRole': {'name': Ent.ROLE_MEMBER, 'expiryDetail': {'expireTime': None}}})
# When downgrading role or simply updating member expireTime, update expiryDetail after role changes
elif expireTime:
postUpdateRoles.append({'fieldMask': 'expiryDetail.expireTime',
'membershipRole': {'name': role, 'expiryDetail': {'expireTime': expireTime if expireTime != NEVER_TIME else None}}})
for crole in current_roles:
if crole not in {Ent.ROLE_MEMBER, role}:
removeRoles.append(crole)
if role not in current_roles:
new_role = {'name': role}
if role == Ent.ROLE_MEMBER and expireTime not in {None, NEVER_TIME}:
new_role['expiryDetail'] = {'expireTime': expireTime}
postUpdateRoles = []
addRoles.append(new_role)
bodys = []
if preUpdateRoles:
bodys.append({'updateRolesParams': preUpdateRoles})
if addRoles:
bodys.append({'addRoles': addRoles})
if removeRoles:
bodys.append({'removeRoles': removeRoles})
if postUpdateRoles:
bodys.append({'updateRolesParams': postUpdateRoles})
errors = False
for body in bodys:
try:
callGAPI(ci.groups().memberships(), 'modifyMembershipRoles',
throwReasons=[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.FAILED_PRECONDITION,
GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=memberName, body=body)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.failedPrecondition,
GAPI.invalidArgument, GAPI.permissionDenied) as e:
_showFailure(group, memberEmail, role, str(e), j, jcount)
errors = True
break
if not errors:
_showSuccess(group, memberEmail, role, None, j, jcount)
Ind.Decrement()
else: #clear
rolesSet = set()
groupMemberType = 'ALL'
isSuspended = None
emailMatchPattern = None
clearMatch = True
qualifier = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[myarg])
elif myarg == 'usersonly':
groupMemberType = Ent.TYPE_USER
elif myarg == 'groupsonly':
groupMemberType = Ent.TYPE_GROUP
elif myarg in {'emailclearpattern', 'emailretainpattern'}:
emailMatchPattern = getREPattern(re.IGNORECASE)
clearMatch = myarg == 'emailclearpattern'
elif myarg == 'preview':
preview = True
elif myarg == 'actioncsv':
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
else:
unknownArgumentExit()
Act.Set(Act.REMOVE)
if not rolesSet:
rolesSet.add(Ent.ROLE_MEMBER)
memberRoles = ','.join(sorted(rolesSet))
i = 0
count = len(entityList)
for group in entityList:
i += 1
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
if not group or checkDynamicGroup(ci, parent, i, count):
continue
printGettingAllEntityItemsForWhom(memberRoles, group, qualifier=qualifier, entityType=entityType)
try:
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
parent=parent, view='FULL',
fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)',
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(entityType, group, i, count)
continue
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([entityType, group], str(e), i, count)
continue
removeMembers = {}
for role in rolesSet:
removeMembers[role] = []
for member in result:
getCIGroupMemberRoleFixType(member)
memberName = member['name']
memberEmail = _getMemberEmail(member)
role = member['role']
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
if emailMatchPattern is None:
removeMembers[role].append({'name': memberName, 'email': memberEmail})
elif member['type'] == Ent.TYPE_CUSTOMER:
pass
elif emailMatchPattern.match(email):
if clearMatch:
removeMembers[role].append({'name': memberName, 'email': memberEmail})
else:
if not clearMatch:
removeMembers[role].append({'name': memberName, 'email': memberEmail})
del result
for role in rolesSet:
_batchRemoveGroupMembers(group, i, count, removeMembers[role], role)
if csvPF:
csvPF.writeCSVfile('Cloud Identity Group Updates')
# gam delete cigroups <GroupEntity>
def doDeleteCIGroups():
doDeleteGroups(ciGroupsAPI=True)
CIGROUP_MEMBER_TYPES_MAP = {
'cbcmbrowser': Ent.TYPE_CBCM_BROWSER,
'chromeosdevice': Ent.TYPE_OTHER,
'customer': Ent.TYPE_CUSTOMER,
'group': Ent.TYPE_GROUP,
'other': Ent.TYPE_OTHER,
'serviceaccount': Ent.TYPE_SERVICE_ACCOUNT,
'user': Ent.TYPE_USER,
}
ALL_CIGROUP_MEMBER_TYPES = {
Ent.TYPE_CBCM_BROWSER, Ent.TYPE_CUSTOMER, Ent.TYPE_GROUP,
Ent.TYPE_OTHER, Ent.TYPE_SERVICE_ACCOUNT, Ent.TYPE_USER}
def getCIGroupMemberTypes(myarg, typesSet):
if myarg in {'type', 'types'}:
for gtype in getString(Cmd.OB_GROUP_TYPE_LIST).lower().replace('_', '').replace(',', ' ').split():
if gtype in CIGROUP_MEMBER_TYPES_MAP:
typesSet.add(CIGROUP_MEMBER_TYPES_MAP[gtype])
else:
invalidChoiceExit(gtype, CIGROUP_MEMBER_TYPES_MAP, True)
else:
return False
return True
# gam info cigroups <GroupEntity>
# [nousers|membertree] [quick] [noaliases] [nojoindate] [showupdatedate]
# [nosecurity|nosecuritysettings]
# [allfields|<CIGroupFieldName>*|(fields <CIGroupFieldNameList>)]
# [roles <GroupRoleList>] [members] [managers] [owners]
# [internal] [internaldomains <DomainList>] [external]
# [types <CIGroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [formatjson]
def doInfoCIGroups():
def printCIGroupMemberTree(group_id, showRole):
if not group_id in cachedGroupMembers:
try:
cachedGroupMembers[group_id] = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
throwReasons=GAPI.MEMBERS_THROW_REASONS,
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
parent=group_id, view='FULL',
fields='nextPageToken,memberships(*)',
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, group_id, i, count)
return
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group_id], str(e), i, count)
return
for member in cachedGroupMembers[group_id]:
member_id = member.get('name', '')
member_id = member_id.split('/')[-1]
member_email = member.get('preferredMemberKey', {}).get('id')
member_type = member.get('type', 'USER').lower()
if showRole:
getCIGroupMemberRoleFixType(member)
printKeyValueList([member['role'].lower(), f'{member_email} ({member_type})'])
else:
writeStdout(f'{Ind.Spaces()}{member_email} ({member_type})\n')
if member_type == 'group':
Ind.Increment()
printCIGroupMemberTree(f'groups/{member_id}', False)
Ind.Decrement()
def initGroupFieldsLists():
if not groupFieldsLists['ci']:
groupFieldsLists['ci'] = ['groupKey']
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
getAliases = getSecuritySettings = getUsers = True
showJoinDate = True
showMemberTree = showUpdateDate = False
FJQC = FormatJSONQuoteChar()
members = []
groupFieldsLists = {'ci': None}
entityType = Ent.MEMBER
rolesSet = set()
typesSet = set()
memberOptions = initMemberOptions()
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
cachedGroupMembers = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'quick':
getAliases = getUsers = False
elif myarg == 'nousers':
getUsers = False
elif myarg == 'membertree':
showMemberTree = True
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
getUsers = True
elif getCIGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif myarg == 'noaliases':
getAliases = False
elif myarg in {'allfields', 'ciallfields', 'allcifields'}:
if not groupFieldsLists['ci']:
groupFieldsLists['ci'] = []
for field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
elif myarg in CIGROUP_FIELDS_CHOICE_MAP:
initGroupFieldsLists()
addFieldToFieldsList(myarg, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
elif myarg in {'fields', 'cifields'}:
initGroupFieldsLists()
for field in _getFieldsList():
if field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
else:
invalidChoiceExit(field, CIGROUP_FIELDS_CHOICE_MAP, True)
elif myarg == 'nojoindate':
showJoinDate = False
elif myarg == 'showupdatedate':
showUpdateDate = True
elif myarg in ['nosecurity', 'nosecuritysettings']:
getSecuritySettings = False
else:
FJQC.GetFormatJSON(myarg)
if not typesSet:
typesSet = ALL_CIGROUP_MEMBER_TYPES
fields = getFieldsFromFieldsList(groupFieldsLists['ci'])
if not showJoinDate and not showUpdateDate:
view = 'BASIC'
pageSize = 1000
else:
view = 'FULL'
pageSize = 500
checkCategory = memberDisplayOptions['showCategory']
i = 0
count = len(entityList)
for group in entityList:
i += 1
_, name, group = convertGroupEmailToCloudID(ci, group, i, count)
if not name:
continue
try:
cigInfo = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields=fields)
group = cigInfo['groupKey']['id']
if not getAliases:
cigInfo.pop('additionalGroupKeys', None)
if getUsers and not showMemberTree:
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
parent=name, view=view, fields='*', pageSize=pageSize)
members = []
for member in result:
getCIGroupMemberRoleFixType(member)
if (member['type'] in typesSet and
_checkMemberRole(member, rolesSet) and
checkCIMemberMatch(member, memberOptions) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
members.append(member)
if getSecuritySettings:
cigInfo['SecuritySettings'] = callGAPI(ci.groups(), 'getSecuritySettings',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=f'{name}/securitySettings', readMask='*')
if FJQC.formatJSON:
if getUsers and not showMemberTree:
cigInfo['members'] = members
printLine(json.dumps(cleanJSON(cigInfo, timeObjects=CIGROUP_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
continue
_showCIGroup(cigInfo, group, i, count)
if getUsers and not showMemberTree:
Ind.Increment()
printEntitiesCount(entityType, members)
Ind.Increment()
for member in members:
memberEmail = member.get('preferredMemberKey', {}).get('id', member['name'])
getCIGroupMemberRoleFixType(member)
memberDetails = [member['role'].lower(), f'{memberEmail} ({member["type"].lower()})']
if checkCategory:
memberDetails[1] += f' ({member["category"]})'
if showJoinDate:
memberDetails.extend(['joined', formatLocalTime(member['createTime']) if 'createTime' in member else UNKNOWN])
if showUpdateDate:
memberDetails.extend(['updated', formatLocalTime(member['updateTime']) if 'updateTime' in member else UNKNOWN])
if 'expireTime' in member:
memberDetails.extend(['expires', formatLocalTime(member['expireTime'])])
printKeyValueList(memberDetails)
Ind.Decrement()
printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.CLOUD_IDENTITY_GROUP)), len(members)])
Ind.Decrement()
elif showMemberTree:
Ind.Increment()
printEntity([Ent.MEMBERSHIP_TREE, ''])
Ind.Increment()
printCIGroupMemberTree(name, True)
Ind.Decrement()
Ind.Decrement()
except GAPI.notFound:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.groupNotFound, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
GAPI.invalid, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden, GAPI.badRequest,
GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
def checkCIGroupShowOwnedBy(showOwnedBy, members):
for member in members:
if member['preferredMemberKey']['id'] == showOwnedBy:
if member['role'] == Ent.ROLE_OWNER:
return True
return False
def updateFieldsForCIGroupMatchPatterns(matchPatterns, fieldsList, csvPF=None):
for field in ['displayName', 'description']:
if field in matchPatterns:
if csvPF is not None:
csvPF.AddField(field, CIGROUP_FIELDS_CHOICE_MAP, fieldsList)
else:
fieldsList.append(field)
CIPOLICY_TIME_OBJECTS = {'createTime', 'updateTime'}
def _filterPolicies(ci, pageMessage, ifilter):
try:
policies = callGAPIpages(ci.policies(), 'list', 'policies',
pageMessage=pageMessage,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
filter=ifilter,
fields='nextPageToken,policies(name,policyQuery(group,orgUnit,sortOrder),type,setting)',
pageSize=100)
# Google returns unordered results, sort them by setting type
return sorted(policies, key=lambda p: p.get('setting', {}).get('type', ''))
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.POLICY, ifilter], str(e))
return []
# Policies where GAM should offer additional guidance and information
CIPOLICY_ADDITIONAL_WARNINGS = {
'settings/drive_and_docs.external_sharing': {
'warningType': 'SUPERSEDED_POLICY',
'warningMessage': 'CAUTION: Drive Sharing settings are superseded by Drive Trust Rules if Trust Rules has been enabled for your domain. Drive Trust Rule settings are not available in the Policy API today so GAM is not able to check if Trust Rules is enabled and if the settings/drive_and_docs.external_sharing policies are actually in effect for your domain. If Drive Trust Rules is enabled for your domain then this settings/drive_and_docs.external_sharing policy does not accurately reflect your current Drive sharing settings.'
}
}
def _getPolicyAppNameFromId(httpObj, app):
app['applicationName'] = UNKNOWN
appId = app['applicationId']
url = f'https://workspace.google.com/marketplace/app/_/{appId}'
try:
resp, content = httpObj.request(url, 'GET')
except:
return
if resp.status != 200:
return
if isinstance(content, bytes):
content = content.decode()
pattern = f'https://workspace.google.com/marketplace/app/(.+?)/{appId}'
a = re.search(pattern, content)
if a:
app['applicationName'] = a.group(1)
def _cleanPolicy(policy, add_warnings, no_appnames,
groupEmailPattern, orgUnitPathPattern,
cd, groups_ci):
# convert any wordlists into spaced strings to reduce output complexity
if policy['setting']['type'] == 'settings/detector.word_list':
policy['setting']['value']['wordList'] = ' '.join(policy['setting']['value']['wordList']['words'])
# get application name for application id
if policy['setting']['type'] == 'settings/workspace_marketplace.apps_allowlist' and not no_appnames:
httpObj = getHttpObj(timeout=10)
for app in policy['setting']['value'].get('apps', []):
_getPolicyAppNameFromId(httpObj, app)
# add any warnings to applicable policies
if add_warnings and policy['setting']['type'] in CIPOLICY_ADDITIONAL_WARNINGS:
policy['warning'] = CIPOLICY_ADDITIONAL_WARNINGS[policy['setting']['type']]
if groupId := policy['policyQuery'].get('group'):
_, _, policy['policyQuery']['groupEmail'] = convertGroupCloudIDToEmail(groups_ci, groupId)
# all groups are in the root OU so the orgUnit attribute is useless
policy['policyQuery'].pop('orgUnit', None)
if groupEmailPattern is not None:
return groupEmailPattern.match(policy['policyQuery']['groupEmail'])
if orgUnitPathPattern is not None:
return False
elif orgId := policy['policyQuery'].get('orgUnit'):
policy['policyQuery']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, orgId)
if orgUnitPathPattern is not None:
return orgUnitPathPattern.match(policy['policyQuery']['orgUnitPath'])
if groupEmailPattern is not None:
return False
return True
def _showPolicy(policy, FJQC, i=0, count=0):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(policy, timeObjects=CIPOLICY_TIME_OBJECTS),
ensure_ascii=False,
sort_keys=True))
return
printEntity([Ent.POLICY, policy['name']], i, count)
Ind.Increment()
policy.pop('name')
showJSON(None, policy, timeObjects=CIPOLICY_TIME_OBJECTS)
printBlankLine()
Ind.Decrement()
def _showPolicies(policies, FJQC, add_warnings, no_appnames,
groupEmailPattern, orgUnitPathPattern,
cd, groups_ci):
count = len(policies)
if groupEmailPattern is None and orgUnitPathPattern is None:
performActionNumItems(count, Ent.POLICY)
else:
performActionModifierNumItems(Msg.MAXIMUM_OF, count, Ent.POLICY)
Ind.Increment()
i = 0
for policy in policies:
i += 1
if _cleanPolicy(policy, add_warnings, no_appnames,
groupEmailPattern, orgUnitPathPattern,
cd, groups_ci):
_showPolicy(policy, FJQC, i, count)
Ind.Decrement()
def _checkPoliciesWithDASA():
if GC.Values[GC.ENABLE_DASA]:
systemErrorExit(USAGE_ERROR_RC,
Msg.COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA.format(Act.ToPerform().lower(), Cmd.ARG_CIPOLICIES))
# gam info policies <CIPolicyNameEntity>
# [nowarnings] [noappnames]
# [formatjson]
def doInfoCIPolicies():
_checkPoliciesWithDASA()
groups_ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
ci = buildGAPIObject(API.CLOUDIDENTITY_POLICY)
cd = buildGAPIObject(API.DIRECTORY)
entityList = getEntityList(Cmd.OB_CIPOLICY_NAME_ENTITY)
FJQC = FormatJSONQuoteChar()
add_warnings = True
no_appnames = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'nowarnings':
add_warnings = False
elif myarg == 'noappnames':
no_appnames=True
else:
FJQC.GetFormatJSON(myarg)
i = 0
count = len(entityList)
for pname in entityList:
i += 1
if pname.startswith('policies/'):
try:
policies = [callGAPI(ci.policies(), 'get',
bailOnInternalError=True,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=pname,
fields='name,policyQuery(group,orgUnit,sortOrder),type,setting')]
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.POLICY, pname], str(e), i, count)
continue
else:
if pname.startswith('settings/'):
pname = pname.split('/')[1]
ifilter = f"setting.type.matches('{pname}')"
printGettingAllAccountEntities(Ent.POLICY, ifilter)
policies = _filterPolicies(ci, getPageMessage(), ifilter)
_showPolicies(policies, FJQC, add_warnings, no_appnames,
None, None, cd, groups_ci)
# gam print policies [todrive <ToDriveAttribute>*]
# [filter <String>] [nowarnings] [noappnames]
# [group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
# [formatjson [quotechar <Character>]]
# gam show policies
# [filter <String>] [nowarnings] [noappnames]
# [group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
# [formatjson]
def doPrintShowCIPolicies():
def _printPolicy(policy):
row = flattenJSON(policy, timeObjects=CIPOLICY_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': policy['name'],
'JSON': json.dumps(cleanJSON(policy, timeObjects=CIPOLICY_TIME_OBJECTS),
ensure_ascii=False,
sort_keys=True)})
_checkPoliciesWithDASA()
groups_ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
ci = buildGAPIObject(API.CLOUDIDENTITY_POLICY)
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
ifilter = None
add_warnings = True
no_appnames = False
groupEmailPattern = orgUnitPathPattern = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'filter':
ifilter = getString(Cmd.OB_STRING)
elif myarg == 'nowarnings':
add_warnings = False
elif myarg == 'noappnames':
no_appnames = True
elif myarg == 'group':
groupEmailPattern = getREPattern(re.IGNORECASE)
elif myarg in {'ou', 'org', 'orgunit'}:
orgUnitPathPattern = getREPattern(re.IGNORECASE)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
printGettingAllAccountEntities(Ent.POLICY, ifilter)
policies = _filterPolicies(ci, getPageMessage(), ifilter)
if not csvPF:
_showPolicies(policies, FJQC, add_warnings, no_appnames,
groupEmailPattern, orgUnitPathPattern,
cd, groups_ci)
else:
for policy in policies:
if _cleanPolicy(policy, add_warnings, no_appnames,
groupEmailPattern, orgUnitPathPattern,
cd, groups_ci):
_printPolicy(policy)
if csvPF:
csvPF.writeCSVfile('Policies')
PRINT_CIGROUPS_JSON_TITLES = ['email', 'JSON']
# gam print cigroups [todrive <ToDriveAttribute>*]
# [(cimember|ciowner <UserItem>)|(select <GroupEntity>)|(query <String>)]
# [showownedby <UserItem>]
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
# [descriptionmatchpattern [not] <REMatchPattern>]
# [basic|allfields|(<CIGroupFieldName>* [fields <CIGroupFieldNameList>])]
# [roles <GroupRoleList>] [memberrestrictions]
# [members|memberscount] [managers|managerscount] [owners|ownerscount] [totalcount] [countsonly]
# [internal] [internaldomains <DomainList>] [external]
# [types <CIGroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [convertcrnl] [delimiter <Character>]
# [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintCIGroups():
def _printGroupRow(groupEntity, groupMembers):
nonlocal itemCount
for member in groupMembers:
getCIGroupMemberRoleFixType(member)
if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, groupMembers):
return
if showItemCountOnly:
itemCount += 1
return
if not keepName:
groupEntity.pop('name', None)
row = {}
if FJQC.formatJSON:
row['email'] = groupEntity['groupKey']['id'].lower()
row['JSON'] = json.dumps(cleanJSON(groupEntity, timeObjects=CIGROUP_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)
if rolesSet and groupMembers is not None:
row['JSON-members'] = json.dumps(groupMembers, ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
return
mapCIGroupFieldNames(groupEntity)
for k, v in iter(groupEntity.pop('labels', {}).items()):
if v == '':
groupEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = True
else:
groupEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
for key, value in sorted(iter(flattenJSON(groupEntity, flattened={}, timeObjects=CIGROUP_TIME_OBJECTS).items())):
csvPF.AddTitles(key)
row[key] = value
if rolesSet and groupMembers is not None:
addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
False, False, True)
csvPF.WriteRow(row)
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
setTrueCustomerId()
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
memberRestrictions = sortHeaders = False
memberDisplayOptions = initPGGroupMemberDisplayOptions()
pageSize = 500
parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}'
groupFieldsLists = {'ci': ['groupKey']}
csvPF = CSVPrintFile(['email'])
FJQC = FormatJSONQuoteChar(csvPF)
rolesSet = set()
typesSet = set()
memberOptions = initMemberOptions()
entitySelection = groupMembers = memberQuery = query = showOwnedBy = None
matchPatterns = {}
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showownedby':
showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user'])
elif myarg in {'cimember', 'enterprisemember', 'ciowner'}:
emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group'])
memberQuery = f"member_key_id == '{emailAddress}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels"
entitySelection = None
if myarg == 'ciowner':
showOwnedBy = emailAddress
elif myarg == 'query':
query = getString(Cmd.OB_QUERY)
entitySelection = None
elif getGroupMatchPatterns(myarg, matchPatterns, True):
pass
elif myarg == 'select':
entitySelection = getEntityList(Cmd.OB_GROUP_ENTITY)
query = None
elif myarg == 'maxresults':
pageSize = getInteger(minVal=1, maxVal=500)
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg in {'allfields', 'ciallfields', 'allcifields'}:
sortHeaders = True
groupFieldsLists = {'ci': []}
for field in CIGROUP_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
elif myarg == 'basic':
sortHeaders = True
groupFieldsLists = {'ci': ['*']}
elif myarg == 'sortheaders':
sortHeaders = getBoolean()
elif myarg in CIGROUP_FIELDS_CHOICE_MAP:
csvPF.AddField(myarg, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
elif myarg in {'fields', 'cifields'}:
for field in _getFieldsList():
if field in CIGROUP_FIELDS_CHOICE_MAP:
csvPF.AddField(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
else:
invalidChoiceExit(field, list(CIGROUP_FIELDS_CHOICE_MAP), True)
elif getPGGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
pass
elif getCIGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif myarg == 'memberrestrictions':
memberRestrictions = True
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if not typesSet:
typesSet = ALL_CIGROUP_MEMBER_TYPES
csvPF.MapTitles('name', 'id')
csvPF.MapTitles('displayName', 'name')
csvPF.RemoveTitles('labels')
updateFieldsForCIGroupMatchPatterns(matchPatterns, groupFieldsLists['ci'], csvPF)
if groupFieldsLists['ci'] and groupFieldsLists['ci'][0] != '*':
keepName = 'name' in groupFieldsLists['ci']
groupFieldsLists['ci'].append('name')
fields = ','.join(set(groupFieldsLists['ci']))
else:
keepName = True
fields = '*'
fieldsnp = f'nextPageToken,groups({fields})'
getRolesSet = rolesSet.copy()
if showOwnedBy:
getRolesSet.add(Ent.ROLE_OWNER)
getRoles = ','.join(sorted(getRolesSet))
if rolesSet:
setMemberDisplayTitles(memberDisplayOptions, csvPF)
if FJQC.formatJSON:
sortHeaders = False
csvPF.SetJSONTitles(PRINT_CIGROUPS_JSON_TITLES)
if rolesSet:
csvPF.AddJSONTitle('JSON-members')
if memberQuery:
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, memberQuery)
try:
result = callGAPIpages(ci.groups().memberships(), 'searchTransitiveGroups', 'memberships',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
throwReasons=GAPI.CIGROUP_LIST_USERKEY_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent='groups/-', query=memberQuery,
fields='nextPageToken,memberships(group,groupKey(id),relationType)', pageSize=pageSize)
entitySelection = [{'email': entity['groupKey']['id'], 'name': entity['group']} for entity in result if entity['relationType'] == 'DIRECT']
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, None], str(e))
return
getFullFieldsList = []
if entitySelection is None:
if groupFieldsLists['ci']:
for field in groupFieldsLists['ci']:
if field in CIGROUP_FULL_FIELDS:
getFullFieldsList.append(field)
else:
getFullFieldsList = list(CIGROUP_FULL_FIELDS)
getFullFields = ','.join(getFullFieldsList)#
cipl = ci
if query:
method = 'search'
if 'parent' not in query:
query += f" && parent == '{parent}'"
kwargs = {'query': query}
if CIGROUP_LOCKED_LABEL in query:
cipl = buildGAPIObject(API.CLOUDIDENTITY_GROUPS_BETA)
else:
method = 'list'
kwargs = {'parent': parent}
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, query)
try:
entityList = callGAPIpages(cipl.groups(), method, 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
view='FULL', fields=fieldsnp, pageSize=pageSize, **kwargs)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, None], str(e))
return
else:
getFullFields = ''
entityList = []
i = 0
count = len(entitySelection)
for group in entitySelection:
i += 1
if isinstance(group, dict):
name = group['name']
groupEmail = group['email']
else:
_, name, groupEmail = convertGroupEmailToCloudID(ci, group, i, count)
printGettingEntityItemForWhom(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
if name:
try:
ciGroup = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields=fields)
entityList.append(ciGroup)
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
itemCount = 0
i = 0
count = len(entityList)
for groupEntity in entityList:
i += 1
groupEmail = groupEntity['groupKey']['id'].lower()
if not checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
continue
if getFullFields:
try:
fullInfo = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=groupEntity['name'], fields=getFullFields)
groupEntity.update(fullInfo)
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
groupMembers = {}
if getRoles:
printGettingEntityItemForWhom(getRoles, groupEmail, i, count)
try:
groupMembers = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
pageMessage=getPageMessage(), messageAttribute=['preferredMemberKey', 'id'],
parent=groupEntity['name'], view='FULL', fields='*', pageSize=pageSize)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
if memberRestrictions:
printGettingEntityItemForWhom(Ent.MEMBER_RESTRICTION, groupEmail, i, count)
try:
secInfo = callGAPI(ci.groups(), 'getSecuritySettings',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=f"{groupEntity['name']}/securitySettings", readMask='*')
if 'memberRestriction' in secInfo:
groupEntity['memberRestrictionQuery'] = secInfo['memberRestriction'].get('query', '')
groupEntity['memberRestrictionEvaluation'] = secInfo['memberRestriction'].get('evaluation', {}).get('state', '')
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
_printGroupRow(groupEntity, groupMembers)
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
if sortHeaders:
sortTitles = ['email']+CIGROUP_PRINT_ORDER
if rolesSet:
setMemberDisplaySortTitles(memberDisplayOptions, sortTitles)
csvPF.SetSortTitles(sortTitles)
csvPF.SortRows('email', False)
csvPF.writeCSVfile('Cloud Identity Groups')
# gam <UserTypeEntity> info cimember <GroupEntity>
def infoCIGroupMembers(entityList):
infoGroupMembers(entityList, True)
# gam info cimember <UserTypeEntity> <GroupEntity>
def doInfoCIGroupMembers():
infoGroupMembers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1], True)
def getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, fieldsList, csvPF):
if query:
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, query)
parent = 'groups/-'
try:
result = callGAPIpages(ci.groups().memberships(), 'searchTransitiveGroups', 'memberships',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
throwReasons=GAPI.CIGROUP_LIST_USERKEY_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=parent, query=query,
fields='nextPageToken,memberships(groupKey(id),relationType)', pageSize=500)
entityList = [entity['groupKey']['id'] for entity in result if entity['relationType'] == 'DIRECT']
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedExit([Ent.CLOUD_IDENTITY_GROUP, parent], str(e))
elif entityList is None:
updateFieldsForCIGroupMatchPatterns(matchPatterns, fieldsList, csvPF)
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, subTitle)
parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}'
try:
entityList = callGAPIpages(ci.groups(), 'list', 'groups',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=parent, view='FULL',
fields=f'nextPageToken,groups({",".join(set(fieldsList))})', pageSize=500)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedExit([Ent.CLOUD_IDENTITY_GROUP, parent], str(e))
else:
clearUnneededGroupMatchPatterns(matchPatterns)
return entityList
def getCIGroupTransitiveMembers(ci, groupName, membersList, i, count):
try:
groupMembers = callGAPIpages(ci.groups().memberships(), 'searchTransitiveMemberships', 'memberships',
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=groupName,
fields='nextPageToken,memberships(*)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupName, i, count)
return False
except GAPI.permissionDenied as e:
entityActionFailedExit([Ent.CLOUD_IDENTITY_GROUP, groupName], str(e))
return False
for member in groupMembers:
membersList.append(getCIGroupTransitiveMemberRoleFixType(groupName, member))
return True
def getCIGroupMembers(ci, groupName, memberRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level, typesSet, groupEmail, kwargs):
nameToPrint = groupEmail if groupEmail else groupName
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, nameToPrint, i, count)
validRoles = _getCIRoleVerification(memberRoles)
if memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP]:
groupMembers = []
if not getCIGroupTransitiveMembers(ci, groupName, groupMembers, i, count):
return
for member in groupMembers:
if _checkMemberRole(member, validRoles):
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
membersList.append(member)
return
try:
groupMembers = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=groupName, **kwargs)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError,
GAPI.permissionDenied, GAPI.serviceNotAvailable):
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, nameToPrint, i, count)
return
checkCategory = memberDisplayOptions['showCategory']
if not memberOptions[MEMBEROPTION_RECURSIVE]:
if memberOptions[MEMBEROPTION_NODUPLICATES]:
for member in groupMembers:
getCIGroupMemberRoleFixType(member)
memberName = member.get('preferredMemberKey', {}).get('id', '')
if (_checkMemberRole(member, validRoles) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions)) and
memberName not in membersSet):
membersSet.add(memberName)
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
membersList.append(member)
else:
for member in groupMembers:
getCIGroupMemberRoleFixType(member)
if (_checkMemberRole(member, validRoles) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
membersList.append(member)
elif memberOptions[MEMBEROPTION_NODUPLICATES]:
groupMemberList = []
for member in groupMembers:
getCIGroupMemberRoleFixType(member)
memberName = member.get('preferredMemberKey', {}).get('id', '')
if member['type'] != Ent.TYPE_GROUP:
if (member['type'] in typesSet and
checkCIMemberMatch(member, memberOptions) and
_checkMemberRole(member, validRoles) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions)) and
memberName not in membersSet):
membersSet.add(memberName)
member['level'] = level
member['subgroup'] = nameToPrint
membersList.append(member)
else:
if memberName not in membersSet:
membersSet.add(memberName)
if (member['type'] in typesSet and
checkCIMemberMatch(member, memberOptions) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
member['level'] = level
member['subgroup'] = nameToPrint
membersList.append(member)
_, gname = member['name'].rsplit('/', 1)
groupMemberList.append((f'groups/{gname}', memberName))
for member in groupMemberList:
getCIGroupMembers(ci, member[0], memberRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level+1, typesSet, member[1], kwargs)
else:
for member in groupMembers:
getCIGroupMemberRoleFixType(member)
memberName = member.get('preferredMemberKey', {}).get('id', '')
if member['type'] != Ent.TYPE_GROUP:
if (member['type'] in typesSet and
checkCIMemberMatch(member, memberOptions) and
_checkMemberRole(member, validRoles) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
member['level'] = level
member['subgroup'] = nameToPrint
membersList.append(member)
else:
if (member['type'] in typesSet and
checkCIMemberMatch(member, memberOptions) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
member['level'] = level
member['subgroup'] = nameToPrint
membersList.append(member)
_, gname = member['name'].rsplit('/', 1)
getCIGroupMembers(ci, f'groups/{gname}', memberRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level+1, typesSet, memberName, kwargs)
CIGROUPMEMBERS_FIELDS_CHOICE_MAP = {
'createtime': 'createTime',
'delivery': 'deliverySetting',
'deliverysettings': 'deliverySetting',
'email': 'preferredMemberKey',
'expiretime': 'expireTime',
'id': 'name',
'memberkey': 'preferredMemberKey',
'name': 'name',
'preferredmemberkey': 'preferredMemberKey',
'role': 'roles',
'roles': 'roles',
'type': 'type',
'updatetime': 'updateTime',
'useremail': 'preferredMemberKey',
}
CIGROUPMEMBERS_DEFAULT_FIELDS = [
'type', 'roles', 'name', 'preferredmemberkey', 'createtime', 'updatetime', 'expiretime']
CIGROUPMEMBERS_SORT_FIELDS = [
'type', 'role', 'id', 'email',
'name', 'preferredMemberKey.id', 'preferredMemberKey.namespace',
'createTime', 'updateTime', 'expireTime'
]
CIGROUPMEMBERS_TIME_OBJECTS = {'createTime', 'updateTime', 'expireTime'}
def _getCIListGroupMembersArgs(listView):
if listView == 'full':
return {'view': 'FULL', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL],
'fields': 'nextPageToken,memberships(*)'}
if listView == 'basic':
return {'view': 'FULL', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL],
'fields': 'nextPageToken,memberships(name,preferredMemberKey,roles,type)'}
return {'view': 'BASIC', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_BASIC],
'fields': 'nextPageToken,memberships(*)'}
# gam print cigroup-members [todrive <ToDriveAttribute>*]
# [(cimember|ciowner <UserItem>)|(cigroup <GroupItem>)|(select <GroupEntity>)]
# [showownedby <UserItem>]
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
# [descriptionmatchpattern [not] <REMatchPattern>]
# [roles <GroupRoleList>] [members] [managers] [owners]
# [types <CIGroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# <CIGroupMembersFieldName>* [fields <CIGroupMembersFieldNameList>]
# [minimal|basic|full]
# [(recursive [noduplicates])|includederivedmembership] [nogroupeemail]
# [formatjson [quotechar <Character>]]
def doPrintCIGroupMembers():
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
setTrueCustomerId()
memberOptions = initMemberOptions()
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
groupColumn = True
subTitle = f'{Msg.ALL} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
fieldsList = []
groupFieldsLists = {'ci': ['groupKey', 'name']}
csvPF = CSVPrintFile(['group'])
FJQC = FormatJSONQuoteChar(csvPF)
entityList = query = showOwnedBy = None
rolesSet = set()
typesSet = set()
matchPatterns = {}
listView = 'full'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showownedby':
showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user'])
elif myarg in {'cimember', 'enterprisemember', 'ciowner'}:
emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group'])
query = f"member_key_id == '{emailAddress}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels"
entityList = None
if myarg == 'ciowner':
showOwnedBy = emailAddress
elif myarg in {'cigroup', 'group'}:
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
subTitle = f'{Ent.Singular(Ent.CLOUD_IDENTITY_GROUP)}={entityList[0]}'
query = None
elif getGroupMatchPatterns(myarg, matchPatterns, True):
pass
elif myarg == 'select':
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
subTitle = f'{Msg.SELECTED} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
query = None
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
pass
elif getCIGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif getFieldsList(myarg, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='preferredMemberKey'):
pass
elif myarg == 'noduplicates':
memberOptions[MEMBEROPTION_NODUPLICATES] = True
elif myarg == 'recursive':
memberOptions[MEMBEROPTION_RECURSIVE] = True
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = False
elif myarg == 'includederivedmembership':
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
memberOptions[MEMBEROPTION_RECURSIVE] = False
elif myarg == 'nogroupemail':
groupColumn = False
elif myarg in {'minimal', 'basic', 'full'}:
listView = myarg
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if listView == 'minimal' and memberOptions[MEMBEROPTION_RECURSIVE]:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('minimal', 'recursive'))
if not typesSet:
typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_CIGROUP_MEMBER_TYPES
fields = ','.join(set(groupFieldsLists['ci']))
entityList = getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, groupFieldsLists['ci'], csvPF)
if not fieldsList:
for field in CIGROUPMEMBERS_DEFAULT_FIELDS:
addFieldToFieldsList(field, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList)
if not groupColumn:
csvPF.RemoveTitles(['group'])
if memberDisplayOptions['showCategory']:
csvPF.AddTitles('category')
if FJQC.formatJSON:
if groupColumn:
csvPF.SetJSONTitles(['group', 'JSON'])
else:
csvPF.SetJSONTitles(['JSON'])
displayFieldsList = fieldsList[:]
if 'roles' in displayFieldsList:
displayFieldsList.remove('roles')
displayFieldsList.append('role')
if not rolesSet:
rolesSet = ALL_GROUP_ROLES
getRolesSet = rolesSet.copy()
if showOwnedBy:
getRolesSet.add(Ent.ROLE_OWNER)
getRoles = ','.join(sorted(getRolesSet))
kwargs = _getCIListGroupMembersArgs(listView)
level = 0
i = 0
count = len(entityList)
for group in entityList:
i += 1
if isinstance(group, dict):
groupEntity = group
else:
_, name, groupEmail = convertGroupEmailToCloudID(ci, group, i, count)
if not name:
continue
try:
groupEntity = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields=fields)
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError,
GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
continue
groupEmail = groupEntity['groupKey']['id'].lower()
if not checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
continue
membersList = []
membersSet = set()
getCIGroupMembers(ci, groupEntity['name'], getRoles, membersList, membersSet, i, count,
memberOptions, memberDisplayOptions, level, typesSet, groupEmail, kwargs)
if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, membersList):
continue
for member in membersList:
if member['role'] not in rolesSet:
continue
row = {}
if groupColumn:
row['group'] = groupEmail
dmember = {}
for field in displayFieldsList:
if field in member:
dmember[field] = member[field]
if memberOptions[MEMBEROPTION_RECURSIVE]:
row['level'] = member['level']
row['subgroup'] = member['subgroup']
if memberDisplayOptions['showCategory']:
row['category'] = member['category']
if listView == 'minimal':
dmember.pop('type', None)
mapCIGroupMemberFieldNames(dmember)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(dmember, flattened=row, timeObjects=CIGROUPMEMBERS_TIME_OBJECTS))
else:
row.update(dmember)
fjrow = {}
if groupColumn:
fjrow['group'] = groupEmail
fjrow['JSON'] = json.dumps(cleanJSON(row, timeObjects=CIGROUPMEMBERS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(fjrow)
if not FJQC.formatJSON:
sortTitles = ['group'] if groupColumn else []
sortTitles.extend(CIGROUPMEMBERS_SORT_FIELDS)
if memberDisplayOptions['showCategory']:
emailIndex = sortTitles.index('email')
sortTitles.insert(emailIndex+1, 'category')
csvPF.SetSortTitles(sortTitles)
csvPF.SortTitles()
csvPF.SetSortTitles([])
if memberOptions[MEMBEROPTION_RECURSIVE]:
csvPF.MoveTitlesToEnd(['level', 'subgroup'])
csvPF.writeCSVfile(f'Cloud Identity Group Members ({subTitle})')
# gam show cigroup-members
# [(cimember|ciowner <UserItem>)|(cigroup <GroupItem>)|(select <GroupEntity>)]
# [showownedby <UserItem>]
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
# [descriptionmatchpattern [not] <REMatchPattern>]
# [roles <GroupRoleList>] [members] [managers] [owners]
# [internal] [internaldomains <DomainList>] [external]
# [types <CIGroupMemberTypeList>]
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
# [minimal|basic|full]
# [(depth <Number>) | includederivedmembership]
def doShowCIGroupMembers():
def _roleOrder(key):
return {Ent.ROLE_OWNER: 0, Ent.ROLE_MANAGER: 1, Ent.ROLE_MEMBER: 2}.get(key, 3)
def _typeOrder(key):
return {Ent.TYPE_CUSTOMER: 0, Ent.TYPE_USER: 1, Ent.TYPE_GROUP: 2,
Ent.TYPE_CBCM_BROWSER: 3, Ent.TYPE_OTHER: 4, Ent.TYPE_EXTERNAL: 5}.get(key, 6)
def _showGroup(groupName, groupEmail, depth):
if includeDerivedMembership:
membersList = []
if not getCIGroupTransitiveMembers(ci, groupName, membersList, i, count):
return
else:
try:
membersList = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
parent=groupName, **kwargs)
for member in membersList:
getCIGroupMemberRoleFixType(member)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError,
GAPI.permissionDenied, GAPI.serviceNotAvailable):
if depth == 0:
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
return
if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, membersList):
return
if depth == 0:
printEntity([Ent.CLOUD_IDENTITY_GROUP, groupEmail], i, count)
if depth == 0 or Ent.TYPE_GROUP in typesSet:
Ind.Increment()
for member in sorted(membersList, key=lambda k: (_roleOrder(k.get('role', Ent.ROLE_MEMBER)), _typeOrder(k['type']))):
if (_checkMemberIsSuspendedIsArchived(member, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
if (_checkMemberRole(member, rolesSet) and
member['type'] in typesSet and
checkCIMemberMatch(member, memberOptions)):
if listView != 'minimal':
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member["preferredMemberKey"]["id"]}'
else:
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["preferredMemberKey"]["id"]}'
if checkCategory:
memberDetails += f', {member["category"]}'
for field in ['createTime', 'updateTime', 'expireTime']:
if field in member:
memberDetails += f', {formatLocalTime(member[field])}'
printKeyValueList([memberDetails])
if not includeDerivedMembership and (member['type'] == Ent.TYPE_GROUP) and (maxdepth == -1 or depth < maxdepth):
_, gname = member['name'].rsplit('/', 1)
_showGroup(f'groups/{gname}', member['preferredMemberKey']['id'], depth+1)
if depth == 0 or Ent.TYPE_GROUP in typesSet:
Ind.Decrement()
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
setTrueCustomerId()
subTitle = f'{Msg.ALL} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
groupFieldsLists = {'ci': ['groupKey', 'name']}
entityList = query = showOwnedBy = None
rolesSet = set()
typesSet = set()
memberOptions = initMemberOptions()
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
matchPatterns = {}
maxdepth = -1
includeDerivedMembership = False
listView = 'full'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'showownedby':
showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user'])
elif myarg in {'cimember', 'enterprisemember', 'ciowner'}:
emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group'])
query = f"member_key_id == '{emailAddress}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels"
entityList = None
if myarg == 'ciowner':
showOwnedBy = emailAddress
elif myarg in {'cigroup', 'group'}:
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
subTitle = f'{Ent.Singular(Ent.CLOUD_IDENTITY_GROUP)}={entityList[0]}'
query = None
elif getGroupMatchPatterns(myarg, matchPatterns, False):
pass
elif myarg == 'select':
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
subTitle = f'{Msg.SELECTED} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
query = None
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
pass
elif getCIGroupMemberTypes(myarg, typesSet):
pass
elif getMemberMatchOptions(myarg, memberOptions):
pass
elif myarg == 'depth':
maxdepth = getInteger(minVal=-1)
elif myarg == 'includederivedmembership':
includeDerivedMembership = True
elif myarg in {'minimal', 'basic', 'full'}:
listView = myarg
else:
unknownArgumentExit()
if not rolesSet:
rolesSet = ALL_GROUP_ROLES
if not typesSet:
typesSet = ALL_CIGROUP_MEMBER_TYPES
checkCategory = memberDisplayOptions['showCategory']
fields = ','.join(set(groupFieldsLists['ci']))
entityList = getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, groupFieldsLists['ci'], None)
kwargs = _getCIListGroupMembersArgs(listView)
i = 0
count = len(entityList)
for group in entityList:
i += 1
if isinstance(group, dict):
groupEntity = group
else:
_, name, groupEmail = convertGroupEmailToCloudID(ci, group, i, count)
if not name:
continue
try:
groupEntity = callGAPI(ci.groups(), 'get',
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
name=name, fields=fields)
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError,
GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
continue
groupEmail = groupEntity['groupKey']['id'].lower()
if checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
_showGroup(groupEntity['name'], groupEmail, 0)
# gam print licenses [todrive <ToDriveAttribute>*]
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite]
# [maxresults <Integer>]
# [countsonly]
def doPrintLicenses(returnFields=None, skus=None, countsOnly=False, returnCounts=False):
lic = buildGAPIObject(API.LICENSING)
setTrueCustomerId()
customerId = _getCustomerId()
csvPF = CSVPrintFile()
products = []
feed = []
licenseCounts = []
maxResults = GC.Values[GC.LICENSE_MAX_RESULTS]
if not returnFields:
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if not returnCounts and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'products', 'product'}:
products = getGoogleProductList()
skus = []
elif myarg in {'skus', 'sku'}:
skus = getGoogleSKUList()
products = []
elif myarg == 'allskus':
skus = SKU.getAllSKUs()
products = []
elif myarg == 'gsuite':
skus = SKU.getGSuiteSKUs()
products = []
elif myarg == 'countsonly':
countsOnly = True
elif myarg == 'maxresults':
maxResults = getInteger(minVal=10, maxVal=1000)
else:
unknownArgumentExit()
if not skus and not products and GM.Globals[GM.LICENSE_SKUS]:
skus = GM.Globals[GM.LICENSE_SKUS]
if not countsOnly:
fields = getItemFieldsFromFieldsList('items', ['productId', 'skuId', 'userId'])
csvPF.SetTitles(['userId', 'productId', 'productDisplay', 'skuId', 'skuDisplay'])
else:
fields = getItemFieldsFromFieldsList('items', ['userId'])
if not returnCounts:
if skus:
csvPF.SetTitles(['productId', 'productDisplay', 'skuId', 'skuDisplay', 'licenses'])
else:
csvPF.SetTitles(['productId', 'productDisplay', 'licenses'])
else:
fields = getItemFieldsFromFieldsList('items', returnFields)
if skus:
for sku in skus:
Ent.SetGetting(Ent.LICENSE)
productId = sku[0]
skuId = sku[1]
productDisplay = SKU.formatProductIdDisplayName(productId)
skuIdDisplay = SKU.formatSKUIdDisplayName(skuId)
try:
feed += callGAPIpages(lic.licenseAssignments(), 'listForProductAndSku', 'items',
pageMessage=getPageMessageForWhom(forWhom=skuIdDisplay),
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
customerId=customerId, productId=productId, skuId=skuId,
maxResults=maxResults, fields=fields)
if countsOnly:
licenseCounts.append([Ent.PRODUCT, productId, Ent.SKU, [skuId, skuIdDisplay][returnCounts], Ent.LICENSE, len(feed)])
feed = []
except (GAPI.invalid, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionNotPerformedWarning([Ent.PRODUCT, productDisplay, Ent.SKU, skuIdDisplay], str(e))
else:
suppressErrorMsg = False
if not products:
suppressErrorMsg = True
products = SKU.getSortedProductList()
for productId in products:
Ent.SetGetting(Ent.LICENSE)
productDisplay = SKU.formatProductIdDisplayName(productId)
try:
feed += callGAPIpages(lic.licenseAssignments(), 'listForProduct', 'items',
pageMessage=getPageMessageForWhom(forWhom=productDisplay),
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
customerId=customerId, productId=productId,
maxResults=maxResults, fields=fields)
if countsOnly:
licenseCounts.append([Ent.PRODUCT, [productId, productDisplay][returnCounts], Ent.LICENSE, len(feed)])
feed = []
except (GAPI.invalid, GAPI.forbidden, GAPI.invalidArgument) as e:
if not suppressErrorMsg:
entityActionNotPerformedWarning([Ent.PRODUCT, productDisplay], str(e))
if countsOnly:
if returnCounts:
return licenseCounts
if skus:
for u_license in licenseCounts:
csvPF.WriteRow({'productId': u_license[1], 'productDisplay': SKU.productIdToDisplayName(u_license[1]),
'skuId': u_license[3], 'skuDisplay': SKU.skuIdToDisplayName(u_license[3]), 'licenses': u_license[5]})
else:
for u_license in licenseCounts:
csvPF.WriteRow({'productId': u_license[1], 'productDisplay': SKU.productIdToDisplayName(u_license[1]), 'licenses': u_license[3]})
csvPF.writeCSVfile('Licenses')
return
if returnFields:
if returnFields == ['userId']:
userIds = []
for u_license in feed:
userId = u_license.get('userId', '').lower()
if userId:
userIds.append(userId)
return userIds
userSkuIds = {}
for u_license in feed:
userId = u_license.get('userId', '').lower()
skuId = u_license.get('skuId')
if userId and skuId:
userSkuIds.setdefault(userId, [])
userSkuIds[userId].append(skuId)
return userSkuIds
for u_license in feed:
userId = u_license.get('userId', '').lower()
productId = u_license.get('productId', '')
skuId = u_license.get('skuId', '')
csvPF.WriteRow({'userId': userId,
'productId': productId, 'productDisplay': SKU.productIdToDisplayName(productId),
'skuId': skuId, 'skuDisplay': SKU.skuIdToDisplayName(skuId)})
csvPF.writeCSVfile('Licenses')
# gam show licenses
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite]
# [maxresults <Integer>]
def doShowLicenses():
licenseCounts = doPrintLicenses(countsOnly=True, returnCounts=True)
for u_license in licenseCounts:
printEntityKVList(u_license[:-2], [Ent.Plural(u_license[-2]), u_license[-1]])
# gam delete alert <AlertID>
# gam undelete alert <AlertID>
def doDeleteOrUndeleteAlert():
alertId = getString(Cmd.OB_ALERT_ID)
if Act.Get() == Act.DELETE:
action = 'delete'
kwargs = {}
else:
action = 'undelete'
kwargs = {'body': {}}
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
if not ac:
return
try:
callGAPI(ac.alerts(), action,
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND],
alertId=alertId, **kwargs)
entityActionPerformed([Ent.ALERT, alertId])
except GAPI.notFound as e:
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
userAlertsServiceNotEnabledWarning(user)
ALERT_TIME_OBJECTS = {'createTime', 'startTime', 'endTime'}
def _showAlert(alert, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(alert, timeObjects=ALERT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.ALERT_ID, alert['alertId']], i, count)
Ind.Increment()
for field in ['createTime', 'startTime', 'endTime']:
if field in alert:
printKeyValueList([field, formatLocalTime(alert[field])])
for field in ['customerId', 'type', 'source', 'deleted', 'securityInvestigationToolLink']:
if field in alert:
printKeyValueList([field, alert[field]])
if 'data' in alert:
showJSON('data', alert['data'])
Ind.Decrement()
# gam info alert <AlertID> [formatjson]
def doInfoAlert():
alertId = getString(Cmd.OB_ALERT_ID)
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
FJQC.GetFormatJSON(myarg)
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
if not ac:
return
try:
alert = callGAPI(ac.alerts(), 'get',
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND],
alertId=alertId)
_showAlert(alert, FJQC)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
userAlertsServiceNotEnabledWarning(user)
ALERT_ORDERBY_CHOICE_MAP = {
'createdate': 'create_time',
'createtime': 'create_time',
}
# gam show alerts [filter <String>] [orderby createtime [ascending|descending]]
# [formatjson]
# gam print alerts [todrive <ToDriveAttribute>*] [filter <String>] [orderby createtime [ascending|descending]]
# [formatjson [quotechar <Character>]]
def doPrintShowAlerts():
csvPF = CSVPrintFile(['alertId', 'createTime']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {}
OBY = OrderBy(ALERT_ORDERBY_CHOICE_MAP)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'filter':
kwargs['filter'] = getString(Cmd.OB_STRING).replace("'", '"')
elif myarg == 'orderby':
OBY.GetChoice()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and not FJQC.formatJSON:
csvPF.SetSortTitles(['alertId', 'createTime', 'startTime', 'endTime', 'customerId', 'type', 'source', 'deleted'])
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
if not ac:
return
try:
alerts = callGAPIpages(ac.alerts(), 'list', 'alerts',
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT],
orderBy=OBY.orderBy, **kwargs)
except (GAPI.badRequest, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.ALERT, None], str(e))
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
userAlertsServiceNotEnabledWarning(user)
return
if not csvPF:
jcount = len(alerts)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.ALERT)
Ind.Increment()
j = 0
for alert in alerts:
j += 1
_showAlert(alert, FJQC, j, jcount)
Ind.Decrement()
else:
for alert in alerts:
row = flattenJSON(alert, timeObjects=ALERT_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'alertId': alert['alertId'],
'createTime': formatLocalTime(alert['createTime']),
'JSON': json.dumps(cleanJSON(alert, timeObjects=ALERT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Alerts')
ALERT_TYPE_MAP = {
'notuseful': 'NOT_USEFUL',
'somewhatuseful': 'SOMEWHAT_USEFUL',
'veryuseful': 'VERY_USEFUL',
}
# gam create alertfeedback <AlertID> not_useful|somewhat_useful|very_useful
def doCreateAlertFeedback():
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
if not ac:
return
alertId = getString(Cmd.OB_ALERT_ID)
body = {'type': getChoice(ALERT_TYPE_MAP, mapChoice=True)}
try:
result = callGAPI(ac.alerts().feedback(), 'create',
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND],
alertId=alertId, body=body)
entityActionPerformed([Ent.ALERT, alertId, Ent.ALERT_FEEDBACK_ID, result['feedbackId']])
except GAPI.notFound as e:
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
except (GAPI.serviceNotAvailable, GAPI.authError):
userAlertsServiceNotEnabledWarning(user)
def _showAlertFeedback(feedback, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(feedback, timeObjects=ALERT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.ALERT_FEEDBACK_ID, feedback['feedbackId']], i, count)
Ind.Increment()
for field in ['createTime']:
if field in feedback:
printKeyValueList([field, formatLocalTime(feedback[field])])
for field in ['alertId', 'customerId', 'type', 'email']:
if field in feedback:
printKeyValueList([field, feedback[field]])
Ind.Decrement()
ALERT_FEEDBACK_ORDERBY_CHOICE_MAP = {
'createdate': 'createTime',
'createtime': 'createTime',
}
# gam show alertfeedback [alert <AlertID>] [filter <String>] [orderby createtime [ascending|descending]]
# [formatjson]
# gam print alertfeedback [todrive <ToDriveAttribute>*] [alert <AlertID>] [filter <String>] [orderby createtime [ascending|descending]]
# [formatjson [quotechar <Character>]]
def doPrintShowAlertFeedback():
csvPF = CSVPrintFile(['feedbackId', 'createTime']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {}
alertId = '-'
OBY = OrderBy(ALERT_FEEDBACK_ORDERBY_CHOICE_MAP)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'alertid':
alertId = getString(Cmd.OB_ALERT_ID)
elif myarg == 'filter':
kwargs['filter'] = getString(Cmd.OB_STRING).replace("'", '"')
elif myarg == 'orderby':
OBY.GetChoice()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and not FJQC.formatJSON:
csvPF.SetSortTitles(['feedbackId', 'createTime', 'alertId', 'customerId', 'type', 'email'])
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
if not ac:
return
try:
feedbacks = callGAPIpages(ac.alerts().feedback(), 'list', 'feedback',
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.BAD_REQUEST],
alertId=alertId, **kwargs)
except (GAPI.notFound, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
userAlertsServiceNotEnabledWarning(user)
return
for sk in OBY.items:
if sk.endswith(' desc'):
field, _ = sk.split(' ')
reverse = True
else:
field = sk
reverse = False
feedbacks = sorted(feedbacks, key=lambda k: k[field], reverse=reverse) #pylint: disable=cell-var-from-loop
if not csvPF:
jcount = len(feedbacks)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.ALERT_FEEDBACK)
Ind.Increment()
j = 0
for feedback in feedbacks:
j += 1
_showAlertFeedback(feedback, FJQC, j, jcount)
Ind.Decrement()
else:
for feedback in feedbacks:
row = flattenJSON(feedback, timeObjects=ALERT_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'feedbackId': feedback['feedbackId'],
'createTime': formatLocalTime(feedback['createTime']),
'JSON': json.dumps(cleanJSON(feedback, timeObjects=ALERT_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Alert Feedbacks')
def ACLRuleDict(rule):
if rule['scope']['type'] != 'default':
return {'Scope': f'{rule["scope"]["type"]}:{rule["scope"]["value"]}', 'Role': rule['role']}
return {'Scope': f'{rule["scope"]["type"]}', 'Role': rule['role']}
def ACLRuleKeyValueList(rule):
if rule['scope']['type'] != 'default':
return ['Scope', f'{rule["scope"]["type"]}:{rule["scope"]["value"]}', 'Role', rule['role']]
return ['Scope', f'{rule["scope"]["type"]}', 'Role', rule['role']]
def formatACLRule(rule):
return formatKeyValueList('(', ACLRuleKeyValueList(rule), ')')
def formatACLScopeRole(scope, role):
if role:
return formatKeyValueList('(', ['Scope', scope, 'Role', role], ')')
return formatKeyValueList('(', ['Scope', scope], ')')
def normalizeRuleId(ruleId):
ruleIdParts = ruleId.split(':', 1)
if (len(ruleIdParts) == 1) or not ruleIdParts[1]:
if ruleIdParts[0] == 'default':
return ruleId
if ruleIdParts[0] == 'domain':
return f'domain:{GC.Values[GC.DOMAIN]}'
return f'user:{normalizeEmailAddressOrUID(ruleIdParts[0], noUid=True)}'
if ruleIdParts[0] in {'user', 'group'}:
return f'{ruleIdParts[0]}:{normalizeEmailAddressOrUID(ruleIdParts[1], noUid=True)}'
return ruleId
def makeRoleRuleIdBody(role, ruleId):
ruleIdParts = ruleId.split(':', 1)
if len(ruleIdParts) == 1:
if ruleIdParts[0] == 'default':
return {'role': role, 'scope': {'type': ruleIdParts[0]}}
if ruleIdParts[0] == 'domain':
return {'role': role, 'scope': {'type': ruleIdParts[0], 'value': GC.Values[GC.DOMAIN]}}
return {'role': role, 'scope': {'type': 'user', 'value': ruleIdParts[0]}}
return {'role': role, 'scope': {'type': ruleIdParts[0], 'value': ruleIdParts[1]}}
BUILDING_ADDRESS_FIELD_MAP = {
'address': 'addressLines',
'addresslines': 'addressLines',
'administrativearea': 'administrativeArea',
'city': 'locality',
'country': 'regionCode',
'language': 'languageCode',
'languagecode': 'languageCode',
'locality': 'locality',
'postalcode': 'postalCode',
'regioncode': 'regionCode',
'state': 'administrativeArea',
'sublocality': 'sublocality',
'zipcode': 'postalCode',
}
def _getBuildingAttributes(body):
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'id':
body['buildingId'] = getString(Cmd.OB_BUILDING_ID, maxLen=100)
elif myarg == 'name':
body['buildingName'] = getString(Cmd.OB_STRING, maxLen=100)
elif myarg in {'lat', 'latitude'}:
body.setdefault('coordinates', {})
body['coordinates']['latitude'] = getFloat(minVal=-180.0, maxVal=180.0)
elif myarg in {'long', 'lng', 'longitude'}:
body.setdefault('coordinates', {})
body['coordinates']['longitude'] = getFloat(minVal=-180.0, maxVal=180.0)
elif myarg == 'description':
body['description'] = getString(Cmd.OB_STRING)
elif myarg == 'floors':
body['floorNames'] = getString(Cmd.OB_STRING).split(',')
elif myarg in BUILDING_ADDRESS_FIELD_MAP:
myarg = BUILDING_ADDRESS_FIELD_MAP[myarg]
body.setdefault('address', {})
if myarg == 'addressLines':
body['address'][myarg] = getStringWithCRsNLs().split('\n')
elif myarg == 'languageCode':
body['address'][myarg] = getLanguageCode(LANGUAGE_CODES_MAP)
else:
body['address'][myarg] = getString(Cmd.OB_STRING, minLen=0)
else:
unknownArgumentExit()
return body
# gam create building <Name> <BuildingAttribute>*
def doCreateBuilding():
cd = buildGAPIObject(API.DIRECTORY)
body = _getBuildingAttributes({'buildingId': str(uuid.uuid4()),
'buildingName': getString(Cmd.OB_NAME, maxLen=100),
'floorNames': ['1']})
try:
callGAPI(cd.resources().buildings(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], body=body)
entityActionPerformed([Ent.BUILDING_ID, body['buildingId'], Ent.BUILDING, body['buildingName']])
except GAPI.duplicate:
entityDuplicateWarning([Ent.BUILDING_ID, body['buildingId'], Ent.BUILDING, body['buildingName']])
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.BUILDING_ID, body['buildingId'], Ent.BUILDING, body['buildingName']], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
def _makeBuildingIdNameMap(cd=None):
GM.Globals[GM.MAKE_BUILDING_ID_NAME_MAP] = False
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
try:
buildings = callGAPIpages(cd.resources().buildings(), 'list', 'buildings',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID],
fields='nextPageToken,buildings(buildingId,buildingName)')
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
for building in buildings:
GM.Globals[GM.MAP_BUILDING_ID_TO_NAME][building['buildingId']] = building['buildingName']
GM.Globals[GM.MAP_BUILDING_NAME_TO_ID][building['buildingName']] = building['buildingId']
def _getBuildingByNameOrId(cd, minLen=1, allowNV=False):
which_building = getString(Cmd.OB_BUILDING_ID, minLen=minLen)
if not which_building or (minLen == 0 and which_building in {'id:', 'uid:'}):
return ''
cg = UID_PATTERN.match(which_building)
if cg:
return cg.group(1)
if allowNV and which_building.startswith('nv:'):
return which_building[3:]
if GM.Globals[GM.MAKE_BUILDING_ID_NAME_MAP]:
_makeBuildingIdNameMap(cd)
# Exact name match, return ID
if which_building in GM.Globals[GM.MAP_BUILDING_NAME_TO_ID]:
return GM.Globals[GM.MAP_BUILDING_NAME_TO_ID][which_building]
# No exact name match, check for case insensitive name matches
which_building_lower = which_building.lower()
ci_matches = []
for buildingName, buildingId in iter(GM.Globals[GM.MAP_BUILDING_NAME_TO_ID].items()):
if buildingName.lower() == which_building_lower:
ci_matches.append({'buildingName': buildingName, 'buildingId': buildingId})
# One match, return ID
if len(ci_matches) == 1:
return ci_matches[0]['buildingId']
# No or multiple name matches, try ID
# Exact ID match, return ID
if which_building in GM.Globals[GM.MAP_BUILDING_ID_TO_NAME]:
return which_building
# No exact ID match, check for case insensitive id match
for buildingId in GM.Globals[GM.MAP_BUILDING_ID_TO_NAME]:
# Match, return ID
if buildingId.lower() == which_building_lower:
return buildingId
# Multiple name matches
if len(ci_matches) > 1:
printErrorMessage(1, Msg.MULTIPLE_BUILDINGS_SAME_NAME.format(len(ci_matches), Ent.Plural(Ent.BUILDING)))
Ind.Increment()
for building in ci_matches:
printEntity([Ent.BUILDING, building['buildingName'], Ent.BUILDING_ID, building['buildingId']])
Ind.Decrement()
Cmd.Backup()
usageErrorExit(Msg.PLEASE_SPECIFY_BUILDING_EXACT_CASE_NAME_OR_ID)
# No matches
entityDoesNotExistExit(Ent.BUILDING, which_building)
def _getBuildingNameById(cd, buildingId):
if GM.Globals[GM.MAKE_BUILDING_ID_NAME_MAP]:
_makeBuildingIdNameMap(cd)
return GM.Globals[GM.MAP_BUILDING_ID_TO_NAME].get(buildingId, buildingId)
# gam update building <BuildIngID> <BuildingAttribute>*
def doUpdateBuilding():
cd = buildGAPIObject(API.DIRECTORY)
buildingId = _getBuildingByNameOrId(cd)
body = _getBuildingAttributes({})
try:
callGAPI(cd.resources().buildings(), 'patch',
throwReasons=[GAPI.DUPLICATE, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], buildingId=buildingId, body=body)
entityActionPerformed([Ent.BUILDING_ID, buildingId])
except GAPI.duplicate:
entityDuplicateWarning([Ent.BUILDING, body['buildingName']])
except GAPI.resourceNotFound:
entityUnknownWarning(Ent.BUILDING_ID, buildingId)
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.BUILDING_ID, buildingId], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
# gam delete building <BuildIngID>
def doDeleteBuilding():
cd = buildGAPIObject(API.DIRECTORY)
buildingId = _getBuildingByNameOrId(cd)
checkForExtraneousArguments()
try:
callGAPI(cd.resources().buildings(), 'delete',
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.CONDITION_NOT_MET,
GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], buildingId=buildingId)
entityActionPerformed([Ent.BUILDING_ID, buildingId])
except GAPI.resourceNotFound:
entityUnknownWarning(Ent.BUILDING_ID, buildingId)
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.BUILDING_ID, buildingId], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
BUILDING_ADDRESS_PRINT_ORDER = ['addressLines', 'sublocality', 'locality', 'administrativeArea', 'postalCode', 'regionCode', 'languageCode']
def _showBuilding(building, delimiter=',', i=0, count=0, FJQC=None):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(building), ensure_ascii=False, sort_keys=True))
return
if 'buildingName' in building:
printEntity([Ent.BUILDING, building['buildingName']], i, count)
Ind.Increment()
printKeyValueList(['buildingId', f'id:{building["buildingId"]}'])
else:
printEntity([Ent.BUILDING_ID, f'id:{building["buildingId"]}'], i, count)
Ind.Increment()
if 'description' in building:
printKeyValueList(['description', building['description']])
if 'floorNames' in building:
printKeyValueList(['floorNames', delimiter.join(building['floorNames'])])
if 'coordinates' in building:
printKeyValueList(['coordinates', None])
Ind.Increment()
printKeyValueList(['latitude', f'{building["coordinates"].get("latitude", 0):4.7f}'])
printKeyValueList(['longitude', f'{building["coordinates"].get("longitude", 0):4.7f}'])
Ind.Decrement()
if 'address' in building:
printKeyValueList(['address', None])
Ind.Increment()
for field in BUILDING_ADDRESS_PRINT_ORDER:
if field in building['address']:
if field != 'addressLines':
printKeyValueList([field, building['address'][field]])
else:
printKeyValueList([field, None])
Ind.Increment()
for line in building['address'][field]:
printKeyValueList([line])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
# gam info building <BuildingID>
# [formatjson]
def doInfoBuilding():
cd = buildGAPIObject(API.DIRECTORY)
buildingId = _getBuildingByNameOrId(cd)
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
FJQC.GetFormatJSON(myarg)
try:
building = callGAPI(cd.resources().buildings(), 'get',
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], buildingId=buildingId)
_showBuilding(building, FJQC=FJQC)
except GAPI.resourceNotFound:
entityUnknownWarning(Ent.BUILDING_ID, buildingId)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
BUILDINGS_FIELDS_CHOICE_MAP = {
'address': 'address',
'buildingid': 'buildingId',
'buildingname': 'buildingName',
'coordinates': 'coordinates',
'description': 'description',
'floors': 'floorNames',
'floornames': 'floorNames',
'id': 'buildingId',
'name': 'buildingName',
}
BUILDINGS_SORT_TITLES = ['buildingId', 'buildingName', 'description', 'floorNames']
# gam print buildings [todrive <ToDriveAttribute>*]
# [allfields|<BuildingFildName>*|(fields <BuildingFieldNameList>)]
# [delimiter <Character>] [formatjson [quotechar <Character>]]
# gam show buildings
# [allfields|<BuildingFildName>*|(fields <BuildingFieldNameList>)]
# [formatjson]
def doPrintShowBuildings():
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['buildingId'], BUILDINGS_SORT_TITLES) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] if csvPF else ','
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'allfields':
fieldsList = []
elif getFieldsList(myarg, BUILDINGS_FIELDS_CHOICE_MAP, fieldsList, initialField='buildingId'):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = getItemFieldsFromFieldsList('buildings', fieldsList)
try:
buildings = callGAPIpages(cd.resources().buildings(), 'list', 'buildings',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
if not csvPF:
jcount = len(buildings)
performActionNumItems(jcount, Ent.BUILDING)
Ind.Increment()
j = 0
for building in buildings:
j += 1
_showBuilding(building, delimiter, j, jcount, FJQC)
Ind.Decrement()
else:
for building in buildings:
if 'buildingId' in building:
building['buildingId'] = f'id:{building["buildingId"]}'
if 'floorNames' in building:
building['floorNames'] = delimiter.join(building['floorNames'])
if 'address' in building and 'addressLines' in building['address']:
building['address']['addressLines'] = '\n'.join(building['address']['addressLines'])
if 'coordinates' in building:
building['coordinates']['latitude'] = f'{building["coordinates"].get("latitude", 0):4.7f}'
building['coordinates']['longitude'] = f'{building["coordinates"].get("longitude", 0):4.7f}'
row = flattenJSON(building)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
else:
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'buildingId': building['buildingId'],
'JSON': json.dumps(cleanJSON(building),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Buildings')
def _getFeatureAttributes(body):
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
return body
# gam create feature name <Name>
def doCreateFeature():
cd = buildGAPIObject(API.DIRECTORY)
body = _getFeatureAttributes({})
try:
callGAPI(cd.resources().features(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], body=body)
entityActionPerformed([Ent.BUILDING, body['name']])
except GAPI.duplicate:
entityDuplicateWarning([Ent.FEATURE, body['name']])
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.FEATURE, body['name']], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
#gam update feature <Name> name <Name>
def doUpdateFeature():
# update does not work for name and name is only field to be updated
# if additional writable fields are added to feature in the future
# we'll add support for update as well as rename
cd = buildGAPIObject(API.DIRECTORY)
oldName = getString(Cmd.OB_STRING)
getChoice(['name'])
body = {'newName': getString(Cmd.OB_STRING)}
checkForExtraneousArguments()
try:
callGAPI(cd.resources().features(), 'rename',
throwReasons=[GAPI.DUPLICATE, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], oldName=oldName, body=body)
entityActionPerformed([Ent.FEATURE, oldName])
except GAPI.duplicate:
entityDuplicateWarning([Ent.FEATURE, body['newName']])
except GAPI.resourceNotFound:
entityUnknownWarning(Ent.FEATURE, oldName)
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.FEATURE, oldName], str(e))
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
# gam delete feature <Name>
def doDeleteFeature():
cd = buildGAPIObject(API.DIRECTORY)
featureKey = getString(Cmd.OB_NAME)
checkForExtraneousArguments()
try:
callGAPI(cd.resources().features(), 'delete',
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], featureKey=featureKey)
entityActionPerformed([Ent.FEATURE, featureKey])
except GAPI.resourceNotFound:
entityUnknownWarning(Ent.FEATURE, featureKey)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
FEATURE_FIELDS_CHOICE_MAP = {
'name': 'name',
}
# gam print features [todrive <ToDriveAttribute>*]
# gam show features
def doPrintShowFeatures():
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile('name') if Act.csvFormat() else None
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'allfields':
fieldsList = []
elif getFieldsList(myarg, FEATURE_FIELDS_CHOICE_MAP, fieldsList):
pass
else:
unknownArgumentExit()
fields = getItemFieldsFromFieldsList('features', fieldsList)
try:
features = callGAPIpages(cd.resources().features(), 'list', 'features',
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
accessErrorExit(cd)
if not csvPF:
jcount = len(features)
performActionNumItems(jcount, Ent.FEATURE)
Ind.Increment()
j = 0
for feature in features:
j += 1
printEntity([Ent.FEATURE, feature['name']], j, jcount)
else:
for feature in features:
csvPF.WriteRowTitles(flattenJSON(feature))
if csvPF:
csvPF.writeCSVfile('Features')
RESOURCE_CATEGORY_MAP = {
'conference': 'CONFERENCE_ROOM',
'conferenceroom': 'CONFERENCE_ROOM',
'room': 'CONFERENCE_ROOM',
'other': 'OTHER',
'categoryunknown': 'CATEGORY_UNKNOWN',
'unknown': 'CATEGORY_UNKNOWN',
}
def _getResourceCalendarAttributes(cd, body, updateMode):
featureChanges = {'add': set(), 'remove': set()}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'name', 'resourcename'}:
body['resourceName'] = getString(Cmd.OB_STRING)
elif myarg in {'description', 'resourcedescription'}:
body['resourceDescription'] = getStringWithCRsNLs()
elif myarg in {'type', 'resourcetype'}:
body['resourceType'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'building', 'buildingid'}:
body['buildingId'] = _getBuildingByNameOrId(cd, minLen=0)
elif myarg == 'capacity':
body['capacity'] = getInteger(minVal=0)
elif myarg in {'feature', 'features', 'featureinstances'}:
body.setdefault('featureInstances', [])
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
body['featureInstances'].append({'feature': {'name': feature}})
elif myarg in {'addfeature', 'addfeatures', 'addfeatureinstances'}:
if not updateMode:
body.setdefault('featureInstances', [])
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
body['featureInstances'].append({'feature': {'name': feature}})
else:
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
featureChanges['add'].add(feature)
elif updateMode and myarg in {'removefeature', 'removefeatures', 'removefeatureinstances'}:
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
featureChanges['remove'].add(feature)
elif myarg in {'floor', 'floorname'}:
body['floorName'] = getString(Cmd.OB_STRING)
elif myarg == 'floorsection':
body['floorSection'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'category', 'resourcecategory'}:
body['resourceCategory'] = getChoice(RESOURCE_CATEGORY_MAP, mapChoice=True)
elif myarg in {'userdescription', 'uservisibledescription'}:
body['userVisibleDescription'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
if ('featureInstances' in body and not body['featureInstances'] and
not featureChanges['add'] and not featureChanges['remove']):
body['featureInstances'] = [{}]
if not updateMode:
return body
return body, featureChanges
# gam create resource <ResourceID> <Name> <ResourceAttribute>*
def doCreateResourceCalendar():
cd = buildGAPIObject(API.DIRECTORY)
body = _getResourceCalendarAttributes(cd, {'resourceId': getString(Cmd.OB_RESOURCE_ID), 'resourceName': getString(Cmd.OB_NAME)}, False)
try:
callGAPI(cd.resources().calendars(), 'insert',
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.SERVICE_NOT_AVAILABLE,
GAPI.REQUIRED, GAPI.DUPLICATE, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
entityActionPerformed([Ent.RESOURCE_CALENDAR, body['resourceId']])
except (GAPI.invalid, GAPI.invalidInput, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], str(e))
except GAPI.required as e:
errMsg = str(e)
if '[resourceCapacity, resourceFloorName]' in errMsg:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], Msg.RESOURCE_CAPACITY_FLOOR_REQUIRED)
elif'[resourceFloorName]' in errMsg:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], Msg.RESOURCE_FLOOR_REQUIRED)
else:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], errMsg)
except GAPI.duplicate:
entityDuplicateWarning([Ent.RESOURCE_CALENDAR, body['resourceId']])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
def _doUpdateResourceCalendars(entityList):
cd = buildGAPIObject(API.DIRECTORY)
body, featureChanges = _getResourceCalendarAttributes(cd, {}, True)
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
try:
if featureChanges['add'] or featureChanges['remove']:
features = callGAPI(cd.resources().calendars(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, fields='featureInstances(feature(name))')
bodyFeatures = body.pop('featureInstances', [])
body['featureInstances'] = []
featureSet = set()
for feature in bodyFeatures:
featureName = feature['feature']['name']
if featureName not in featureChanges['remove'] and featureName not in featureSet:
body['featureInstances'].append({'feature': {'name': featureName}})
featureSet.add(featureName)
for feature in features.get('featureInstances', []):
featureName = feature['feature']['name']
if featureName not in featureChanges['remove'] and featureName not in featureSet:
body['featureInstances'].append({'feature': {'name': featureName}})
featureSet.add(featureName)
for featureName in featureChanges['add']:
if featureName not in featureChanges['remove'] and featureName not in featureSet:
body['featureInstances'].append({'feature': {'name': featureName}})
featureSet.add(featureName)
if not body['featureInstances']:
body['featureInstances'] = [{}]
callGAPI(cd.resources().calendars(), 'patch',
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.SERVICE_NOT_AVAILABLE, GAPI.REQUIRED,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, body=body, fields='')
entityActionPerformed([Ent.RESOURCE_CALENDAR, resourceId], i, count)
except (GAPI.invalid, GAPI.invalidInput, GAPI.serviceNotAvailable, GAPI.required) as e:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, resourceId], str(e), i, count)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
# gam update resources <ResourceEntity> <ResourceAttribute>*
def doUpdateResourceCalendars():
_doUpdateResourceCalendars(getEntityList(Cmd.OB_RESOURCE_ENTITY))
# gam update resource <ResourceID> <ResourceAttribute>*
def doUpdateResourceCalendar():
_doUpdateResourceCalendars(getStringReturnInList(Cmd.OB_RESOURCE_ID))
def _doDeleteResourceCalendars(entityList):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
try:
callGAPI(cd.resources().calendars(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId)
entityActionPerformed([Ent.RESOURCE_CALENDAR, resourceId], i, count)
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, resourceId], str(e), i, count)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
# gam delete resources <ResourceEntity>
def doDeleteResourceCalendars():
_doDeleteResourceCalendars(getEntityList(Cmd.OB_RESOURCE_ENTITY))
# gam delete resource <ResourceID>
def doDeleteResourceCalendar():
_doDeleteResourceCalendars(getStringReturnInList(Cmd.OB_RESOURCE_ID))
def _getResourceACLsCalSettings(cal, resource, getCalSettings, getCalPermissions, i, count):
calId = resource['resourceEmail']
try:
if getCalPermissions:
acls = callGAPIpages(cal.acl(), 'list', 'items',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
calendarId=calId, fields='nextPageToken,items(id,role,scope)')
else:
acls = {}
if getCalSettings:
settings = callGAPI(cal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
calendarId=calId)
settings.pop('etag', None)
settings.pop('kind', None)
resource.update({'calendar': settings})
return (True, acls)
except (GAPI.forbidden, GAPI.serviceNotAvailable, GAPI.authError, GAPI.notACalendarUser) as e:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, calId], str(e), i, count)
except GAPI.notFound:
entityUnknownWarning(Ent.RESOURCE_CALENDAR, calId, i, count)
return (False, None)
RESOURCE_DFLT_FIELDS = ['resourceId', 'resourceName', 'resourceEmail', 'resourceDescription', 'resourceType']
RESOURCE_ADDTL_FIELDS = [
'buildingId', # buildingId must be first element
'capacity',
'featureInstances',
'floorName',
'floorSection',
'generatedResourceName',
'resourceCategory',
'userVisibleDescription',
]
RESOURCE_ALL_FIELDS = RESOURCE_DFLT_FIELDS+RESOURCE_ADDTL_FIELDS
RESOURCE_FIELDS_WITH_CRS_NLS = {'resourceDescription'}
def _showResource(cd, resource, i, count, FJQC, acls=None, noSelfOwner=False):
def _showResourceField(title, resource, field):
if field in resource:
if field not in RESOURCE_FIELDS_WITH_CRS_NLS:
printKeyValueList([title, resource[field]])
else:
printKeyValueWithCRsNLs(title, resource[field])
if 'buildingId' in resource:
resource['buildingName'] = _getBuildingNameById(cd, resource['buildingId'])
resource['buildingId'] = f'id:{resource["buildingId"]}'
if FJQC.formatJSON:
if acls:
resource['acls'] = [{'id': rule['id'], 'role': rule['role']} for rule in acls]
printLine(json.dumps(cleanJSON(resource), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.RESOURCE_ID, resource['resourceId']], i, count)
Ind.Increment()
_showResourceField('Name', resource, 'resourceName')
_showResourceField('Email', resource, 'resourceEmail')
_showResourceField('Type', resource, 'resourceType')
_showResourceField('Description', resource, 'resourceDescription')
if 'featureInstances' in resource:
resource['featureInstances'] = ', '.join([a_feature['feature']['name'] for a_feature in resource.pop('featureInstances')])
if 'buildingId' in resource:
_showResourceField('buildingId', resource, 'buildingId')
_showResourceField('buildingName', resource, 'buildingName')
for field in RESOURCE_ADDTL_FIELDS[1:]:
_showResourceField(field, resource, field)
calendar = resource.get('calendar')
if calendar:
_showCalendarSettings(calendar, 0, 0)
if acls:
j = 0
jcount = len(acls)
printEntitiesCount(Ent.CALENDAR_ACL, acls)
Ind.Increment()
for rule in acls:
j += 1
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == resource['resourceEmail']:
continue
printKeyValueListWithCount(ACLRuleKeyValueList(rule), j, jcount)
Ind.Decrement()
Ind.Decrement()
def _doInfoResourceCalendars(entityList):
cd = buildGAPIObject(API.DIRECTORY)
getCalSettings = getCalPermissions = noSelfOwner = False
FJQC = FormatJSONQuoteChar()
acls = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
getCalPermissions = True
elif myarg == 'noselfowner':
noSelfOwner = True
elif myarg == Cmd.ARG_CALENDAR:
getCalSettings = True
else:
FJQC.GetFormatJSON(myarg)
if getCalSettings or getCalPermissions:
cal = buildGAPIObject(API.CALENDAR)
fields = ','.join(RESOURCE_ALL_FIELDS)
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
try:
resource = callGAPI(cd.resources().calendars(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, fields=fields)
if getCalSettings or getCalPermissions:
status, acls = _getResourceACLsCalSettings(cal, resource, getCalSettings, getCalPermissions, i, count)
if not status:
continue
_showResource(cd, resource, i, count, FJQC, acls, noSelfOwner)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
# gam info resources <ResourceEntity>
# [acls] [noselfowner] [calendar] [formatjson]
def doInfoResourceCalendars():
_doInfoResourceCalendars(getEntityList(Cmd.OB_RESOURCE_ENTITY))
# gam info resource <ResourceID>
# [acls] [noselfowner] [calendar] [formatjson]
def doInfoResourceCalendar():
_doInfoResourceCalendars(getStringReturnInList(Cmd.OB_RESOURCE_ID))
RESOURCE_FIELDS_CHOICE_MAP = {
'description': 'resourceDescription',
'building': 'buildingId',
'buildingid': 'buildingId',
'capacity': 'capacity',
'category': 'resourceCategory',
'email': 'resourceEmail',
'feature': 'featureInstances',
'features': 'featureInstances',
'featureinstances': 'featureInstances',
'floor': 'floorName',
'floorname': 'floorName',
'floorsection': 'floorSection',
'generatedresourcename': 'generatedResourceName',
'id': 'resourceId',
'name': 'resourceName',
'resourcecategory': 'resourceCategory',
'resourcedescription': 'resourceDescription',
'resourceemail': 'resourceEmail',
'resourceid': 'resourceId',
'resourcename': 'resourceName',
'resourcetype': 'resourceType',
'type': 'resourceType',
'userdescription': 'userVisibleDescription',
'uservisibledescription': 'userVisibleDescription',
}
# gam show resources
# [allfields|<ResourceFieldName>*|(fields <ResourceFieldNameList>)]
# [query <String>]
# [acls] [noselfowner] [calendar] [convertcrnl] [formatjson]
# gam print resources [todrive <ToDriveAttribute>*]
# [allfields|<ResourceFieldName>*|(fields <ResourceFieldNameList>)]
# [query <String>]
# [acls] [noselfowner] [calendar] [convertcrnl] [formatjson [quotechar <Character>]]
# [showitemcountonly]
def doPrintShowResourceCalendars():
cd = buildGAPIObject(API.DIRECTORY)
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
getCalSettings = getCalPermissions = noSelfOwner = False
acls = query = None
fieldsList = []
csvPF = CSVPrintFile() if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
showItemCountOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'query':
query = getString(Cmd.OB_QUERY)
elif myarg == 'allfields':
fieldsList = RESOURCE_ALL_FIELDS[:]
elif myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
getCalPermissions = True
elif myarg == 'noselfowner':
noSelfOwner = True
elif myarg == Cmd.ARG_CALENDAR:
getCalSettings = True
elif myarg in RESOURCE_FIELDS_CHOICE_MAP:
if not fieldsList:
fieldsList = ['resourceId']
fieldsList.append(RESOURCE_FIELDS_CHOICE_MAP[myarg])
elif myarg == 'fields':
if not fieldsList:
fieldsList = ['resourceId']
for field in _getFieldsList():
if field in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
getCalPermissions = True
elif field == Cmd.ARG_CALENDAR:
getCalSettings = True
elif field in RESOURCE_FIELDS_CHOICE_MAP:
fieldsList.append(RESOURCE_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, RESOURCE_FIELDS_CHOICE_MAP, True)
elif myarg in {'convertcrnl', 'converttextnl'}:
convertCRNL = True
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if not fieldsList:
fieldsList = RESOURCE_DFLT_FIELDS[:]
if getCalSettings or getCalPermissions:
cal = buildGAPIObject(API.CALENDAR)
fields = getItemFieldsFromFieldsList('items', fieldsList+['resourceEmail'])
else:
fields = getItemFieldsFromFieldsList('items', fieldsList)
if 'buildingId' in fieldsList:
fieldsList.append('buildingName')
if csvPF:
if not FJQC.formatJSON:
csvPF.AddTitles(fieldsList)
csvPF.SetSortTitles(RESOURCE_DFLT_FIELDS)
else:
if 'resourceName' in fieldsList:
sortTitles = ['resourceId', 'resourceName', 'JSON']
else:
sortTitles = ['resourceId', 'JSON']
csvPF.AddJSONTitles(sortTitles)
printGettingAllAccountEntities(Ent.RESOURCE_CALENDAR)
try:
resources = callGAPIpages(cd.resources().calendars(), 'list', 'items',
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='resourceName',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
query=query, customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
except GAPI.invalidInput as e:
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, ''], str(e))
return
count = len(resources)
if showItemCountOnly:
writeStdout(f'{count}\n')
return
i = 0
for resource in resources:
i += 1
if getCalSettings or getCalPermissions:
status, acls = _getResourceACLsCalSettings(cal, resource, getCalSettings, getCalPermissions, i, count)
if not status:
continue
if not csvPF:
_showResource(cd, resource, i, count, FJQC, acls, noSelfOwner)
else:
if 'buildingId' in resource:
resource['buildingName'] = _getBuildingNameById(cd, resource['buildingId'])
resource['buildingId'] = f'id:{resource["buildingId"]}'
if not FJQC.formatJSON:
if 'featureInstances' in resource:
resource['featureInstances'] = ', '.join([a_feature['feature']['name'] for a_feature in resource.pop('featureInstances')])
row = {}
for field in fieldsList:
if convertCRNL and field in RESOURCE_FIELDS_WITH_CRS_NLS:
row[field] = escapeCRsNLs(resource.get(field, ''))
else:
row[field] = resource.get(field, '')
if getCalSettings and 'calendar' in resource:
flattenJSON(resource['calendar'], flattened=row)
if getCalPermissions:
for rule in acls:
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == resource['resourceEmail']:
continue
csvPF.WriteRowTitles(flattenJSON(rule, flattened=row.copy()))
else:
csvPF.WriteRowTitles(row)
else:
if getCalPermissions:
resource['acls'] = []
for rule in acls:
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == resource['resourceEmail']:
continue
resource['acls'].append({'id': rule['id'], 'role': rule['role']})
row = {'resourceId': resource['resourceId'], 'JSON': json.dumps(cleanJSON(resource), ensure_ascii=False, sort_keys=True)}
if 'resourceName' in resource:
row['resourceName'] = resource['resourceName']
csvPF.WriteRow(row)
if csvPF:
csvPF.writeCSVfile('Resources')
# Calendar commands utilities
def normalizeCalendarId(calId, user):
if not user or calId.lower() != 'primary':
return convertUIDtoEmailAddress(calId, emailTypes=['user', 'resource'])
return user
def checkCalendarExists(cal, calId, i, count, showMessage=False):
if not cal:
cal = buildGAPIObject(API.CALENDAR)
try:
return callGAPI(cal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
calendarId=calId, fields='id')['id']
except GAPI.notFound as e:
if showMessage:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e))
return None
except GAPI.notACalendarUser:
if showMessage:
userCalServiceNotEnabledWarning(calId, i, count)
return None
def validateCalendar(calId, i=0, count=0, noClientAccess=False):
cal = None
if not calId.endswith('.calendar.google.com'):
calId, cal = buildGAPIServiceObject(API.CALENDAR, calId, i, count, displayError=noClientAccess)
if not cal:
if noClientAccess:
return (calId, None)
cal = buildGAPIObject(API.CALENDAR)
try:
callGAPI(cal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
calendarId=calId, fields='')
return (calId, cal)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
return (calId, None)
def getNormalizedCalIdCal(cal, calId, user, i=0, count=0):
if not cal:
return validateCalendar(calId, i, count)
return (normalizeCalendarId(calId, user), cal)
CALENDAR_ACL_ROLES_MAP = {
'editor': 'writer',
'freebusy': 'freeBusyReader',
'freebusyreader': 'freeBusyReader',
'owner': 'owner',
'read': 'reader',
'reader': 'reader',
'writer': 'writer',
'none': 'none',
}
ACL_SCOPE_CHOICES = ['default', 'user', 'group', 'domain'] # default must be first element
def getACLScope():
scopeType, scopeValue = getChoiceAndValue(Cmd.OB_ACL_SCOPE, ACL_SCOPE_CHOICES[1:], ':')
if scopeType:
if scopeType != 'domain':
scopeValue = normalizeEmailAddressOrUID(scopeValue, noUid=True)
else:
scopeValue = scopeValue.lower()
return (scopeType, scopeValue)
scopeType = getChoice(ACL_SCOPE_CHOICES, defaultChoice='user')
if scopeType == 'domain':
entity = getString(Cmd.OB_DOMAIN_NAME, optional=True)
if entity:
scopeValue = entity.lower()
else:
scopeValue = GC.Values[GC.DOMAIN]
elif scopeType != 'default':
scopeValue = getEmailAddress(noUid=True)
else:
scopeValue = None
return (scopeType, scopeValue)
def getCalendarACLScope():
scopeType, scopeValue = getACLScope()
if scopeType != 'default':
return {'list': [f'{scopeType}:{scopeValue}'], 'dict': None}
return {'list': [scopeType], 'dict': None}
def getCalendarSiteACLScopeEntity():
ACLScopeEntity = {'list': getEntityList(Cmd.OB_ACL_SCOPE_ENTITY), 'dict': None}
if isinstance(ACLScopeEntity['list'], dict):
ACLScopeEntity['dict'] = ACLScopeEntity['list']
return ACLScopeEntity
def getCalendarACLSendNotifications():
return getBoolean() if checkArgumentPresent('sendnotifications') else True
def getCalendarCreateUpdateACLsOptions(getScopeEntity):
role = getChoice(CALENDAR_ACL_ROLES_MAP, mapChoice=True)
ACLScopeEntity = getCalendarSiteACLScopeEntity() if getScopeEntity else getCalendarACLScope()
sendNotifications = getCalendarACLSendNotifications()
checkForExtraneousArguments()
return (role, ACLScopeEntity, sendNotifications)
def getCalendarDeleteACLsOptions(getScopeEntity):
rolesMap = CALENDAR_ACL_ROLES_MAP.copy()
rolesMap['id'] = 'id'
role = getChoice(rolesMap, defaultChoice=None, mapChoice=True)
ACLScopeEntity = getCalendarSiteACLScopeEntity() if getScopeEntity else getCalendarACLScope()
checkForExtraneousArguments()
return (role, ACLScopeEntity)
def _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, j, jcount, ACLScopeEntity, showAction=True):
if ACLScopeEntity['dict']:
if origUser:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
ruleIds = ACLScopeEntity['dict'][calId]
else:
ruleIds = ACLScopeEntity['dict'][origUser][calId]
else:
ruleIds = ACLScopeEntity['dict'][calId]
else:
ruleIds = ACLScopeEntity['list']
calId, cal = getNormalizedCalIdCal(origCal, calId, user, j, jcount)
if not cal:
return (calId, cal, None, 0)
kcount = len(ruleIds)
if kcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if showAction:
entityPerformActionNumItems([Ent.CALENDAR, calId], kcount, Ent.CALENDAR_ACL, j, jcount)
return (calId, cal, ruleIds, kcount)
def _processCalendarACLs(cal, function, entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications):
result = True
if function == 'insert':
kwargs = {'body': makeRoleRuleIdBody(role, ruleId), 'fields': '', 'sendNotifications': sendNotifications}
elif function == 'patch':
kwargs = {'ruleId': ruleId, 'body': {'role': role}, 'fields': '', 'sendNotifications': sendNotifications}
else: # elif function == 'delete':
kwargs = {'ruleId': ruleId}
try:
callGAPI(cal.acl(), function,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER, GAPI.INVALID_SCOPE_VALUE,
GAPI.ILLEGAL_ACCESS_ROLE_FOR_DEFAULT, GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWNER_ACL,
GAPI.FORBIDDEN, GAPI.AUTH_ERROR, GAPI.CONDITION_NOT_MET],
calendarId=calId, **kwargs)
entityActionPerformed([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, role)], k, kcount)
except GAPI.notFound as e:
if not checkCalendarExists(cal, calId, j, jcount):
entityUnknownWarning(entityType, calId, j, jcount)
result = False
else:
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, role)], str(e), k, kcount)
except (GAPI.invalid, GAPI.invalidParameter, GAPI.invalidScopeValue,
GAPI.illegalAccessRoleForDefault, GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnerAcl,
GAPI.forbidden, GAPI.authError, GAPI.conditionNotMet) as e:
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, role)], str(e), k, kcount)
return result
def _createCalendarACLs(cal, entityType, calId, j, jcount, role, ruleIds, kcount, sendNotifications):
Ind.Increment()
k = 0
for ruleId in ruleIds:
k += 1
ruleId = normalizeRuleId(ruleId)
if not _processCalendarACLs(cal, 'insert', entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications):
break
Ind.Decrement()
def _doCalendarsCreateACLs(origUser, user, origCal, calIds, count, role, ACLScopeEntity, sendNotifications):
i = 0
for calId in calIds:
i += 1
calId, cal, ruleIds, jcount = _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, i, count, ACLScopeEntity)
if jcount == 0:
continue
_createCalendarACLs(cal, Ent.CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
# gam calendar <CalendarEntity> create <CalendarACLRole> <CalendarACLScope> [sendnotifications <Boolean>]
def doCalendarsCreateACL(calIds):
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(False)
_doCalendarsCreateACLs(None, None, None, calIds, len(calIds), role, ACLScopeEntity, sendNotifications)
# gam calendars <CalendarEntity> create acls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
def doCalendarsCreateACLs(calIds):
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
_doCalendarsCreateACLs(None, None, None, calIds, len(calIds), role, ACLScopeEntity, sendNotifications)
def _updateDeleteCalendarACLs(cal, function, entityType, calId, j, jcount, role, ruleIds, kcount, sendNotifications):
Ind.Increment()
k = 0
for ruleId in ruleIds:
k += 1
ruleId = normalizeRuleId(ruleId)
if not _processCalendarACLs(cal, function, entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications):
break
Ind.Decrement()
def _doUpdateDeleteCalendarACLs(origUser, user, origCal, function, calIds, count, ACLScopeEntity, role, sendNotifications):
i = 0
for calId in calIds:
i += 1
calId, cal, ruleIds, jcount = _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, i, count, ACLScopeEntity)
if jcount == 0:
continue
_updateDeleteCalendarACLs(cal, function, Ent.CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
# gam calendar <CalendarEntity> update <CalendarACLRole> <CalendarACLScope> [sendnotifications <Boolean>]
def doCalendarsUpdateACL(calIds):
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(False)
_doUpdateDeleteCalendarACLs(None, None, None, 'patch', calIds, len(calIds), ACLScopeEntity, role, sendNotifications)
# gam calendars <CalendarEntity> update acls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
def doCalendarsUpdateACLs(calIds):
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
_doUpdateDeleteCalendarACLs(None, None, None, 'patch', calIds, len(calIds), ACLScopeEntity, role, sendNotifications)
# gam calendar <CalendarEntity> delete [<CalendarACLRole>] <CalendarACLScope>
def doCalendarsDeleteACL(calIds):
role, ACLScopeEntity = getCalendarDeleteACLsOptions(False)
_doUpdateDeleteCalendarACLs(None, None, None, 'delete', calIds, len(calIds), ACLScopeEntity, role, False)
# gam calendars <CalendarEntity> delete acls <CalendarACLScopeEntity>
def doCalendarsDeleteACLs(calIds):
role, ACLScopeEntity = getCalendarDeleteACLsOptions(True)
_doUpdateDeleteCalendarACLs(None, None, None, 'delete', calIds, len(calIds), ACLScopeEntity, role, False)
def _showCalendarACL(user, entityType, calId, acl, k, kcount, FJQC):
if FJQC.formatJSON:
if entityType == Ent.CALENDAR:
if user:
printLine(json.dumps(cleanJSON({'primaryEmail': user, 'calendarId': calId, 'acl': acl}),
ensure_ascii=False, sort_keys=True))
else:
printLine(json.dumps(cleanJSON({'calendarId': calId, 'acl': acl}),
ensure_ascii=False, sort_keys=True))
else:
printLine(json.dumps(cleanJSON({'resourceId': user, 'resourceEmail': calId, 'acl': acl}),
ensure_ascii=False, sort_keys=True))
else:
printKeyValueListWithCount(ACLRuleKeyValueList(acl), k, kcount)
def _infoCalendarACLs(cal, user, entityType, calId, j, jcount, ruleIds, kcount, FJQC):
Ind.Increment()
k = 0
for ruleId in ruleIds:
k += 1
ruleId = normalizeRuleId(ruleId)
try:
result = callGAPI(cal.acl(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_SCOPE_VALUE, GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
calendarId=calId, ruleId=ruleId, fields='id,role,scope')
_showCalendarACL(user, entityType, calId, result, k, kcount, FJQC)
except (GAPI.notFound, GAPI.invalid) as e:
if not checkCalendarExists(cal, calId, j, jcount):
entityUnknownWarning(entityType, calId, j, jcount)
break
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, None)], str(e), k, kcount)
except (GAPI.invalidScopeValue, GAPI.forbidden, GAPI.authError) as e:
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, None)], str(e), k, kcount)
Ind.Decrement()
def _doInfoCalendarACLs(origUser, user, origCal, calIds, count, ACLScopeEntity, FJQC):
i = 0
for calId in calIds:
i += 1
calId, cal, ruleIds, jcount = _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, i, count, ACLScopeEntity, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
_infoCalendarACLs(cal, user, Ent.CALENDAR, calId, i, count, ruleIds, jcount, FJQC)
def _getCalendarInfoACLOptions():
return FormatJSONQuoteChar(formatJSONOnly=True)
# gam calendars <CalendarEntity> info acl|acls <CalendarACLScopeEntity>
# [formatjson]
def doCalendarsInfoACLs(calIds):
ACLScopeEntity = getCalendarSiteACLScopeEntity()
FJQC = _getCalendarInfoACLOptions()
_doInfoCalendarACLs(None, None, None, calIds, len(calIds), ACLScopeEntity, FJQC)
def _printShowCalendarACLs(cal, user, entityType, calId, i, count, csvPF, FJQC, noSelfOwner, addCSVData):
if csvPF:
printGettingEntityItemForWhom(Ent.CALENDAR_ACL, calId, i, count)
try:
acls = callGAPIpages(cal.acl(), 'list', 'items',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
calendarId=calId, fields='nextPageToken,items(id,role,scope)')
except (GAPI.forbidden, GAPI.authError) as e:
entityActionFailedWarning([entityType, calId], str(e), i, count)
return
except GAPI.notFound:
entityUnknownWarning(entityType, calId, i, count)
return
jcount = len(acls)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
if not noSelfOwner:
entityPerformActionNumItems([entityType, calId], jcount, Ent.CALENDAR_ACL, i, count)
else:
entityPerformActionModifierNumItems([entityType, calId], Msg.MAXIMUM_OF, jcount, Ent.CALENDAR_ACL, i, count)
Ind.Increment()
j = 0
for rule in acls:
j += 1
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == calId:
continue
_showCalendarACL(user, entityType, calId, rule, j, jcount, FJQC)
Ind.Decrement()
else:
if entityType == Ent.CALENDAR:
if acls:
for rule in acls:
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == calId:
continue
row = {'calendarId': calId}
if user:
row['primaryEmail'] = user
if addCSVData:
row.update(addCSVData)
flattenJSON(rule, flattened=row)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'calendarId': calId, 'JSON': json.dumps(cleanJSON(rule),
ensure_ascii=False, sort_keys=False)}
if user:
row['primaryEmail'] = user
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowNoFilter(row)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT] and user:
csvPF.WriteRowNoFilter({'calendarId': calId, 'primaryEmail': user})
else: # Ent.RESOURCE_CALENDAR
for rule in acls:
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == calId:
continue
row = {'resourceId': user, 'resourceEmail': calId}
if addCSVData:
row.update(addCSVData)
flattenJSON(rule, flattened=row)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'resourceId': user, 'resourceEmail': calId, 'JSON': json.dumps(cleanJSON(rule),
ensure_ascii=False, sort_keys=False)}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowNoFilter(row)
def _getCalendarPrintShowACLOptions(titles):
csvPF = CSVPrintFile(titles, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
noSelfOwner = False
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'noselfowner':
noSelfOwner = True
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF:
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if FJQC.formatJSON:
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
csvPF.MoveJSONTitlesToEnd(['JSON'])
csvPF.SetSortAllTitles()
return (csvPF, FJQC, noSelfOwner, addCSVData)
# gam calendars <CalendarEntity> print acls [todrive <ToDriveAttribute>*]
# [noselfowner] (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam calendars <CalendarEntity> show acls
# [noselfowner]
# [formatjson]
# gam calendar <CalendarEntity> printacl [todrive <ToDriveAttribute>*]
# [noselfowner] (addcsvdata <FieldName> <String>)*
# [formatjson]
# gam calendar <CalendarEntity> showacl
# [noselfowner]
# [formatjson]
def doCalendarsPrintShowACLs(calIds):
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['calendarId'])
count = len(calIds)
i = 0
for calId in calIds:
i += 1
calId, cal = validateCalendar(calId, i, count)
if not cal:
continue
_printShowCalendarACLs(cal, None, Ent.CALENDAR, calId, i, count, csvPF, FJQC, noSelfOwner, addCSVData)
if csvPF:
csvPF.writeCSVfile('Calendar ACLs')
EVENT_TYPE_BIRTHDAY = 'birthday'
EVENT_TYPE_DEFAULT = 'default'
EVENT_TYPE_FOCUSTIME = 'focusTime'
EVENT_TYPE_FROMGMAIL = 'fromGmail'
EVENT_TYPE_OUTOFOFFICE = 'outOfOffice'
EVENT_TYPE_WORKINGLOCATION = 'workingLocation'
EVENT_TYPES_CHOICE_MAP = {
'birthday': EVENT_TYPE_BIRTHDAY,
'default': EVENT_TYPE_DEFAULT,
'focustime': EVENT_TYPE_FOCUSTIME,
'fromgmail': EVENT_TYPE_FROMGMAIL,
'outofoffice': EVENT_TYPE_OUTOFOFFICE,
'workinglocation': EVENT_TYPE_WORKINGLOCATION,
}
EVENT_TYPE_PROPERTIES_NAME_MAP = {
EVENT_TYPE_DEFAULT: None,
EVENT_TYPE_FOCUSTIME: f'{EVENT_TYPE_FOCUSTIME}Properties',
EVENT_TYPE_FROMGMAIL: None,
EVENT_TYPE_OUTOFOFFICE: f'{EVENT_TYPE_OUTOFOFFICE}Properties',
EVENT_TYPE_WORKINGLOCATION: f'{EVENT_TYPE_WORKINGLOCATION}Properties',
}
EVENT_TYPE_ENTITY_MAP = {
EVENT_TYPE_BIRTHDAY: Ent.EVENT_BIRTHDAY,
EVENT_TYPE_DEFAULT: None,
EVENT_TYPE_FOCUSTIME: Ent.EVENT_FOCUSTIME,
EVENT_TYPE_FROMGMAIL: None,
EVENT_TYPE_OUTOFOFFICE: Ent.EVENT_OUTOFOFFICE,
EVENT_TYPE_WORKINGLOCATION: Ent.EVENT_WORKINGLOCATION,
}
def _getEventTypes():
typesList = []
for field in _getFieldsList():
if field in EVENT_TYPES_CHOICE_MAP:
addFieldToFieldsList(field, EVENT_TYPES_CHOICE_MAP, typesList)
else:
invalidChoiceExit(field, EVENT_TYPES_CHOICE_MAP, True)
# return ','.join(typesList)
return typesList
LIST_EVENTS_DISPLAY_PROPERTIES = {
'alwaysincludeemail': ('alwaysIncludeEmail', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
'icaluid': ('iCalUID', {GC.VAR_TYPE: GC.TYPE_STRING}),
'maxattendees': ('maxAttendees', {GC.VAR_TYPE: GC.TYPE_INTEGER, GC.VAR_LIMITS: (1, None)}),
'orderby': ('orderBy', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': {'starttime': 'startTime', 'updated': 'updated'}}),
'timezone': ('timeZone', {GC.VAR_TYPE: GC.TYPE_STRING}),
}
LIST_EVENTS_SELECT_PROPERTIES = {
'after': ('timeMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'before': ('timeMax', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'endtime': ('timeMax', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'includedeleted': ('showDeleted', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
'includehidden': ('showHiddenInvitations', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
'privateextendedproperty': ('privateExtendedProperty', {GC.VAR_TYPE: GC.TYPE_STRING}),
'sharedextendedproperty': ('sharedExtendedProperty', {GC.VAR_TYPE: GC.TYPE_STRING}),
'showdeletedevents': ('showDeleted', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
'showhiddeninvitations': ('showHiddenInvitations', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
'singleevents': ('singleEvents', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
'starttime': ('timeMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'timemax': ('timeMax', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'timemin': ('timeMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'updated': ('updatedMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'updatedmin': ('updatedMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
'eventtype': ('eventTypes', {GC.VAR_TYPE: GC.TYPE_CHOICE_LIST}),
'eventtypes': ('eventTypes', {GC.VAR_TYPE: GC.TYPE_CHOICE_LIST}),
}
LIST_EVENTS_MATCH_FIELDS = {
'attendees': ['attendees', 'email'],
'attendeesonlydomainlist': ['attendees', 'onlydomainlist'],
'attendeesdomainlist': ['attendees', 'domainlist'],
'attendeesnotdomainlist': ['attendees', 'notdomainlist'],
'attendeespattern': ['attendees', 'match'],
'attendeesstatus': ['attendees', 'status'],
'description': ['description'],
'hangoutlink': ['hangoutLink'],
'location': ['location'],
'summary': ['summary'],
'creatorname': ['creator', 'displayName'],
'creatoremail': ['creator', 'email'],
'organizername': ['organizer', 'displayName'],
'organizeremail': ['organizer', 'email'],
'organizerself': ['organizer', 'self'],
'organisername': ['organizer', 'displayName'],
'organiseremail': ['organizer', 'email'],
'organiserself': ['organizer', 'self'],
'status': ['status'],
'transparency': ['transparency'],
'visibility': ['visibility'],
}
def _getCalendarListEventsProperty(myarg, attributes, kwargs):
attrName, attribute = attributes.get(myarg, (None, None))
if not attrName:
return False
attrType = attribute[GC.VAR_TYPE]
if attrType == GC.TYPE_BOOLEAN:
kwargs[attrName] = True
elif attrType == GC.TYPE_STRING:
kwargs[attrName] = getString(Cmd.OB_STRING)
elif attrType == GC.TYPE_CHOICE:
kwargs[attrName] = getChoice(attribute['choices'], mapChoice=True)
elif attrType == GC.TYPE_DATETIME:
kwargs[attrName] = getTimeOrDeltaFromNow()
elif attrType == GC.TYPE_INTEGER:
minVal, maxVal = attribute[GC.VAR_LIMITS]
kwargs[attrName] = getInteger(minVal=minVal, maxVal=maxVal)
else: # elif attrType == GC.TYPE_CHOICE_LIST:
if attrName == 'eventTypes':
kwargs[attrName] = _getEventTypes()
return True
def _getCalendarListEventsDisplayProperty(myarg, calendarEventEntity):
return _getCalendarListEventsProperty(myarg, LIST_EVENTS_DISPLAY_PROPERTIES, calendarEventEntity['kwargs'])
def initCalendarEventEntity():
return {'list': [], 'queries': [], 'kwargs': {}, 'dict': None,
'matches': [], 'maxinstances': -1, 'countsOnly': False, 'showDayOfWeek': False}
def getCalendarEventEntity():
calendarEventEntity = initCalendarEventEntity()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'event', 'events'}:
entitySelector = getEntitySelector()
if entitySelector:
entityList = getEntitySelection(entitySelector, False)
if isinstance(entityList, dict):
calendarEventEntity['dict'] = entityList
else:
calendarEventEntity['list'] = entityList
else:
calendarEventEntity['list'].extend(convertEntityToList(getString(Cmd.OB_EVENT_ID)))
elif myarg in {'id', 'eventid'}:
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
elif myarg in {'q', 'query', 'eventquery'}:
calendarEventEntity['queries'].append(getString(Cmd.OB_QUERY))
elif myarg == 'matchfield':
matchField = getChoice(LIST_EVENTS_MATCH_FIELDS, mapChoice=True)
if matchField[0] == 'organizer' and matchField[1] == 'self':
calendarEventEntity['matches'].append((matchField, getBoolean()))
elif matchField[0] != 'attendees' or matchField[1] == 'match':
calendarEventEntity['matches'].append((matchField, getREPattern(re.IGNORECASE)))
elif matchField[0] == 'attendees' and matchField[1] in {'onlydomainlist', 'domainlist', 'notdomainlist'}:
calendarEventEntity['matches'].append((matchField, set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').split())))
elif matchField[1] == 'email':
calendarEventEntity['matches'].append((matchField, getNormalizedEmailAddressEntity()))
else: #status
calendarEventEntity['matches'].append((matchField,
getChoice(CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP, defaultChoice=False, mapChoice=True),
getChoice(CALENDAR_ATTENDEE_STATUS_CHOICE_MAP, defaultChoice='needsAction', mapChoice=True),
getNormalizedEmailAddressEntity()))
elif myarg == 'maxinstances':
calendarEventEntity['maxinstances'] = getInteger(minVal=-1)
elif _getCalendarListEventsProperty(myarg, LIST_EVENTS_SELECT_PROPERTIES, calendarEventEntity['kwargs']):
pass
else:
Cmd.Backup()
break
return calendarEventEntity
CALENDAR_EVENT_SENDUPDATES_CHOICE_MAP = {'all': 'all', 'externalonly': 'externalOnly', 'none': 'none'}
def _getCalendarSendUpdates(myarg, parameters):
if myarg == 'sendnotifications':
parameters['sendUpdates'] = 'all' if getBoolean() else 'none'
elif myarg == 'notifyattendees':
parameters['sendUpdates'] = 'all'
elif myarg == 'sendupdates':
parameters['sendUpdates'] = getChoice(CALENDAR_EVENT_SENDUPDATES_CHOICE_MAP, mapChoice=True)
else:
return False
return True
CALENDAR_MIN_COLOR_INDEX = 1
CALENDAR_MAX_COLOR_INDEX = 24
CALENDAR_EVENT_MIN_COLOR_INDEX = 1
CALENDAR_EVENT_MAX_COLOR_INDEX = 11
CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP = {
'optional': True,
'required': False,
'true': True,
'false': False,
'': None,
}
CALENDAR_ATTENDEE_STATUS_CHOICE_MAP = {
'accepted': 'accepted',
'declined': 'declined',
'needsaction': 'needsAction',
'tentative': 'tentative',
'': None,
}
CALENDAR_EVENT_STATUS_CHOICES = ['confirmed', 'tentative', 'cancelled']
CALENDAR_EVENT_TRANSPARENCY_CHOICES = ['opaque', 'transparent']
CALENDAR_EVENT_VISIBILITY_CHOICES = ['default', 'public', 'private', 'confedential']
EVENT_JSON_CLEAR_FIELDS = ['created', 'creator', 'endTimeUpspecified', 'hangoutLink', 'htmlLink', 'eventType',
'privateCopy', 'locked', 'recurringEventId', 'updated']
EVENT_JSON_INSERT_CLEAR_FIELDS = ['iCalUID', 'id', 'organizer']
EVENT_JSON_UPDATE_CLEAR_FIELDS = ['iCalUID', 'id', 'organizer']
EVENT_JSON_SUBFIELD_CLEAR_FIELDS = {
'attendees': ['id', 'organizer', 'self'],
'attachments': ['fileId', 'iconLink', 'mimeType', 'title'],
'organizer': ['id', 'self'],
}
EVENT_JSONATTENDEES_SUBFIELD_CLEAR_FIELDS = {
'attendees': ['id', 'organizer', 'self'],
}
def _getCalendarEventAttribute(myarg, body, parameters, function):
def clearJSONfields(body, clearFields):
for field in clearFields:
body.pop(field, None)
def clearJSONsubfields(body, clearFields):
for field, subfields in iter(clearFields.items()):
if field in body:
if isinstance(body[field], list):
for item in body[field]:
for subfield in subfields:
item.pop(subfield, None)
else:
for subfield in subfields:
body.pop(subfield, None)
cd = None
if function == 'insert' and myarg in {'id', 'eventid'}:
body['id'] = getEventID()
elif function == 'import' and myarg == 'icaluid':
body['iCalUID'] = getString(Cmd.OB_ICALUID)
elif myarg == 'description':
body['description'] = getStringWithCRsNLs()
elif function == 'update' and myarg == 'replacedescription':
parameters['replaceDescription'].append(getREPatternSubstitution(re.IGNORECASE))
elif myarg == 'location':
body['location'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'source':
body['source'] = {'title': getString(Cmd.OB_STRING), 'url': getString(Cmd.OB_URL)}
elif myarg == 'summary':
body['summary'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'start', 'starttime'}:
body['start'] = getEventTime()
elif myarg in {'originalstart', 'originalstarttime'}:
body['originalStart'] = getEventTime()
elif myarg in {'end', 'endtime'}:
body['end'] = getEventTime()
elif myarg == 'allday':
body['start'] = body['end'] = {'date': getYYYYMMDD()}
elif myarg == 'range':
body['start'] = {'date': getYYYYMMDD()}
body['end'] = {'date': getYYYYMMDD()}
elif myarg == 'timerange':
body['start'] = {'dateTime': getTimeOrDeltaFromNow()}
body['end'] = {'dateTime': getTimeOrDeltaFromNow()}
elif myarg == 'birthday':
body['eventType'] = EVENT_TYPE_BIRTHDAY
body['visibility'] = 'private'
body['transparency'] = 'transparent'
bday = getYYYYMMDD(returnDateTime=True)
body['start'] = body['end'] = {'date': bday.strftime(YYYYMMDD_FORMAT)}
if bday.month != 2 or bday.day != 29:
body['recurrence'] = ['RRULE:FREQ=YEARLY']
else:
body['recurrence'] = ['RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1']
elif myarg == 'attachment':
body.setdefault('attachments', [])
body['attachments'].append({'title': getString(Cmd.OB_STRING), 'fileUrl': getString(Cmd.OB_URL)})
elif function == 'update' and myarg == 'clearattachments':
body['attachments'] = []
elif myarg in {'hangoutsmeet', 'googlemeet'}:
body['conferenceData'] = {'createRequest': {'conferenceSolutionKey': {'type': 'hangoutsMeet'}, 'requestId': f'{str(uuid.uuid4())}'}}
elif function == 'update' and myarg in {'clearhangoutsmeet', 'cleargooglemeet'}:
body['conferenceData'] = None
elif myarg == 'recurrence':
body.setdefault('recurrence', [])
body['recurrence'].append(getString(Cmd.OB_RECURRENCE))
elif myarg == 'timezone':
parameters['timeZone'] = getString(Cmd.OB_STRING)
elif function == 'update' and myarg == 'replacemode':
parameters['replaceMode'] = True
elif function == 'update' and myarg == 'clearattendees':
parameters['clearAttendees'] = True
elif function == 'update' and myarg == 'removeattendee':
parameters['removeAttendees'].add(getEmailAddress(noUid=True))
elif function == 'update' and myarg == 'selectremoveattendees':
_, attendeeList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
for attendee in attendeeList:
parameters['removeAttendees'].add(normalizeEmailAddressOrUID(attendee, noUid=True))
elif myarg == 'attendee':
parameters['attendees'].append({'email': getEmailAddress(noUid=True)})
elif myarg == 'optionalattendee':
parameters['attendees'].append({'email': getEmailAddress(noUid=True), 'optional': True})
elif myarg in {'attendeestatus', 'selectattendees'}:
optional = getChoice(CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP, defaultChoice=None, mapChoice=True)
responseStatus = getChoice(CALENDAR_ATTENDEE_STATUS_CHOICE_MAP, defaultChoice=None, mapChoice=True)
if myarg == 'attendeestatus':
attendeeList = [getEmailAddress(noUid=True)]
else:
_, attendeeList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
for attendee in attendeeList:
addAttendee = {'email': normalizeEmailAddressOrUID(attendee, noUid=True)}
if optional is not None:
addAttendee['optional'] = optional
if responseStatus is not None:
addAttendee['responseStatus'] = responseStatus
parameters['attendees'].append(addAttendee)
elif function == 'update' and myarg == 'clearresources':
parameters['clearResources'] = True
elif myarg == 'resource':
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
parameters['attendees'].append({'email': _validateResourceId(cd, getString(Cmd.OB_RESOURCE_ID), 0, 0, True),
'responseStatus': 'accepted', 'resource': True})
elif myarg == 'removeresource':
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
parameters['removeAttendees'].add(_validateResourceId(cd, getString(Cmd.OB_RESOURCE_ID), 0, 0, True))
elif myarg == 'json':
jsonData = getJSON(EVENT_JSON_CLEAR_FIELDS)
if function == 'insert':
body.update(jsonData)
clearJSONfields(body, EVENT_JSON_INSERT_CLEAR_FIELDS)
elif function == 'import':
if 'id' in jsonData:
jsonData['iCalUID'] = jsonData.pop('id')
body.update(jsonData)
elif function == 'update':
if 'event' in jsonData and 'attendees' in jsonData['event']:
parameters['attendees'].extend(jsonData['event'].pop('attendees'))
clearJSONsubfields(parameters, EVENT_JSONATTENDEES_SUBFIELD_CLEAR_FIELDS)
body.update(jsonData)
clearJSONfields(body, EVENT_JSON_UPDATE_CLEAR_FIELDS)
clearJSONsubfields(body, EVENT_JSON_SUBFIELD_CLEAR_FIELDS)
if ('conferenceData' in body and body['conferenceData'] and
'createRequest' in body['conferenceData'] and
'status' in body['conferenceData']['createRequest']):
body['conferenceData']['createRequest']['status'].pop('statusCode', None)
elif myarg == 'jsonattendees':
jsonData = getJSON([])
if 'event' in jsonData and 'attendees' in jsonData['event']:
parameters['attendees'].extend(jsonData['event']['attendees'])
elif 'attendees' in jsonData:
parameters['attendees'].extend(jsonData['attendees'])
clearJSONsubfields(parameters, EVENT_JSONATTENDEES_SUBFIELD_CLEAR_FIELDS)
elif function != 'import' and _getCalendarSendUpdates(myarg, parameters):
pass
elif myarg == 'anyonecanaddself':
body['anyoneCanAddSelf'] = getBoolean()
elif myarg == 'guestscaninviteothers':
body['guestsCanInviteOthers'] = getBoolean()
elif myarg == 'guestscantinviteothers':
body['guestsCanInviteOthers'] = False
elif myarg == 'guestscanmodify':
body['guestsCanModify'] = getBoolean()
elif myarg == 'guestscanseeotherguests':
body['guestsCanSeeOtherGuests'] = getBoolean()
elif myarg == 'guestscantseeotherguests':
body['guestsCanSeeOtherGuests'] = False
elif myarg == 'status':
body['status'] = getChoice(CALENDAR_EVENT_STATUS_CHOICES)
elif myarg == 'tentative':
body['status'] = 'tentative'
elif myarg == 'transparency':
body['transparency'] = getChoice(CALENDAR_EVENT_TRANSPARENCY_CHOICES)
elif myarg == 'available':
body['transparency'] = 'transparent'
elif myarg == 'visibility':
body['visibility'] = getChoice(CALENDAR_EVENT_VISIBILITY_CHOICES)
elif myarg in {'color', 'colour'}:
body['colorId'] = getChoice(CALENDAR_EVENT_COLOR_MAP, mapChoice=True)
elif myarg in {'colorindex', 'colorid', 'colourindex', 'colourid'}:
body['colorId'] = getInteger(CALENDAR_EVENT_MIN_COLOR_INDEX, CALENDAR_EVENT_MAX_COLOR_INDEX)
elif myarg == 'noreminders':
body['reminders'] = {'overrides': [], 'useDefault': False}
elif myarg == 'reminder':
body.setdefault('reminders', {'overrides': [], 'useDefault': False})
body['reminders']['overrides'].append(getCalendarReminder())
body['reminders']['useDefault'] = False
elif myarg == 'sequence':
body['sequence'] = getInteger(minVal=0)
elif myarg == 'privateproperty':
body.setdefault('extendedProperties', {})
body['extendedProperties'].setdefault('private', {})
key = getString(Cmd.OB_PROPERTY_KEY)
body['extendedProperties']['private'][key] = getString(Cmd.OB_PROPERTY_VALUE, minLen=0)
elif myarg == 'sharedproperty':
body.setdefault('extendedProperties', {})
body['extendedProperties'].setdefault('shared', {})
key = getString(Cmd.OB_PROPERTY_KEY)
body['extendedProperties']['shared'][key] = getString(Cmd.OB_PROPERTY_VALUE, minLen=0)
elif function == 'update' and myarg == 'clearprivateproperty':
body.setdefault('extendedProperties', {})
body['extendedProperties'].setdefault('private', {})
body['extendedProperties']['private'][getString(Cmd.OB_PROPERTY_KEY)] = None
elif function == 'update' and myarg == 'clearsharedproperty':
body.setdefault('extendedProperties', {})
body['extendedProperties'].setdefault('shared', {})
body['extendedProperties']['shared'][getString(Cmd.OB_PROPERTY_KEY)] = None
elif function == 'import' and myarg in {'organizername', 'organisername'}:
body.setdefault('organizer', {})
body['organizer']['displayName'] = getString(Cmd.OB_NAME)
elif function == 'import' and myarg in {'organizeremail', 'organiseremail'}:
body.setdefault('organizer', {})
body['organizer']['email'] = getEmailAddress(noUid=True)
else:
return False
return True
def _getEventMatchFields(calendarEventEntity, fieldsList):
for match in calendarEventEntity['matches']:
if match[0][0] != 'attendees':
fieldsList.append(match[0][0])
else:
fieldsList.append('attendees/email')
if match[0][1] == 'status':
fieldsList.extend(['attendees/optional', 'attendees/responseStatus'])
def _eventMatches(event, match):
if match[0][0] != 'attendees':
eventAttr = event
for attr in match[0]:
eventAttr = eventAttr.get(attr, '')
if not eventAttr:
break
if match[0][0] == 'organizer' and match[0][1] == 'self':
return bool(eventAttr) == match[1]
if match[0][0] != 'hangoutLink':
return match[1].search(eventAttr) is not None
# vkj-przn-nvg or vkjprznnvg
return match[1].search(eventAttr) is not None or match[1].search(eventAttr.replace('-', '')) is not None
attendees = [attendee['email'] for attendee in event.get('attendees', []) if 'email' in attendee]
if not attendees:
return False
if match[0][1] == 'email':
for attendee in match[1]:
if attendee not in attendees:
return False
return True
if match[0][1] == 'match':
for attendee in attendees:
if match[1].search(attendee) is not None:
return True
return False
if match[0][1] == 'onlydomainlist':
for attendee in attendees:
_, domain = attendee.lower().split('@', 1)
if domain not in match[1]:
return False
return True
if match[0][1] == 'domainlist':
for attendee in attendees:
_, domain = attendee.lower().split('@', 1)
if domain in match[1]:
return True
return False
if match[0][1] == 'notdomainlist':
for attendee in attendees:
_, domain = attendee.lower().split('@', 1)
if domain not in match[1]:
return True
return False
# status
for matchEmail in match[3]:
for attendee in event['attendees']:
if 'email' in attendee and matchEmail == attendee['email']:
if attendee.get('optional', False) != match[1] or attendee.get('responseStatus') != match[2]:
return False
break
else:
return False
return True
def _validateCalendarGetEventIDs(origUser, user, origCal, calId, j, jcount, calendarEventEntity, doIt=True, showAction=True):
if calendarEventEntity['dict']:
if origUser:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
calEventIds = calendarEventEntity['dict'][calId]
else:
calEventIds = calendarEventEntity['dict'][origUser][calId]
else:
calEventIds = calendarEventEntity['dict'][calId]
else:
calEventIds = calendarEventEntity['list']
calId, cal = getNormalizedCalIdCal(origCal, calId, user, j, jcount)
if not cal:
return (calId, cal, None, 0)
if not calEventIds:
fieldsList = ['id']
_getEventMatchFields(calendarEventEntity, fieldsList)
fields = ','.join(fieldsList)
try:
eventIdsSet = set()
calEventIds = []
if len(calendarEventEntity['queries']) <= 1:
if len(calendarEventEntity['queries']) == 1:
calendarEventEntity['kwargs']['q'] = calendarEventEntity['queries'][0]
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, fields=f'nextPageToken,items({fields})',
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
for event in events:
for match in calendarEventEntity['matches']:
if not _eventMatches(event, match):
break
else:
calEventIds.append(event['id'])
else:
for query in calendarEventEntity['queries']:
calendarEventEntity['kwargs']['q'] = query
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, fields=f'nextPageToken,items({fields})',
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
for event in events:
for match in calendarEventEntity['matches']:
if not _eventMatches(event, match):
break
else:
eventId = event['id']
if eventId not in eventIdsSet:
calEventIds.append(eventId)
eventIdsSet.add(eventId)
kcount = len(calEventIds)
if kcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.CALENDAR, calId], Ent.EVENT, kcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.EVENT)), j, jcount)
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (calId, cal, None, 0)
except GAPI.notFound:
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
return (calId, cal, None, 0)
except (GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
return (calId, cal, None, 0)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, j, jcount)
return (calId, cal, None, 0)
else:
kcount = len(calEventIds)
if kcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not doIt:
if showAction:
entityNumEntitiesActionNotPerformedWarning([Ent.CALENDAR, calId], Ent.EVENT, kcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
return (calId, cal, None, 0)
if showAction:
entityPerformActionNumItems([Ent.CALENDAR, calId], kcount, Ent.EVENT, j, jcount)
return (calId, cal, calEventIds, kcount)
def _validateCalendarGetEvents(origUser, user, origCal, calId, j, jcount, calendarEventEntity,
fieldsList, showAction):
if calendarEventEntity['dict']:
if origUser:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
calEventIds = calendarEventEntity['dict'][calId]
else:
calEventIds = calendarEventEntity['dict'][origUser][calId]
else:
calEventIds = calendarEventEntity['dict'][calId]
else:
calEventIds = calendarEventEntity['list']
calId, cal = getNormalizedCalIdCal(origCal, calId, user, j, jcount)
if not cal:
return (calId, cal, [], 0)
eventIdsSet = set()
eventsList = []
fields = getFieldsFromFieldsList(fieldsList)
ifields = getItemFieldsFromFieldsList('items', fieldsList)
try:
if not calEventIds:
if len(calendarEventEntity['queries']) <= 1:
if len(calendarEventEntity['queries']) == 1:
calendarEventEntity['kwargs']['q'] = calendarEventEntity['queries'][0]
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, fields=ifields,
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
for event in events:
for match in calendarEventEntity['matches']:
if not _eventMatches(event, match):
break
else:
eventsList.append(event)
else:
for query in calendarEventEntity['queries']:
calendarEventEntity['kwargs']['q'] = query
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, fields=ifields,
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
for event in events:
for match in calendarEventEntity['matches']:
if not _eventMatches(event, match):
break
else:
eventId = event['id']
if eventId not in eventIdsSet:
eventsList.append(event)
eventIdsSet.add(eventId)
else:
k = 0
for eventId in calEventIds:
k += 1
if eventId not in eventIdsSet:
eventsList.append(callGAPI(cal.events(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
calendarId=calId, eventId=eventId, fields=fields))
eventIdsSet.add(eventId)
kcount = len(eventsList)
if showAction:
entityPerformActionNumItems([Ent.CALENDAR, calId], kcount, Ent.EVENT, j, jcount)
if kcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (calId, cal, eventsList, kcount)
except (GAPI.notFound, GAPI.deleted) as e:
if not checkCalendarExists(cal, calId, j, jcount):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
else:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
except (GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, j, jcount)
return (calId, cal, [], 0)
def _getCalendarCreateImportUpdateEventOptions(function, entityType):
body = {}
parameters = {'clearAttendees': False, 'replaceMode': False, 'clearResources': False,
'attendees': [], 'removeAttendees': set(),
'replaceDescription': [], 'sendUpdates': 'none',
'csvPF': None, 'FJQC': FormatJSONQuoteChar(None), 'showDayOfWeek': False}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'csv':
parameters['csvPF'] = CSVPrintFile(['primaryEmail', 'calendarId', 'id'] if entityType == Ent.USER else ['calendarId', 'id'], 'sortall', indexedTitles=EVENT_INDEXED_TITLES)
parameters['FJQC'].SetCsvPF(parameters['csvPF'])
elif parameters['csvPF'] and myarg == 'todrive':
parameters['csvPF'].GetTodriveParameters()
elif myarg == 'showdayofweek':
parameters['showDayOfWeek'] = True
elif _getCalendarEventAttribute(myarg, body, parameters, function):
pass
else:
parameters['FJQC'].GetFormatJSONQuoteChar(myarg, True)
return (body, parameters)
def _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
if ('recurrence' in body) and (('start' in body) or ('end' in body)):
timeZone = parameters.get('timeZone')
if not timeZone:
try:
timeZone = callGAPI(cal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, fields='timeZone')['timeZone']
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
return False
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
return False
if 'start' in body:
body['start']['timeZone'] = timeZone
if 'end' in body:
body['end']['timeZone'] = timeZone
return True
def _getEventDaysOfWeek(event):
for attr in ['start', 'end']:
if attr in event:
if 'date' in event[attr]:
try:
dateTime = datetime.datetime.strptime(event[attr]['date'], YYYYMMDD_FORMAT)
event[attr]['dayOfWeek'] = calendarlib.day_abbr[dateTime.weekday()]
except ValueError:
pass
elif 'dateTime' in event[attr]:
try:
dateTime, _ = iso8601.parse_date(event[attr]['dateTime'])
event[attr]['dayOfWeek'] = calendarlib.day_abbr[dateTime.weekday()]
except (iso8601.ParseError, OverflowError):
pass
def _createCalendarEvents(user, origCal, function, calIds, count, body, parameters):
if parameters['attendees']:
body['attendees'] = parameters.pop('attendees')
fields = 'id' if parameters['csvPF'] is None else '*'
i = 0
for calId in calIds:
i += 1
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
if not cal:
continue
if not _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
continue
event = {'id': body.get('id', UNKNOWN)}
if function == 'import' and body.get('status', '') == 'cancelled':
entityActionNotPerformedWarning([Ent.CALENDAR, calId, Ent.EVENT, body.get('iCalUID', event['id'])], Msg.EVENT_IS_CANCELED, count)
continue
try:
if function == 'insert':
event = callGAPI(cal.events(), 'insert',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.INVALID, GAPI.REQUIRED, GAPI.TIME_RANGE_EMPTY, GAPI.EVENT_DURATION_EXCEEDS_LIMIT,
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.DUPLICATE, GAPI.FORBIDDEN,
GAPI.MALFORMED_WORKING_LOCATION_EVENT, GAPI.BAD_REQUEST],
calendarId=calId, conferenceDataVersion=1, sendUpdates=parameters['sendUpdates'], supportsAttachments=True, body=body, fields=fields)
else:
event = callGAPI(cal.events(), 'import_',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.INVALID, GAPI.REQUIRED, GAPI.TIME_RANGE_EMPTY, GAPI.EVENT_DURATION_EXCEEDS_LIMIT,
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.DUPLICATE, GAPI.FORBIDDEN,
GAPI.MALFORMED_WORKING_LOCATION_EVENT, GAPI.BAD_REQUEST,
GAPI.PARTICIPANT_IS_NEITHER_ORGANIZER_NOR_ATTENDEE],
calendarId=calId, conferenceDataVersion=1, supportsAttachments=True, body=body, fields=fields)
if parameters['csvPF'] is None:
entityActionPerformed([Ent.CALENDAR, calId, Ent.EVENT, event['id']], i, count)
else:
if parameters['showDayOfWeek']:
_getEventDaysOfWeek(event)
_printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'])
except (GAPI.invalid, GAPI.required, GAPI.timeRangeEmpty, GAPI.eventDurationExceedsLimit,
GAPI.requiredAccessLevel, GAPI.participantIsNeitherOrganizerNorAttendee,
GAPI.malformedWorkingLocationEvent, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, event['id']], str(e), i, count)
except GAPI.duplicate as e:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, event['id']], str(e), i, count)
except GAPI.forbidden as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
break
if parameters['csvPF']:
parameters['csvPF'].writeCSVfile('Calendar Created Events')
# gam calendars <CalendarEntity> create event [id <String>] <EventAddAttribute>+
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
# gam calendar <UserItem> addevent [id <String>] <EventAddAttribute>+
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def doCalendarsCreateEvent(calIds):
body, parameters = _getCalendarCreateImportUpdateEventOptions('insert', Ent.CALENDAR)
_createCalendarEvents(None, None, 'insert', calIds, len(calIds), body, parameters)
# gam calendars <CalendarEntity> import event icaluid <iCalUID> <EventImportAttribute>+
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def doCalendarsImportEvent(calIds):
body, parameters = _getCalendarCreateImportUpdateEventOptions('import', Ent.CALENDAR)
_createCalendarEvents(None, None, 'import', calIds, len(calIds), body, parameters)
def _updateCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, body, parameters):
updateFieldList = []
if parameters['replaceDescription']:
updateFieldList.append('description')
if not parameters['replaceMode'] and (parameters['attendees'] or parameters['removeAttendees'] or parameters['clearResources']):
updateFieldList.append('attendees')
updateFields = ','.join(updateFieldList)
if 'attendees' not in updateFieldList:
if parameters['attendees']:
body['attendees'] = parameters.pop('attendees')
elif parameters['clearAttendees']:
body['attendees'] = []
pfields = '' if parameters['csvPF'] is None else '*'
i = 0
for calId in calIds:
i += 1
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity)
if jcount == 0:
continue
if not _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
continue
Ind.Increment()
j = 0
for eventId in calEventIds:
j += 1
try:
if updateFieldList:
event = callGAPI(cal.events(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
calendarId=calId, eventId=eventId, fields=updateFields)
if 'description' in updateFieldList and 'description' in event:
body['description'] = event['description']
for replacement in parameters['replaceDescription']:
body['description'] = re.sub(replacement[0], replacement[1], body['description'])
if 'attendees' in updateFieldList:
if not parameters['clearAttendees']:
if 'attendees' in event:
body['attendees'] = event['attendees']
for addAttendee in parameters['attendees']:
for attendee in body['attendees']:
if attendee['email'].lower() == addAttendee['email']:
attendee.update(addAttendee)
break
else:
body['attendees'].append(addAttendee)
elif parameters['attendees']:
body['attendees'] = parameters['attendees']
else:
body['attendees'] = []
elif parameters['attendees']:
body['attendees'] = parameters['attendees']
else:
body['attendees'] = []
if parameters['removeAttendees']:
body['attendees'] = [attendee for attendee in body['attendees'] if attendee['email'].lower() not in parameters['removeAttendees']]
if parameters['clearResources']:
body['attendees'] = [attendee for attendee in body['attendees'] if not attendee['email'].lower().endswith('@resource.calendar.google.com')]
event = callGAPI(cal.events(), 'patch',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.REQUIRED, GAPI.TIME_RANGE_EMPTY, GAPI.EVENT_DURATION_EXCEEDS_LIMIT,
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.CANNOT_CHANGE_ORGANIZER_OF_INSTANCE,
GAPI.MALFORMED_WORKING_LOCATION_EVENT],
calendarId=calId, eventId=eventId, conferenceDataVersion=1, sendUpdates=parameters['sendUpdates'], supportsAttachments=True,
body=body, fields=pfields)
if parameters['csvPF'] is None:
entityActionPerformed([Ent.CALENDAR, calId, Ent.EVENT, eventId], j, jcount)
else:
if parameters['showDayOfWeek']:
_getEventDaysOfWeek(event)
_printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'])
except (GAPI.notFound, GAPI.deleted) as e:
if not checkCalendarExists(cal, calId, j, jcount):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
break
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
except (GAPI.forbidden, GAPI.invalid, GAPI.required, GAPI.timeRangeEmpty, GAPI.eventDurationExceedsLimit,
GAPI.requiredAccessLevel, GAPI.cannotChangeOrganizerOfInstance, GAPI.malformedWorkingLocationEvent) as e:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
break
Ind.Decrement()
if parameters['csvPF']:
parameters['csvPF'].writeCSVfile('Calendar Updated Events')
# gam calendars <CalendarEntity> update events [<EventEntity>] [replacemode] <EventUpdateAttribute>+ [<EventNotificationAttribute>]
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def doCalendarsUpdateEvents(calIds):
calendarEventEntity = getCalendarEventEntity()
body, parameters = _getCalendarCreateImportUpdateEventOptions('update', Ent.CALENDAR)
_updateCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, body, parameters)
# gam calendar <CalendarEntity> updateevent <EventID> [replacemode] <EventUpdateAttribute>+ [<EventNotificationAttribute>]
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def doCalendarsUpdateEventsOld(calIds):
calendarEventEntity = initCalendarEventEntity()
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
body, parameters = _getCalendarCreateImportUpdateEventOptions('update', Ent.CALENDAR)
_updateCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, body, parameters)
def _getCalendarDeleteEventOptions(calendarEventEntity=None):
parameters = {'sendUpdates': 'none', 'doIt': False}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getCalendarSendUpdates(myarg, parameters):
pass
elif calendarEventEntity and myarg in {'id', 'eventid'}:
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
elif myarg == 'doit':
parameters['doIt'] = True
else:
unknownArgumentExit()
return parameters
def _deleteCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, parameters):
i = 0
for calId in calIds:
i += 1
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity, doIt=parameters['doIt'])
if jcount == 0:
continue
Ind.Increment()
j = 0
for eventId in calEventIds:
j += 1
try:
callGAPI(cal.events(), 'delete',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.REQUIRED, GAPI.REQUIRED_ACCESS_LEVEL],
calendarId=calId, eventId=eventId, sendUpdates=parameters['sendUpdates'])
entityActionPerformed([Ent.CALENDAR, calId, Ent.EVENT, eventId], j, jcount)
except (GAPI.notFound, GAPI.deleted) as e:
if not checkCalendarExists(cal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
break
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
except (GAPI.forbidden, GAPI.invalid, GAPI.required, GAPI.requiredAccessLevel) as e:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
break
Ind.Decrement()
# gam calendars <CalendarEntity> delete event <EventEntity> [doit] [<EventNotificationAttribute>]
def doCalendarsDeleteEvents(calIds):
calendarEventEntity = getCalendarEventEntity()
parameters = _getCalendarDeleteEventOptions()
_deleteCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, parameters)
# gam calendar <CalendarEntity> deleteevent (id|eventid <EventID>)+ [doit] [<EventNotificationAttribute>]
def doCalendarsDeleteEventsOld(calIds):
calendarEventEntity = initCalendarEventEntity()
parameters = _getCalendarDeleteEventOptions(calendarEventEntity)
_deleteCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, parameters)
def _getCalendarMoveEventsOptions(calendarEventEntity=None):
parameters = {'sendUpdates': 'none'}
newCalId = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getCalendarSendUpdates(myarg, parameters):
pass
elif calendarEventEntity and myarg in {'id', 'eventid'}:
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
elif calendarEventEntity and myarg == 'destination':
newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM))
else:
unknownArgumentExit()
return (parameters, newCalId)
def _moveCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, newCalId, parameters):
i = 0
for calId in calIds:
i += 1
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity)
if jcount == 0:
continue
kvList = [Ent.USER, user] if user else []
kvList.extend([Ent.CALENDAR, calId])
Ind.Increment()
j = 0
for eventId in calEventIds:
j += 1
kvListEvent = kvList+[Ent.EVENT, eventId]
kvListEventNewCal = kvListEvent+[Ent.CALENDAR, newCalId]
try:
callGAPI(cal.events(), 'move',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.REQUIRED_ACCESS_LEVEL,
GAPI.INVALID, GAPI.BAD_REQUEST,
GAPI.CANNOT_CHANGE_ORGANIZER, GAPI.CANNOT_CHANGE_ORGANIZER_OF_INSTANCE],
calendarId=calId, eventId=eventId, destination=newCalId, sendUpdates=parameters['sendUpdates'], fields='')
entityModifierNewValueActionPerformed(kvListEvent, Act.MODIFIER_TO, f'{Ent.Singular(Ent.CALENDAR)}: {newCalId}', j, jcount)
except GAPI.notFound as e:
if not checkCalendarExists(cal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
break
entityActionFailedWarning(kvListEventNewCal, Ent.TypeNameMessage(Ent.EVENT, eventId, str(e)), j, jcount)
except GAPI.requiredAccessLevel:
# Correct "You need to have reader access to this calendar." to "Writer access required to both calendars."
entityActionFailedWarning(kvListEventNewCal, Msg.WRITER_ACCESS_REQUIRED_TO_BOTH_CALENDARS, j, jcount)
except (GAPI.forbidden, GAPI.invalid, GAPI.badRequest,
GAPI.cannotChangeOrganizer, GAPI.cannotChangeOrganizerOfInstance) as e:
entityActionFailedWarning(kvListEventNewCal, str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
break
Ind.Decrement()
# gam calendars <CalendarEntity> move events <EventEntity> to|destination <CalendarItem> [<EventNotificationAttribute>]
def doCalendarsMoveEvents(calIds):
calendarEventEntity = getCalendarEventEntity()
checkArgumentPresent(['to', 'destination'])
newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM))
parameters, _ = _getCalendarMoveEventsOptions()
if not checkCalendarExists(None, newCalId, 0, 0, True):
return
_moveCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, newCalId, parameters)
# gam calendars <CalendarEntity> moveevent (id|eventid <EventID>)+ destination <CalendarItem> [<EventNotificationAttribute>]
def doCalendarsMoveEventsOld(calIds):
calendarEventEntity = initCalendarEventEntity()
parameters, newCalId = _getCalendarMoveEventsOptions(calendarEventEntity)
if not checkCalendarExists(None, newCalId, 0, 0, True):
return
_moveCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, newCalId, parameters)
def _purgeCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, parameters, emptyTrash):
body = {'summary': f'GamPurgeCalendar-{random.randint(1, 99999):05}'}
if user:
entityValueList = [Ent.USER, user, Ent.CALENDAR, body['summary']]
else:
entityValueList = [Ent.CALENDAR, body['summary']]
i = 0
for calId in calIds:
i += 1
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
if not cal:
continue
try:
purgeCalId = callGAPI(cal.calendars(), 'insert',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.FORBIDDEN],
body=body, fields='id')['id']
Act.Set(Act.CREATE)
entityActionPerformed(entityValueList)
Ind.Increment()
if not emptyTrash:
Act.Set(Act.DELETE)
_deleteCalendarEvents(origUser, user, cal, [calId], count, calendarEventEntity, parameters)
Act.Set(Act.MOVE)
calendarEventEntity['kwargs']['showDeleted'] = True
_moveCalendarEvents(origUser, user, cal, [calId], count, calendarEventEntity, purgeCalId, parameters)
calendarEventEntity['kwargs'].pop('showDeleted')
Ind.Decrement()
callGAPI(cal.calendars(), 'delete',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
calendarId=purgeCalId)
Act.Set(Act.REMOVE)
entityActionPerformed(entityValueList)
except (GAPI.notFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, body['summary']], str(e))
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
# gam calendars <CalendarEntity> purge event <EventEntity> [doit] [<EventNotificationAttribute>]
def doCalendarsPurgeEvents(calIds):
calendarEventEntity = getCalendarEventEntity()
parameters = _getCalendarDeleteEventOptions()
_purgeCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, parameters, False)
def _wipeCalendarEvents(user, origCal, calIds, count):
i = 0
for calId in calIds:
i += 1
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
if not cal:
continue
try:
callGAPI(cal.calendars(), 'clear',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID,
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
calendarId=calId)
entityActionPerformed([Ent.CALENDAR, calId], i, count)
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
# gam calendars <CalendarEntity> wipe events
# gam calendar <CalendarEntity> wipe
def doCalendarsWipeEvents(calIds):
checkArgumentPresent([Cmd.ARG_EVENT, Cmd.ARG_EVENTS])
checkForExtraneousArguments()
_wipeCalendarEvents(None, None, calIds, len(calIds))
def _emptyCalendarTrash(user, origCal, calIds, count):
i = 0
for calId in calIds:
i += 1
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
if not cal:
continue
Act.Set(Act.PURGE)
calendarEventEntity = initCalendarEventEntity()
try:
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
calendarId=calId, showDeleted=True, fields='nextPageToken,items(id,status,organizer(self),recurringEventId)',
maxResults=GC.Values[GC.EVENT_MAX_RESULTS])
except (GAPI.notFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
continue
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
continue
for event in events:
if event['status'] == 'cancelled' and event.get('organizer', {}).get('self', user is None) and not event.get('recurringEventId', ''):
calendarEventEntity['list'].append(event['id'])
jcount = len(calendarEventEntity['list'])
if not user:
entityPerformActionNumItems([Ent.CALENDAR, calId], jcount, Ent.TRASHED_EVENT, i, count)
Ind.Increment()
if jcount > 0:
_purgeCalendarEvents(user, user, cal, [calId], 1, calendarEventEntity, {'sendUpdates': 'none', 'doIt': True}, True)
if not user:
Ind.Decrement()
# gam calendars <CalendarEntity> empty calendartrash
def doCalendarsEmptyTrash(calIds):
checkForExtraneousArguments()
Act.Set(Act.PURGE)
_emptyCalendarTrash(None, None, calIds, len(calIds))
EVENT_SHOW_ORDER = ['id', 'summary', 'status', 'description', 'location',
'start', 'end', 'endTimeUnspecified',
'creator', 'organizer', 'created', 'updated', 'iCalUID']
EVENT_PRINT_ORDER = ['id', 'summary', 'status', 'description', 'location',
'created', 'updated', 'iCalUID']
EVENT_TIME_OBJECTS = {'created', 'updated'}
def _showCalendarEvent(primaryEmail, calId, eventEntityType, event, k, kcount, FJQC):
if FJQC.formatJSON:
if primaryEmail:
printLine(json.dumps(cleanJSON({'primaryEmail': primaryEmail, 'calendarId': calId, 'event': event},
timeObjects=EVENT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
else:
printLine(json.dumps(cleanJSON({'calendarId': calId, 'event': event},
timeObjects=EVENT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([eventEntityType, event['id']], k, kcount)
skipObjects = {'id'}
Ind.Increment()
for field in EVENT_SHOW_ORDER:
if field in event:
if field != 'description':
showJSON(field, event[field], skipObjects, EVENT_TIME_OBJECTS)
else:
printKeyValueWithCRsNLs(field, event[field])
skipObjects.add(field)
showJSON(None, event, skipObjects)
Ind.Decrement()
def _printCalendarEvent(user, calId, event, csvPF, FJQC):
row = {'calendarId': calId, 'id': event['id']}
if user:
row['primaryEmail'] = user
flattenJSON(event, flattened=row, timeObjects=EVENT_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'calendarId': calId, 'id': event['id'],
'JSON': json.dumps(cleanJSON(event, timeObjects=EVENT_TIME_OBJECTS),
ensure_ascii=False, sort_keys=False)}
if user:
row['primaryEmail'] = user
csvPF.WriteRowNoFilter(row)
def _printShowCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity,
csvPF, FJQC, fieldsList):
i = 0
for calId in calIds:
i += 1
if csvPF:
printGettingEntityItemForWhom(Ent.EVENT, calId, i, count)
calId, _, events, jcount = _validateCalendarGetEvents(origUser, user, origCal, calId, i, count, calendarEventEntity,
fieldsList, not csvPF and not FJQC.formatJSON and not calendarEventEntity['countsOnly'])
if not csvPF:
if not calendarEventEntity['countsOnly']:
Ind.Increment()
j = 0
for event in events:
j += 1
if calendarEventEntity['showDayOfWeek']:
_getEventDaysOfWeek(event)
_showCalendarEvent(user, calId, Ent.EVENT, event, j, jcount, FJQC)
Ind.Decrement()
else:
printKeyValueList([Ent.Singular(Ent.CALENDAR), calId, Ent.Choose(Ent.EVENT, jcount), jcount])
else:
if not calendarEventEntity['countsOnly']:
if events:
for event in events:
if calendarEventEntity['showDayOfWeek']:
_getEventDaysOfWeek(event)
_printCalendarEvent(user, calId, event, csvPF, FJQC)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT] and user:
csvPF.WriteRowNoFilter({'calendarId': calId, 'primaryEmail': user, 'id': ''})
else:
row = {'calendarId': calId}
if user:
row['primaryEmail'] = user
row['events'] = jcount
csvPF.WriteRow(row)
EVENT_FIELDS_CHOICE_MAP = {
'anyonecanaddself': 'anyoneCanAddSelf',
'attachments': 'attachments',
'attendees': 'attendees',
'attendeesomitted': 'attendeesOmitted',
'colorid': 'colorId',
'conferencedata': 'conferenceData',
'created': 'created',
'creator': 'creator',
'description': 'description',
'end': 'end',
'endtime': 'end',
'endtimeunspecified': 'endTimeUnspecified',
'extendedproperties': 'extendedProperties',
'focustimeproperties': 'focusTimeProperties',
'gadget': 'gadget',
'guestscaninviteothers': 'guestsCanInviteOthers',
'guestscanmodify': 'guestsCanModify',
'guestscanseeotherguests': 'guestsCanSeeOtherGuests',
'hangoutlink': 'hangoutLink',
'htmllink': 'htmlLink',
'eventtype': 'eventType',
'icaluid': 'iCalUID',
'id': 'id',
'location': 'location',
'locked': 'locked',
'organizer': 'organizer',
'organiser': 'organizer',
'originalstart': 'originalStartTime',
'originalstarttime': 'originalStartTime',
'outofofficeproperties': 'outOfOfficeProperties',
'privatecopy': 'privateCopy',
'recurrence': 'recurrence',
'recurringeventid': 'recurringEventId',
'reminders': 'reminders',
'sequence': 'sequence',
'source': 'source',
'start': 'start',
'starttime': 'start',
'status': 'status',
'summary': 'summary',
'transparency': 'transparency',
'updated': 'updated',
'visibility': 'visibility',
'workinglocationproperties': 'workingLocationProperties',
}
EVENT_ATTACHMENTS_SUBFIELDS_CHOICE_MAP = {
'fileid': 'fileId',
'fileurl': 'fileUrl',
'iconlink': 'iconLink',
'mimetype': 'mimeType',
'title': 'title',
}
EVENT_ATTENDEES_SUBFIELDS_CHOICE_MAP = {
'additionalguests': 'additionalGuests',
'comment': 'comment',
'displayname': 'displayName',
'email': 'email',
'id': 'id',
'optional': 'optional',
'organizer': 'organizer',
'organiser': 'organizer',
'resource': 'resource',
'responsestatus': 'responseStatus',
'self': 'self',
}
EVENT_CONFERENCEDATA_SUBFIELDS_CHOICE_MAP = {
'conferenceid': 'conferenceId',
'conferencesolution': 'conferenceSolution',
'createrequest': 'createRequest',
'entrypoints': 'entryPoints',
'notes': 'notes',
'signature': 'signature',
}
EVENT_CREATOR_SUBFIELDS_CHOICE_MAP = {
'displayname': 'displayName',
'email': 'email',
'id': 'id',
'self': 'self',
}
EVENT_FOCUSTIME_SUBFIELDS_CHOICE_MAP = {
'autodeclinemode': 'autoDeclineMode',
'chatstatus': 'chatStatus',
'declinemessage': 'declineMessage',
}
EVENT_ORGANIZER_SUBFIELDS_CHOICE_MAP = {
'displayname': 'displayName',
'email': 'email',
'id': 'id',
'self': 'self',
}
EVENT_OUTOFOFFICE_SUBFIELDS_CHOICE_MAP = {
'autodeclinemode': 'autoDeclineMode',
'declinemessage': 'declineMessage',
}
EVENT_WORKINGLOCATION_SUBFIELDS_CHOICE_MAP = {
'homeoffice': 'homeOffice',
'customlocation': 'customLocation',
'officelocation': 'officeLocation',
}
EVENT_SUBFIELDS_CHOICE_MAP = {
'attachments': EVENT_ATTACHMENTS_SUBFIELDS_CHOICE_MAP,
'attendees': EVENT_ATTENDEES_SUBFIELDS_CHOICE_MAP,
'conferencedata': EVENT_CONFERENCEDATA_SUBFIELDS_CHOICE_MAP,
'creator': EVENT_CREATOR_SUBFIELDS_CHOICE_MAP,
'focustimeproperties': EVENT_FOCUSTIME_SUBFIELDS_CHOICE_MAP,
'organizer': EVENT_ORGANIZER_SUBFIELDS_CHOICE_MAP,
'organiser': EVENT_ORGANIZER_SUBFIELDS_CHOICE_MAP,
'outofofficeproperties': EVENT_OUTOFOFFICE_SUBFIELDS_CHOICE_MAP,
'workinglocationproperties': EVENT_WORKINGLOCATION_SUBFIELDS_CHOICE_MAP,
}
def _getEventFields(fieldsList):
if not fieldsList:
fieldsList.append('id')
for field in _getFieldsList():
if field.find('.') == -1:
if field in EVENT_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, EVENT_FIELDS_CHOICE_MAP, fieldsList)
else:
invalidChoiceExit(field, EVENT_FIELDS_CHOICE_MAP, True)
else:
field, subField = field.split('.', 1)
if field in EVENT_SUBFIELDS_CHOICE_MAP:
if subField in EVENT_SUBFIELDS_CHOICE_MAP[field]:
fieldsList.append(f'{EVENT_FIELDS_CHOICE_MAP[field]}.{EVENT_SUBFIELDS_CHOICE_MAP[field][subField]}')
else:
invalidChoiceExit(subField, list(EVENT_SUBFIELDS_CHOICE_MAP[field]), True)
else:
invalidChoiceExit(field, list(EVENT_SUBFIELDS_CHOICE_MAP), True)
def _addEventEntitySelectFields(calendarEventEntity, fieldsList):
if fieldsList:
_getEventMatchFields(calendarEventEntity, fieldsList)
if calendarEventEntity['maxinstances'] != -1:
fieldsList.append('recurrence')
def _getCalendarInfoEventOptions(calendarEventEntity):
FJQC = FormatJSONQuoteChar()
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'fields':
_getEventFields(fieldsList)
elif myarg == 'showdayofweek':
calendarEventEntity['showDayOfWeek'] = True
else:
FJQC.GetFormatJSON(myarg)
_addEventEntitySelectFields(calendarEventEntity, fieldsList)
return (FJQC, fieldsList)
def _infoCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, FJQC, fieldsList):
fields = getFieldsFromFieldsList(fieldsList)
ifields = getItemFieldsFromFieldsList('items', fieldsList)
i = 0
for calId in calIds:
i += 1
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for eventId in calEventIds:
j += 1
try:
event = callGAPI(cal.events(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
calendarId=calId, eventId=eventId, fields=fields)
if calendarEventEntity['maxinstances'] == -1 or 'recurrence' not in event:
if calendarEventEntity['showDayOfWeek']:
_getEventDaysOfWeek(event)
_showCalendarEvent(user, calId, Ent.EVENT, event, j, jcount, FJQC)
else:
instances = callGAPIpages(cal.events(), 'instances', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
calendarId=calId, eventId=eventId, fields=ifields,
maxItems=calendarEventEntity['maxinstances'], maxResults=GC.Values[GC.EVENT_MAX_RESULTS])
lcount = len(instances)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.EVENT, event['id']], lcount, Ent.INSTANCE, j, jcount)
Ind.Increment()
l = 0
for instance in instances:
l += 1
if calendarEventEntity['showDayOfWeek']:
_getEventDaysOfWeek(instance)
_showCalendarEvent(user, calId, Ent.INSTANCE, instance, l, lcount, FJQC)
Ind.Decrement()
except (GAPI.notFound, GAPI.deleted) as e:
if not checkCalendarExists(cal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
break
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
except (GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
break
Ind.Decrement()
# gam calendars <CalendarEntity> info events <EventEntity> [maxinstances <Number>]
# [fields <EventFieldNameList>] [showdayofweek]
# [formatjson]
def doCalendarsInfoEvents(calIds):
calendarEventEntity = getCalendarEventEntity()
FJQC, fieldsList = _getCalendarInfoEventOptions(calendarEventEntity)
_infoCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, FJQC, fieldsList)
EVENT_INDEXED_TITLES = ['attendees', 'attachments', 'recurrence']
def _getCalendarPrintShowEventOptions(calendarEventEntity, entityType):
csvPF = CSVPrintFile(['primaryEmail', 'calendarId', 'id'] if entityType == Ent.USER else ['calendarId', 'id'], 'sortall', indexedTitles=EVENT_INDEXED_TITLES) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCalendarListEventsDisplayProperty(myarg, calendarEventEntity):
pass
elif myarg == 'fields':
_getEventFields(fieldsList)
elif myarg == 'countsonly':
calendarEventEntity['countsOnly'] = True
elif myarg == 'showdayofweek':
calendarEventEntity['showDayOfWeek'] = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if calendarEventEntity['countsOnly']:
fieldsList = ['id']
if csvPF:
if calendarEventEntity['countsOnly']:
csvPF.AddTitles(['events'])
elif not FJQC.formatJSON and not fieldsList:
csvPF.AddSortTitles(EVENT_PRINT_ORDER)
_addEventEntitySelectFields(calendarEventEntity, fieldsList)
return (csvPF, FJQC, fieldsList)
# gam calendars <CalendarEntity> print events <EventEntity> <EventDisplayProperties>*
# [fields <EventFieldNameList>] [showdayofweek]
# [countsonly] [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
# gam calendars <CalendarEntity> show events <EventEntity> <EventDisplayProperties>*
# [fields <EventFieldNameList>] [showdayofweek]
# [countsonly] [formatjson]
def doCalendarsPrintShowEvents(calIds):
calendarEventEntity = getCalendarEventEntity()
csvPF, FJQC, fieldsList = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.CALENDAR)
_printShowCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity,
csvPF, FJQC, fieldsList)
if csvPF:
csvPF.writeCSVfile('Calendar Events')
# <CalendarSettings> ::==
# [description <String>] [location <String>] [summary <String>] [timezone <TimeZone>]
def _getCalendarSetting(myarg, body):
if myarg == 'description':
body['description'] = getStringWithCRsNLs()
elif myarg == 'location':
body['location'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'summary':
body['summary'] = getString(Cmd.OB_STRING)
elif myarg == 'timezone':
body['timeZone'] = getString(Cmd.OB_STRING)
else:
return False
return True
def getCalendarSettings(summaryRequired=False):
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getCalendarSetting(myarg, body):
pass
else:
unknownArgumentExit()
if summaryRequired and not body.get('summary', None):
missingArgumentExit('summary <String>')
return body
# gam calendars <CalendarEntity> modify <CalendarSettings>
def doCalendarsModifySettings(calIds):
body = getCalendarSettings(summaryRequired=False)
count = len(calIds)
i = 0
for calId in calIds:
i += 1
calId, cal = validateCalendar(calId, i, count)
if not cal:
continue
try:
callGAPI(cal.calendars(), 'patch',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, body=body)
entityActionPerformed([Ent.CALENDAR, calId], i, count)
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
def _showCalendarSettings(calendar, j, jcount):
printEntity([Ent.CALENDAR, calendar['id']], j, jcount)
Ind.Increment()
if 'summaryOverride' in calendar or 'summary' in calendar:
printKeyValueList(['Summary', calendar.get('summaryOverride', calendar.get('summary', ''))])
if 'description' in calendar:
printKeyValueWithCRsNLs('Description', calendar.get('description', ''))
if 'location' in calendar:
printKeyValueList(['Location', calendar.get('location', '')])
if 'timeZone' in calendar:
printKeyValueList(['Timezone', calendar.get('timeZone', '')])
if 'conferenceProperties' in calendar:
printKeyValueList(['ConferenceProperties', None])
Ind.Increment()
printKeyValueList(['AllowedConferenceSolutionTypes', ','.join(calendar.get('conferenceProperties', {}).get('allowedConferenceSolutionTypes', []))])
Ind.Decrement()
Ind.Decrement()
CALENDAR_SETTINGS_FIELDS_CHOICE_MAP = {
'conferenceproperties': 'conferenceProperties',
'description': 'description',
'id': 'id',
'location': 'location',
'summary': 'summary',
'timezone': 'timeZone',
}
# gam calendars <CalendarEntity> print settings [todrive <ToDriveAttribute>*]
# [fields <CalendarSettingsFieldList>]
# [formatjson] [quotechar <Character>}
# gam calendars <CalendarEntity> show settings
# [fields <CalendarSettingsFieldList>]
# [formatjson]
def doCalendarsPrintShowSettings(calIds):
csvPF = CSVPrintFile(['calendarId'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getFieldsList(myarg, CALENDAR_SETTINGS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = getFieldsFromFieldsList(fieldsList)
count = len(calIds)
i = 0
for calId in calIds:
i += 1
calId, cal = validateCalendar(calId, i, count)
if not cal:
continue
try:
calendar = callGAPI(cal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
calendarId=calId, fields=fields)
if not csvPF:
if not FJQC.formatJSON:
_showCalendarSettings(calendar, i, count)
else:
printLine(json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True))
else:
row = flattenJSON(calendar)
if not FJQC.formatJSON:
row['calendarId'] = row.pop('id')
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'calendarId': calId, 'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
except (GAPI.notFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(calId, i, count)
if csvPF:
csvPF.writeCSVfile('Calendar Settings')
def _validateResourceId(cd, resourceId, i, count, exitOnNotFound):
try:
return callGAPI(cd.resources().calendars(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, fields='resourceEmail')['resourceEmail']
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
if exitOnNotFound:
entityDoesNotExistExit(Ent.RESOURCE_CALENDAR, resourceId, i, count)
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
return None
def _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity, showAction=True):
calId = _validateResourceId(cd, resourceId, i, count, False)
if not calId:
return (None, None, 0)
if ACLScopeEntity['dict']:
ruleIds = ACLScopeEntity['dict'][resourceId]
else:
ruleIds = ACLScopeEntity['list']
jcount = len(ruleIds)
if showAction:
entityPerformActionNumItems([Ent.RESOURCE_CALENDAR, resourceId], jcount, Ent.CALENDAR_ACL, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (calId, ruleIds, jcount)
# gam resource <ResourceID> create calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
# gam resources <ResourceEntity> create calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
def doResourceCreateCalendarACLs(entityList):
cal = buildGAPIObject(API.CALENDAR)
cd = buildGAPIObject(API.DIRECTORY)
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity)
if jcount == 0:
continue
_createCalendarACLs(cal, Ent.RESOURCE_CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
def _resourceUpdateDeleteCalendarACLs(entityList, function, ACLScopeEntity, role, sendNotifications):
cal = buildGAPIObject(API.CALENDAR)
cd = buildGAPIObject(API.DIRECTORY)
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity)
if jcount == 0:
continue
_updateDeleteCalendarACLs(cal, function, Ent.RESOURCE_CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
# gam resource <ResourceID> update calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
# gam resources <ResourceEntity> update calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
def doResourceUpdateCalendarACLs(entityList):
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
_resourceUpdateDeleteCalendarACLs(entityList, 'patch', ACLScopeEntity, role, sendNotifications)
# gam resource <ResourceID> delete calendaracls [<CalendarACLRole>] <CalendarACLScopeEntity>
# gam resources <ResourceEntity> delete calendaracls [<CalendarACLRole>] <CalendarACLScopeEntity>
def doResourceDeleteCalendarACLs(entityList):
role, ACLScopeEntity = getCalendarDeleteACLsOptions(True)
_resourceUpdateDeleteCalendarACLs(entityList, 'delete', ACLScopeEntity, role, False)
# gam resource <ResourceID> info calendaracls <CalendarACLScopeEntity>
# [formatjson]
# gam resources <ResourceEntity> info calendaracls <CalendarACLScopeEntity>
# [formatjson]
def doResourceInfoCalendarACLs(entityList):
cal = buildGAPIObject(API.CALENDAR)
cd = buildGAPIObject(API.DIRECTORY)
ACLScopeEntity = getCalendarSiteACLScopeEntity()
FJQC = _getCalendarInfoACLOptions()
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
_infoCalendarACLs(cal, resourceId, Ent.RESOURCE_CALENDAR, calId, i, count, ruleIds, jcount, FJQC)
# gam resource <ResourceID> print calendaracls [todrive <ToDriveAttribute>*]
# [noselfowner] (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam resources <ResourceEntity> print calendaracls [todrive <ToDriveAttribute>*]
# [noselfowner] (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam resource <ResourceID> show calendaracls
# [noselfowner]
# [formatjson]
# gam resources <ResourceEntity> show calendaracls
# [noselfowner]
# [formatjson]
def doResourcePrintShowCalendarACLs(entityList):
cal = buildGAPIObject(API.CALENDAR)
cd = buildGAPIObject(API.DIRECTORY)
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['resourceId', 'resourceEmail'])
i = 0
count = len(entityList)
for resourceId in entityList:
i += 1
calId = _validateResourceId(cd, resourceId, i, count, False)
if not calId:
continue
_printShowCalendarACLs(cal, resourceId, Ent.RESOURCE_CALENDAR, calId, i, count, csvPF, FJQC, noSelfOwner, addCSVData)
if csvPF:
csvPF.writeCSVfile('Resource Calendar ACLs')
def _showSchema(schema, i=0, count=0):
printEntity([Ent.USER_SCHEMA, schema['schemaName']], i, count)
Ind.Increment()
for a_key in schema:
if a_key not in {'kind', 'etag', 'schemaName', 'fields'}:
printKeyValueList([a_key, schema[a_key]])
for field in schema['fields']:
printKeyValueList(['Field', field['fieldName']])
Ind.Increment()
for a_key in field:
if a_key not in {'kind', 'etag', 'fieldName'}:
if a_key != 'numericIndexingSpec':
printKeyValueList([a_key, field[a_key]])
else:
printKeyValueList([a_key, ''])
Ind.Increment()
for s_key in field[a_key]:
printKeyValueList([s_key, field[a_key][s_key]])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
SCHEMA_FIELDTYPE_CHOICE_MAP = {
'bool': 'BOOL',
'boolean': 'BOOL',
'date': 'DATE',
'double': 'DOUBLE',
'email': 'EMAIL',
'int64': 'INT64',
'phone': 'PHONE',
'string': 'STRING',
}
# gam create schema|schemas <SchemaName> [displayname <String>] <SchemaFieldDefinition>+
# gam update schema|schemas <SchemaEntity> [displayname <String>] <SchemaFieldDefinition>+
def doCreateUpdateUserSchemas():
cd = buildGAPIObject(API.DIRECTORY)
updateCmd = Act.Get() == Act.UPDATE
if not updateCmd:
entityList = getStringReturnInList(Cmd.OB_SCHEMA_NAME)
else:
entityList = getEntityList(Cmd.OB_SCHEMA_ENTITY)
addBody = {'schemaName': '', 'fields': []}
deleteFields = []
schemaDisplayName = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'field':
fieldName = getString(Cmd.OB_FIELD_NAME)
a_field = {'fieldName': fieldName.replace(' ', '_'), 'displayName': fieldName, 'fieldType': 'STRING'}
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument == 'displayname':
a_field['displayName'] = getString(Cmd.OB_FIELD_NAME)
elif argument == 'type':
a_field['fieldType'] = getChoice(SCHEMA_FIELDTYPE_CHOICE_MAP, mapChoice=True)
elif argument in {'multivalued', 'multivalue'}:
a_field['multiValued'] = True
elif argument == 'indexed':
a_field['indexed'] = True
elif argument == 'restricted':
a_field['readAccessType'] = 'ADMINS_AND_SELF'
elif argument == 'range':
a_field['numericIndexingSpec'] = {'minValue': getInteger(), 'maxValue': getInteger()}
elif argument == 'endfield':
break
elif argument == 'field':
Cmd.Backup()
break
else:
unknownArgumentExit()
addBody['fields'].append(a_field)
elif updateCmd and myarg == 'deletefield':
deleteFields.append(getString(Cmd.OB_FIELD_NAME).replace(' ', '_'))
elif myarg == 'displayname':
schemaDisplayName = getString(Cmd.OB_SCHEMA_NAME)
else:
unknownArgumentExit()
i = 0
count = len(entityList)
if not updateCmd:
if not addBody['fields']:
missingArgumentExit('SchemaFieldDefinition')
elif schemaDisplayName and count > 1:
usageErrorExit(Msg.DISPLAYNAME_NOT_ALLOWED_WHEN_UPDATING_MULTIPLE_SCHEMAS)
for schemaName in entityList:
i += 1
try:
if updateCmd:
oldBody = callGAPI(cd.schemas(), 'get',
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaName, fields='schemaName,displayName,fields')
for field in oldBody['fields']:
field.pop('etag', None)
field.pop('kind', None)
field.pop('fieldId', None)
badDelete = False
for delField in deleteFields:
fieldNameLower = delField.lower()
for n, field in enumerate(oldBody['fields']):
if field['fieldName'].lower() == fieldNameLower:
del oldBody['fields'][n]
break
else:
entityActionNotPerformedWarning([Ent.USER_SCHEMA, schemaName, Ent.FIELD, delField], Msg.DOES_NOT_EXIST)
badDelete = True
if badDelete:
continue
for addField in addBody['fields']:
fieldNameLower = addField['fieldName'].lower()
for n, field in enumerate(oldBody['fields']):
if field['fieldName'].lower() == fieldNameLower:
del oldBody['fields'][n]
break
oldBody['fields'].extend(addBody['fields'])
if not oldBody['fields']:
entityActionNotPerformedWarning([Ent.USER_SCHEMA, schemaName],
Msg.SCHEMA_WOULD_HAVE_NO_FIELDS.format(Ent.Singular(Ent.USER_SCHEMA), Ent.Plural(Ent.FIELD)))
continue
if schemaDisplayName:
oldBody['displayName'] = schemaDisplayName
result = callGAPI(cd.schemas(), 'update',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.FIELD_IN_USE],
customerId=GC.Values[GC.CUSTOMER_ID], body=oldBody, schemaKey=schemaName)
entityActionPerformed([Ent.USER_SCHEMA, result['schemaName']], i, count)
else:
addBody['schemaName'] = schemaName.replace(' ', '_')
addBody['displayName'] = schemaDisplayName if schemaDisplayName else schemaName
result = callGAPI(cd.schemas(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], body=addBody, fields='schemaName')
entityActionPerformed([Ent.USER_SCHEMA, result['schemaName']], i, count)
except GAPI.duplicate:
entityDuplicateWarning([Ent.USER_SCHEMA, schemaName], i, count)
except (GAPI.conditionNotMet, GAPI.fieldInUse, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER_SCHEMA, schemaName], str(e), i, count)
except (GAPI.badRequest, GAPI.resourceNotFound):
checkEntityAFDNEorAccessErrorExit(cd, Ent.USER_SCHEMA, schemaName, i, count)
# gam delete schema|schemas <SchemaEntity>
def doDeleteUserSchemas():
cd = buildGAPIObject(API.DIRECTORY)
entityList = getEntityList(Cmd.OB_SCHEMA_ENTITY)
checkForExtraneousArguments()
i = 0
count = len(entityList)
for schemaKey in entityList:
i += 1
try:
callGAPI(cd.schemas(), 'delete',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.FIELD_IN_USE],
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaKey)
entityActionPerformed([Ent.USER_SCHEMA, schemaKey], i, count)
except GAPI.fieldInUse as e:
entityActionFailedWarning([Ent.USER_SCHEMA, schemaKey], str(e), i, count)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.USER_SCHEMA, schemaKey, i, count)
# gam info schema|schemas <SchemaEntity>
def doInfoUserSchemas():
cd = buildGAPIObject(API.DIRECTORY)
entityList = getEntityList(Cmd.OB_SCHEMA_ENTITY)
checkForExtraneousArguments()
i = 0
count = len(entityList)
for schemaKey in entityList:
i += 1
try:
schema = callGAPI(cd.schemas(), 'get',
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaKey)
_showSchema(schema, i, count)
except (GAPI.invalid, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.USER_SCHEMA, schemaKey, i, count)
SCHEMAS_SORT_TITLES = ['schemaId', 'schemaName', 'displayName']
SCHEMAS_INDEXED_TITLES = ['fields']
# gam print schema|schemas [todrive <ToDriveAttribute>*]
# gam show schema|schemas
def doPrintShowUserSchemas():
csvPF = CSVPrintFile(SCHEMAS_SORT_TITLES, 'sortall', SCHEMAS_INDEXED_TITLES) if Act.csvFormat() else None
cd = buildGAPIObject(API.DIRECTORY)
getTodriveOnly(csvPF)
try:
result = callGAPI(cd.schemas(), 'list',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID])
jcount = len(result.get('schemas', [])) if (result) else 0
if not csvPF:
performActionNumItems(jcount, Ent.USER_SCHEMA)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
else:
if not csvPF:
Ind.Increment()
j = 0
for schema in result['schemas']:
j += 1
_showSchema(schema, j, jcount)
Ind.Decrement()
else:
for schema in result['schemas']:
csvPF.WriteRowTitles(flattenJSON(schema))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
if csvPF:
csvPF.writeCSVfile('User Schemas')
def _copyStorageObjects(objects, target_bucket, target_prefix):
"""Copies objects to target_bucket.
Args:
objects: list of object dicts
[
{
bucket: source bucket,
name: source object name,
(optional) md5Hash: source file hash value
},
...
]
target_bucket: target bucket id
target_prefix: prefix name to prepend to target object
"""
def process_rewrite(request_id, response, exception):
fileIndex = int(request_id)
if exception:
http_status, reason, message = checkGAPIError(exception)
# Poor man's backoff/retry
if http_status == 429 or http_status > 499:
writeStderr(f'Temporary error: {http_status} - {message}, Backing off: 10 seconds\n')
flushStderr()
time.sleep(10)
next_batch.add(s.objects().rewrite(**files_to_copy[fileIndex]['method']), request_id=request_id)
return
if reason in GAPI.REASON_EXCEPTION_MAP:
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
raise exception(message)
source_displayname = files_to_copy[fileIndex]['source_displayname']
target_displayname = files_to_copy[fileIndex]['target_displayname']
if response.get('done'):
source_md5 = files_to_copy[fileIndex]['md5Hash']
target_md5 = response['resource']['md5Hash']
if source_md5 != target_md5:
systemErrorExit(GOOGLE_API_ERROR_RC, f'Target file {target_displayname} checksum {target_md5} does not match source {source_md5}. This should not happen')
entityActionPerformedMessage([Ent.CLOUD_STORAGE_FILE, None], f'{1:>7.2%} Complete', fileIndex+1, totalFiles)
Ind.Increment()
writeStdout(formatKeyValueList(Ind.Spaces(), ['Source', source_displayname], '\n'))
writeStdout(formatKeyValueList(Ind.Spaces(), ['Target', target_displayname], '\n'))
Ind.Decrement()
else:
total_bytes = float(response.get('objectSize'))
done_bytes = float(response.get('totalBytesRewritten'))
entityActionPerformedMessage([Ent.CLOUD_STORAGE_FILE, None], f'{(done_bytes / total_bytes):>7.2%} Complete', fileIndex+1, totalFiles)
Ind.Increment()
writeStdout(formatKeyValueList(Ind.Spaces(), ['Source', source_displayname], '\n'))
writeStdout(formatKeyValueList(Ind.Spaces(), ['Target', target_displayname], '\n'))
Ind.Decrement()
files_to_copy[fileIndex]['method']['rewriteToken'] = response.get('rewriteToken')
next_batch.add(s.objects().rewrite(**files_to_copy[fileIndex]['method']), request_id=request_id)
action = Act.Get()
Act.Set(Act.COPY)
s = buildGAPIObject(API.STORAGEWRITE)
sbatch = s.new_batch_http_request(callback=process_rewrite)
files_to_copy = []
for object_ in objects:
files_to_copy.append(
{
'md5Hash': object_['md5Hash'],
'source_displayname': f'{object_["bucket"]}:{object_["name"]}',
'target_displayname': f'{target_bucket}:{target_prefix}{object_["name"]}',
'method': {
'destinationBucket': target_bucket,
'destinationObject': f'{target_prefix}{object_["name"]}',
'sourceBucket': object_['bucket'],
'sourceObject': object_['name'],
'maxBytesRewrittenPerCall': 1048576, # uncomment to easily test multiple rewrite API calls per object
},
})
totalFiles = len(files_to_copy)
i = 0
try:
for file in files_to_copy:
while len(sbatch._order) == 100:
next_batch = s.new_batch_http_request(callback=process_rewrite)
sbatch.execute()
sbatch = next_batch
sbatch.add(s.objects().rewrite(**file['method']), request_id=str(i))
i += 1
while len(sbatch._order) > 0:
next_batch = s.new_batch_http_request(callback=process_rewrite)
sbatch.execute()
sbatch = next_batch
except GAPI.notFound:
Act.Set(action)
entityDoesNotExistExit(Ent.CLOUD_STORAGE_BUCKET, target_bucket)
except Exception:
ClientAPIAccessDeniedExit()
Act.Set(action)
def _getCloudStorageObject(s, bucket, s_object, localFilename, expectedMd5=None, zipToStdout=False, j=0, jcount=0):
if not zipToStdout:
localFilename = cleanFilepath(localFilename)
entityValueList = [Ent.DRIVE_FILE, localFilename]
if os.path.exists(localFilename):
printEntityMessage(entityValueList, Msg.EXISTS)
if not expectedMd5:
return # nothing to verify, just assume we're good.
if md5MatchesFile(localFilename, expectedMd5):
return
printEntityMessage(entityValueList, Msg.DOWNLOADING_AGAIN_AND_OVER_WRITING)
entityPerformAction(entityValueList)
file_path = os.path.dirname(localFilename)
if not os.path.exists(file_path):
os.makedirs(file_path)
f = openFile(localFilename, 'wb')
else:
f = openFile('-', 'wb')
try:
request = s.objects().get_media(bucket=bucket, object=s_object)
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
while not done:
status, done = downloader.next_chunk()
if not zipToStdout and status.progress() < 1.0:
entityActionPerformedMessage([Ent.CLOUD_STORAGE_FILE, s_object], f'{status.progress():>7.2%}', j, jcount)
if not zipToStdout:
entityModifierNewValueActionPerformed([Ent.CLOUD_STORAGE_FILE, s_object], Act.MODIFIER_TO, localFilename, j, jcount)
closeFile(f, True)
if expectedMd5 and not md5MatchesFile(localFilename, expectedMd5):
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(localFilename, Msg.CORRUPT_FILE))
except googleapiclient.http.HttpError as e:
mg = HTTP_ERROR_PATTERN.match(str(e))
entityModifierNewValueActionFailedWarning([Ent.CLOUD_STORAGE_FILE, s_object], Act.MODIFIER_TO, localFilename, mg.group(1) if mg else str(e), j, jcount)
TAKEOUT_EXPORT_PATTERN = re.compile(r'(takeout-export-[a-f,0-9,-]*)')
# gam copy storagebucket sourcebucket <StorageBucketName> targetbucket <StorageBucketName>
# [sourceprefix <String>] [targetprefix <String>]
def doCopyCloudStorageBucket():
s = buildGAPIObject(API.STORAGEREAD)
source_bucket = None
target_bucket = None
source_prefix = None
target_prefix = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'sourcebucket':
source_bucket = getString(Cmd.OB_URL)
elif myarg == 'targetbucket':
target_bucket = getString(Cmd.OB_URL)
elif myarg == 'sourceprefix':
source_prefix = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'targetprefix':
target_prefix = getString(Cmd.OB_STRING, minLen=0)
else:
unknownArgumentExit()
if not target_bucket:
missingArgumentExit('targetbucket')
if not source_bucket:
missingArgumentExit('sourcebucket')
printGettingAllAccountEntities(Ent.FILE)
pageMessage = getPageMessage()
try:
objects = callGAPIpages(s.objects(), 'list', 'items',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
bucket=source_bucket, prefix=source_prefix,
fields='nextPageToken,items(name,bucket,md5Hash)')
except GAPI.notFound:
entityDoesNotExistExit(Ent.CLOUD_STORAGE_BUCKET, source_bucket)
except GAPI.forbidden as e:
entityActionFailedExit([Ent.CLOUD_STORAGE_BUCKET, source_bucket], str(e))
_copyStorageObjects(objects, target_bucket, target_prefix)
# gam download storagebucket <TakeoutBucketName>
# [targetfolder <FilePath>]
def doDownloadCloudStorageBucket():
bucket_url = getString(Cmd.OB_STRING)
targetFolder = GC.Values[GC.DRIVE_DIR]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'targetfolder':
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
else:
unknownArgumentExit()
bucket_match = re.search(TAKEOUT_EXPORT_PATTERN, bucket_url)
if not bucket_match:
systemErrorExit(ACTION_NOT_PERFORMED_RC, f'Could not find a takeout-export-* bucket in {bucket_url}')
bucket = bucket_match.group(1)
s = buildGAPIObject(API.STORAGEREAD)
printGettingAllAccountEntities(Ent.FILE)
pageMessage = getPageMessage()
try:
objects = callGAPIpages(s.objects(), 'list', 'items',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
bucket=bucket, projection='noAcl', fields='nextPageToken,items(name,md5Hash)')
except GAPI.notFound:
entityDoesNotExistExit(Ent.CLOUD_STORAGE_BUCKET, bucket)
except GAPI.forbidden as e:
entityActionFailedExit([Ent.CLOUD_STORAGE_BUCKET, bucket], str(e))
count = len(objects)
i = 0
for s_object in objects:
i += 1
printGettingEntityItem(Ent.FILE, s_object['name'], i, count)
expectedMd5 = base64.b64decode(s_object['md5Hash']).hex()
_getCloudStorageObject(s, bucket, s_object['name'], os.path.join(targetFolder, s_object['name']),
expectedMd5=expectedMd5)
# gam download storagefile <StorageBucketObjectName>
# [targetfolder <FilePath>] [overwrite [<Boolean>]] [nogcspath [Boolean>]]
def doDownloadCloudStorageFile():
bucket, s_object, bucketObject = getBucketObjectName()
targetFolder = GC.Values[GC.DRIVE_DIR]
overwrite = nogcspath = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'targetfolder':
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
elif myarg == 'overwrite':
overwrite = getBoolean()
elif myarg == 'nogcspath':
nogcspath = getBoolean()
else:
unknownArgumentExit()
s_obpaths = s_object.rsplit('/', 1)
s_obfile = s_obpaths[-1]
if len(s_obpaths) > 1 and not nogcspath:
targetFolder = os.path.join(targetFolder, s_obpaths[0])
filename, _ = uniqueFilename(targetFolder, s_obfile, overwrite)
filepath = os.path.dirname(filename)
if not os.path.exists(filepath):
os.makedirs(filepath)
s = buildGAPIObject(API.STORAGEREAD)
printGettingEntityItem(Ent.FILE, s_object)
f = openFile(filename, 'wb')
try:
request = s.objects().get_media(bucket=bucket, object=s_object)
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
while not done:
_, done = downloader.next_chunk()
entityModifierNewValueActionPerformed([Ent.CLOUD_STORAGE_FILE, s_object], Act.MODIFIER_TO, filename)
closeFile(f, True)
except googleapiclient.http.HttpError as e:
mg = HTTP_ERROR_PATTERN.match(str(e))
entityActionFailedWarning([Ent.CLOUD_STORAGE_FILE, bucketObject], mg.group(1) if mg else str(e))
def formatVaultNameId(vaultName, vaultId):
return f'{vaultName}({vaultId})'
def convertExportNameToID(v, nameOrId, matterId, matterNameId):
cg = UID_PATTERN.match(nameOrId)
if cg:
try:
export = callGAPI(v.matters().exports(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, exportId=cg.group(1))
return (export['id'], export['name'], formatVaultNameId(export['id'], export['name']))
except (GAPI.notFound, GAPI.badRequest):
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, nameOrId])
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
nameOrIdlower = nameOrId.lower()
try:
exports = callGAPIpages(v.matters().exports(), 'list', 'exports',
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields='exports(id,name),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
for export in exports:
if export['name'].lower() == nameOrIdlower:
return (export['id'], export['name'], formatVaultNameId(export['id'], export['name']))
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, nameOrId])
def convertHoldNameToID(v, nameOrId, matterId, matterNameId):
cg = UID_PATTERN.match(nameOrId)
if cg:
try:
hold = callGAPI(v.matters().holds(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=cg.group(1))
return (hold['holdId'], hold['name'], formatVaultNameId(hold['holdId'], hold['name']))
except (GAPI.notFound, GAPI.badRequest):
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, nameOrId])
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
nameOrIdlower = nameOrId.lower()
try:
holds = callGAPIpages(v.matters().holds(), 'list', 'holds',
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields='holds(holdId,name),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
for hold in holds:
if hold['name'].lower() == nameOrIdlower:
return (hold['holdId'], hold['name'], formatVaultNameId(hold['holdId'], hold['name']))
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, nameOrId])
def convertMatterNameToID(v, nameOrId, state=None):
cg = UID_PATTERN.match(nameOrId)
if cg:
try:
matter = callGAPI(v.matters(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=cg.group(1), view='BASIC', fields='matterId,name,state')
return (matter['matterId'], matter['name'], formatVaultNameId(matter['name'], matter['matterId']), matter['state'])
except (GAPI.notFound, GAPI.forbidden):
entityDoesNotExistExit(Ent.VAULT_MATTER, nameOrId)
try:
matters = callGAPIpages(v.matters(), 'list', 'matters',
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
view='BASIC', state=state, fields='matters(matterId,name,state),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
nameOrIdlower = nameOrId.lower()
ids = []
states = []
for matter in matters:
if matter['name'].lower() == nameOrIdlower:
nameOrId = matter['name']
ids.append(matter['matterId'])
states.append(matter['state'])
if len(ids) == 1:
return (ids[0], nameOrId, formatVaultNameId(nameOrId, ids[0]), states[0])
if not ids:
entityDoesNotExistExit(Ent.VAULT_MATTER, nameOrId)
else:
entityIsNotUniqueExit(Ent.VAULT_MATTER, nameOrId, Ent.VAULT_MATTER_ID, ids)
def convertQueryNameToID(v, nameOrId, matterId, matterNameId):
cg = UID_PATTERN.match(nameOrId)
if cg:
try:
query = callGAPI(v.matters().savedQueries(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, savedQueryId=cg.group(1))
return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']))
except (GAPI.notFound, GAPI.badRequest):
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, nameOrId])
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
nameOrIdlower = nameOrId.lower()
try:
queries = callGAPIpages(v.matters().savedQueries(), 'list', 'savedQueries',
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields='savedQueries(savedQueryId,displayName),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
ClientAPIAccessDeniedExit(str(e))
for query in queries:
if query['displayName'].lower() == nameOrIdlower:
return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']))
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, nameOrId])
def getMatterItem(v, state=None):
matterId, _, matterNameId, _ = convertMatterNameToID(v, getString(Cmd.OB_MATTER_ITEM), state=state)
return (matterId, matterNameId)
def warnMatterNotOpen(v, matter, matterNameId, j, jcount):
if v is not None:
try:
matter['state'] = callGAPI(v.matters(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matter['matterId'], view='BASIC', fields='state')['state']
except (GAPI.notFound, GAPI.forbidden, GAPI.invalidArgument):
matter['state'] = 'Unknown'
else:
setSysExitRC(DATA_NOT_AVALIABLE_RC)
message = formatKeyValueList('',
Ent.FormatEntityValueList([Ent.VAULT_MATTER, matterNameId])+
[Msg.MATTER_NOT_OPEN.format(matter['state'])],
currentCount(j, jcount))
writeStderr(f'\n{Ind.Spaces()}{WARNING_PREFIX}{message}\n')
def _cleanVaultExport(export, cd):
query = export.get('query')
if query:
if cd is not None:
if 'orgUnitInfo' in query:
query['orgUnitInfo']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, query['orgUnitInfo']['orgUnitId'])
VAULT_EXPORT_TIME_OBJECTS = {'versionDate', 'createTime', 'startTime', 'endTime'}
def _showVaultExport(matterNameId, export, cd, FJQC, k=0, kcount=0):
_cleanVaultExport(export, cd)
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(export, timeObjects=VAULT_EXPORT_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
return
if matterNameId is not None:
printEntity([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, formatVaultNameId(export['name'], export['id'])], k, kcount)
Ind.Increment()
showJSON(None, export, timeObjects=VAULT_EXPORT_TIME_OBJECTS)
Ind.Decrement()
VAULT_SEARCH_METHODS_MAP = {
'account': 'ACCOUNT',
'accounts': 'ACCOUNT',
'entireorg': 'ENTIRE_ORG',
'everyone': 'ENTIRE_ORG',
'org': 'ORG_UNIT',
'orgunit': 'ORG_UNIT',
'ou': 'ORG_UNIT',
'room': 'ROOM',
'rooms': 'ROOM',
'shareddrive': 'SHARED_DRIVE',
'shareddrives': 'SHARED_DRIVE',
'sitesurl': 'SITES_URL',
'teamdrive': 'SHARED_DRIVE',
'teamdrives': 'SHARED_DRIVE',
}
VAULT_CORPUS_ARGUMENT_MAP = {
'calendar': 'CALENDAR',
'drive': 'DRIVE',
'gemini': 'GEMINI',
'groups': 'GROUPS',
'hangoutschat': 'HANGOUTS_CHAT',
'mail': 'MAIL',
'voice': 'VOICE',
}
VAULT_COUNTS_CORPUS_ARGUMENT_MAP = {
'mail': 'MAIL',
'groups': 'GROUPS',
}
VAULT_RESPONSE_STATUS_MAP = {
'accepted': 'ATTENDEE_RESPONSE_ACCEPTED',
'declined': 'ATTENDEE_RESPONSE_DECLINED',
'needsaction': 'ATTENDEE_RESPONSE_NEEDS_ACTION',
'tentative': 'ATTENDEE_RESPONSE_TENTATIVE',
}
VAULT_VOICE_COVERED_DATA_MAP = {
'calllogs': 'CALL_LOGS',
'textmessages': 'TEXT_MESSAGES',
'voicemails': 'VOICEMAILS',
}
VAULT_EXPORT_DATASCOPE_MAP = {
'alldata': 'ALL_DATA',
'helddata': 'HELD_DATA',
'unprocesseddata': 'UNPROCESSED_DATA',
}
VAULT_EXPORT_FORMAT_MAP = {
'ics': 'ICS',
'mbox': 'MBOX',
'pst': 'PST',
'xml': 'XML',
}
VAULT_CORPUS_EXPORT_FORMATS = {
'CALENDAR': ['ICS', 'PST'],
'DRIVE': [],
'GEMINI': ['XML'],
'GROUPS': ['MBOX', 'PST'],
'HANGOUTS_CHAT': ['MBOX', 'PST'],
'MAIL': ['MBOX', 'PST'],
'VOICE' : ['MBOX', 'PST'],
}
VAULT_CSE_OPTION_MAP = {
'any': 'CLIENT_SIDE_ENCRYPTED_OPTION_ANY',
'encrypted': 'CLIENT_SIDE_ENCRYPTED_OPTION_ENCRYPTED',
'unencrypted': 'CLIENT_SIDE_ENCRYPTED_OPTION_UNENCRYPTED',
}
VAULT_EXPORT_REGION_MAP = {
'any': 'ANY',
'europe': 'EUROPE',
'us': 'US',
}
VAULT_CORPUS_OPTIONS_MAP = {
'CALENDAR': 'calendarOptions',
'DRIVE': 'driveOptions',
'GEMINI': 'geminiOptions',
'GROUPS': 'groupsOptions',
'HANGOUTS_CHAT': 'hangoutsChatOptions',
'MAIL': 'mailOptions',
'VOICE': 'voiceOptions',
}
VAULT_CORPUS_QUERY_MAP = {
'CALENDAR': None,
'DRIVE': 'driveQuery',
'GEMINI': None,
'GROUPS': 'groupsQuery',
'MAIL': 'mailQuery',
'HANGOUTS_CHAT': 'hangoutsChatQuery',
'VOICE': 'voiceQuery',
}
VAULT_QUERY_ARGS = [
'corpus', 'scope', 'terms', 'start', 'starttime', 'end', 'endtime', 'timezone',
# calendar
'locationquery', 'peoplequery', 'minuswords', 'responsestatuses', 'caldendarversiondate',
# drive
'driveclientsideencryption', 'driveversiondate', 'includeshareddrives', 'includeteamdrives',
# hangoutsChat
'includerooms',
# mail
'mailclientsideencryption', 'excludedrafts',
# voice
'covereddata',
] + list(VAULT_SEARCH_METHODS_MAP.keys())
def _buildVaultQuery(myarg, query, corpusArgumentMap):
if not query:
query['dataScope'] = 'ALL_DATA'
if myarg == 'corpus':
query['corpus'] = getChoice(corpusArgumentMap, mapChoice=True)
elif myarg in VAULT_SEARCH_METHODS_MAP:
if query.get('searchMethod'):
Cmd.Backup()
usageErrorExit(Msg.MULTIPLE_SEARCH_METHODS_SPECIFIED.format(formatChoiceList(VAULT_SEARCH_METHODS_MAP)))
searchMethod = VAULT_SEARCH_METHODS_MAP[myarg]
query['searchMethod'] = searchMethod
if searchMethod == 'ACCOUNT':
query['accountInfo'] = {'emails': getNormalizedEmailAddressEntity()}
elif searchMethod == 'ORG_UNIT':
query['orgUnitInfo'] = {'orgUnitId': getOrgUnitId()[1]}
elif searchMethod == 'SHARED_DRIVE':
query['sharedDriveInfo'] = {'sharedDriveIds': getString(Cmd.OB_SHAREDDRIVE_ID_LIST).replace(',', ' ').split()}
elif searchMethod == 'ROOM':
query['hangoutsChatInfo'] = {'roomId': getString(Cmd.OB_ROOM_LIST).replace(',', ' ').split()}
elif searchMethod == 'SITES_URL':
query['sitesUrlInfo'] = {'urls': getString(Cmd.OB_URL_LIST).replace(',', ' ').split()}
elif myarg == 'scope':
query['dataScope'] = getChoice(VAULT_EXPORT_DATASCOPE_MAP, mapChoice=True)
elif myarg == 'terms':
query['terms'] = getString(Cmd.OB_STRING)
elif myarg in {'start', 'starttime'}:
query['startTime'] = getTimeOrDeltaFromNow()
elif myarg in {'end', 'endtime'}:
query['endTime'] = getTimeOrDeltaFromNow()
elif myarg == 'timezone':
query['timeZone'] = getString(Cmd.OB_STRING)
# calendar
elif myarg == 'locationquery':
query.setdefault('calendarOptions', {})['locationQuery'] = shlexSplitList(getString(Cmd.OB_STRING_LIST))
elif myarg == 'peoplequery':
query.setdefault('calendarOptions', {})['peopleQuery'] = shlexSplitList(getString(Cmd.OB_STRING_LIST))
elif myarg == 'minuswords':
query.setdefault('calendarOptions', {})['minusWords'] = shlexSplitList(getString(Cmd.OB_STRING_LIST))
elif myarg == 'responsestatuses':
query.setdefault('calendarOptions', {})['responseStatuses'] = []
for response in getString(Cmd.OB_FIELD_NAME_LIST).lower().replace('_', '').replace(',', ' ').split():
if response in VAULT_RESPONSE_STATUS_MAP:
query['calendarOptions']['responseStatuses'].append(VAULT_RESPONSE_STATUS_MAP[response])
else:
invalidChoiceExit(response, VAULT_RESPONSE_STATUS_MAP, True)
elif myarg == 'calendarversiondate':
query.setdefault('calendarOptions', {})['versionDate'] = getTimeOrDeltaFromNow()
# drive
elif myarg == 'driveversiondate':
query.setdefault('driveOptions', {})['versionDate'] = getTimeOrDeltaFromNow()
elif myarg in {'includeshareddrives', 'includeteamdrives'}:
query.setdefault('driveOptions', {})['includeSharedDrives'] = getBoolean()
elif myarg == 'driveclientsideencryption':
query.setdefault('driveOptions', {})['clientSideEncryptedOption'] = getChoice(VAULT_CSE_OPTION_MAP, mapChoice=True)
# hangoutsChat
elif myarg == 'includerooms':
query['hangoutsChatOptions'] = {'includeRooms': getBoolean()}
# mail
elif myarg == 'excludedrafts':
query['mailOptions'] = {'excludeDrafts': getBoolean()}
elif myarg == 'mailclientsideencryption':
query.setdefault('mailOptions', {})['clientSideEncryptedOption'] = getChoice(VAULT_CSE_OPTION_MAP, mapChoice=True)
# voice
elif myarg == 'covereddata':
query['voiceOptions'] = {'coveredData': getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)}
def _validateVaultQuery(body, corpusArgumentMap):
if 'corpus' not in body['query']:
missingArgumentExit(f'corpus {formatChoiceList(corpusArgumentMap)}')
if 'searchMethod' not in body['query']:
missingArgumentExit(formatChoiceList(VAULT_SEARCH_METHODS_MAP))
if 'exportOptions' in body:
for corpus, options in iter(VAULT_CORPUS_OPTIONS_MAP.items()):
if body['query']['corpus'] != corpus:
body['exportOptions'].pop(options, None)
# gam create vaultexport|export matter <MatterItem> [name <String>] corpus calendar|drive|gemini|groups|hangouts_chat|mail|voice
# (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone
# (shareddrives|teamdrives <TeamDriveIDList>) | (rooms <RoomList>) | (sitesurl <URLList>)
# [scope <all_data|held_data|unprocessed_data>]
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
# [locationquery <StringList>] [peoplequery <StringList>] [minuswords <StringList>]
# [responsestatuses <AttendeeStatus>(,<AttendeeStatus>)*] [calendarversiondate <Date>|<Time>]
# [includeshareddrives <Boolean>] [driveversiondate <Date>|<Time>] [includeaccessinfo <Boolean>]
# [driveclientsideencryption any|encrypted|unencrypted]
# [includerooms <Boolean>]
# [excludedrafts <Boolean>] [mailclientsideencryption any|encrypted|unencrypted]
# [showconfidentialmodecontent <Boolean>] [usenewexport <Boolean>] [exportlinkeddrivefiles <Boolean>]
# [covereddata calllogs|textmessages|voicemails]
# [format ics|mbox|pst|xml]
# [region any|europe|us] [showdetails|returnidonly]
def doCreateVaultExport():
v = buildGAPIObject(API.VAULT)
matterId = None
body = {'query': {'dataScope': 'ALL_DATA'}, 'exportOptions': {}}
exportFormat = None
showConfidentialModeContent = None
exportLinkedDriveFiles = None
returnIdOnly = showDetails = False
useNewExport = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v, state='OPEN')
body['matterId'] = matterId
elif myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
elif myarg in VAULT_QUERY_ARGS:
_buildVaultQuery(myarg, body['query'], VAULT_CORPUS_ARGUMENT_MAP)
elif myarg == 'usenewexport':
useNewExport = getBoolean()
elif myarg == 'format':
exportFormat = getChoice(VAULT_EXPORT_FORMAT_MAP, mapChoice=True)
elif myarg == 'showconfidentialmodecontent':
showConfidentialModeContent = getBoolean()
elif myarg == 'exportlinkeddrivefiles':
exportLinkedDriveFiles = getBoolean()
elif myarg == 'region':
body['exportOptions']['region'] = getChoice(VAULT_EXPORT_REGION_MAP, mapChoice=True)
elif myarg == 'includeaccessinfo':
body['exportOptions'].setdefault('driveOptions', {})['includeAccessInfo'] = getBoolean()
elif myarg == 'covereddata':
body['exportOptions'].setdefault('voiceOptions', {})['coveredData'] = getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)
elif myarg == 'showdetails':
showDetails = True
returnIdOnly = False
elif myarg == 'returnidonly':
returnIdOnly = True
showDetails = False
else:
unknownArgumentExit()
if not matterId:
missingArgumentExit('matter')
_validateVaultQuery(body, VAULT_CORPUS_ARGUMENT_MAP)
if exportFormat is not None:
if not exportFormat in VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']]:
invalidChoiceExit(exportFormat, VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']], False)
elif body['query']['corpus'] != 'DRIVE':
exportFormat = VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']][0]
if 'name' not in body:
body['name'] = f'GAM {body["query"]["corpus"]} Export - {ISOformatTimeStamp(todaysTime())}'
optionsField = VAULT_CORPUS_OPTIONS_MAP[body['query']['corpus']]
if body['query']['corpus'] != 'DRIVE':
body['exportOptions'][optionsField] = {'exportFormat': exportFormat}
if body['query']['corpus'] == 'MAIL':
if showConfidentialModeContent is not None:
body['exportOptions'][optionsField]['showConfidentialModeContent'] = showConfidentialModeContent
if useNewExport is not None:
body['exportOptions'][optionsField]['useNewExport'] = useNewExport
if exportLinkedDriveFiles is not None:
body['exportOptions'][optionsField]['exportLinkedDriveFiles'] = exportLinkedDriveFiles
try:
export = callGAPI(v.matters().exports(), 'create',
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.BAD_REQUEST, GAPI.BACKEND_ERROR, GAPI.INVALID_ARGUMENT,
GAPI.INVALID, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.QUOTA_EXCEEDED],
matterId=matterId, body=body)
if not returnIdOnly:
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, formatVaultNameId(export['name'], export['id'])])
if showDetails:
_showVaultExport(None, export, None, None)
else:
writeStdout(f'{export["id"]}\n')
except (GAPI.alreadyExists, GAPI.badRequest, GAPI.backendError, GAPI.invalidArgument,
GAPI.invalid, GAPI.failedPrecondition, GAPI.forbidden, GAPI.quotaExceeded) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, body.get('name')], str(e))
# gam delete vaultexport|export <ExportItem> matter <MatterItem>
# gam delete vaultexport|export <MatterItem> <ExportItem>
def doDeleteVaultExport():
v = buildGAPIObject(API.VAULT)
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
else:
exportName = getString(Cmd.OB_EXPORT_ITEM)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
else:
unknownArgumentExit()
try:
callGAPI(v.matters().exports(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, exportId=exportId)
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId])
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
VAULT_EXPORT_FIELDS_CHOICE_MAP = {
'cloudstoragesink': 'cloudStorageSink',
'createtime': 'createTime',
'exportoptions': 'exportOptions',
'id': 'id',
'matterid': 'matterId',
'name': 'name',
'query': 'query',
'requester': 'requester',
'requester.displayname': 'requester.displayName',
'requester.email': 'requester.email',
'stats': 'stats',
'stats.exportedartifactcount': 'stats.exportedArtifactCount',
'stats.sizeinbytes': 'stats.sizeInBytes',
'stats.totalartifactcount': 'stats.totalArtifactCount',
'status': 'status',
}
# gam info vaultexport|export <ExportItem> matter <MatterItem>
# [fields <VaultExportFieldNameList>] [shownames]
# [formatjson]
# gam info vaultexport|export <MatterItem> <ExportItem>
# [fields <VaultExportFieldNameList>] [shownames]
# [formatjson]
def doInfoVaultExport():
v = buildGAPIObject(API.VAULT)
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
else:
exportName = getString(Cmd.OB_EXPORT_ITEM)
cd = None
fieldsList = []
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
elif getFieldsList(myarg, VAULT_EXPORT_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
export = callGAPI(v.matters().exports(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, exportId=exportId, fields=fields)
_showVaultExport(matterNameId, export, cd, FJQC)
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
VAULT_EXPORT_STATUS_MAP = {'completed': 'COMPLETED', 'failed': 'FAILED', 'inprogress': 'IN_PROGRESS'}
PRINT_VAULT_EXPORTS_TITLES = ['matterId', 'matterName', 'id', 'name']
# gam print vaultexports|exports [todrive <ToDriveAttribute>*]
# [matters <MatterItemList>] [exportstatus <ExportStatusList>]
# [fields <VaultExportFieldNameList>] [shownames]
# [formatjson [quotechar <Character>]]
# [oneitemperrow]
# gam show vaultexports|exports
# [matters <MatterItemList>] [exportstatus <ExportStatusList>]
# [fields <VaultExportFieldNameList>] [shownames]
# [formatjson]
def doPrintShowVaultExports():
def _printVaultExport(export):
row = flattenJSON(export, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_EXPORT_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
'id': export['id'], 'name': export['name'],
'JSON': json.dumps(cleanJSON(export, timeObjects=VAULT_EXPORT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_VAULT_EXPORTS_TITLES, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar()
matters = []
exportStatusList = []
cd = None
fieldsList = []
oneItemPerRow = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'matter', 'matters'}:
matters = shlexSplitList(getString(Cmd.OB_MATTER_ITEM_LIST))
elif myarg == 'exportstatus':
for state in getString(Cmd.OB_STATE_NAME_LIST).lower().replace('_', '').replace(',', ' ').split():
if state in VAULT_EXPORT_STATUS_MAP:
exportStatusList.append(VAULT_EXPORT_STATUS_MAP[state])
else:
invalidChoiceExit(state, list(VAULT_EXPORT_STATUS_MAP), True)
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
elif getFieldsList(myarg, VAULT_EXPORT_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
pass
elif csvPF and myarg == 'oneitemperrow':
oneItemPerRow = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
fields = getItemFieldsFromFieldsList('exports', fieldsList)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(PRINT_VAULT_EXPORTS_TITLES+['JSON'])
exportStatuses = set(exportStatusList)
exportQualifier = f' ({",".join(exportStatusList)})' if exportStatusList else ''
if not matters:
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
try:
results = callGAPIpages(v.matters(), 'list', 'matters',
pageMessage=getPageMessage(),
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_EXPORT, None], str(e))
return
else:
results = []
for matter in matters:
matterId, matterName, _, state = convertMatterNameToID(v, matter)
results.append({'matterId': matterId, 'name': matterName, 'state': state})
jcount = len(results)
if not csvPF:
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
j = 0
for matter in results:
j += 1
matterId = matter['matterId']
matterName = matter['name']
matterNameId = formatVaultNameId(matterName, matterId)
if csvPF:
printGettingAllEntityItemsForWhom(Ent.VAULT_EXPORT, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}',
j, jcount, qualifier=exportQualifier)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
if matter['state'] == 'OPEN':
try:
exports = callGAPIpages(v.matters().exports(), 'list', 'exports',
pageMessage=pageMessage,
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields=fields)
except GAPI.failedPrecondition:
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
continue
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_EXPORT, None], str(e))
break
else:
warnMatterNotOpen(None, matter, matterNameId, j, jcount)
continue
kcount = len(exports)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], kcount, Ent.VAULT_EXPORT, j, jcount)
Ind.Increment()
k = 0
for export in exports:
k += 1
if not exportStatuses or export['status'] in exportStatuses:
_showVaultExport(matterNameId, export, cd, FJQC, k, kcount)
Ind.Decrement()
else:
for export in exports:
if not exportStatuses or export['status'] in exportStatuses:
_cleanVaultExport(export, cd)
if not oneItemPerRow or not export.get('cloudStorageSink', {}).get('files'):
_printVaultExport(export)
else:
for file in export['cloudStorageSink'].pop('files'):
export['cloudStorageSink']['files'] = file
_printVaultExport(export)
if csvPF:
csvPF.writeCSVfile('Vault Exports')
def md5MatchesFile(filename, expected_md5, j=0, jcount=0):
action = Act.Get()
Act.Set(Act.VERIFY)
try:
f = openFile(filename, 'rb')
hash_md5 = hashlib.md5()
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
closeFile(f)
actual_hash = hash_md5.hexdigest()
if actual_hash == expected_md5:
entityActionPerformed([Ent.FILE, filename, Ent.MD5HASH, expected_md5], j, jcount)
Act.Set(action)
return True
entityActionFailedWarning([Ent.FILE, filename, Ent.MD5HASH, expected_md5], Msg.DOES_NOT_MATCH.format(actual_hash), j, jcount)
Act.Set(action)
return False
except IOError as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
# gam copy vaultexport|export <ExportItem> matter <MatterItem>
# [targetbucket <String>] [targetprefix <String>]
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
# [copyattempts <Integer>] [retryinterval <Integer>]
# gam copy vaultexport|export <MatterItem> <ExportItem>
# [targetbucket <String>] [targetprefix <String>]
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
# [copyattempts <Integer>] [retryinterval <Integer>]
def doCopyVaultExport():
v = buildGAPIObject(API.VAULT)
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
else:
exportName = getString(Cmd.OB_EXPORT_ITEM)
target_bucket = None
target_prefix = ''
bucketMatchPattern = objectMatchPattern = None
copyAttempts = 1
retryInterval = 30
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
elif myarg == 'targetbucket':
target_bucket = getString(Cmd.OB_URL)
elif myarg == 'targetprefix':
target_prefix = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'bucketmatchpattern':
bucketMatchPattern = getREPattern(re.IGNORECASE)
elif myarg == 'objectmatchpattern':
objectMatchPattern = getREPattern(re.IGNORECASE)
elif myarg == 'copyattempts':
copyAttempts = getInteger(minVal=1)
elif myarg == 'retryinterval':
retryInterval = getInteger(minVal=10)
else:
unknownArgumentExit()
if not target_bucket:
missingArgumentExit('targetbucket')
attempts = 0
while True:
try:
export = callGAPI(v.matters().exports(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, exportId=exportId)
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
return
if export['status'] == 'COMPLETED':
break
attempts += 1
entityActionNotPerformedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], Msg.EXPORT_NOT_COMPLETE.format(export['status']), attempts, copyAttempts)
if attempts >= copyAttempts:
return
time.sleep(retryInterval)
objects = []
for s_file in export['cloudStorageSink']['files']:
if ((bucketMatchPattern and not bucketMatchPattern.match(s_file['bucketName'])) or
(objectMatchPattern and not objectMatchPattern.match(s_file['objectName']))):
continue
# Convert to md5Hash format Storage API uses because OF COURSE they differ
md5Hash = base64.b64encode(bytes.fromhex(s_file['md5Hash'])).decode()
objects.append({'bucket': s_file['bucketName'], 'name': s_file['objectName'], 'md5Hash': md5Hash})
_copyStorageObjects(objects, target_bucket, target_prefix)
ZIP_EXTENSION_PATTERN = re.compile(r'^.*\.zip$', re.IGNORECASE)
# gam download vaultexport|export <ExportItem> matter <MatterItem>
# [targetfolder <FilePath>] [targetname <FileName>] [noverify] [noextract] [ziptostdout]
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
# [downloadattempts <Integer>] [retryinterval <Integer>]
# gam download vaultexport|export <MatterItem> <ExportItem>
# [targetfolder <FilePath>] [targetname <FileName>] [noverify] [noextract] [ziptostdout]
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
# [downloadattempts <Integer>] [retryinterval <Integer>]
def doDownloadVaultExport():
def extract_nested_zip(zippedFile):
""" Extract a zip file including any nested zip files
Delete the zip file(s) after extraction
"""
Act.Set(Act.UNZIP)
performAction(Ent.FILE, zippedFile)
Ind.Increment()
with zipfile.ZipFile(zippedFile, 'r') as zfile:
inner_files = zfile.infolist()
for inner_file in inner_files:
Act.Set(Act.EXTRACT)
performAction(Ent.FILE, inner_file.filename)
innerFilePath = zfile.extract(inner_file, targetFolder)
if ZIP_EXTENSION_PATTERN.match(inner_file.filename):
extract_nested_zip(innerFilePath)
Ind.Decrement()
try:
os.remove(zippedFile)
except OSError as e:
stderrWarningMsg(e)
v = buildGAPIObject(API.VAULT)
s = buildGAPIObject(API.STORAGEREAD)
verifyFiles = extractFiles = True
targetFolder = GC.Values[GC.DRIVE_DIR]
targetName = None
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
else:
exportName = getString(Cmd.OB_EXPORT_ITEM)
zipToStdout = False
bucketMatchPattern = objectMatchPattern = None
downloadAttempts = 1
retryInterval = 30
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
elif myarg == 'targetname':
targetName = getString(Cmd.OB_FILE_NAME)
elif myarg == 'targetfolder':
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
elif myarg == 'noverify':
verifyFiles = False
elif myarg == 'noextract':
extractFiles = False
elif myarg == 'ziptostdout':
zipToStdout = True
verifyFiles = extractFiles = False
elif myarg == 'bucketmatchpattern':
bucketMatchPattern = getREPattern(re.IGNORECASE)
elif myarg == 'objectmatchpattern':
objectMatchPattern = getREPattern(re.IGNORECASE)
elif myarg == 'downloadattempts':
downloadAttempts = getInteger(minVal=1)
elif myarg == 'retryinterval':
retryInterval = getInteger(minVal=10)
else:
unknownArgumentExit()
attempts = 0
while True:
try:
export = callGAPI(v.matters().exports(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, exportId=exportId)
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
return
if export['status'] == 'COMPLETED':
break
attempts += 1
entityActionNotPerformedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], Msg.EXPORT_NOT_COMPLETE.format(export['status']), attempts, downloadAttempts)
if attempts >= downloadAttempts:
return
time.sleep(retryInterval)
jcount = len(export['cloudStorageSink']['files'])
if not zipToStdout:
if not bucketMatchPattern and not objectMatchPattern:
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], jcount, Ent.CLOUD_STORAGE_FILE)
else:
entityPerformActionModifierNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], Msg.MAXIMUM_OF, jcount, Ent.CLOUD_STORAGE_FILE)
Ind.Increment()
j = 0
extCounts = {}
for s_file in export['cloudStorageSink']['files']:
j += 1
bucket = s_file['bucketName']
s_object = s_file['objectName']
if ((bucketMatchPattern and not bucketMatchPattern.match(bucket)) or
(objectMatchPattern and not objectMatchPattern.match(s_object))):
continue
filename = os.path.join(targetFolder, cleanFilename(s_object))
if zipToStdout and not ZIP_EXTENSION_PATTERN.match(filename):
continue
if targetName:
_, s_objectFilename = s_object.rsplit('/', 1)
if s_objectFilename.find('.') != -1:
s_objectFilename, s_objectExtension = s_objectFilename.rsplit('.', 1)
else:
s_objectExtension = ''
if targetName.find('#') == -1:
extCounts.setdefault(s_objectExtension, 0)
extCounts[s_objectExtension] += 1
if s_objectExtension:
filename = f"{targetName}-{extCounts[s_objectExtension]}.{s_objectExtension}"
else:
filename = f"{targetName}-{extCounts[s_objectExtension]}"
else:
filename = targetName.replace('#objectname#', s_object).replace('#filename#', s_objectFilename).replace('#extension#', s_objectExtension)
filename = os.path.join(targetFolder, cleanFilename(filename))
Act.Set(Act.DOWNLOAD)
if not zipToStdout:
performAction(Ent.CLOUD_STORAGE_FILE, s_object, j, jcount)
Ind.Increment()
_getCloudStorageObject(s, bucket, s_object, filename,
expectedMd5=s_file['md5Hash'] if verifyFiles else None,
zipToStdout=zipToStdout, j=j, jcount=jcount)
if extractFiles and ZIP_EXTENSION_PATTERN.match(filename):
Act.Set(Act.EXTRACT)
extract_nested_zip(filename)
Act.Set(Act.DOWNLOAD)
Ind.Decrement()
Ind.Decrement()
def _cleanVaultHold(hold, cd):
if cd is not None:
if 'accounts' in hold:
accountType = 'group' if hold['corpus'] == 'GROUPS' else 'user'
for i in range(0, len(hold['accounts'])):
hold['accounts'][i]['email'] = convertUIDtoEmailAddress(f'uid:{hold["accounts"][i]["accountId"]}', cd, accountType)
if 'orgUnit' in hold:
hold['orgUnit']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, hold['orgUnit']['orgUnitId'])
query = hold.get('query')
if query:
if 'driveQuery' in hold['query']:
hold['query']['driveQuery'].pop('includeTeamDriveFiles', None)
VAULT_HOLD_TIME_OBJECTS = {'holdTime', 'updateTime', 'startTime', 'endTime'}
def _showVaultHold(matterNameId, hold, cd, FJQC, k=0, kcount=0):
_cleanVaultHold(hold, cd)
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(hold, timeObjects=VAULT_HOLD_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
return
if matterNameId is not None:
printEntity([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])], k, kcount)
Ind.Increment()
showJSON(None, hold, timeObjects=VAULT_HOLD_TIME_OBJECTS)
Ind.Decrement()
def _getHoldQueryParameters(myarg, queryParameters):
if myarg == 'query':
queryParameters['queryLocation'] = Cmd.Location()
queryParameters['query'] = getString(Cmd.OB_QUERY)
elif myarg == 'terms':
queryParameters['terms'] = getString(Cmd.OB_STRING)
elif myarg in {'start', 'starttime'}:
queryParameters['startTime'] = getTimeOrDeltaFromNow()
elif myarg in {'end', 'endtime'}:
queryParameters['endTime'] = getTimeOrDeltaFromNow()
elif myarg == 'includerooms':
queryParameters['includeRooms'] = getBoolean()
elif myarg in {'includeshareddrives', 'includeteamdrives'}:
queryParameters['includeSharedDriveFiles'] = getBoolean()
elif myarg == 'covereddata':
queryParameters['coveredData'] = getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)
else:
return False
return True
def _setHoldQuery(body, queryParameters):
queryType = VAULT_CORPUS_QUERY_MAP[body['corpus']]
if queryType is None:
return
body['query'] = {queryType: {}}
if body['corpus'] == 'DRIVE':
if queryParameters.get('query'):
try:
body['query'][queryType] = json.loads(queryParameters['query'])
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
Cmd.SetLocation(queryParameters['queryLocation'])
usageErrorExit(f'{str(e)}: {queryParameters["query"]}')
elif queryParameters.get('includeSharedDriveFiles'):
body['query'][queryType]['includeSharedDriveFiles'] = queryParameters['includeSharedDriveFiles']
elif body['corpus'] in {'GROUPS', 'MAIL'}:
if queryParameters.get('query'):
body['query'][queryType]['terms'] = queryParameters['query']
elif queryParameters.get('terms'):
body['query'][queryType]['terms'] = queryParameters['terms']
if queryParameters.get('startTime'):
body['query'][queryType]['startTime'] = queryParameters['startTime']
if queryParameters.get('endTime'):
body['query'][queryType]['endTime'] = queryParameters['endTime']
elif body['corpus'] == 'HANGOUTS_CHAT':
if queryParameters.get('includeRooms'):
body['query'][queryType]['includeRooms'] = queryParameters['includeRooms']
elif body['corpus'] == 'VOICE':
if queryParameters.get('coveredData'):
body['query'][queryType]['coveredData'] = queryParameters['coveredData']
# gam create vaulthold|hold matter <MatterItem> [name <String>] corpus calendar|drive|mail|groups|hangouts_chat|voice
# [(accounts|groups|users <EmailItemList>) | (orgunit|org|ou <OrgUnit>)]
# [query <QueryVaultCorpus>]
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [includerooms <Boolean>]
# [covereddata calllogs|textmessages|voicemails]
# [includeshareddrives|includeteamdrives <Boolean>]
# [showdetails|returnidonly]
def doCreateVaultHold():
v = buildGAPIObject(API.VAULT)
body = {}
matterId = None
accounts = []
queryParameters = {}
returnIdOnly = showDetails = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
elif myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
elif myarg == 'corpus':
body['corpus'] = getChoice(VAULT_CORPUS_ARGUMENT_MAP, mapChoice=True)
elif myarg in {'accounts', 'users', 'groups'}:
accountsLocation = Cmd.Location()
accounts = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
elif myarg in {'ou', 'org', 'orgunit'}:
body['orgUnit'] = {'orgUnitId': getOrgUnitId()[1]}
elif _getHoldQueryParameters(myarg, queryParameters):
pass
elif myarg == 'showdetails':
showDetails = True
returnIdOnly = False
elif myarg == 'returnidonly':
returnIdOnly = True
showDetails = False
else:
unknownArgumentExit()
if matterId is None:
missingArgumentExit('matter')
if not body.get('corpus'):
missingArgumentExit(f'corpus {"|".join(VAULT_CORPUS_ARGUMENT_MAP)}')
if 'name' not in body:
body['name'] = f'GAM {body["corpus"]} Hold - {ISOformatTimeStamp(todaysTime())}'
_setHoldQuery(body, queryParameters)
if accounts:
body['accounts'] = []
cd = buildGAPIObject(API.DIRECTORY)
accountType = 'group' if body['corpus'] == 'GROUPS' else 'user'
for account in accounts:
body['accounts'].append({'accountId': convertEmailAddressToUID(account, cd, accountType, accountsLocation)})
try:
hold = callGAPI(v.matters().holds(), 'create',
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.BAD_REQUEST, GAPI.BACKEND_ERROR, GAPI.FAILED_PRECONDITION,
GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, body=body)
if not returnIdOnly:
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])])
if showDetails:
_showVaultHold(None, hold, None, None)
else:
writeStdout(f'{hold["holdId"]}\n')
except (GAPI.alreadyExists, GAPI.badRequest, GAPI.backendError, GAPI.failedPrecondition,
GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, body.get('name')], str(e))
# gam update vaulthold|hold <HoldItem> matter <MatterItem>
# [([addaccounts|addgroups|addusers <EmailItemList>] [removeaccounts|removegroups|removeusers <EmailItemList>]) | (orgunit|org|ou <OrgUnit>)]
# [query <QueryVaultCorpus>]
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [includerooms <Boolean>]
# [covereddata calllogs|textmessages|voicemails]
# [includeshareddrives|includeteamdrives <Boolean>]
# [showdetails]
def doUpdateVaultHold():
v = buildGAPIObject(API.VAULT)
holdName = getString(Cmd.OB_HOLD_ITEM)
body = {}
cd = matterId = None
addAccounts = []
addAccountIds = []
removeAccounts = []
removeAccountIds = []
queryParameters = {}
showDetails = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
holdId, holdName, holdNameId = convertHoldNameToID(v, holdName, matterId, matterNameId)
elif myarg in {'addusers', 'addaccounts', 'addgroups'}:
addAccountsLocation = Cmd.Location()
addAccounts = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
elif myarg in {'removeusers', 'removeaccounts', 'removegroups'}:
removeAccountsLocation = Cmd.Location()
removeAccounts = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
elif myarg in {'ou', 'org', 'orgunit'}:
body['orgUnit'] = {'orgUnitId': getOrgUnitId()[1]}
elif _getHoldQueryParameters(myarg, queryParameters):
pass
elif myarg == 'showdetails':
showDetails = True
else:
unknownArgumentExit()
if matterId is None:
missingArgumentExit('matter')
try:
old_body = callGAPI(v.matters().holds(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=holdId, fields='name,corpus,query,orgUnit')
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
return
accountType = 'group' if old_body['corpus'] == 'GROUPS' else 'user'
if addAccounts:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
for account in addAccounts:
addAccountIds.append({'email': account, 'id': convertEmailAddressToUID(account, cd, accountType, addAccountsLocation)})
if removeAccounts:
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
for account in removeAccounts:
removeAccountIds.append({'email': account, 'id': convertEmailAddressToUID(account, cd, accountType, removeAccountsLocation)})
if queryParameters or body.get('orgUnit'):
body['corpus'] = old_body['corpus']
if 'orgUnit' in old_body and 'orgUnit' not in body:
# bah, API requires this to be sent on update even when it's not changing
body['orgUnit'] = old_body['orgUnit']
if queryParameters:
_setHoldQuery(body, queryParameters)
else:
body['query'] = old_body['query']
if body:
try:
hold = callGAPI(v.matters().holds(), 'update',
throwReas=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=holdId, body=body)
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId])
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
return
jcount = len(addAccountIds)
if jcount > 0:
Act.Set(Act.ADD)
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], jcount, Ent.ACCOUNT)
Ind.Increment()
j = 0
for account in addAccountIds:
j += 1
try:
callGAPI(v.matters().holds().accounts(), 'create',
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.BACKEND_ERROR, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=holdId, body={'accountId': account['id']})
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], j, jcount)
except (GAPI.alreadyExists, GAPI.backendError) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], str(e), j, jcount)
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, None], str(e))
return
Ind.Decrement()
jcount = len(removeAccountIds)
if jcount > 0:
Act.Set(Act.REMOVE)
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], jcount, Ent.ACCOUNT)
Ind.Increment()
j = 0
for account in removeAccountIds:
j += 1
try:
callGAPI(v.matters().holds().accounts(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.BACKEND_ERROR, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=holdId, accountId=account['id'])
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], j, jcount)
except (GAPI.alreadyExists, GAPI.backendError) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], str(e), j, jcount)
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, None], str(e))
return
Ind.Decrement()
if showDetails:
_showVaultHold(None, hold, cd, None)
# gam delete vaulthold|hold <HoldItem> matter <MatterItem>
# gam delete vaulthold|hold <MatterItem> <HoldItem>
def doDeleteVaultHold():
v = buildGAPIObject(API.VAULT)
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
holdId, holdName, holdNameId = convertHoldNameToID(v, getString(Cmd.OB_HOLD_ITEM), matterId, matterNameId)
else:
holdName = getString(Cmd.OB_HOLD_ITEM)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
holdId, holdName, holdNameId = convertHoldNameToID(v, holdName, matterId, matterNameId)
else:
unknownArgumentExit()
try:
callGAPI(v.matters().holds(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=holdId)
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId])
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
VAULT_HOLD_FIELDS_CHOICE_MAP = {
'accounts': 'accounts',
'accounts.acountid': 'accounts.accountId',
'accounts.email': 'accounts.email',
'accounts.firstname': 'accounts.firstName',
'accounts.holdtime': 'accounts.holdTime',
'accounts.lastname': 'accounts.lastName',
'corpus': 'corpus',
'holdid': 'holdId',
'name': 'name',
'orgunit': 'orgUnit',
'orgunit.holdtime': 'orgUnit.holdTime',
'orgunit.ordunitid': 'orgUnit.orgUnitId',
'query': 'query',
'updatetime': 'updateTime',
}
# gam info vaulthold|hold <HoldItem> matter <MatterItem>
# [fields <VaultHoldFieldNameList>] [shownames]
# [formatjson]
# gam info vaulthold|hold <MatterItem> <HoldItem>
# [fields <VaultHoldFieldNameList>] [shownames]
# [formatjson]
def doInfoVaultHold():
v = buildGAPIObject(API.VAULT)
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
holdId, holdName, holdNameId = convertHoldNameToID(v, getString(Cmd.OB_HOLD_ITEM), matterId, matterNameId)
else:
holdName = getString(Cmd.OB_HOLD_ITEM)
cd = None
fieldsList = []
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
holdId, holdName, holdNameId = convertHoldNameToID(v, holdName, matterId, matterNameId)
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
elif getFieldsList(myarg, VAULT_HOLD_FIELDS_CHOICE_MAP, fieldsList, initialField=['holdId', 'name']):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
hold = callGAPI(v.matters().holds(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, holdId=holdId, fields=fields)
_showVaultHold(matterNameId, hold, cd, FJQC)
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
PRINT_VAULT_HOLDS_TITLES = ['matterId', 'matterName', 'holdId', 'name', 'updateTime']
# gam print vaultholds|holds [todrive <ToDriveAttribute>*] [matters <MatterItemList>]
# [fields <VaultHoldFieldNameList>] [shownames]
# [formatjson [quotechar <Character>]]
# [oneitemperrow]
# gam show vaultholds|holds [matters <MatterItemList>]
# [fields <VaultHoldFieldNameList>] [shownames]
# [formatjson]
def doPrintShowVaultHolds():
def _printVaultHold(hold):
row = flattenJSON(hold, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_HOLD_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
'holdId': hold['holdId'], 'name': hold['name'],
'JSON': json.dumps(cleanJSON(hold, timeObjects=VAULT_HOLD_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_VAULT_HOLDS_TITLES, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar()
matters = []
cd = None
fieldsList = []
oneItemPerRow = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'matter', 'matters'}:
matters = shlexSplitList(getString(Cmd.OB_MATTER_ITEM_LIST))
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
elif getFieldsList(myarg, VAULT_HOLD_FIELDS_CHOICE_MAP, fieldsList, initialField=['holdId', 'name']):
pass
elif csvPF and myarg == 'oneitemperrow':
oneItemPerRow = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
fields = getItemFieldsFromFieldsList('holds', fieldsList)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(PRINT_VAULT_HOLDS_TITLES+['JSON'])
if not matters:
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
try:
results = callGAPIpages(v.matters(), 'list', 'matters',
pageMessage=getPageMessage(),
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e))
return
else:
results = []
for matter in matters:
matterId, matterName, _, state = convertMatterNameToID(v, matter)
results.append({'matterId': matterId, 'name': matterName, 'state': state})
jcount = len(results)
if not csvPF:
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
j = 0
for matter in results:
j += 1
matterId = matter['matterId']
matterName = matter['name']
matterNameId = formatVaultNameId(matterName, matterId)
if csvPF:
printGettingAllEntityItemsForWhom(Ent.VAULT_HOLD, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}', j, jcount)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
if matter['state'] == 'OPEN':
try:
holds = callGAPIpages(v.matters().holds(), 'list', 'holds',
pageMessage=pageMessage,
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields=fields)
except GAPI.failedPrecondition:
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
continue
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e))
break
else:
warnMatterNotOpen(None, matter, matterNameId, j, jcount)
continue
kcount = len(holds)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], kcount, Ent.VAULT_HOLD, j, jcount)
Ind.Increment()
k = 0
for hold in holds:
k += 1
_showVaultHold(matterNameId, hold, cd, FJQC, k, kcount)
Ind.Decrement()
else:
for hold in holds:
_cleanVaultHold(hold, cd)
if not oneItemPerRow or not hold.get('accounts', []):
_printVaultHold(hold)
else:
for account in hold.pop('accounts'):
hold['account'] = account
_printVaultHold(hold)
if csvPF:
csvPF.writeCSVfile('Vault Holds')
PRINT_USER_VAULT_HOLDS_TITLES = ['User', 'matterId', 'matterName', 'holdId', 'name', 'orgUnitId', 'orgUnitPath']
# gam <UserTypeEntity> print vaultholds|holds [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show vaultholds|holds
def printShowUserVaultHolds(entityList):
cd = buildGAPIObject(API.DIRECTORY)
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_USER_VAULT_HOLDS_TITLES, 'sortall') if Act.csvFormat() else None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
try:
matters = callGAPIpages(v.matters(), 'list', 'matters',
pageMessage=getPageMessage(),
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e))
return
jcount = len(matters)
j = 0
for matter in matters:
j += 1
matterId = matter['matterId']
matterName = matter['name']
matterNameId = formatVaultNameId(matterName, matterId)
printGettingAllEntityItemsForWhom(Ent.VAULT_HOLD, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}', j, jcount)
try:
matter['holds'] = callGAPIpages(v.matters().holds(), 'list', 'holds',
pageMessage=getPageMessageForWhom(),
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields='holds(holdId,name,accounts(accountId,email),orgUnit(orgUnitId)),nextPageToken')
except GAPI.failedPrecondition:
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e), j, jcount)
totalHolds = 0
_, _, entityList = getEntityArgument(entityList)
for user in entityList:
user = normalizeEmailAddressOrUID(user)
orgUnits = getAllParentOrgUnitsForUser(cd, user)
for matter in matters:
matterId = matter['matterId']
matterName = matter['name']
matterNameId = formatVaultNameId(matterName, matterId)
for hold in matter.get('holds', []):
if 'orgUnit' in hold:
orgUnitId = hold['orgUnit'].get('orgUnitId')
if orgUnitId in orgUnits:
if not csvPF:
printEntityMessage([Ent.USER, user,
Ent.ORGANIZATIONAL_UNIT, orgUnits[orgUnitId],
Ent.VAULT_MATTER, formatVaultNameId(matter['name'], matter['matterId']),
Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])],
Msg.ON_VAULT_HOLD)
else:
csvPF.WriteRow({'User': user, 'matterId': matterId, 'matterName': matterName,
'holdId': hold['holdId'], 'name': hold['name'],
'orgUnitId': orgUnitId, 'orgUnitPath': orgUnits[orgUnitId]})
totalHolds += 1
else:
for account in hold.get('accounts', []):
if user == account.get('email', '').lower() or user == account.get('accountId', ''):
if not csvPF:
printEntityMessage([Ent.USER, user,
Ent.VAULT_MATTER, formatVaultNameId(matter['name'], matter['matterId']),
Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])],
Msg.ON_VAULT_HOLD)
else:
csvPF.WriteRow({'User': user, 'matterId': matterId, 'matterName': matterName,
'holdId': hold['holdId'], 'name': hold['name']})
totalHolds += 1
break
if csvPF:
csvPF.writeCSVfile('User Vault Holds')
else:
printKeyValueList(['Total Holds', totalHolds])
def _cleanVaultQuery(query, cd):
if 'query' in query:
if cd is not None:
if 'orgUnitInfo' in query['query']:
query['query']['orgUnitInfo']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, query['query']['orgUnitInfo']['orgUnitId'])
if 'sharedDriveInfo' in query['query']:
query['query']['sharedDriveInfo']['sharedDriveNames'] = []
for sharedDriveId in query['query']['sharedDriveInfo']['sharedDriveIds']:
query['query']['sharedDriveInfo']['sharedDriveNames'].append(_getSharedDriveNameFromId(sharedDriveId))
query['query'].pop('searchMethod', None)
query['query'].pop('teamDriveInfo', None)
VAULT_QUERY_TIME_OBJECTS = {'createTime', 'endTime', 'startTime', 'versionDate'}
def _showVaultQuery(matterNameId, query, cd, FJQC, k=0, kcount=0):
_cleanVaultQuery(query, cd)
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(query, timeObjects=VAULT_QUERY_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
return
printEntity([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, formatVaultNameId(query['displayName'], query['savedQueryId'])], k, kcount)
Ind.Increment()
showJSON(None, query, timeObjects=VAULT_QUERY_TIME_OBJECTS)
Ind.Decrement()
VAULT_QUERY_FIELDS_CHOICE_MAP = {
'createtime': 'createTime',
'displayname': 'displayName',
'matterid': 'matterId',
'name': 'displayName',
'query': 'query',
'queryid': 'savedQueryId',
'savedqueryid': 'savedQueryId',
}
# gam info vaultquery <QueryItem> matter <MatterItem>
# [fields <VaultQueryFieldNameList>] [shownames]
# [formatjson]
# gam info vaultquery <MatterItem> <QueryItem>
# [fields <VaultQueryFieldNameList>] [shownames]
# [formatjson]
def doInfoVaultQuery():
v = buildGAPIObject(API.VAULT)
if not Cmd.ArgumentIsAhead('matter'):
matterId, matterNameId = getMatterItem(v)
queryId, queryName, queryNameId = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
else:
queryName = getString(Cmd.OB_QUERY_ITEM)
cd = None
fieldsList = []
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'matter':
matterId, matterNameId = getMatterItem(v)
queryId, queryName, queryNameId = convertQueryNameToID(v, queryName, matterId, matterNameId)
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
if GM.Globals[GM.ADMIN_DRIVE] is None:
return
elif getFieldsList(myarg, VAULT_QUERY_FIELDS_CHOICE_MAP, fieldsList, initialField=['savedQueryId', 'displayName']):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
query = callGAPI(v.matters().savedQueries(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, savedQueryId=queryId, fields=fields)
_showVaultQuery(matterNameId, query, cd, FJQC)
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, queryNameId], str(e))
PRINT_VAULT_QUERIES_TITLES = ['matterId', 'matterName', 'savedQueryId', 'displayName']
# gam print vaultqueries [todrive <ToDriveAttribute>*] [matters <MatterItemList>]
# [fields <VaultQueryFieldNameList>] [shownames]
# [formatjson [quotechar <Character>]]
# gam show vaultqueries [matters <MatterItemList>]
# [fields <VaultQueryFieldNameList>] [shownames]
# [formatjson]
def doPrintShowVaultQueries():
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_VAULT_QUERIES_TITLES, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
matters = []
cd = None
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'matter', 'matters'}:
matters = shlexSplitList(getString(Cmd.OB_MATTER_ITEM_LIST))
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
if GM.Globals[GM.ADMIN_DRIVE] is None:
return
elif getFieldsList(myarg, VAULT_QUERY_FIELDS_CHOICE_MAP, fieldsList, initialField=['savedQueryId', 'displayName']):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
fields = getItemFieldsFromFieldsList('savedQueries', fieldsList)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(PRINT_VAULT_QUERIES_TITLES+['JSON'])
if not matters:
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
try:
results = callGAPIpages(v.matters(), 'list', 'matters',
pageMessage=getPageMessage(),
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_QUERY, None], str(e))
return
else:
results = []
for matter in matters:
matterId, matterName, _, state = convertMatterNameToID(v, matter)
results.append({'matterId': matterId, 'name': matterName, 'state': state})
jcount = len(results)
if not csvPF:
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
j = 0
for matter in results:
j += 1
matterId = matter['matterId']
matterName = matter['name']
matterNameId = formatVaultNameId(matterName, matterId)
if csvPF:
printGettingAllEntityItemsForWhom(Ent.VAULT_QUERY, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}', j, jcount)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
if matter['state'] == 'OPEN':
try:
queries = callGAPIpages(v.matters().savedQueries(), 'list', 'savedQueries',
pageMessage=pageMessage,
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, fields=fields)
except GAPI.failedPrecondition:
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
continue
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_QUERY, None], str(e))
break
else:
warnMatterNotOpen(None, matter, matterNameId, j, jcount)
continue
kcount = len(queries)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], kcount, Ent.VAULT_QUERY, j, jcount)
Ind.Increment()
k = 0
for query in queries:
k += 1
_showVaultQuery(matterNameId, query, cd, FJQC, k, kcount)
Ind.Decrement()
else:
for query in queries:
_cleanVaultQuery(query, cd)
row = flattenJSON(query, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_QUERY_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
'savedQueryId': query['savedQueryId'], 'displayName': query['displayName'],
'JSON': json.dumps(cleanJSON(query, timeObjects=VAULT_QUERY_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Vault Saved Queries')
def validateCollaborators(cd):
collaborators = []
for collaborator in getEntityList(Cmd.OB_COLLABORATOR_ENTITY):
collaborators.append({'email': collaborator, 'id': convertEmailAddressToUID(collaborator, cd)})
return collaborators
def _cleanVaultMatter(matter, cd):
if cd is not None:
if 'matterPermissions' in matter:
for i in range(0, len(matter['matterPermissions'])):
matter['matterPermissions'][i]['email'] = convertUserIDtoEmail(matter["matterPermissions"][i]["accountId"], cd)
def _showVaultMatter(matter, cd, FJQC, j=0, jcount=0):
_cleanVaultMatter(matter, cd)
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(matter), ensure_ascii=False, sort_keys=False))
return
printEntity([Ent.VAULT_MATTER, formatVaultNameId(matter['name'], matter['matterId'])], j, jcount)
Ind.Increment()
showJSON(None, matter)
Ind.Decrement()
# gam create vaultmatter|matter [name <String>] [description <string>]
# [collaborator|collaborators <CollaboratorItemList>] [sendemails <Boolean>] [ccme <Boolean>]
# [showdetails|returnidonly]
def doCreateVaultMatter():
v = buildGAPIObject(API.VAULT)
body = {}
cbody = {'matterPermission': {'role': 'COLLABORATOR', 'accountId': ''}, 'sendEmails': False, 'ccMe': False}
collaborators = []
cd = None
returnIdOnly = showDetails = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
elif myarg == 'description':
body['description'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'collaborator', 'collaborators'}:
if not cd:
cd = buildGAPIObject(API.DIRECTORY)
collaborators.extend(validateCollaborators(cd))
elif myarg == 'sendemails':
cbody['sendEmails'] = getBoolean()
elif myarg == 'ccme':
cbody['ccMe'] = getBoolean()
elif myarg == 'showdetails':
showDetails = True
returnIdOnly = False
elif myarg == 'returnidonly':
returnIdOnly = True
showDetails = False
else:
unknownArgumentExit()
if 'name' not in body:
body['name'] = f'GAM Matter - {ISOformatTimeStamp(todaysTime())}'
try:
matter = callGAPI(v.matters(), 'create',
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
body=body)
matterId = matter['matterId']
matterNameId = formatVaultNameId(matter['name'], matterId)
if not returnIdOnly:
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
else:
writeStdout(f'{matterId}\n')
except (GAPI.alreadyExists, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, body['name']], str(e))
return
jcount = len(collaborators)
if jcount > 0:
Act.Set(Act.ADD)
if not returnIdOnly:
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], jcount, Ent.COLLABORATOR)
Ind.Increment()
j = 0
for collaborator in collaborators:
j += 1
cbody['matterPermission']['accountId'] = collaborator['id']
try:
callGAPI(v.matters(), 'addPermissions',
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, body=cbody)
if not returnIdOnly:
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
except (GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
break
Ind.Decrement()
if showDetails:
_showVaultMatter(matter, cd, None)
VAULT_MATTER_ACTIONS = {
'close': Act.CLOSE,
'reopen': Act.REOPEN,
'delete': Act.DELETE,
'undelete': Act.UNDELETE,
}
def doActionVaultMatter(action, matterId=None, matterNameId=None, v=None):
if v is None:
v = buildGAPIObject(API.VAULT)
matterId, matterNameId = getMatterItem(v)
else:
Act.Set(VAULT_MATTER_ACTIONS[action])
checkForExtraneousArguments()
action_kwargs = {} if action == 'delete' else {'body': {}}
try:
callGAPI(v.matters(), action,
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, **action_kwargs)
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
# gam close vaultmatter|matter <MatterItem>
def doCloseVaultMatter():
doActionVaultMatter('close')
# gam reopen vaultmatter|matter <MatterItem>
def doReopenVaultMatter():
doActionVaultMatter('reopen')
# gam delete vaultmatter|matter <MatterItem>
def doDeleteVaultMatter():
doActionVaultMatter('delete')
# gam undelete vaultmatter|matter <MatterItem>
def doUndeleteVaultMatter():
doActionVaultMatter('undelete')
# gam update vaultmatter|matter <MatterItem> [name <String>] [description <string>]
# [addcollaborator|addcollaborators <CollaboratorItemList>] [removecollaborator|removecollaborators <CollaboratorItemList>]
def doUpdateVaultMatter():
v = buildGAPIObject(API.VAULT)
matterId, matterNameId = getMatterItem(v)
body = {}
addCollaborators = []
removeCollaborators = []
cd = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'action':
action = getChoice(VAULT_MATTER_ACTIONS)
doActionVaultMatter(action, matterId, matterNameId, v)
return
if myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
elif myarg == 'description':
body['description'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'addcollaborator', 'addcollaborators'}:
if not cd:
cd = buildGAPIObject(API.DIRECTORY)
addCollaborators.extend(validateCollaborators(cd))
elif myarg in {'removecollaborator', 'removecollaborators'}:
if not cd:
cd = buildGAPIObject(API.DIRECTORY)
removeCollaborators.extend(validateCollaborators(cd))
else:
unknownArgumentExit()
if body:
try:
if 'name' not in body or 'description' not in body:
# bah, API requires name/description to be sent on update even when it's not changing
result = callGAPI(v.matters(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, view='BASIC')
body.setdefault('name', result['name'])
body.setdefault('description', result.get('description'))
callGAPI(v.matters(), 'update',
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN],
matterId=matterId, body=body)
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
return
jcount = len(addCollaborators)
if jcount > 0:
Act.Set(Act.ADD)
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], jcount, Ent.COLLABORATOR)
Ind.Increment()
j = 0
for collaborator in addCollaborators:
j += 1
try:
callGAPI(v.matters(), 'addPermissions',
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, body={'matterPermission': {'role': 'COLLABORATOR', 'accountId': collaborator['id']}})
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
except (GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
break
Ind.Decrement()
jcount = len(removeCollaborators)
if jcount > 0:
Act.Set(Act.REMOVE)
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], jcount, Ent.COLLABORATOR)
Ind.Increment()
j = 0
for collaborator in removeCollaborators:
j += 1
try:
callGAPI(v.matters(), 'removePermissions',
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, body={'accountId': collaborator['id']})
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
except (GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
break
Ind.Decrement()
VAULT_MATTER_FIELDS_CHOICE_MAP = {
'matterid': 'matterId',
'name': 'name',
'description': 'description',
'state': 'state',
'matterpermissions': 'matterPermissions',
}
# gam info vaultmatter|matter <MatterItem>
# [basic|full|(fields <VaultMatterFieldNameList>)]
# [formatjson]
def doInfoVaultMatter():
v = buildGAPIObject(API.VAULT)
matterId, matterNameId = getMatterItem(v)
view = 'FULL'
fieldsList = []
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in PROJECTION_CHOICE_MAP:
view = PROJECTION_CHOICE_MAP[myarg]
elif getFieldsList(myarg, VAULT_MATTER_FIELDS_CHOICE_MAP, fieldsList, initialField=['matterId', 'name']):
pass
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
try:
matter = callGAPI(v.matters(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
matterId=matterId, view=view, fields=fields)
cd = buildGAPIObject(API.DIRECTORY) if 'matterPermissions' in matter else None
_showVaultMatter(matter, cd, FJQC)
except (GAPI.notFound, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
VAULT_MATTER_STATE_MAP = {'open': 'OPEN', 'closed': 'CLOSED', 'deleted': 'DELETED'}
PRINT_VAULT_MATTERS_TITLES = ['matterId', 'name', 'description', 'state']
# gam print vaultmatters|matters [todrive <ToDriveAttribute>*] [matterstate <MatterStateList>]
# [basic|full|(fields <VaultMatterFieldNameList>)]
# [formatjson [quotechar <Character>]]
# gam show vaultmatters|matters [matterstate <MatterStateList>]
# [basic|full|(fields <VaultMatterFieldNameList>)]
# [formatjson]
def doPrintShowVaultMatters():
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_VAULT_MATTERS_TITLES, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
cd = None
view = 'FULL'
fieldsList = []
matterStatesList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'matterstate':
for state in getString(Cmd.OB_STATE_NAME_LIST).lower().replace('_', '').replace(',', ' ').split():
if state in VAULT_MATTER_STATE_MAP:
matterStatesList.append(VAULT_MATTER_STATE_MAP[state])
else:
invalidChoiceExit(state, list(VAULT_MATTER_STATE_MAP), True)
elif myarg in PROJECTION_CHOICE_MAP:
view = PROJECTION_CHOICE_MAP[myarg]
elif getFieldsList(myarg, VAULT_MATTER_FIELDS_CHOICE_MAP, fieldsList, initialField=['matterId', 'name']):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if fieldsList and matterStatesList:
fieldsList.append('state')
fields = f'nextPageToken,matters({getFieldsFromFieldsList(fieldsList)})' if fieldsList else None
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(PRINT_VAULT_MATTERS_TITLES+['JSON'])
# If no states are set, there is no filtering; if 1 state is set, the API can filter; else GAM filters
matterStates = set()
stateParm = None
if matterStatesList:
if len(matterStatesList) == 1:
stateParm = matterStatesList[0]
else:
matterStates = set(matterStatesList)
qualifier = f' ({",".join(matterStatesList)})'
else:
qualifier = ''
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=qualifier)
try:
matters = callGAPIpages(v.matters(), 'list', 'matters',
pageMessage=getPageMessage(),
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT, GAPI.INVALID_ARGUMENT],
view=view, state=stateParm, fields=fields)
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.VAULT_MATTER, None], str(e))
return
jcount = len(matters)
if view == 'FULL':
cd = buildGAPIObject(API.DIRECTORY)
if not csvPF:
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not FJQC.formatJSON:
performActionNumItems(jcount, Ent.VAULT_MATTER)
Ind.Increment()
j = 0
for matter in matters:
j += 1
if not matterStates or matter['state'] in matterStates:
_showVaultMatter(matter, cd, FJQC, j, jcount)
Ind.Decrement()
else:
for matter in matters:
if not matterStates or matter['state'] in matterStates:
_cleanVaultMatter(matter, cd)
row = flattenJSON(matter)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'matterId': matter['matterId'], 'name': matter['name'],
'JSON': json.dumps(cleanJSON(matter), ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Vault Matters')
PRINT_VAULT_COUNTS_TITLES = ['account', 'count', 'error']
# gam print vaultcounts [todrive <ToDriveAttributes>*]
# matter <MatterItem> corpus mail|groups
# (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone
# [scope <all_data|held_data|unprocessed_data>]
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
# [excludedrafts <Boolean>]
# [wait <Integer>]
# gam print vaultcounts [todrive <ToDriveAttributes>*]
# matter <MatterItem> operation <String> [wait <Integer>]
def doPrintVaultCounts():
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_VAULT_COUNTS_TITLES, 'sortall')
matterId = name = None
operationWait = 15
body = {'view': 'ALL'}
query = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'matter':
matterId, _ = getMatterItem(v)
elif myarg == 'operation':
name = getString(Cmd.OB_STRING)
elif myarg in VAULT_QUERY_ARGS:
_buildVaultQuery(myarg, query, VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
elif myarg == 'wait':
operationWait = getInteger(minVal=1)
else:
unknownArgumentExit()
if not matterId:
missingArgumentExit('matter')
if name:
operation = {'name': name}
doWait = False
else:
body['query'] = query
_validateVaultQuery(body, VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
try:
operation = callGAPI(v.matters(), 'count',
throwReasons=[GAPI.INVALID_ARGUMENT],
matterId=matterId, body=body)
except GAPI.invalidArgument as e:
entityActionFailedExit([Ent.VAULT_MATTER, matterId], str(e))
doWait = True
printGettingAllAccountEntities(Ent.VAULT_MATTER_ARTIFACT, qualifier=f' for {Ent.Singular(Ent.VAULT_OPERATION)}: {operation["name"]}',
accountType=Ent.VAULT_MATTER)
while not operation.get('done'):
if doWait:
stderrEntityMessage([Ent.VAULT_OPERATION, operation['name']], Msg.IS_NOT_DONE_CHECKING_IN_SECONDS.format(operationWait))
time.sleep(operationWait)
try:
operation = callGAPI(v.operations(), 'get',
throwReasons=[GAPI.NOT_FOUND],
name=operation['name'])
except GAPI.notFound as e:
entityActionFailedExit([Ent.VAULT_OPERATION, operation['name']], str(e))
doWait = True
response = operation.get('response', {})
query = operation['metadata']['query']
search_method = query.get('searchMethod')
# ARGH count results don't include accounts with zero items.
# so we keep track of which accounts we searched and can report
# zero data for them.
if search_method == 'ACCOUNT':
query_accounts = query.get('accountInfo', {}).get('emails', [])
elif search_method == 'ENTIRE_ORG':
query_accounts = getItemsToModify(Cmd.ENTITY_ALL_USERS, '')
elif search_method == 'ORG_UNIT':
query_accounts = getItemsToModify(Cmd.ENTITY_OU, query['orgUnitInfo']['orgUnitId'])
mailcounts = response.get('mailCountResult', {})
groupcounts = response.get('groupsCountResult', {})
for a_count in [mailcounts, groupcounts]:
for errored_account in a_count.get('accountCountErrors', []):
email = errored_account.get('account', {}).get('email', '')
if email:
csvPF.WriteRow({'account': email, 'error': errored_account.get('errorType')})
if email in query_accounts:
query_accounts.remove(email)
for account in a_count.get('nonQueryableAccounts', []):
csvPF.WriteRow({'account': account, 'error': 'Not queried because not on hold'})
if account in query_accounts:
query_accounts.remove(account)
for account in a_count.get('accountCounts', []):
email = account.get('account', {}).get('email', '')
if email:
csvPF.WriteRow({'account': email, 'count': account.get('count', 0)})
if email in query_accounts:
query_accounts.remove(email)
for account in query_accounts:
csvPF.WriteRow({'account': account, 'count': 0})
csvPF.writeCSVfile('Vault Counts')
# gam [<UserTypeEntity>] create site <SiteName> <SiteAttribute>*
# gam [<UserTypeEntity>] update site <SiteEntity> <SiteAttribute>+
# gam [<UserTypeEntity>] info site <SiteEntity> [withmappings] [role|roles all|<SiteACLRoleList>]
# gam [<UserTypeEntity>] print sites [todrive <ToDriveAttribute>*] [domain|domains <DomainNameEntity>] [includeallsites]
# [withmappings] [role|roles all|<SiteACLRoleList>] [startindex <Number>] [maxresults <Number>] [convertcrnl] [delimiter <Character>]
# gam [<UserTypeEntity>] show sites [domain|domains <DomainNameEntity>] [includeallsites]
# [withmappings] [role|roles all|<SiteACLRoleList>] [startindex <Number>] [maxresults <Number>] [convertcrnl]
# gam [<UserTypeEntity>] create siteacls <SiteEntity> <SiteACLRole> <SiteACLScopeEntity>
# gam [<UserTypeEntity>] update siteacls <SiteEntity> <SiteACLRole> <SiteACLScopeEntity>
# gam [<UserTypeEntity>] delete siteacls <SiteEntity> <SiteACLScopeEntity>
# gam [<UserTypeEntity>] info siteacls <SiteEntity> <SiteACLScopeEntity>
# gam [<UserTypeEntity>] show siteacls <SiteEntity>
# gam [<UserTypeEntity>] print siteacls <SiteEntity> [todrive <ToDriveAttribute>*]
# gam [<UserTypeEntity>] print siteactivity <SiteEntity> [todrive <ToDriveAttribute>*]
# [startindex <Number>] [maxresults <Number>] [updated_min <Date>] [updated_max <Date>]
def deprecatedUserSites(_):
deprecatedCommandExit()
def deprecatedDomainSites():
deprecatedCommandExit()
# <CSVFileInput> [keyfield <FieldName>] [datafield <FieldName>]
def _getGroupOrgUnitMap():
def getKeyFieldInfo(keyword, defaultField):
if not checkArgumentPresent(keyword):
field = defaultField
else:
field = getString(Cmd.OB_FIELD_NAME)
if field not in fieldnames:
csvFieldErrorExit(field, fieldnames, backupArg=True)
return field
filename = getString(Cmd.OB_FILE_NAME)
f, csvFile, fieldnames = openCSVFileReader(filename)
keyField = getKeyFieldInfo('keyfield', 'Group')
dataField = getKeyFieldInfo('datafield', 'OrgUnit')
groupOrgUnitMap = {}
for row in csvFile:
group = row[keyField].strip().lower()
orgUnit = row[dataField].strip()
if not group or not orgUnit:
systemErrorExit(USAGE_ERROR_RC, Msg.GROUP_MAPS_TO_OU_INVALID_ROW.format(filename, group, orgUnit))
orgUnit = makeOrgUnitPathAbsolute(orgUnit)
if group in groupOrgUnitMap:
origOrgUnit = groupOrgUnitMap[group]
if origOrgUnit != orgUnit:
systemErrorExit(USAGE_ERROR_RC, Msg.GROUP_MAPS_TO_MULTIPLE_OUS.format(filename, group, ','.join([origOrgUnit, orgUnit])))
groupOrgUnitMap[group] = orgUnit
closeFile(f)
return groupOrgUnitMap
class PasswordOptions():
def __init__(self, updateCmd):
self.password = ''
self.notFoundPassword = ''
self.b64DecryptPassword = False
self.clearPassword = True
self.hashPassword = True
self.ignoreNullPassword = False
self.makeRandomPassword = not updateCmd
self.makeUniqueRandomPassword = False
self.makeCleanPassword = True
self.cleanPasswordLen = 25
self.randomPasswordChars = None
self.promptForPassword = False
self.promptForUniquePassword = False
self.notifyPasswordSet = False
self.updateCmd = updateCmd
self.filename = ''
def GetPassword(self):
return getString(Cmd.OB_PASSWORD, minLen=1 if not self.ignoreNullPassword else 0, maxLen=100)
def SetCleanPasswordLen(self):
self.cleanPasswordLen = getInteger(minVal=8, maxVal=100, default=25)
def CreateRandomPassword(self):
rnd = SystemRandom()
if not self.makeCleanPassword:
# Generate a password with unicode chars that are not allowed in passwords.
# We expect "password random nohash" to fail but no one should be using that.
# Our goal here is to purposefully block logins with this password.
if not self.randomPasswordChars:
self.randomPasswordChars = [chr(i) for i in range(1, 55296)]
return ''.join(rnd.choice(self.randomPasswordChars) for _ in range(4096))
# Generate a clean password that can be used for logins
return ''.join(rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(self.cleanPasswordLen))
def ProcessArgument(self, myarg, notify, notFoundBody):
if myarg == 'ignorenullpassword':
self.ignoreNullPassword = True
elif myarg == 'notifypassword':
password = self.GetPassword()
if password:
notify['password'] = password
self.notifyPasswordSet = True
elif myarg == 'nohash':
self.hashPassword = False
elif self.updateCmd and myarg == 'notfoundpassword':
up = 'password'
password = self.GetPassword()
if password:
if password.lower() == 'blocklogin':
self.makeCleanPassword = False
notFoundBody[up] = self.CreateRandomPassword()
elif password.lower() in {'random', 'uniquerandom'}:
self.SetCleanPasswordLen()
self.makeCleanPassword = True
notFoundBody[up] = self.CreateRandomPassword()
else:
notFoundBody[up] = password
self.notFoundPassword = notFoundBody[up]
elif myarg in {'lograndompassword', 'logpassword'}:
self.filename = getString(Cmd.OB_FILE_NAME)
else:
return False
return True
HASH_FUNCTION_MAP = {
'base64-md5': 'MD5',
'base64-sha1': 'SHA-1',
'crypt': 'crypt',
'md5': 'MD5',
'sha': 'SHA-1',
'sha-1': 'SHA-1',
'sha1': 'SHA-1',
}
def ProcessPropertyArgument(self, myarg, up, body):
if up == 'password':
password = self.GetPassword()
if password:
body[up] = password
self.makeRandomPassword = self.makeUniqueRandomPassword =\
self.promptForPassword = self.promptForUniquePassword = False
if password.lower() == 'blocklogin':
self.makeRandomPassword = True
self.makeCleanPassword = False
elif password.lower() == 'random':
self.SetCleanPasswordLen()
self.makeRandomPassword = self.makeCleanPassword = True
elif password.lower() == 'uniquerandom':
self.SetCleanPasswordLen()
if self.updateCmd:
self.makeUniqueRandomPassword = self.makeCleanPassword = True
else:
self.makeRandomPassword = self.makeCleanPassword = True
elif password.lower() == 'prompt':
self.promptForPassword = True
elif password.lower() == 'uniqueprompt':
if self.updateCmd:
self.promptForUniquePassword = True
else:
self.promptForPassword = True
else:
self.password = password
elif up == 'hashFunction':
body[up] = self.HASH_FUNCTION_MAP[myarg]
self.clearPassword = self.hashPassword = False
self.b64DecryptPassword = myarg.startswith('base64')
else:
return False
return True
def FinalizePassword(self, body, notify, up):
if not self.notifyPasswordSet:
notify[up] = body[up] if self.clearPassword else Msg.CONTACT_ADMINISTRATOR_FOR_PASSWORD
if self.hashPassword:
body[up] = sha512_crypt.hash(body[up], rounds=10000)
body['hashFunction'] = 'crypt'
elif self.b64DecryptPassword:
if body[up].lower()[:5] in ['{md5}', '{sha}']:
body[up] = body[up][5:]
try:
body[up] = base64.b64decode(body[up]).hex()
except Exception:
pass
def AssignPassword(self, body, notify, notFoundBody, createIfNotFound, user=None):
up = 'password'
if self.makeRandomPassword or self.makeUniqueRandomPassword:
body[up] = self.CreateRandomPassword()
self.password = body[up]
elif user and (self.promptForPassword or self.promptForUniquePassword):
body[up] = readStdin(f'Enter password for {user}: ')
self.password = body[up]
elif self.promptForPassword:
body[up] = readStdin('Enter password: ')
self.password = body[up]
if up in body:
self.FinalizePassword(body, notify, up)
elif 'hashFunction' in body:
body.pop('hashFunction')
if createIfNotFound:
if not notFoundBody:
if up in body:
notFoundBody[up] = body[up]
if 'hashfunction' in body:
notFoundBody['hashfunction'] = body['hashFunction']
notify['notFoundPassword'] = notify[up]
self.notFoundPassword = self.password
else:
notify['notFoundPassword'] = notify[up] if notify[up] else notFoundBody[up] if self.clearPassword else Msg.CONTACT_ADMINISTRATOR_FOR_PASSWORD
self.FinalizePassword(notFoundBody, notify, up)
UPDATE_USER_ARGUMENT_TO_PROPERTY_MAP = {
'address': 'addresses',
'addresses': 'addresses',
'archive': 'archived',
'archived': 'archived',
'base64-md5': 'hashFunction',
'base64-sha1': 'hashFunction',
'changepassword': 'changePasswordAtNextLogin',
'changepasswordatnextlogin': 'changePasswordAtNextLogin',
'crypt': 'hashFunction',
'customerid': 'customerId',
'displayname': 'displayName',
'email': 'primaryEmail',
'emails': 'emails',
'externalid': 'externalIds',
'externalids': 'externalIds',
'familyname': 'familyName',
'firstname': 'givenName',
'gal': 'includeInGlobalAddressList',
'gender': 'gender',
'givenname': 'givenName',
'im': 'ims',
'ims': 'ims',
'includeinglobaladdresslist': 'includeInGlobalAddressList',
'ipwhitelisted': 'ipWhitelisted',
'keyword': 'keywords',
'keywords': 'keywords',
'language': 'languages',
'languages': 'languages',
'lastname': 'familyName',
'location': 'locations',
'locations': 'locations',
'md5': 'hashFunction',
'note': 'notes',
'notes': 'notes',
'org': 'orgUnitPath',
'organization': 'organizations',
'organizations': 'organizations',
'organisation': 'organizations',
'organisations': 'organizations',
'orgunitpath': 'orgUnitPath',
'otheremail': 'emails',
'otheremails': 'emails',
'ou': 'orgUnitPath',
'password': 'password',
'phone': 'phones',
'phones': 'phones',
'posix': 'posixAccounts',
'posixaccounts': 'posixAccounts',
'primaryemail': 'primaryEmail',
'recoveryemail': 'recoveryEmail',
'recoveryphone': 'recoveryPhone',
'relation': 'relations',
'relations': 'relations',
'sha': 'hashFunction',
'sha-1': 'hashFunction',
'sha1': 'hashFunction',
'ssh': 'sshPublicKeys',
'sshkeys': 'sshPublicKeys',
'sshpublickeys': 'sshPublicKeys',
'suspend': 'suspended',
'suspended': 'suspended',
'username': 'primaryEmail',
'website': 'websites',
'websites': 'websites',
}
ADDRESS_ARGUMENT_TO_FIELD_MAP = {
'country': 'country',
'countrycode': 'countryCode',
'extendedaddress': 'extendedAddress',
'locality': 'locality',
'pobox': 'poBox',
'postalcode': 'postalCode',
'region': 'region',
'streetaddress': 'streetAddress',
}
ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
'costcenter': 'costCenter',
'costcentre': 'costCenter',
'department': 'department',
'description': 'description',
'domain': 'domain',
'fulltimeequivalent': 'fullTimeEquivalent',
'location': 'location',
'name': 'name',
'symbol': 'symbol',
'title': 'title',
}
# (MultiValue, IgnoreEmpty)
SCHEMA_VALUE_PROCESS_MAP = {
'multivalued': (True, False),
'multivalue': (True, False),
'value': (True, False),
'multinonempty': (True, True),
'scalarnonempty': (False, True)
}
USER_JSON_SKIP_FIELDS = ['agreedToTerms', 'aliases', 'creationTime', 'customerId', 'deletionTime', 'groups', 'id',
'isAdmin', 'isDelegatedAdmin', 'isEnforcedIn2Sv', 'isEnrolledIn2Sv', 'isMailboxSetup',
'lastLoginTime', 'licenses', 'primaryEmail', 'thumbnailPhotoEtag', 'thumbnailPhotoUrl',
'ipWhiteListed', 'posixAccounts', 'suspensionReason', 'nonEditableAliases']
ALLOW_EMPTY_CUSTOM_TYPE = 'allowEmptyCustomType'
def getUserAttributes(cd, updateCmd, noUid=False):
def getKeywordAttribute(keywords, attrdict, **opts):
if Cmd.ArgumentsRemaining():
keyword = Cmd.Current().strip().lower()
if keyword in keywords[UProp.PTKW_KEYWORD_LIST]:
Cmd.Advance()
if keyword != keywords[UProp.PTKW_CL_CUSTOM_KEYWORD]:
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = keyword
attrdict.pop(keywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD], None)
return
if Cmd.ArgumentsRemaining():
customType = Cmd.Current().strip()
if customType or opts.get(ALLOW_EMPTY_CUSTOM_TYPE, False):
Cmd.Advance()
if keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]:
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
else:
attrdict.pop(keywords[UProp.PTKW_ATTR_TYPE_KEYWORD], None)
attrdict[keywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = customType
return
missingArgumentExit('custom attribute type')
elif DEFAULT_CHOICE in opts:
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = opts[DEFAULT_CHOICE]
return
elif keywords[UProp.PTKW_CL_CUSTOM_KEYWORD]:
if keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]:
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
else:
attrdict.pop(keywords[UProp.PTKW_ATTR_TYPE_KEYWORD], None)
attrdict[keywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = Cmd.Current()
Cmd.Advance()
return
invalidChoiceExit(keyword, keywords[UProp.PTKW_KEYWORD_LIST], False)
elif DEFAULT_CHOICE in opts:
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = opts[DEFAULT_CHOICE]
return
missingChoiceExit(keywords[UProp.PTKW_KEYWORD_LIST])
def primaryNotPrimary(pnp, entry):
if pnp == 'notprimary':
return True
if pnp == 'primary':
entry['primary'] = True
primary['location'] = Cmd.Location()
return True
return False
def getPrimaryNotPrimaryChoice(entry, defaultChoice):
if getChoice({'primary': True, 'notprimary': False}, defaultChoice=defaultChoice, mapChoice=True):
entry['primary'] = True
primary['location'] = Cmd.Location()
def checkClearBodyList(body, itemName):
if checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
body.pop(itemName, None)
body[itemName] = None
return True
return False
def appendItemToBodyList(body, itemName, itemValue, checkBlankField=None, checkSystemId=False):
if (itemName in body) and (body[itemName] is None):
del body[itemName]
body.setdefault(itemName, [])
if checkBlankField is None or itemValue[checkBlankField]:
# Throw an error if multiple items are marked primary
if itemValue.get('primary', False):
for citem in body[itemName]:
if citem.get('primary', False):
if not checkSystemId or itemValue.get('systemId') == citem.get('systemId'):
Cmd.SetLocation(primary['location']-1)
usageErrorExit(Msg.MULTIPLE_ITEMS_MARKED_PRIMARY.format(itemName))
body[itemName].append(itemValue)
def _splitSchemaNameDotFieldName(sn_fn, fnRequired=True):
if sn_fn.find('.') != -1:
schemaName, fieldName = sn_fn.split('.', 1)
schemaName = schemaName.strip()
fieldName = fieldName.strip()
if schemaName and fieldName:
return (schemaName, fieldName)
elif not fnRequired:
schemaName = sn_fn.strip()
if schemaName:
return (schemaName, None)
invalidArgumentExit(Cmd.OB_SCHEMA_NAME_FIELD_NAME)
parameters = {
'verifyNotInvitable': False,
'createIfNotFound': False,
'noActionIfAlias': False,
'notifyOnUpdate': True,
'setChangePasswordOnCreate': False,
'immutableOUs': set(),
'addNumericSuffixOnDuplicate': 0,
'lic': None,
LICENSE_PRODUCT_SKUIDS: [],
}
if updateCmd:
body = {}
else:
body = {'name': {'givenName': UNKNOWN, 'familyName': UNKNOWN}}
body['primaryEmail'] = getEmailAddress(noUid=noUid)
notFoundBody = {}
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
primary = {}
updatePrimaryEmail = {}
groupOrgUnitMap = None
tagReplacements = _initTagReplacements()
addGroups = {}
addAliases = []
PwdOpts = PasswordOptions(updateCmd)
resolveConflictAccount = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'notify':
notify['recipients'] = getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
elif myarg == 'subject':
notify['subject'] = getString(Cmd.OB_STRING)
elif myarg in SORF_MSG_FILE_ARGUMENTS:
notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
elif myarg == 'html':
notify['html'] = getBoolean()
elif myarg == 'from':
notify['from'] = getString(Cmd.OB_EMAIL_ADDRESS)
elif myarg == 'replyto':
notify['replyto'] = getString(Cmd.OB_EMAIL_ADDRESS)
elif myarg == 'mailbox':
notify['mailbox'] = getString(Cmd.OB_EMAIL_ADDRESS)
elif PwdOpts.ProcessArgument(myarg, notify, notFoundBody):
pass
elif _getTagReplacement(myarg, tagReplacements, True):
pass
elif myarg == 'admin':
value = getBoolean()
if updateCmd or value:
Cmd.Backup()
unknownArgumentExit()
elif myarg == 'verifynotinvitable':
parameters['verifyNotInvitable'] = True
elif myarg == 'alwaysevict':
resolveConflictAccount = False
elif updateCmd and myarg == 'createifnotfound':
parameters['createIfNotFound'] = True
elif updateCmd and myarg == 'noactionifalias':
parameters['noActionIfAlias'] = True
elif updateCmd and myarg == 'notifyonupdate':
parameters['notifyOnUpdate'] = getBoolean()
elif updateCmd and myarg == 'setchangepasswordoncreate':
parameters['setChangePasswordOnCreate'] = getBoolean()
elif updateCmd and myarg in {'immutableous', 'immutableorgs', 'immutableorgunitpaths'}:
parameters['immutableOUs'] = set(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
elif not updateCmd and myarg == 'addnumericsuffixonduplicate':
parameters['addNumericSuffixOnDuplicate'] = getInteger(minVal=0, default=0)
elif not updateCmd and myarg in {'license', 'licence', 'licenses', 'licences'}:
if parameters['lic'] is None:
parameters['lic'] = buildGAPIObject(API.LICENSING)
parameters[LICENSE_PRODUCT_SKUIDS] = getGoogleSKUList(allowUnknownProduct=True)
if checkArgumentPresent(['product', 'productid']):
productId = getGoogleProduct()
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
productSku = (productId, productSku[1])
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
if not productSku[0]:
invalidChoiceExit(productSku[1], SKU.getSortedSKUList(), True)
elif updateCmd and myarg == 'updateoufromgroup':
groupOrgUnitMap = _getGroupOrgUnitMap()
elif updateCmd and myarg == 'updateprimaryemail':
updatePrimaryEmail = getREPatternSubstitution(re.IGNORECASE)
elif myarg == 'json':
body.update(getJSON(USER_JSON_SKIP_FIELDS))
if 'name' in body and 'fullName' in body['name']:
body['name'].pop('fullName')
if 'sshPublicKeys' in body and 'fingerprint' in body['sshPublicKeys']:
body['sshPublicKeys'].pop('fingerprint')
for location in body.get('locations', []):
location.pop('buildingName', None)
elif myarg == 'employeeid':
entry = {'type': 'organization', 'value': getString(Cmd.OB_STRING, minLen=0)}
appendItemToBodyList(body, 'externalIds', entry, 'value')
elif myarg == 'manager':
entry = {'type': 'manager', 'value': getString(Cmd.OB_STRING, minLen=0)}
appendItemToBodyList(body, 'relations', entry, 'value')
elif myarg in UPDATE_USER_ARGUMENT_TO_PROPERTY_MAP:
up = UPDATE_USER_ARGUMENT_TO_PROPERTY_MAP[myarg]
userProperty = UProp.PROPERTIES[up]
propertyClass = userProperty[UProp.CLASS]
if UProp.TYPE_KEYWORDS in userProperty:
typeKeywords = userProperty[UProp.TYPE_KEYWORDS]
clTypeKeyword = typeKeywords[UProp.PTKW_CL_TYPE_KEYWORD]
if up == 'givenName':
body.setdefault('name', {})
body['name'][up] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
elif up == 'familyName':
body.setdefault('name', {})
body['name'][up] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
elif up == 'displayName':
body.setdefault('name', {})
body['name']['displayName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=256)
elif PwdOpts.ProcessPropertyArgument(myarg, up, body):
pass
elif propertyClass == UProp.PC_BOOLEAN:
body[up] = getBoolean()
elif up == 'primaryEmail':
if updateCmd:
body[up] = getEmailAddress(noUid=True)
elif body[up] != getEmailAddress(noUid=True):
Cmd.Backup()
unknownArgumentExit()
elif up == 'recoveryEmail':
rcvryEmail = getEmailAddress(noUid=True, optional=True)
body[up] = rcvryEmail if rcvryEmail is not None else ""
elif up == 'recoveryPhone':
body[up] = getString(Cmd.OB_STRING, minLen=0)
if body[up] and body[up][0] != '+':
body[up] = '+' + body[up]
elif up == 'customerId':
body[up] = getString(Cmd.OB_STRING)
elif up == 'orgUnitPath':
body[up] = getOrgUnitItem(pathOnly=True, cd=cd)
elif up == 'languages':
if checkClearBodyList(body, up):
continue
for language in getString(Cmd.OB_LANGUAGE_LIST).replace('_', '-').replace(',', ' ').split():
langItem = {}
if language[-1] == '+':
suffix = '+'
language = language[:-1]
langItem['preference'] = 'preferred'
elif language[-1] == '-':
suffix = '-'
language = language[:-1]
langItem['preference'] = 'not_preferred'
else:
suffix = ''
if language.lower() in LANGUAGE_CODES_MAP:
langItem['languageCode'] = LANGUAGE_CODES_MAP[language.lower()]
else:
if suffix:
Cmd.Backup()
usageErrorExit(Msg.SUFFIX_NOT_ALLOWED_WITH_CUSTOMLANGUAGE.format(suffix, language))
langItem['customLanguage'] = language
appendItemToBodyList(body, up, langItem)
elif up == 'gender':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
if checkArgumentPresent('addressmeas'):
entry['addressMeAs'] = getString(Cmd.OB_STRING, minLen=0)
body[up] = entry
elif up == 'addresses':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
if checkArgumentPresent(['unstructured', 'formatted']):
entry['sourceIsStructured'] = False
entry['formatted'] = getStringWithCRsNLs()
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument in ADDRESS_ARGUMENT_TO_FIELD_MAP:
value = getString(Cmd.OB_STRING, minLen=0)
if value:
entry[ADDRESS_ARGUMENT_TO_FIELD_MAP[argument]] = value
elif primaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
appendItemToBodyList(body, up, entry)
elif up == 'ims':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
getChoice([UProp.IM_PROTOCOLS[UProp.PTKW_CL_TYPE_KEYWORD]])
getKeywordAttribute(UProp.IM_PROTOCOLS, entry)
# Backwards compatability: notprimary|primary on either side of IM address
getPrimaryNotPrimaryChoice(entry, False)
entry['im'] = getString(Cmd.OB_STRING, minLen=0)
getPrimaryNotPrimaryChoice(entry, entry.get('primary', False))
appendItemToBodyList(body, up, entry, 'im')
elif up == 'keywords':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
appendItemToBodyList(body, up, entry, 'value')
elif up == 'locations':
if checkClearBodyList(body, up):
continue
entry = {'type': 'desk'}
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument == clTypeKeyword:
getKeywordAttribute(typeKeywords, entry)
elif argument == 'area':
entry['area'] = getString(Cmd.OB_STRING)
elif argument in {'building', 'buildingid'}:
entry['buildingId'] = _getBuildingByNameOrId(cd, allowNV=True)
elif argument in {'floor', 'floorname'}:
entry['floorName'] = getString(Cmd.OB_STRING, minLen=0)
elif argument in {'section', 'floorsection'}:
entry['floorSection'] = getString(Cmd.OB_STRING, minLen=0)
elif argument in {'desk', 'deskcode'}:
entry['deskCode'] = getString(Cmd.OB_STRING, minLen=0)
elif argument == 'endlocation':
break
else:
unknownArgumentExit()
if 'area' not in entry:
missingArgumentExit('area <String>')
appendItemToBodyList(body, up, entry)
elif up == 'notes':
if checkClearBodyList(body, up):
continue
entry = {}
getKeywordAttribute(typeKeywords, entry, defaultChoice='text_plain')
entry['value'] = getStringWithCRsNLsOrFile()[0]
body[up] = entry
elif up == 'organizations':
if checkClearBodyList(body, up):
continue
entry = {}
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument == clTypeKeyword:
getKeywordAttribute(typeKeywords, entry)
elif argument == typeKeywords[UProp.PTKW_CL_CUSTOMTYPE_KEYWORD]:
entry[typeKeywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = getString(Cmd.OB_STRING, minLen=0)
entry.pop(typeKeywords[UProp.PTKW_ATTR_TYPE_KEYWORD], None)
elif argument in ORGANIZATION_ARGUMENT_TO_FIELD_MAP:
argument = ORGANIZATION_ARGUMENT_TO_FIELD_MAP[argument]
if argument != 'fullTimeEquivalent':
value = getString(Cmd.OB_STRING, minLen=0)
if value:
entry[argument] = value
else:
entry[argument] = getInteger(minVal=0, maxVal=100000, default=0)
elif primaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
appendItemToBodyList(body, up, entry)
elif up == 'phones':
if checkClearBodyList(body, up):
continue
entry = {}
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument == clTypeKeyword:
getKeywordAttribute(typeKeywords, entry)
elif argument == 'value':
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
elif primaryNotPrimary(argument, entry):
break
else:
unknownArgumentExit()
appendItemToBodyList(body, up, entry, 'value')
elif up == 'posixAccounts':
if checkClearBodyList(body, up):
continue
entry = {}
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument in {'username', 'name'}:
entry['username'] = getString(Cmd.OB_STRING)
elif argument == 'uid':
entry['uid'] = getInteger(minVal=1000)
elif argument == 'gid':
entry['gid'] = getInteger(minVal=0)
elif argument in {'system', 'systemid'}:
entry['systemId'] = getString(Cmd.OB_STRING, minLen=0)
elif argument in {'home', 'homedirectory'}:
entry['homeDirectory'] = getString(Cmd.OB_STRING, minLen=0)
elif argument == 'shell':
entry['shell'] = getString(Cmd.OB_STRING, minLen=0)
elif argument == 'gecos':
entry['gecos'] = getString(Cmd.OB_STRING, minLen=0)
elif argument == 'primary':
primary['location'] = Cmd.Location()
entry['primary'] = getBoolean()
elif argument in {'os', 'operatingsystemtype'}:
entry['operatingSystemType'] = getChoice(['linux', 'unspecified', 'windows'])
elif argument == 'endposix':
break
else:
unknownArgumentExit()
if 'username' not in entry:
missingArgumentExit('username <String>')
if 'uid' not in entry:
missingArgumentExit('uid <Integer>')
appendItemToBodyList(body, up, entry, checkSystemId=True)
elif up == 'relations':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
appendItemToBodyList(body, up, entry, 'value')
elif up == 'emails':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
entry['address'] = getEmailAddress(noUid=True, minLen=0)
appendItemToBodyList(body, up, entry, 'address')
elif up == 'sshPublicKeys':
if checkClearBodyList(body, up):
continue
entry = {}
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument == 'expires':
entry['expirationTimeUsec'] = getInteger(minVal=0)
elif argument == 'key':
entry['key'] = getString(Cmd.OB_STRING)
elif argument == 'endssh':
break
else:
unknownArgumentExit()
if 'key' not in entry:
missingArgumentExit('key <String>')
appendItemToBodyList(body, up, entry)
elif up == 'externalIds':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry, allowEmptyCustomType=True)
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
appendItemToBodyList(body, up, entry, 'value')
elif up == 'websites':
if checkClearBodyList(body, up):
continue
entry = {}
getChoice([clTypeKeyword], defaultChoice=None)
getKeywordAttribute(typeKeywords, entry)
entry['value'] = getString(Cmd.OB_URL, minLen=0)
getPrimaryNotPrimaryChoice(entry, False)
appendItemToBodyList(body, up, entry, 'value')
elif myarg in {'group', 'groups'}:
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
delivery_settings = getDeliverySettings()
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
addGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': delivery_settings}
elif myarg in {'alias', 'aliases'}:
addAliases.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
elif myarg == 'clearschema':
if not updateCmd:
unknownArgumentExit()
schemaName, fieldName = _splitSchemaNameDotFieldName(getString(Cmd.OB_SCHEMA_NAME_FIELD_NAME), False)
up = 'customSchemas'
body.setdefault(up, {})
body[up].setdefault(schemaName, {})
if fieldName is None:
try:
schema = callGAPI(cd.schemas(), 'get',
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaName, fields='fields(fieldName)')
for field in schema['fields']:
body[up][schemaName][field['fieldName']] = None
except (GAPI.invalid, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
entityDoesNotExistWarning(Ent.USER_SCHEMA, schemaName)
unknownArgumentExit()
else:
body[up][schemaName][fieldName] = None
elif myarg.find('.') >= 0:
schemaName, fieldName = _splitSchemaNameDotFieldName(Cmd.Previous())
up = 'customSchemas'
body.setdefault(up, {})
body[up].setdefault(schemaName, {})
multivalue, ignoreEmpty = getChoice(SCHEMA_VALUE_PROCESS_MAP, defaultChoice=(False, False), mapChoice=True)
if multivalue:
body[up][schemaName].setdefault(fieldName, [])
typeKeywords = UProp.PROPERTIES[up][UProp.TYPE_KEYWORDS]
clTypeKeyword = typeKeywords[UProp.PTKW_CL_TYPE_KEYWORD]
schemaValue = {}
if checkArgumentPresent(clTypeKeyword):
getKeywordAttribute(typeKeywords, schemaValue)
else:
schemaValue['type'] = 'work'
schemaValue['value'] = getString(Cmd.OB_STRING, minLen=0)
if schemaValue['value'] or not ignoreEmpty:
body[up][schemaName][fieldName].append(schemaValue)
else:
schemaValue = getString(Cmd.OB_STRING, minLen=0)
if schemaValue or not ignoreEmpty:
body[up][schemaName][fieldName] = schemaValue
elif updateCmd:
body[up][schemaName][fieldName] = None
else:
unknownArgumentExit()
if PwdOpts.promptForPassword or PwdOpts.promptForUniquePassword:
if not updateCmd:
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'], body['primaryEmail'])
elif not PwdOpts.promptForUniquePassword:
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'])
elif not PwdOpts.makeUniqueRandomPassword:
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'])
return (body, notify, tagReplacements, addGroups, addAliases, PwdOpts,
updatePrimaryEmail, notFoundBody, groupOrgUnitMap, parameters,
resolveConflictAccount)
def createUserAddToGroups(cd, user, addGroups, i, count):
action = Act.Get()
Act.Set(Act.ADD)
Ind.Increment()
_addUserToGroups(cd, user, set(addGroups), addGroups, i, count)
Ind.Decrement()
Act.Set(action)
def createUserAddAliases(cd, user, aliasList, i, count):
action = Act.Get()
Act.Set(Act.ADD)
Ind.Increment()
_addUserAliases(cd, user, aliasList, i, count)
Ind.Decrement()
Act.Set(action)
# gam create user <EmailAddress> <UserAttribute>
# [verifynotinvitable|alwaysevict]
# (groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
# [alias|aliases <EmailAddressList>]
# [license <SKUID> [product|productid <ProductID>]]
# [notify <EmailAddressList>
# [subject <String>]
# [notifypassword <String>]
# [from <EmailAaddress>]
# [replyto <EmailAaddress>]
# [<NotifyMessageContent>]
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
# [logpassword <FileName>] [ignorenullpassword]
# [addnumericsuffixonduplicate <Number>]
def doCreateUser():
cd = buildGAPIObject(API.DIRECTORY)
body, notify, tagReplacements, addGroups, addAliases, PwdOpts, \
_, _, _, \
parameters, resolveConflictAccount = getUserAttributes(cd, False, noUid=True)
suffix = 0
originalEmail = body['primaryEmail']
atLoc = originalEmail.find('@')
fields = '*' if tagReplacements['subs'] else 'primaryEmail,name'
while True:
user = body['primaryEmail']
if parameters['verifyNotInvitable']:
isInvitableUser, _ = _getIsInvitableUser(None, user)
if isInvitableUser:
entityActionNotPerformedWarning([Ent.USER, user], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
return
try:
result = callGAPI(cd.users(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.CONDITION_NOT_MET,
GAPI.LIMIT_EXCEEDED],
body=body,
fields=fields,
resolveConflictAccount=resolveConflictAccount)
entityActionPerformed([Ent.USER, user])
break
except GAPI.duplicate:
if duplicateAliasGroupUserWarning(cd, [Ent.USER, user]) == Ent.USER and parameters['addNumericSuffixOnDuplicate'] > 0:
parameters['addNumericSuffixOnDuplicate'] -= 1
suffix +=1
body['primaryEmail'] = originalEmail[0:atLoc]+str(suffix)+originalEmail[atLoc:]
setSysExitRC(0)
continue
return
except GAPI.invalidSchemaValue:
entityActionFailedExit([Ent.USER, user], Msg.INVALID_SCHEMA_VALUE)
except GAPI.invalidOrgunit:
entityActionFailedExit([Ent.USER, user], Msg.INVALID_ORGUNIT)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter, GAPI.conditionNotMet, GAPI.limitExceeded) as e:
entityActionFailedExit([Ent.USER, user], str(e))
if PwdOpts.filename and PwdOpts.password:
writeFile(PwdOpts.filename, f'{user},{PwdOpts.password}\n', mode='a', continueOnError=True)
if addGroups:
createUserAddToGroups(cd, result['primaryEmail'], addGroups, 0, 0)
if addAliases:
createUserAddAliases(cd, result['primaryEmail'], addAliases, 0, 0)
if notify.get('recipients'):
sendCreateUpdateUserNotification(result, notify, tagReplacements)
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
productId = productSku[0]
skuId = productSku[1]
try:
callGAPI(parameters['lic'].licenseAssignments(), 'insert',
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
productId=productId, skuId=skuId, body={'userId': user}, fields='')
entityActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)])
except (GAPI.internalError, GAPI.duplicate, GAPI.conditionNotMet, GAPI.invalid,
GAPI.userNotFound, GAPI.forbidden, GAPI.backendError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], str(e))
def verifyUserPrimaryEmail(cd, user, createIfNotFound, i, count):
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=user, fields='id,primaryEmail')
if (result['primaryEmail'].lower() == user) or (result['id'] == user):
return True
entityActionNotPerformedWarning([Ent.USER, user], Msg.NOT_A_PRIMARY_EMAIL_ADDRESS, i, count)
return False
except GAPI.userNotFound:
if createIfNotFound:
return True
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
pass
entityUnknownWarning(Ent.USER, user, i, count)
return False
# gam <UserTypeEntity> update user <UserAttribute>*
# [verifynotinvitable|alwaysevict] [noactionifalias]
# [updateprimaryemail <RESEarchPattern> <RESubstitution>]
# [updateoufromgroup <CSVFileInput> [keyfield <FieldName>] [datafield <FieldName>]]
# [immutableous <OrgUnitEntity>]|
# [clearschema <SchemaName>|<SchemaNameField>]
# [createifnotfound] [notfoundpassword (random [<Integer>])|blocklogin|<Password>]
# (groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
# [alias|aliases <EmailAddressList>]
# [notify <EmailAddressList>
# [subject <String>]
# [notifypassword <String>]
# [from <EmailAaddress>]
# [replyto <EmailAaddress>]
# [<NotifyMessageContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
# [notifyonupdate [<Boolean>]]
# [logpassword <FileName>] [ignorenullpassword]
def updateUsers(entityList):
def waitingForCreationToComplete(sleep_time):
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.USER), sleep_time))
time.sleep(sleep_time)
cd = buildGAPIObject(API.DIRECTORY)
ci = None
errorRetries = 5
updateRetryDelay = 5
body, notify, tagReplacements, addGroups, addAliases, PwdOpts, \
updatePrimaryEmail, notFoundBody, groupOrgUnitMap, \
parameters, resolveConflictAccount = getUserAttributes(cd, True)
vfe = 'primaryEmail' in body and body['primaryEmail'][:4].lower() == 'vfe@'
if body.get('orgUnitPath', '') and parameters['immutableOUs']:
ubody = body.copy()
checkImmutableOUs = True
else:
checkImmutableOUs = False
i, count, entityList = getEntityArgument(entityList)
fields = '*' if tagReplacements['subs'] else 'primaryEmail,name'
for user in entityList:
i += 1
user = userKey = normalizeEmailAddressOrUID(user)
if parameters['noActionIfAlias'] and not verifyUserPrimaryEmail(cd, user, parameters['createIfNotFound'], i, count):
continue
if checkImmutableOUs:
body = ubody.copy()
try:
if vfe:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=userKey, fields='primaryEmail,id')
userKey = result['id']
userPrimary = result['primaryEmail']
userName, userDomain = splitEmailAddress(userPrimary)
body['primaryEmail'] = f'vfe.{userName}.{random.randint(1, 99999):05d}@{userDomain}'
body['emails'] = [{'type': 'custom',
'customType': 'former_employee',
'primary': False, 'address': userPrimary}]
elif updatePrimaryEmail:
if updatePrimaryEmail[0].search(user) is not None:
body['primaryEmail'] = re.sub(updatePrimaryEmail[0], updatePrimaryEmail[1], user)
else:
body.pop('primaryEmail', None)
if not body:
entityActionNotPerformedWarning([Ent.USER, user], Msg.PRIMARY_EMAIL_DID_NOT_MATCH_PATTERN.format(updatePrimaryEmail[0].pattern), i, count)
if groupOrgUnitMap:
try:
groups = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=userKey, orderBy='email', fields='nextPageToken,groups(email)')
except (GAPI.invalidMember, GAPI.invalidInput):
entityUnknownWarning(Ent.USER, userKey, i, count)
continue
groupList = []
for group in groups:
orgUnit = groupOrgUnitMap.get(group['email'].lower())
if orgUnit:
groupList.append(group['email'])
jcount = len(groupList)
if jcount != 1:
entityActionNotPerformedWarning([Ent.USER, user], Msg.USER_BELONGS_TO_N_GROUPS_THAT_MAP_TO_ORGUNITS.format(jcount, ','.join(groupList)), i, count)
continue
body['orgUnitPath'] = orgUnit
if checkImmutableOUs:
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS,
userKey=userKey, fields='orgUnitPath')
if result['orgUnitPath'] in parameters['immutableOUs']:
body.pop('orgUnitPath')
except Exception:
pass
if body:
if 'primaryEmail' in body and parameters['verifyNotInvitable']:
isInvitableUser, ci = _getIsInvitableUser(ci, body['primaryEmail'])
if isInvitableUser:
entityActionNotPerformedWarning([Ent.USER, body['primaryEmail']], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT, i, count)
continue
if PwdOpts.makeUniqueRandomPassword or PwdOpts.promptForUniquePassword:
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'], userKey)
retry = 0
while True:
try:
result = callGAPI(cd.users(), 'update',
throwReasons=[GAPI.CONDITION_NOT_MET, GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.DUPLICATE,
GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES, GAPI.CONFLICT],
userKey=userKey, body=body, fields=fields)
entityActionPerformed([Ent.USER, user], i, count)
if PwdOpts.filename and PwdOpts.password:
writeFile(PwdOpts.filename, f'{userKey},{PwdOpts.password}\n', mode='a', continueOnError=True)
if parameters['notifyOnUpdate'] and notify.get('recipients') and notify['password']:
sendCreateUpdateUserNotification(result, notify, tagReplacements, i, count, createMessage=False)
break
except GAPI.conditionNotMet as e:
retry += 1
if ('User creation is not complete' not in str(e)) or retry > errorRetries:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
break
waitingForCreationToComplete(updateRetryDelay)
continue
except GAPI.userNotFound:
if parameters['createIfNotFound']:
if notFoundBody and (count == 1) and not vfe and ('password' in notFoundBody) and ('name' in body) and ('givenName' in body['name']) and ('familyName' in body['name']):
if 'primaryEmail' not in body:
body['primaryEmail'] = user
body.update(notFoundBody)
if parameters['setChangePasswordOnCreate']:
body['changePasswordAtNextLogin'] = True
Act.Set(Act.CREATE)
try:
result = callGAPI(cd.users(), 'insert',
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.CONDITION_NOT_MET],
body=body,
fields=fields,
resolveConflictAccount=resolveConflictAccount)
entityActionPerformed([Ent.USER, body['primaryEmail']], i, count)
if PwdOpts.filename and PwdOpts.notFoundPassword:
writeFile(PwdOpts.filename, f'{user},{PwdOpts.notFoundPassword}\n', mode='a', continueOnError=True)
if addGroups:
createUserAddToGroups(cd, result['primaryEmail'], addGroups, i, count)
if addAliases:
createUserAddAliases(cd, result['primaryEmail'], addAliases, i, count)
if notify.get('recipients'):
notify['password'] = notify['notFoundPassword']
sendCreateUpdateUserNotification(result, notify, tagReplacements, i, count)
except GAPI.duplicate:
duplicateAliasGroupUserWarning(cd, [Ent.USER, body['primaryEmail']], i, count)
else:
entityActionFailedWarning([Ent.USER, user], Msg.UNABLE_TO_CREATE_NOT_FOUND_USER, i, count)
else:
entityUnknownWarning(Ent.USER, user, i, count)
break
else:
entityActionNotPerformedWarning([Ent.USER, user], Msg.NO_CHANGES, i, count)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
except GAPI.invalidSchemaValue:
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_SCHEMA_VALUE, i, count)
except GAPI.duplicate as e:
entityActionFailedWarning([Ent.USER, user, Ent.USER, body['primaryEmail']], str(e), i, count)
except GAPI.invalidOrgunit:
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_ORGUNIT, i, count)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter, GAPI.insufficientArchivedUserLicenses,
GAPI.conflict, GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
# gam update users <UserTypeEntity> ...
def doUpdateUsers():
updateUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1])
# gam update user <UserItem> ...
def doUpdateUser():
updateUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
# gam <UserTypeEntity> delete users [noactionifalias]
def deleteUsers(entityList):
cd = buildGAPIObject(API.DIRECTORY)
noActionIfAlias = checkArgumentPresent('noactionifalias')
checkForExtraneousArguments()
i, count, entityList = getEntityArgument(entityList)
for user in entityList:
i += 1
user = normalizeEmailAddressOrUID(user)
if noActionIfAlias and not verifyUserPrimaryEmail(cd, user, False, i, count):
continue
try:
callGAPI(cd.users(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
GAPI.CONDITION_NOT_MET],
userKey=user)
entityActionPerformed([Ent.USER, user], i, count)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.USER, user], Msg.CAN_NOT_DELETE_USER_WITH_VAULT_HOLD.format(str(e), user), i, count)
# gam delete users <UserTypeEntity> [noactionifalias]
def doDeleteUsers():
deleteUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1])
# gam delete user <UserItem> [noactionifalias]
def doDeleteUser():
deleteUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
# gam <UserEntity> undelete users [ou|org|orgunit <OrgUnitPath>]
def undeleteUsers(entityList):
cd = buildGAPIObject(API.DIRECTORY)
if checkArgumentPresent(['ou', 'org', 'orgunit']):
entitySelector = getEntitySelector()
if entitySelector:
orgUnitPaths = getEntitySelection(entitySelector, True)
else:
orgUnitPaths = [getOrgUnitItem()]
userOrgUnitLists = orgUnitPaths if isinstance(orgUnitPaths, dict) else None
else:
orgUnitPaths = ['/']
userOrgUnitLists = None
checkForExtraneousArguments()
i, count, entityList = getEntityArgument(entityList)
for user in entityList:
i += 1
origUser = user
user = normalizeEmailAddressOrUID(user)
user_uid = user if user.find('@') == -1 else None
if not user_uid:
printEntityKVList([Ent.DELETED_USER, user],
[Msg.LOOKING_UP_GOOGLE_UNIQUE_ID, None],
i, count)
try:
deleted_users = callGAPIpages(cd.users(), 'list', 'users',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], showDeleted=True, orderBy='email',
maxResults=GC.Values[GC.USER_MAX_RESULTS])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
matching_users = []
for deleted_user in deleted_users:
if str(deleted_user['primaryEmail']).lower() == user:
matching_users.append(deleted_user)
jcount = len(matching_users)
if jcount == 0:
entityUnknownWarning(Ent.DELETED_USER, user, i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if jcount > 1:
entityActionNotPerformedWarning([Ent.DELETED_USER, user],
Msg.PLEASE_SELECT_ENTITY_TO_PROCESS.format(jcount, Ent.Plural(Ent.DELETED_USER), 'undelete', 'uid:<String>'),
i, count)
Ind.Increment()
j = 0
for matching_user in matching_users:
printEntity([Ent.UNIQUE_ID, matching_user['id']], j, jcount)
Ind.Increment()
for attr_name in ['creationTime', 'lastLoginTime', 'deletionTime']:
if attr_name in matching_user:
printKeyValueList([attr_name, formatLocalTime(matching_user[attr_name])])
Ind.Decrement()
Ind.Decrement()
setSysExitRC(MULTIPLE_DELETED_USERS_FOUND_RC)
continue
user_uid = matching_users[0]['id']
if userOrgUnitLists:
orgUnitPaths = userOrgUnitLists[origUser]
try:
callGAPI(cd.users(), 'undelete',
throwReasons=[GAPI.DELETED_USER_NOT_FOUND, GAPI.INVALID_ORGUNIT,
GAPI.DOMAIN_NOT_FOUND, GAPI.DOMAIN_CANNOT_USE_APIS,
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.DUPLICATE,
GAPI.LIMIT_EXCEEDED],
userKey=user_uid, body={'orgUnitPath': makeOrgUnitPathAbsolute(orgUnitPaths[0])})
entityActionPerformed([Ent.DELETED_USER, user], i, count)
except GAPI.deletedUserNotFound:
entityUnknownWarning(Ent.DELETED_USER, user, i, count)
except GAPI.invalidOrgunit:
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_ORGUNIT, i, count)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
GAPI.invalid, GAPI.duplicate, GAPI.limitExceeded) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
# gam undelete users <UserEntity> [ou|org|orgunit <OrgUnitPath>]
def doUndeleteUsers():
undeleteUsers(getEntityList(Cmd.OB_USER_ENTITY))
# gam undelete user <UserItem> [ou|org|orgunit <OrgUnitPath>]
def doUndeleteUser():
undeleteUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
# gam <UserTypeEntity> suspend users [noactionifalias]
# gam <UserTypeEntity> unsuspend users [noactionifalias]
def suspendUnsuspendUsers(entityList):
cd = buildGAPIObject(API.DIRECTORY)
noActionIfAlias = checkArgumentPresent('noactionifalias')
checkForExtraneousArguments()
body = {'suspended': Act.Get() == Act.SUSPEND}
i, count, entityList = getEntityArgument(entityList)
for user in entityList:
i += 1
user = normalizeEmailAddressOrUID(user)
if noActionIfAlias and not verifyUserPrimaryEmail(cd, user, False, i, count):
continue
try:
callGAPI(cd.users(), 'update',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN, GAPI.BAD_REQUEST],
userKey=user, body=body)
entityActionPerformed([Ent.USER, user], i, count)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
# gam suspend users <UserTypeEntity> [noactionifalias]
# gam unsuspend users <UserTypeEntity> [noactionifalias]
def doSuspendUnsuspendUsers():
suspendUnsuspendUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1])
# gam suspend user <UserItem> [noactionifalias]
# gam unsuspend user <UserItem> [noactionifalias]
def doSuspendUnsuspendUser():
suspendUnsuspendUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
# gam <UserTypeEntity> signout
# gam <UserTypeEntity> turnoff2sv
def signoutTurnoff2SVUsers(entityList):
cd = buildGAPIObject(API.DIRECTORY)
if Act.Get() == Act.SIGNOUT:
service = cd.users()
function = 'signOut'
else:
service = cd.twoStepVerification()
function = 'turnOff'
checkForExtraneousArguments()
i, count, entityList = getEntityArgument(entityList)
for user in entityList:
i += 1
user = normalizeEmailAddressOrUID(user)
try:
callGAPI(service, function,
throwReasons=[GAPI.NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT,
GAPI.DOMAIN_NOT_FOUND, GAPI.DOMAIN_CANNOT_USE_APIS,
GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
userKey=user)
entityActionPerformed([Ent.USER, user], i, count)
except (GAPI.notFound, GAPI.userNotFound):
entityUnknownWarning(Ent.USER, user, i, count)
except (GAPI.invalid, GAPI.invalidInput, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.authError) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
# gam <UserTypeEntity> waitformailbox [retries <Number>]
def waitForMailbox(entityList):
def showRetryStatus(user, exists, i, count):
kvList = ['Exists', exists, 'mailboxIsSetup', False]
if maxRetries:
kvList.extend(['Retry', f'{retries}/{maxRetries}'])
else:
kvList.extend(['Retry', f'{retries}'])
printEntityKVList([Ent.USER, user], kvList, i, count)
if maxRetries and retries >= maxRetries:
entityActionFailedWarning([Ent.USER, user], Msg.RETRIES_EXHAUSTED.format(maxRetries), i, count)
return False
time.sleep(3)
return True
cd = buildGAPIObject(API.DIRECTORY)
maxRetries = 0
while Cmd.ArgumentsRemaining():
argument = getArgument()
if argument == 'retries':
maxRetries = getInteger(minVal=0)
else:
unknownArgumentExit()
i, count, entityList = getEntityArgument(entityList)
for user in entityList:
i += 1
user = normalizeEmailAddressOrUID(user)
retries = 0
performAction(Ent.USER, user, i, count)
Ind.Increment()
while True:
retries += 1
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND],
userKey=user, fields='isMailboxSetup')
if result.get('isMailboxSetup', False):
entityActionPerformed([Ent.USER, user], i, count)
break
if not showRetryStatus(user, True, i, count):
break
except GAPI.userNotFound:
if not showRetryStatus(user, False, i, count):
break
except (GAPI.invalid, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.authError) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
break
except KeyboardInterrupt:
entityActionFailedWarning([Ent.USER, user], Msg.CHECK_INTERRUPTED, i, count)
break
Ind.Decrement()
def getUserLicenses(lic, user, skus):
def _callbackGetLicense(_, response, exception):
if exception is None:
if response and 'skuId' in response:
licenses.append(response['skuId'])
licenses = []
svcargs = dict([('userId', user['primaryEmail']), ('productId', None), ('skuId', None), ('fields', 'skuId')]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(lic.licenseAssignments(), 'get')
dbatch = lic.new_batch_http_request(callback=_callbackGetLicense)
for sku in skus:
svcparms = svcargs.copy()
svcparms['productId'] = sku[0]
svcparms['skuId'] = sku[1]
dbatch.add(method(**svcparms))
dbatch.execute()
return licenses
USER_NAME_PROPERTY_PRINT_ORDER = [
'givenName',
'familyName',
'fullName',
'displayName',
]
USER_LANGUAGE_PROPERTY_PRINT_ORDER = [
'languages',
]
USER_SCALAR_PROPERTY_PRINT_ORDER = [
'isAdmin',
'isDelegatedAdmin',
'isEnrolledIn2Sv',
'isEnforcedIn2Sv',
'agreedToTerms',
'ipWhitelisted',
'suspended',
'suspensionReason',
'archived',
'changePasswordAtNextLogin',
'id',
'customerId',
'isMailboxSetup',
'includeInGlobalAddressList',
'creationTime',
'lastLoginTime',
'deletionTime',
'orgUnitPath',
'recoveryEmail',
'recoveryPhone',
'thumbnailPhotoUrl',
]
USER_ARRAY_PROPERTY_PRINT_ORDER = [
'gender',
'keywords',
'notes',
'addresses',
'locations',
'organizations',
'relations',
'emails',
'ims',
'phones',
'posixAccounts',
'sshPublicKeys',
'externalIds',
'websites',
]
USER_ADDRESSES_PROPERTY_PRINT_ORDER = [
'primary',
'sourceIsStructured',
'formatted',
'extendedAddress',
'streetAddress',
'poBox',
'locality',
'region',
'postalCode',
'country',
'countryCode',
]
USER_LOCATIONS_PROPERTY_PRINT_ORDER = [
'area',
'buildingId',
'buildingName',
'floorName',
'floorSection',
'deskCode',
]
USER_ORGANIZATIONS_PROPERTY_PRINT_ORDER = [
'name',
'description',
'domain',
'symbol',
'location',
'costCenter',
'department',
'title',
'fullTimeEquivalent',
'primary',
]
USER_POSIX_PROPERTY_PRINT_ORDER = [
'accountId',
'uid',
'gid',
'systemId',
'operatingSystemType',
'homeDirectory',
'shell',
'gecos',
'primary',
]
USER_SSH_PROPERTY_PRINT_ORDER = [
'expirationTimeUsec',
'fingerprint',
]
USER_FIELDS_CHOICE_MAP = {
'address': 'addresses',
'addresses': 'addresses',
'admin': ['isAdmin', 'isDelegatedAdmin'],
'agreed2terms': 'agreedToTerms',
'agreedtoterms': 'agreedToTerms',
'aliases': ['aliases', 'nonEditableAliases'],
'archived': 'archived',
'changepassword': 'changePasswordAtNextLogin',
'changepasswordatnextlogin': 'changePasswordAtNextLogin',
'creationtime': 'creationTime',
'customerid': 'customerId',
'deletiontime': 'deletionTime',
'displayname': 'name.displayName',
'email': 'emails',
'emails': 'emails',
'employeeid': 'externalIds',
'externalid': 'externalIds',
'externalids': 'externalIds',
'familyname': 'name.familyName',
'firstname': 'name.givenName',
'fullname': 'name.fullName',
'gal': 'includeInGlobalAddressList',
'givenname': 'name.givenName',
'gender': ['gender.type', 'gender.customGender', 'gender.addressMeAs'],
'id': 'id',
'im': 'ims',
'ims': 'ims',
'includeinglobaladdresslist': 'includeInGlobalAddressList',
'ipwhitelisted': 'ipWhitelisted',
'isadmin': ['isAdmin', 'isDelegatedAdmin'],
'isdelegatedadmin': ['isAdmin', 'isDelegatedAdmin'],
'isenforcedin2sv': 'isEnforcedIn2Sv',
'isenrolledin2sv': 'isEnrolledIn2Sv',
'is2svenforced': 'isEnforcedIn2Sv',
'is2svenrolled': 'isEnrolledIn2Sv',
'ismailboxsetup': 'isMailboxSetup',
'keyword': 'keywords',
'keywords': 'keywords',
'language': 'languages',
'languages': 'languages',
'lastlogintime': 'lastLoginTime',
'lastname': 'name.familyName',
'location': 'locations',
'locations': 'locations',
'manager': 'relations',
'name': ['name.givenName', 'name.familyName', 'name.fullName', 'name.displayName'],
'nicknames': ['aliases', 'nonEditableAliases'],
'noneditablealiases': ['aliases', 'nonEditableAliases'],
'note': 'notes',
'notes': 'notes',
'org': 'orgUnitPath',
'organization': 'organizations',
'organizations': 'organizations',
'organisation': 'organizations',
'organisations': 'organizations',
'orgunit': 'orgUnitPath',
'orgunitpath': 'orgUnitPath',
'otheremail': 'emails',
'otheremails': 'emails',
'ou': 'orgUnitPath',
'phone': 'phones',
'phones': 'phones',
'photo': 'thumbnailPhotoUrl',
'photourl': 'thumbnailPhotoUrl',
'posix': 'posixAccounts',
'posixaccounts': 'posixAccounts',
'primaryemail': 'primaryEmail',
'recoveryemail': 'recoveryEmail',
'recoveryphone': 'recoveryPhone',
'relation': 'relations',
'relations': 'relations',
'ssh': 'sshPublicKeys',
'sshkeys': 'sshPublicKeys',
'sshpublickeys': 'sshPublicKeys',
'suspended': ['suspended', 'suspensionReason'],
'thumbnailphotourl': 'thumbnailPhotoUrl',
'username': 'primaryEmail',
'website': 'websites',
'websites': 'websites',
}
INFO_USER_OPTIONS = {'noaliases', 'nobuildingnames', 'nogroups', 'nolicenses', 'nolicences', 'noschemas', 'schemas', 'userview'}
USER_SKIP_OBJECTS = {'thumbnailPhotoEtag'}
USER_TIME_OBJECTS = {'creationTime', 'deletionTime', 'lastLoginTime'}
def _formatLanguagesList(propertyValue, delimiter):
languages = []
for language in propertyValue:
if 'languageCode' in language:
lang = language['languageCode']
if language.get('preference') == 'preferred':
lang += '+'
elif language.get('preference') == 'not_preferred':
lang += '-'
else:
lang = language.get('customLanguage')
if lang:
languages.append(lang)
return delimiter.join(languages)
def _initSchemaParms(projection):
return {'projection': projection, 'customFieldMask': None, 'selectedSchemaFields': {}}
def _getSchemaNameList(schemaParms):
customFieldMask = getString(Cmd.OB_SCHEMA_NAME_LIST).replace(' ', ',')
if customFieldMask.lower() == 'all':
schemaParms['projection'] = 'full'
schemaParms['customFieldMask'] = None
schemaParms['selectedSchemaFields'] = {}
else:
schemaParms['projection'] = 'custom'
customFieldMaskList = []
for schemaField in customFieldMask.split(','):
if schemaField.find('.') == -1:
customFieldMaskList.append(schemaField)
else:
schemaName, fieldName = schemaField.split('.', 1)
customFieldMaskList.append(schemaName)
schemaParms['selectedSchemaFields'] .setdefault(schemaName, set())
schemaParms['selectedSchemaFields'][schemaName].add(fieldName)
schemaParms['customFieldMask'] = ','.join(customFieldMaskList)
def _filterSchemaFields(userEntity, schemaParms):
schemas = userEntity.pop('customSchemas', None)
if schemas is None:
return
customSchemas = {}
for schema in sorted(schemas):
if schema in schemaParms['selectedSchemaFields']:
for field, value in sorted(iter(schemas[schema].items())):
if field not in schemaParms['selectedSchemaFields'][schema]:
continue
customSchemas.setdefault(schema, {})
customSchemas[schema][field] = value
else:
customSchemas[schema] = schemas[schema]
userEntity['customSchemas'] = customSchemas
def infoUsers(entityList):
def printUserCIGroupMap(parent, group_name_mappings, seen_group_count, edges, direction):
for a_parent, a_child in edges:
if a_parent == parent:
output = f'{Ind.Spaces()}{a_child}: {group_name_mappings[a_child]} ({direction})'
if seen_group_count[a_child] > 1:
output += ' *'
printLine(output)
Ind.Increment()
printUserCIGroupMap(a_child, group_name_mappings, seen_group_count, edges, 'inherited')
Ind.Decrement()
def _showType(row, typeKey, typeCustomValue, customTypeKey, defaultType=None):
if typeKey in row:
if row[typeKey] != typeCustomValue or not row.get(customTypeKey):
printKeyValueList([typeKey, row[typeKey]])
else:
printKeyValueList([typeKey, row[typeKey]])
Ind.Increment()
printKeyValueList([customTypeKey, row[customTypeKey]])
Ind.Decrement()
return True
if customTypeKey in row:
printKeyValueList([customTypeKey, row[customTypeKey]])
return True
if defaultType:
printKeyValueList([typeKey, defaultType])
return True
return False
cd = buildGAPIObject(API.DIRECTORY)
ci = None
getAliases = getBuildingNames = getCIGroupsTree = getGroups = getLicenses = getSchemas = not GC.Values[GC.QUICK_INFO_USER]
getGroupsTree = False
FJQC = FormatJSONQuoteChar()
schemaParms = _initSchemaParms('full')
viewType = 'admin_view'
fieldsList = []
groups = []
memberships = []
skus = SKU.getAllSKUs() if not GM.Globals[GM.LICENSE_SKUS] else GM.Globals[GM.LICENSE_SKUS]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'quick':
getAliases = getBuildingNames = getCIGroupsTree = getGroups = getGroupsTree = getLicenses = getSchemas = False
elif myarg in {'noaliases', 'aliases'}:
getAliases = myarg == 'aliases'
elif myarg in {'nobuildingnames', 'buildingnames'}:
getBuildingNames = myarg == 'buildingnames'
elif myarg in {'nogroups', 'groups', 'grouptree', 'cigrouptree'}:
getGroups = getGroupsTree = getCIGroupsTree = False
getGroups = myarg == 'groups'
getGroupsTree = myarg == 'grouptree'
getCIGroupsTree = myarg == 'cigrouptree'
elif myarg in {'nolicenses', 'nolicences', 'licenses', 'licences'}:
getLicenses = myarg in {'licenses', 'licences'}
elif myarg == 'noschemas':
getSchemas = False
schemaParms = _initSchemaParms('basic')
elif myarg in {'allschemas', 'custom', 'schemas', 'customschemas'}:
if myarg == 'allschemas':
schemaParms = _initSchemaParms('full')
else:
_getSchemaNameList(schemaParms)
getSchemas = True
elif myarg in {'products', 'product'}:
skus = SKU.convertProductListToSKUList(getGoogleProductList())
elif myarg in {'sku', 'skus'}:
skus = getGoogleSKUList()
elif myarg == 'userview':
viewType = 'domain_public'
getGroups = getLicenses = False
elif getFieldsList(myarg, USER_FIELDS_CHOICE_MAP, fieldsList):
pass
# Ignore info group arguments that may have come from whatis
elif myarg in INFO_GROUP_OPTIONS:
pass
else:
FJQC.GetFormatJSON(myarg)
if fieldsList:
fieldsList.append('primaryEmail')
if getSchemas:
fieldsList.append('customSchemas')
if getAliases:
fieldsList.extend(['aliases', 'nonEditableAliases'])
fields = getFieldsFromFieldsList(fieldsList)
if getLicenses:
lic = buildGAPIObject(API.LICENSING)
# Special case; for info users, 'all users' means 'all users_ns_susp'
if isinstance(entityList, dict) and entityList.get('entityType') == Cmd.ENTITY_ALL_USERS:
entityList['entityType'] = Cmd.ENTITY_ALL_USERS_NS_SUSP
groupParents = {}
i, count, entityList = getEntityArgument(entityList)
for userEmail in entityList:
i += 1
userEmail = normalizeEmailAddressOrUID(userEmail)
try:
user = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND],
userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
viewType=viewType, fields=fields)
groups = []
memberships = []
if getGroups or getGroupsTree:
kwargs = {}
if GC.Values[GC.ENABLE_DASA]:
# Allows groups.list() to function but will limit
# returned groups to those in same domain as user
# so only do this for DASA admins
kwargs['domain'] = GC.Values[GC.DOMAIN]
try:
groups = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=user['primaryEmail'], orderBy='email', fields='nextPageToken,groups(name,email)', **kwargs)
except (GAPI.forbidden, GAPI.domainNotFound):
### Print some message
pass
elif getCIGroupsTree:
ci, memberships = getCIGroupMembershipGraph(ci, user['primaryEmail'])
if memberships is None:
getCIGroupsTree = False
licenses = getUserLicenses(lic, user, skus) if getLicenses else []
if FJQC.formatJSON:
if getGroups or getGroupsTree:
user['groups'] = groups
if getGroupsTree:
for group in user['groups']:
groupEmail = group['email']
if groupEmail not in groupParents:
getGroupParents(cd, groupParents, groupEmail, group['name'], {})
addJsonGroupParents(groupParents, group, groupEmail)
if getLicenses:
user['licenses'] = [SKU.formatSKUIdDisplayName(u_license) for u_license in licenses]
if getBuildingNames:
for location in user.get('locations', []):
location['buildingName'] = _getBuildingNameById(cd, location.get('buildingId', ''))
if not getAliases:
user.pop('aliases', None)
user.pop('nonEditableAliases', None)
if not getSchemas:
user.pop('customSchemas', None)
printLine(json.dumps(cleanJSON(user, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
continue
printEntity([Ent.USER, user['primaryEmail']], i, count)
Ind.Increment()
printKeyValueList(['Settings', None])
Ind.Increment()
if 'name' in user:
for up in USER_NAME_PROPERTY_PRINT_ORDER:
if up in user['name']:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user['name'][up]])
up = 'languages'
if up in user:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], _formatLanguagesList(user[up], ',')])
for up in USER_SCALAR_PROPERTY_PRINT_ORDER:
if up in user:
if up not in USER_TIME_OBJECTS:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user[up]])
else:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], formatLocalTime(user[up])])
Ind.Decrement()
for up in USER_ARRAY_PROPERTY_PRINT_ORDER:
if up not in user:
continue
propertyValue = user[up]
userProperty = UProp.PROPERTIES[up]
propertyClass = userProperty[UProp.CLASS]
propertyTitle = userProperty[UProp.TITLE]
if UProp.TYPE_KEYWORDS in userProperty:
typeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_KEYWORD]
typeCustomValue = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
customTypeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]
if propertyClass == UProp.PC_ARRAY:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
for row in propertyValue:
_showType(row, typeKey, typeCustomValue, customTypeKey)
Ind.Increment()
for key in row:
if key in [typeKey, customTypeKey]:
continue
printKeyValueList([key, row[key]])
Ind.Decrement()
Ind.Decrement()
elif propertyClass == UProp.PC_GENDER:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
_showType(propertyValue, typeKey, typeCustomValue, customTypeKey)
if 'addressMeAs' in propertyValue:
printKeyValueList(['addressMeAs', propertyValue['addressMeAs']])
Ind.Decrement()
elif propertyClass == UProp.PC_ADDRESSES:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
for row in propertyValue:
_showType(row, typeKey, typeCustomValue, customTypeKey)
Ind.Increment()
for key in USER_ADDRESSES_PROPERTY_PRINT_ORDER:
if key in row:
if key != 'formatted':
printKeyValueList([key, row[key]])
else:
printKeyValueWithCRsNLs(key, row[key])
Ind.Decrement()
Ind.Decrement()
elif propertyClass == UProp.PC_EMAILS:
if propertyValue:
needTitle = True
for row in propertyValue:
if row['address'].lower() == user['primaryEmail'].lower():
continue
if needTitle:
needTitle = False
printKeyValueList([propertyTitle, None])
Ind.Increment()
if not _showType(row, typeKey, typeCustomValue, customTypeKey):
if not getAliases:
continue
printKeyValueList([typeKey, 'alias'])
Ind.Increment()
for key in row:
if key in [typeKey, customTypeKey]:
continue
printKeyValueList([key, row[key]])
Ind.Decrement()
if not needTitle:
Ind.Decrement()
elif propertyClass == UProp.PC_IMS:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
protocolKey = UProp.IM_PROTOCOLS[UProp.PTKW_ATTR_TYPE_KEYWORD]
protocolCustomValue = UProp.IM_PROTOCOLS[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
customProtocolKey = UProp.IM_PROTOCOLS[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]
for row in propertyValue:
_showType(row, typeKey, typeCustomValue, customTypeKey)
Ind.Increment()
_showType(row, protocolKey, protocolCustomValue, customProtocolKey)
for key in row:
if key in [typeKey, customTypeKey, protocolKey, customProtocolKey]:
continue
printKeyValueList([key, row[key]])
Ind.Decrement()
Ind.Decrement()
elif propertyClass == UProp.PC_NOTES:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
if isinstance(propertyValue, dict):
typeVal = propertyValue.get(typeKey, 'text_plain')
printKeyValueList([typeKey, typeVal])
Ind.Increment()
if typeVal == 'text_html':
printKeyValueWithCRsNLs('value', dehtml(propertyValue['value']))
else:
printKeyValueWithCRsNLs('value', propertyValue['value'])
Ind.Decrement()
else:
printKeyValueList([Ind.MultiLineText(propertyValue)])
Ind.Decrement()
elif propertyClass == UProp.PC_LOCATIONS:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
if isinstance(propertyValue, list):
for row in propertyValue:
_showType(row, typeKey, typeCustomValue, customTypeKey)
Ind.Increment()
if getBuildingNames:
row['buildingName'] = _getBuildingNameById(cd, row.get('buildingId', ''))
for key in USER_LOCATIONS_PROPERTY_PRINT_ORDER:
if key in row:
printKeyValueList([key, row[key]])
Ind.Decrement()
else:
printKeyValueList([Ind.MultiLineText(propertyValue)])
Ind.Decrement()
elif propertyClass == UProp.PC_ORGANIZATIONS:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
for row in propertyValue:
_showType(row, typeKey, typeCustomValue, customTypeKey)
Ind.Increment()
for key in USER_ORGANIZATIONS_PROPERTY_PRINT_ORDER:
if key in row:
printKeyValueList([key, row[key]])
Ind.Decrement()
Ind.Decrement()
elif propertyClass == UProp.PC_POSIX:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
if isinstance(propertyValue, list):
for row in propertyValue:
printKeyValueList(['username', row.get('username')])
Ind.Increment()
for key in USER_POSIX_PROPERTY_PRINT_ORDER:
if key in row:
printKeyValueList([key, row[key]])
Ind.Decrement()
else:
printKeyValueList([Ind.MultiLineText(propertyValue)])
Ind.Decrement()
elif propertyClass == UProp.PC_SSH:
if propertyValue:
printKeyValueList([propertyTitle, None])
Ind.Increment()
if isinstance(propertyValue, list):
for row in propertyValue:
printKeyValueList(['key', row['key']])
Ind.Increment()
for key in USER_SSH_PROPERTY_PRINT_ORDER:
if key in row:
printKeyValueList([key, row[key]])
Ind.Decrement()
else:
printKeyValueList([Ind.MultiLineText(propertyValue)])
Ind.Decrement()
if getSchemas:
up = 'customSchemas'
if up in user:
propertyValue = user[up]
userProperty = UProp.PROPERTIES[up]
propertyTitle = userProperty[UProp.TITLE]
typeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_KEYWORD]
typeCustomValue = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
customTypeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]
if schemaParms['selectedSchemaFields']:
_filterSchemaFields(user, schemaParms)
propertyValue = user[up]
if propertyValue:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], None])
Ind.Increment()
for schema in sorted(propertyValue):
printKeyValueList(['Schema', schema])
Ind.Increment()
for field in propertyValue[schema]:
if isinstance(propertyValue[schema][field], list):
printKeyValueList([field])
Ind.Increment()
for an_item in propertyValue[schema][field]:
_showType(an_item, typeKey, typeCustomValue, customTypeKey, defaultType='work')
Ind.Increment()
printKeyValueList(['value', an_item['value']])
Ind.Decrement()
Ind.Decrement()
else:
printKeyValueList([field, propertyValue[schema][field]])
Ind.Decrement()
Ind.Decrement()
if getAliases:
for up in ['aliases', 'nonEditableAliases']:
propertyValue = user.get(up, [])
if propertyValue:
printEntitiesCount([Ent.NONEDITABLE_ALIAS, Ent.EMAIL_ALIAS][up == 'aliases'], propertyValue)
Ind.Increment()
for alias in propertyValue:
printKeyValueList(['alias', alias])
Ind.Decrement()
if getGroups:
printEntitiesCount(Ent.GROUP, groups)
Ind.Increment()
for group in groups:
printKeyValueList([group['name'], group['email']])
Ind.Decrement()
elif getGroupsTree:
printEntity([Ent.GROUP_MEMBERSHIP_TREE, ''])
Ind.Increment()
for group in groups:
groupEmail = group['email']
if groupEmail not in groupParents:
getGroupParents(cd, groupParents, groupEmail, group['name'], {})
showGroupParents(groupParents, groupEmail, None, 0, 0)
Ind.Decrement()
elif getCIGroupsTree:
printEntity([Ent.GROUP_MEMBERSHIP_TREE, ''])
if memberships:
Ind.Increment()
group_name_mapping = {}
group_displayname_mapping = {}
groups = memberships.get('groups', [])
for group in groups:
group_name = group.get('name')
group_key = group.get('groupKey', {})
group_email = group_key.get('id', '')
group_display_name = group.get('displayName', '')
group_name_mapping[group_name] = group_email
group_displayname_mapping[group_email] = group_display_name
edges = []
seen_group_count = {}
for adj in memberships.get('adjacencyList', []):
group_name = adj.get('group', '')
group_email = group_name_mapping[group_name]
for edge in adj.get('edges', []):
seen_group_count[group_email] = seen_group_count.get(group_email, 0) + 1
member_email = edge.get('preferredMemberKey', {}).get('id')
edges.append((member_email, group_email))
printUserCIGroupMap(user['primaryEmail'], group_displayname_mapping, seen_group_count, edges, 'direct')
if max(seen_group_count.values()) > 1:
printLine(f'{Ind.Spaces()}* {Msg.USER_HAS_MULTIPLE_DIRECT_OR_INHERITED_MEMBERSHIPS_IN_GROUP}')
Ind.Decrement()
if licenses:
printEntitiesCount(Ent.LICENSE, licenses)
Ind.Increment()
for u_license in licenses:
printKeyValueList([SKU.formatSKUIdDisplayName(u_license)])
Ind.Decrement()
Ind.Decrement()
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, userEmail, i, count)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError) as e:
entityActionFailedWarning([Ent.USER, userEmail], str(e), i, count)
except (GAPI.invalidInput, GAPI.invalidMember) as e:
if schemaParms['customFieldMask']:
entityActionFailedWarning([Ent.USER, userEmail], invalidUserSchema(schemaParms['customFieldMask']), i, count)
else:
entityActionFailedWarning([Ent.USER, userEmail], str(e), i, count)
# gam info users <UserTypeEntity>
# [quick]
# [noaliases|aliases]
# [nobuildingnames|buildingnames]
# [nogroups|groups|grouptree|cigrouptree]
# [nolicenses|nolicences|licenses|licences]
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
# [noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
# [userview] <UserFieldName>* [fields <UserFieldNameList>]
# [formatjson]
def doInfoUsers():
infoUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS, delayGet=True)[1])
# gam info user <UserItem>
# [quick]
# [noaliases|aliases]
# [nobuildingnames|buildingnames]
# [nogroups|groups|grouptree|cigrouptree]
# [nolicenses|nolicences|licenses|licences]
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
# [noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
# [userview] <UserFieldName>* [fields <UserFieldNameList>]
# [formatjson]
# gam info user
def doInfoUser():
if Cmd.ArgumentsRemaining():
infoUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
else:
infoUsers([_getAdminEmail()])
USERS_ORDERBY_CHOICE_MAP = {
'familyname': 'familyName',
'lastname': 'familyName',
'givenname': 'givenName',
'firstname': 'givenName',
'email': 'email',
}
USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails', 'externalIds',
'ims', 'keywords', 'locations', 'organizations',
'phones', 'posixAccounts', 'relations', 'sshPublicKeys', 'websites']
# gam print users [todrive <ToDriveAttribute>*]
# ([domain|domains <DomainNameEntity>] [(query <QueryUser>)|(queries <QueryUserList>)]
# [limittoou <OrgUnitItem>] [deleted_only|only_deleted])|[select <UserTypeEntity>]
# [groups|groupsincolumns]
# [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser]
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
# [emailpart|emailparts|username] [schemas|custom all|<SchemaNameList>]
# [orderby <UserOrderByFieldName> [ascending|descending]]
# [userview] [basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
# [delimiter <Character>] [sortheaders] [formatjson [quotechar <Character>]] [quoteplusphonenumbers]
# [convertcrnl]
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
# [showitemcountonly]
# [showvalidcolumn] (addcsvdata <FieldName> <String>)*
#
# gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
# [groups|groupsincolumns]
# [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser]
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
# [emailpart|emailparts|username] [schemas|custom all|<SchemaNameList>]
# [orderby <UserOrderByFieldName> [ascending|descending]]
# [userview] [basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
# [delimiter <Character>] [sortheaders] [formatjson [quotechar <Character>]] [quoteplusphonenumbers]
# [convertcrnl]
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
# [showitemcountonly]
# [showvalidcolumn] (addcsvdata <FieldName> <String>)*
#
# gam print users [todrive <ToDriveAttribute>*]
# ([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)]
# [limittoou <OrgUnitItem>] [deleted_only|only_deleted])|[select <UserTypeEntity>]
# [formatjson [quotechar <Character>]] [countonly]
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
# [showitemcountonly]
# [showvalidcolumn] (addcsvdata <FieldName> <String>)*
#
# gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]] [countonly]
# [issuspended <Boolean>]
# [showitemcountonly]
def doPrintUsers(entityList=None):
def _writeUserEntity(userEntity):
if addCSVData:
userEntity.update(addCSVData)
row = flattenJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'primaryEmail': userEntity['primaryEmail']}
if showValidColumn:
row[showValidColumn] = userEntity[showValidColumn]
row['JSON'] = json.dumps(cleanJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
def _printUser(userEntity, i, count):
if isSuspended is None or isSuspended == userEntity.get('suspended', isSuspended):
if showValidColumn:
userEntity[showValidColumn] = True
userEmail = userEntity['primaryEmail']
if printOptions['emailParts']:
if userEmail.find('@') != -1:
userEntity['primaryEmailLocal'], userEntity['primaryEmailDomain'] = splitEmailAddress(userEmail)
if 'languages' in userEntity and not FJQC.formatJSON:
userEntity['languages'] = _formatLanguagesList(userEntity.pop('languages'), delimiter)
for location in userEntity.get('locations', []):
location['buildingName'] = _getBuildingNameById(cd, location.get('buildingId', ''))
if quotePlusPhoneNumbers:
for phone in userEntity.get('phones', []):
phoneNumber = phone.get('value', '')
if phoneNumber.startswith('+'):
phone['value'] = "'"+phoneNumber
if schemaParms['selectedSchemaFields']:
_filterSchemaFields(userEntity, schemaParms)
if printOptions['getGroupFeed']:
printGettingAllEntityItemsForWhom(Ent.GROUP_MEMBERSHIP, userEmail, i, count)
try:
groups = callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=userEmail, orderBy='email', fields='nextPageToken,groups(email)')
numGroups = len(groups)
if not printOptions['groupsInColumns']:
userEntity['GroupsCount'] = numGroups
userEntity['Groups'] = delimiter.join([groupname['email'] for groupname in groups])
else:
if numGroups > printOptions['maxGroups']:
printOptions['maxGroups'] = numGroups
userEntity['Groups'] = numGroups
for j, group in enumerate(groups):
userEntity[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}'] = group['email']
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, userEmail)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
if aliasMatchPattern and 'aliases' in userEntity:
userEntity['aliases'] = [alias for alias in userEntity['aliases'] if aliasMatchPattern.match(alias)]
if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
if printOptions['getLicenseFeed']:
u_licenses = licenses.get(userEmail.lower(), [])
else:
u_licenses = getUserLicenses(lic, userEntity, skus)
if not oneLicensePerRow:
userEntity['LicensesCount'] = len(u_licenses)
if u_licenses:
userEntity['Licenses'] = delimiter.join(u_licenses)
userEntity['LicensesDisplay'] = delimiter.join([SKU.skuIdToDisplayName(skuId) for skuId in u_licenses])
else:
u_licenses = []
if not oneLicensePerRow:
_writeUserEntity(userEntity)
else:
if u_licenses:
for skuId in u_licenses:
userEntity['License'] = skuId
userEntity['LicenseDisplay'] = SKU.skuIdToDisplayName(skuId)
_writeUserEntity(userEntity)
else:
userEntity['License'] = userEntity['LicenseDisplay'] = ''
_writeUserEntity(userEntity)
def _updateDomainCounts(emailAddress):
nonlocal domainCounts
atLoc = emailAddress.find('@')
if atLoc == -1:
dom = UNKNOWN
else:
dom = emailAddress[atLoc+1:].lower()
domainCounts.setdefault(dom, 0)
domainCounts[dom] += 1
_PRINT_USER_REASON_TO_MESSAGE_MAP = {GAPI.RESOURCE_NOT_FOUND: Msg.DOES_NOT_EXIST}
def _callbackPrintUser(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
_printUser(response, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason in GAPI.USER_GET_THROW_REASONS:
if not showValidColumn:
entityUnknownWarning(Ent.USER, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
_writeUserEntity({'primaryEmail': ri[RI_ITEM], showValidColumn: False})
elif (reason == GAPI.INVALID_INPUT) and schemaParms['customFieldMask']:
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], invalidUserSchema(schemaParms['customFieldMask']), int(ri[RI_J]), int(ri[RI_JCOUNT]))
elif reason not in GAPI.DEFAULT_RETRY_REASONS:
errMsg = getHTTPError(_PRINT_USER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
printKeyValueList([ERROR, errMsg])
else:
waitOnFailure(1, 10, reason, message)
try:
user = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.RATE_LIMIT_EXCEEDED],
userKey=ri[RI_ITEM], projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
viewType=viewType, fields=fields)
_printUser(user, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.userNotFound, GAPI.resourceNotFound):
if not showValidColumn:
entityUnknownWarning(Ent.USER, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
_writeUserEntity({'primaryEmail': ri[RI_ITEM], showValidColumn: False})
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.rateLimitExceeded) as e:
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.invalidInput as e:
if schemaParms['customFieldMask']:
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], invalidUserSchema(schemaParms['customFieldMask']), int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
cd = buildGAPIObject(API.DIRECTORY)
fieldsList = ['primaryEmail']
csvPF = CSVPrintFile(fieldsList, indexedTitles=USERS_INDEXED_TITLES)
FJQC = FormatJSONQuoteChar(csvPF)
printOptions = {
'countOnly': False,
'emailParts': False,
'getGroupFeed': False,
'getLicenseFeed': False,
'getLicenseFeedByUser': False,
'groupsInColumns': False,
'scalarsFirst': False,
'sortHeaders': False,
'maxGroups': 0
}
kwargsDict = initUserGroupDomainQueryFilters()
licenses = {}
lic = None
skus = None
maxResults = GC.Values[GC.USER_MAX_RESULTS]
schemaParms = _initSchemaParms('basic')
projectionSet = False
oneLicensePerRow = quotePlusPhoneNumbers = showDeleted = False
aliasMatchPattern = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None
viewType = 'admin_view'
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showValidColumn = ''
showItemCountOnly = False
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif entityList is None and myarg == 'limittoou':
orgUnitPath = getOrgUnitItem(pathOnly=True, cd=cd)
orgUnitPathLower = orgUnitPath.lower()
elif entityList is None and getUserGroupDomainQueryFilters(myarg, kwargsDict):
pass
elif entityList is None and myarg in {'deletedonly', 'onlydeleted'}:
showDeleted = True
elif entityList is None and myarg == 'select':
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
elif myarg == 'issuspended':
isSuspended = getBoolean()
elif myarg == 'orderby':
orderBy, sortOrder = getOrderBySortOrder(USERS_ORDERBY_CHOICE_MAP)
elif myarg == 'userview':
viewType = 'domain_public'
elif myarg in {'allfields', 'basic'}:
schemaParms = _initSchemaParms('basic')
projectionSet = printOptions['sortHeaders'] = True
fieldsList = []
elif myarg == 'full':
if schemaParms['projection'] != 'custom':
schemaParms = _initSchemaParms(myarg)
projectionSet = printOptions['sortHeaders'] = True
fieldsList = []
elif myarg in {'custom', 'schemas', 'customschemas'}:
projectionSet = True
_getSchemaNameList(schemaParms)
if fieldsList:
fieldsList.append('customSchemas')
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'sortheaders':
printOptions['sortHeaders'] = getBoolean()
elif myarg == 'scalarsfirst':
printOptions['scalarsFirst'] = getBoolean()
elif csvPF.GetFieldsListTitles(myarg, USER_FIELDS_CHOICE_MAP, fieldsList, initialField='primaryEmail'):
pass
elif myarg == 'groups':
printOptions['getGroupFeed'] = True
printOptions['groupsInColumns'] = False
elif myarg == 'groupsincolumns':
printOptions['getGroupFeed'] = True
printOptions['groupsInColumns'] = True
elif myarg in {'license', 'licenses', 'licence', 'licences'}:
printOptions['getLicenseFeed'] = True
printOptions['getLicenseFeedByUser'] = False
elif myarg in {'licensebyuser', 'licensesbyuser', 'licencebyuser', 'licencesbyuser'}:
printOptions['getLicenseFeedByUser'] = True
printOptions['getLicenseFeed'] = False
elif myarg in {'onelicenseperrow', 'onelicenceperrow'}:
oneLicensePerRow = True
elif myarg in {'products', 'product'}:
skus = SKU.convertProductListToSKUList(getGoogleProductList())
elif myarg in {'sku', 'skus'}:
skus = getGoogleSKUList()
elif myarg == 'aliasmatchpattern':
aliasMatchPattern = getREPattern(re.IGNORECASE)
elif myarg in {'emailpart', 'emailparts', 'username'}:
printOptions['emailParts'] = True
elif myarg in {'countonly', 'countsonly'}:
printOptions['countOnly'] = True
elif myarg == 'maxresults':
maxResults = getInteger(minVal=1, maxVal=500)
elif myarg == 'quoteplusphonenumbers':
quotePlusPhoneNumbers = True
elif myarg == 'showitemcountonly':
showItemCountOnly = True
elif myarg == 'showvalidcolumn':
showValidColumn = 'Valid'
elif myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
_, _, entityList = getEntityArgument(entityList)
if printOptions['countOnly']:
fieldsList = ['primaryEmail']
domainCounts = {}
if not FJQC.formatJSON:
csvPF.SetTitles(['domain', 'count'])
else:
csvPF.SetJSONTitles(['JSON'])
else:
if FJQC.formatJSON:
printOptions['sortHeaders'] = False
titles = ['primaryEmail']
if showValidColumn:
titles.append(showValidColumn)
titles.append('JSON')
csvPF.SetJSONTitles(titles)
else:
if showValidColumn:
csvPF.AddTitles([showValidColumn])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if printOptions['getGroupFeed']:
if not printOptions['groupsInColumns']:
csvPF.AddTitles(['GroupsCount', 'Groups'])
else:
csvPF.AddTitles(['Groups'])
if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
if not oneLicensePerRow:
licenseTitles = ['LicensesCount', 'Licenses', 'LicensesDisplay']
else:
licenseTitles = ['License', 'LicenseDisplay']
csvPF.AddTitles(licenseTitles)
if printOptions['getLicenseFeed']:
if skus is None and GM.Globals[GM.LICENSE_SKUS]:
skus = GM.Globals[GM.LICENSE_SKUS]
licenses = doPrintLicenses(returnFields=['userId', 'skuId'], skus=skus)
elif printOptions['getLicenseFeedByUser']:
lic = buildGAPIObject(API.LICENSING)
if skus is None:
skus = SKU.getAllSKUs() if not GM.Globals[GM.LICENSE_SKUS] else GM.Globals[GM.LICENSE_SKUS]
if entityList is None:
sortRows = False
if orgUnitPath is not None and fieldsList:
fieldsList.append('orgUnitPath')
fields = getItemFieldsFromFieldsList('users', fieldsList)
itemCount = 0
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
kwargs = kwargsQuery[0]
query = kwargsQuery[1]
query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended)
printGettingAllAccountEntities(Ent.USER, pquery)
pageMessage = getPageMessage(showFirstLastItems=True)
try:
feed = yieldGAPIpages(cd.users(), 'list', 'users',
pageMessage=pageMessage, messageAttribute='primaryEmail',
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.INVALID_ORGUNIT, GAPI.INVALID_INPUT,
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN,
GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
retryReasons=[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
query=query, fields=fields,
showDeleted=showDeleted, orderBy=orderBy, sortOrder=sortOrder, viewType=viewType,
projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
maxResults=maxResults, **kwargs)
for users in feed:
if orgUnitPath is None:
if showItemCountOnly:
itemCount += len(users)
elif not printOptions['countOnly']:
for user in users:
_printUser(user, 0, 0)
else:
for user in users:
_updateDomainCounts(user['primaryEmail'])
else:
if showItemCountOnly:
for user in users:
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
itemCount += 1
elif not printOptions['countOnly']:
for user in users:
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
_printUser(user, 0, 0)
else:
for user in users:
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
_updateDomainCounts(user['primaryEmail'])
except GAPI.domainNotFound:
entityActionFailedWarning([Ent.USER, None, Ent.DOMAIN, kwargs['domain']], Msg.NOT_FOUND)
continue
except (GAPI.invalidOrgunit, GAPI.invalidInput) as e:
if query and not schemaParms['customFieldMask']:
entityActionFailedWarning([Ent.USER, None], invalidQuery(query))
elif schemaParms['customFieldMask'] and not query:
entityActionFailedWarning([Ent.USER, None], invalidUserSchema(schemaParms['customFieldMask']))
elif query and schemaParms['customFieldMask']:
entityActionFailedWarning([Ent.USER, None], f'{invalidQuery(query)} or {invalidUserSchema(schemaParms["customFieldMask"])}')
else:
entityActionFailedWarning([Ent.USER, None], str(e))
continue
except (GAPI.unknownError, GAPI.failedPrecondition) as e:
entityActionFailedExit([Ent.USER, None], str(e))
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
accessErrorExit(cd)
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
else:
if showItemCountOnly:
writeStdout(f'{len(entityList)}\n')
return
sortRows = True
# If no individual fields were specified (allfields, basic, full) or individual fields other than primaryEmail were specified, look up each user
if isSuspended is not None and fieldsList:
fieldsList.append('suspended')
if projectionSet or len(set(fieldsList)) > 1 or showValidColumn:
jcount = len(entityList)
fields = getFieldsFromFieldsList(fieldsList)
if GC.Values[GC.BATCH_SIZE] > 1 and jcount > 1:
svcargs = dict([('userKey', None), ('fields', fields),
('projection', schemaParms['projection']), ('customFieldMask', schemaParms['customFieldMask']),
('viewType', viewType)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(cd.users(), 'get')
dbatch = cd.new_batch_http_request(callback=_callbackPrintUser)
bcount = 0
j = 0
for userEntity in entityList:
j += 1
svcparms = svcargs.copy()
svcparms['userKey'] = normalizeEmailAddressOrUID(userEntity)
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, svcparms['userKey']))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = cd.new_batch_http_request(callback=_callbackPrintUser)
bcount = 0
if bcount > 0:
dbatch.execute()
else:
j = 0
for userEntity in entityList:
j += 1
userEmail = normalizeEmailAddressOrUID(userEntity)
try:
user = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.RATE_LIMIT_EXCEEDED],
userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
viewType=viewType, fields=fields)
_printUser(user, j, jcount)
except (GAPI.userNotFound, GAPI.resourceNotFound):
if not showValidColumn:
entityUnknownWarning(Ent.USER, userEmail, j, jcount)
else:
_writeUserEntity({'primaryEmail': userEmail, showValidColumn: False})
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.rateLimitExceeded) as e:
entityActionFailedWarning([Ent.USER, userEmail], str(e), j, jcount)
except GAPI.invalidInput as e:
if schemaParms['customFieldMask']:
entityActionFailedWarning([Ent.USER, userEmail], invalidUserSchema(schemaParms['customFieldMask']), j, jcount)
else:
entityActionFailedWarning([Ent.USER, userEmail], str(e), j, jcount)
# The only field specified was primaryEmail, just list the users/count the domains
elif not printOptions['countOnly']:
for userEntity in entityList:
_printUser({'primaryEmail': normalizeEmailAddressOrUID(userEntity)}, 0, 0)
else:
for userEntity in entityList:
_updateDomainCounts(normalizeEmailAddressOrUID(userEntity))
if not printOptions['countOnly']:
if not FJQC.formatJSON:
if printOptions['sortHeaders']:
sortTitles = ['primaryEmail']
if printOptions['scalarsFirst']:
sortTitles.extend([f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{field}' for field in USER_NAME_PROPERTY_PRINT_ORDER]+sorted(USER_LANGUAGE_PROPERTY_PRINT_ORDER+USER_SCALAR_PROPERTY_PRINT_ORDER))
csvPF.SetSortTitles(sortTitles)
csvPF.SortTitles()
csvPF.SetSortTitles([])
if printOptions['getGroupFeed']:
if not printOptions['groupsInColumns']:
csvPF.MoveTitlesToEnd(['GroupsCount', 'Groups'])
else:
csvPF.MoveTitlesToEnd(['Groups']+[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}' for j in range(printOptions['maxGroups'])])
if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
csvPF.MoveTitlesToEnd(licenseTitles)
if sortRows and orderBy:
orderBy = 'primaryEmail' if orderBy == 'email' else f'name.{orderBy}'
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
else:
if sortRows and orderBy == 'email':
csvPF.SortRows('primaryEmail', reverse=sortOrder == 'DESCENDING')
elif not FJQC.formatJSON:
for domain, count in sorted(iter(domainCounts.items())):
csvPF.WriteRowNoFilter({'domain': domain, 'count': count})
else:
csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(domainCounts), ensure_ascii=False, sort_keys=True)})
if printOptions['countOnly']:
csvPF.SetIndexedTitles([])
csvPF.writeCSVfile('Users' if not printOptions['countOnly'] else 'User Domain Counts')
# gam <UserTypeEntity> print users
def doPrintUserEntity(entityList):
if not Cmd.ArgumentsRemaining():
writeEntityNoHeaderCSVFile(Ent.USER, entityList)
else:
doPrintUsers(entityList)
# gam <UserTypeEntity> print userlist [todrive <ToDriveAttribute>*]
# [title <String>]
# [delimiter <Character>] [formatjson] [quotechar <Character>]
def doPrintUserList(entityList):
csvPF = CSVPrintFile(['title', 'count', 'users'])
FJQC = FormatJSONQuoteChar(csvPF)
title = 'Users'
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'title':
title = getString(Cmd.OB_STRING)
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
_, count, entityList = getEntityArgument(entityList)
if not FJQC.formatJSON:
csvPF.WriteRow({'title': title, 'count': count, 'users': delimiter.join(entityList)})
else:
csvPF.WriteRow({'title': title, 'count': count, 'users': json.dumps(cleanJSON(entityList), ensure_ascii=False, sort_keys=True)})
csvPF.writeCSVfile('User List')
# gam print usercountsbyorgunit [todrive <ToDriveAttribute>*]
# [domain <String>]
def doPrintUserCountsByOrgUnit():
def _printUserCounts(title, v):
csvPF.WriteRow({'orgUnitPath': title, 'archived': v['archived'], 'active': v['active'], 'suspended': v['suspended'], 'total': v['total']})
USER_COUNTS_FIELDS = ['archived', 'active', 'suspended', 'total']
USER_COUNTS_ZERO_FIELDS = {'archived': 0, 'active': 0, 'suspended': 0, 'total': 0}
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['orgUnitPath']+USER_COUNTS_FIELDS)
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'domain':
kwargs = {'domain': getString(Cmd.OB_DOMAIN_NAME)}
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if 'domain' in kwargs:
printGettingAllEntityItemsForWhom(Ent.USER, kwargs['domain'], entityType=Ent.DOMAIN)
pageMessage = getPageMessageForWhom()
title = f"Total({kwargs['domain']})"
else:
printGettingAllAccountEntities(Ent.USER)
pageMessage = getPageMessage()
title = f"Total({kwargs['customer']})"
userCounts = {}
try:
result = callGAPIpages(cd.users(), 'list', 'users',
pageMessage=pageMessage,
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN],
orderBy='email', fields='nextPageToken,users(orgUnitPath,archived,suspended)',
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.domainNotFound):
if 'domain' in kwargs:
checkEntityDNEorAccessErrorExit(cd, Ent.DOMAIN, kwargs['domain'])
else:
checkEntityDNEorAccessErrorExit(cd, Ent.CUSTOMER_ID, kwargs['customer'])
return
for user in result:
orgUnitPath = user['orgUnitPath']
if orgUnitPath not in userCounts:
userCounts[orgUnitPath] = USER_COUNTS_ZERO_FIELDS.copy()
if user['suspended'] or user['archived']:
if user['archived']:
userCounts[orgUnitPath]['archived'] += 1
if user['suspended']:
userCounts[orgUnitPath]['suspended'] += 1
else:
userCounts[orgUnitPath]['active'] += 1
userCounts[orgUnitPath]['total'] += 1
totalCounts = USER_COUNTS_ZERO_FIELDS.copy()
for k, v in sorted(iter(userCounts.items())):
_printUserCounts(k, v)
for f in USER_COUNTS_FIELDS:
totalCounts[f] += v[f]
_printUserCounts(title, totalCounts)
csvPF.writeCSVfile('User Counts by OrgUnit')
def isolateCIUserInvitatonsEmail(name):
''' converts long name into email address'''
return name.split('/')[-1]
def quotedCIUserInvitatonsEmail(customer, email):
return f"{customer}/userinvitations/{quote_plus(email, safe='@')}"
def _getCIUserInvitationsEntity(ci=None, email=None):
if ci is None:
ci = buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS)
customer = _getCustomersCustomerIdWithC()
if email is None:
email = getString(Cmd.OB_EMAIL_ADDRESS)
pattern = re.compile(rf'^{customer}/userinvitations/(.+)$')
mg = pattern.match(email)
if mg:
email = mg.group(1)
else:
email = normalizeEmailAddressOrUID(email, noUid=True)
return (quotedCIUserInvitatonsEmail(customer, email), email, ci)
def _getIsInvitableUser(ci, email):
name, _, ci = _getCIUserInvitationsEntity(ci, email)
try:
result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
return (result['isInvitableUser'], ci)
except GAPI.notFound:
return (False, ci)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied):
return (False, ci)
# gam send userinvitation <EmailAddress>
# gam cancel userinvitation <EmailAddress>
def doCIUserInvitationsAction():
name, user, ci = _getCIUserInvitationsEntity()
checkForExtraneousArguments()
if Act.Get() == Act.CANCEL:
action = 'cancel'
else:
Act.Set(Act.SEND)
action = 'send'
entityPerformAction([Ent.USER_INVITATION, user])
try:
result = callGAPI(ci.customers().userinvitations(), action,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
name = result.get('response', {}).get('name')
if name:
result['response']['name'] = isolateCIUserInvitatonsEmail(name)
Ind.Increment()
showJSON(None, result)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.USER_INVITATION, f'{user}')
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER_INVITATION, f'{user}'], str(e))
CI_USERINVITATION_TIME_OBJECTS = {'updateTime'}
def _showUserInvitation(invitation, FJQC, i=0, count=0):
if FJQC is not None and FJQC.formatJSON:
invitation['email'] = isolateCIUserInvitatonsEmail(invitation['name'])
printLine(json.dumps(cleanJSON(invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.USER_INVITATION, isolateCIUserInvitatonsEmail(invitation['name'])], i, count)
Ind.Increment()
showJSON(None, invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS)
Ind.Decrement()
# gam check userinvitation|isinvitable <EmailAddress>
def doCheckCIUserInvitations():
name, user, ci = _getCIUserInvitationsEntity()
checkForExtraneousArguments()
try:
result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
printEntity([Ent.USER_INVITATION, user])
Ind.Increment()
showJSON(None, result)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.USER_INVITATION, f'{user}')
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER_INVITATION, f'{user}'], str(e))
def infoCIUserInvitations(name, user, ci, FJQC):
try:
invitation = callGAPI(ci.customers().userinvitations(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
_showUserInvitation(invitation, FJQC)
except GAPI.notFound:
entityUnknownWarning(Ent.USER_INVITATION, f'{user}')
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER_INVITATION, f'{user}'], str(e))
# gam info userinvitation <EmailAddress> [formatjson]
def doInfoCIUserInvitations():
name, user, ci = _getCIUserInvitationsEntity()
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
infoCIUserInvitations(name, user, ci, FJQC)
CI_USERINVITATION_ORDERBY_CHOICE_MAP = {
'email': 'email',
'updatetime': 'update_time',
}
CI_USERINVITATION_STATE_CHOICE_MAP = {
'accepted': 'ACCEPTED',
'declined': 'DECLINED',
'invited': 'INVITED',
'notyetsent': 'NOT_YET_SENT',
}
# gam show userinvitations
# [state notyetsent|invited|accepted|declined]
# [orderby email|updatetime [ascending|descending]]
# [formatjson]
# gam print userinvitations [todrive <ToDriveAttribute>*]
# [state notyetsent|invited|accepted|declined]
# [orderby email|updatetime [ascending|descending]]
# [[formatjson [quotechar <Character>]]
def doPrintShowCIUserInvitations():
def _printUserInvitation(invitation):
invitation['email'] = isolateCIUserInvitatonsEmail(invitation['name'])
row = flattenJSON(invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'email': invitation['email'],
'JSON': json.dumps(cleanJSON(invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
ci = buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS)
customer = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['email']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(CI_USERINVITATION_ORDERBY_CHOICE_MAP)
ifilter = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'state':
state = getChoice(CI_USERINVITATION_STATE_CHOICE_MAP, mapChoice=True)
ifilter = f"state=='{state}'"
elif myarg == 'orderby':
OBY.GetChoice()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
printGettingAllAccountEntities(Ent.USER_INVITATION, ifilter)
pageMessage = getPageMessage()
try:
invitations = callGAPIpages(ci.customers().userinvitations(), 'list', 'userInvitations',
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
pageMessage=pageMessage,
parent=customer, filter=ifilter, orderBy=OBY.orderBy)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER_INVITATION, None], str(e))
return
if not csvPF:
jcount = len(invitations)
performActionNumItems(jcount, Ent.USER_INVITATION)
Ind.Increment()
j = 0
for invitation in invitations:
j += 1
_showUserInvitation(invitation, FJQC, j, jcount)
Ind.Decrement()
else:
for invitation in invitations:
_printUserInvitation(invitation)
if csvPF:
csvPF.writeCSVfile('User Invitations')
# gam <UserTypeEntity> check isinvitable [todrive <ToDriveAttribute>*]
# /batch is broken for Cloud Identity. Once fixed move this to using batch.
# Current serial implementation will be SLOW...
def checkCIUserIsInvitable(users):
ci = buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS)
customer = _getCustomersCustomerIdWithC()
csvPF = CSVPrintFile(['invitableUsers'])
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = convertUIDtoEmailAddress(user)
name = quotedCIUserInvitatonsEmail(customer, user)
try:
result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.PERMISSION_DENIED],
name=name)
if result.get('isInvitableUser'):
csvPF.WriteRow({'invitableUsers': user})
except GAPI.notFound:
entityUnknownWarning(Ent.USER_INVITATION, f'{user}', i, count)
except (GAPI.invalid, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER_INVITATION, user], str(e), i, count)
return
csvPF.writeCSVfile('Invitable Users')
INBOUNDSSO_MODE_CHOICE_MAP = {
'ssooff': 'SSO_OFF',
'samlsso': 'SAML_SSO',
'domainwidesamlifenabled': 'DOMAIN_WIDE_SAML_IF_ENABLED'
}
def getCIOrgunitID(cd, orgunit):
ou_id = getOrgUnitId(cd, orgunit)[1]
if ou_id.startswith('id:'):
ou_id = ou_id[3:]
return f'orgUnits/{ou_id}'
def _getInboundSSOProfiles(ci):
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
try:
return callGAPIpages(ci.inboundSamlSsoProfiles(), 'list', 'inboundSamlSsoProfiles',
throwReasons=GAPI.CISSO_LIST_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
filter=f'customer=="{customer}"')
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.INBOUND_SSO_PROFILE, customer], str(e))
return []
def _convertInboundSSOProfileDisplaynameToName(ci=None, displayName=''):
if displayName.lower().startswith('id:') or displayName.lower().startswith('uid:'):
displayName = displayName.split(':', 1)[1]
if not displayName.startswith('inboundSamlSsoProfiles/'):
displayName = f'inboundSamlSsoProfiles/{displayName}'
return displayName
if not ci:
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
profiles = _getInboundSSOProfiles(ci)
matches = []
for profile in profiles:
if displayName.lower() == profile.get('displayName', '').lower():
matches.append(profile)
if len(matches) == 1:
return matches[0]['name']
if len(matches) == 0:
usageErrorExit(Msg.NO_SSO_PROFILE_MATCHES.format(displayName))
errMsg = Msg.MULTIPLE_SSO_PROFILES_MATCH.format(displayName)
for m in matches:
errMsg += f' {m["name"]} {m["displayName"]}\n'
usageErrorExit(errMsg)
def _getInboundSSOProfileArguments(body):
returnNameOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['displayName'] = getString(Cmd.OB_STRING)
elif myarg == 'entityid':
body.setdefault('idpConfig', {})['entityId'] = getString(Cmd.OB_STRING)
elif myarg == 'loginurl':
body.setdefault('idpConfig', {})['singleSignOnServiceUri'] = getString(Cmd.OB_STRING)
elif myarg == 'logouturl':
body.setdefault('idpConfig', {})['logoutRedirectUri'] = getString(Cmd.OB_STRING)
elif myarg == 'changepasswordurl':
body.setdefault('idpConfig', {})['changePasswordUri'] = getString(Cmd.OB_STRING)
elif myarg == 'returnnameonly':
returnNameOnly = True
else:
unknownArgumentExit()
return (returnNameOnly, body)
def _showInboundSSOProfile(profile, FJQC, i=0, count=0):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(profile), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.INBOUND_SSO_PROFILE, profile['name']], i, count)
Ind.Increment()
showJSON(None, profile)
Ind.Decrement()
def _processInboundSSOProfileResult(result, returnNameOnly, kvlist, function):
if GC.Values[GC.DEBUG_LEVEL] > 0:
writeStderr(f'inboundSSOProfileResult: {result}\n')
if result.get('done', False):
if 'error' not in result:
if 'response' in result:
if not returnNameOnly:
_showInboundSSOProfile(result['response'], None)
else:
writeStdout(f'{result["response"]["name"]}\n')
else:
entityActionPerformed(kvlist)
else:
entityActionFailedWarning(kvlist, result['error']['message'])
elif not returnNameOnly:
entityActionPerformedMessage(kvlist, Msg.ACTION_IN_PROGRESS.format(f'{function} inboundssoprofile'))
else:
writeStdout('inProgress\n')
# gam create inboundssoprofile [name <SSOProfileName>]
# [entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
# [returnnameonly]
def doCreateInboundSSOProfile():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
body = {'customer': normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID]),
'displayName': 'SSO Profile'
}
returnNameOnly, body = _getInboundSSOProfileArguments(body)
kvlist = [Ent.INBOUND_SSO_PROFILE, body['displayName']]
try:
result = callGAPI(ci.inboundSamlSsoProfiles(), 'create',
throwReasons=GAPI.CISSO_CREATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
body=body)
_processInboundSSOProfileResult(result, returnNameOnly, kvlist, 'create')
except (GAPI.failedPrecondition, GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam update inboundssoprofile <SSOProfileItem>
# [entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
# [returnnameonly]
def doUpdateInboundSSOProfile():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
name = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
returnNameOnly, body = _getInboundSSOProfileArguments({})
kvlist = [Ent.INBOUND_SSO_PROFILE, name]
try:
result = callGAPI(ci.inboundSamlSsoProfiles(), 'patch',
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name, updateMask=','.join(body.keys()), body=body)
_processInboundSSOProfileResult(result, returnNameOnly, kvlist, 'update')
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam delete inboundssoprofile <SSOProfileItem>
def doDeleteInboundSSOProfile():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
name = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
checkForExtraneousArguments()
kvlist = [Ent.INBOUND_SSO_PROFILE, name]
try:
result = callGAPI(ci.inboundSamlSsoProfiles(), 'delete',
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name)
_processInboundSSOProfileResult(result, True, kvlist, 'delete')
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
def _getInboundSSOProfile(ci, name):
kvlist = [Ent.INBOUND_SSO_PROFILE, name]
try:
return callGAPI(ci.inboundSamlSsoProfiles(), 'get',
throwReasons=GAPI.CISSO_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name)
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
return None
# gam info inboundssoprofile <SSOProfileItem> [formatjson]
def doInfoInboundSSOProfile():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
name = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
profile = _getInboundSSOProfile(ci, name)
if profile:
_showInboundSSOProfile(profile, FJQC)
# gam show inboundssoprofile
# [formatjson]
# gam print inboundssoprofile [todrive <ToDriveAttribute>*]
# [[formatjson [quotechar <Character>]]
def doPrintShowInboundSSOProfiles():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
cfilter = f'customer=="{customer}"'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF:
printGettingAllAccountEntities(Ent.INBOUND_SSO_PROFILE, cfilter)
profiles = _getInboundSSOProfiles(ci)
if not csvPF:
count = len(profiles)
if not FJQC.formatJSON:
performActionNumItems(count, Ent.INBOUND_SSO_PROFILE)
Ind.Increment()
i = 0
for profile in profiles:
i += 1
_showInboundSSOProfile(profile, FJQC, i, count)
Ind.Decrement()
else:
for profile in profiles:
row = flattenJSON(profile)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': profile['name'],
'JSON': json.dumps(cleanJSON(profile),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Inbound SSO Profiles')
def getInboundSSOProfileCredentials(ci, profile):
try:
return callGAPIpages(ci.inboundSamlSsoProfiles().idpCredentials(), 'list', 'idpCredentials',
throwReasons=GAPI.CISSO_LIST_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
parent=profile)
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.INBOUND_SSO_PROFILE, profile], str(e))
return None
def getInboundSSOCredentialsName():
name = getString(Cmd.OB_STRING)
if name.startswith('id:') or name.startswith('uid:'):
name = name.split(':', 1)[1]
return name
INBOUNDSSO_CREDENTIALS_TIME_OBJECTS = ['updateTime']
def _showInboundSSOCredentials(credentials, FJQC, i=0, count=0):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(credentials, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.INBOUND_SSO_CREDENTIALS, credentials['name']], i, count)
Ind.Increment()
showJSON(None, credentials, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS)
Ind.Decrement()
def _processInboundSSOCredentialsResult(result, kvlist, function):
if result.get('done', False):
if 'error' not in result:
if 'response' in result:
_showInboundSSOCredentials(result['response'], None)
else:
entityActionPerformed(kvlist)
else:
entityActionFailedWarning(kvlist, result['error']['message'])
else:
entityActionPerformedMessage(kvlist, Msg.ACTION_IN_PROGRESS.format(f'{function} inboundssocredentials'))
# gam create inboundssocredentials profile <SSOProfileItem>
# (pemfile <FileName>)|(generatekey [keysize 1024|2048|4096]) [replaceolddest]
def doCreateInboundSSOCredential():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
profile = None
generateKey = replaceOldest = False
keySize = 2048
pemData = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'profile':
profile = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
elif myarg == 'pemfile':
pemData = readFile(getString(Cmd.OB_FILE_NAME))
elif myarg == 'generatekey':
generateKey = True
elif myarg == 'replaceoldest':
replaceOldest = True
elif myarg == 'keysize':
keySize=int(getChoice([1024, 2048, 4096]))
else:
unknownArgumentExit()
if not profile:
missingArgumentExit('profile')
if not pemData and not generateKey:
missingArgumentExit('pemfile|generatekey')
if pemData and generateKey:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('pemfile', 'generatekey'))
if replaceOldest:
credentials = getInboundSSOProfileCredentials(ci, profile)
if credentials is None:
return
count = len(credentials)
if count == 2:
oldest_key = min(credentials, key=lambda x:x['updateTime'])
action = Act.Get()
Act.Set(Act.DELETE)
doDeleteInboundSSOCredential(ci=ci, name=oldest_key['name'])
Act.Set(action)
else:
writeStdout(Msg.NO_CREDENTIALS_REPLACEMENT.format(Ent.Singular(Ent.INBOUND_SSO_PROFILE), profile,
count, Ent.Choose(Ent.INBOUND_SSO_CREDENTIALS, count)))
if generateKey:
privKey, pemData = _generatePrivateKeyAndPublicCert('', '', 'GAM', keySize, b64enc_pub=False)
timestamp = datetime.datetime.now(GC.Values[GC.TIMEZONE]).strftime('%Y%m%d-%I%M%S')
priv_file = f'privatekey-{timestamp}.pem'
writeFile(priv_file, privKey)
writeStdout(Msg.WROTE_PRIVATE_KEY_DATA.format(priv_file))
pub_file = f'publiccert-{timestamp}.pem'
writeFile(pub_file, pemData)
writeStdout(Msg.WROTE_PUBLIC_CERTIFICATE.format(pub_file))
kvlist = [Ent.INBOUND_SSO_CREDENTIALS, profile]
try:
result = callGAPI(ci.inboundSamlSsoProfiles().idpCredentials(), 'add',
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
parent=profile, body={'pemData': pemData})
_processInboundSSOCredentialsResult(result, kvlist, 'create')
except GAPI.notFound as e:
entityActionFailedWarning([Ent.INBOUND_SSO_PROFILE, profile], str(e))
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam delete inboundssocredential <SSOCredentialsName>
def doDeleteInboundSSOCredential(ci=None, name=None):
if not ci:
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
if not name:
name = getInboundSSOCredentialsName()
checkForExtraneousArguments()
kvlist = [Ent.INBOUND_SSO_CREDENTIALS, name]
try:
result = callGAPI(ci.inboundSamlSsoProfiles().idpCredentials(), 'delete',
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name)
_processInboundSSOCredentialsResult(result, kvlist, 'delete')
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam info inboundssocredential <SSOCredentialsName> [formatjson]
def doInfoInboundSSOCredential():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
name = getInboundSSOCredentialsName()
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
kvlist = [Ent.INBOUND_SSO_CREDENTIALS, name]
try:
credentials = callGAPI(ci.inboundSamlSsoProfiles().idpCredentials(), 'get',
throwReasons=GAPI.CISSO_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name)
_showInboundSSOCredentials(credentials, FJQC)
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam show inboundssocredentials [profile|profiles <SSOProfileItemList>]
# [formatjson]
# gam print inboundssocredentials [profile|profiles <SSOProfileItemList>]
# [[formatjson [quotechar <Character>]]
def doPrintShowInboundSSOCredentials():
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
profiles = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'profile', 'profiles'}:
profiles = [_convertInboundSSOProfileDisplaynameToName(ci, profile) for profile in getString(Cmd.OB_STRING_LIST).split(',')]
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not profiles:
profiles = [p['name'] for p in _getInboundSSOProfiles(ci)]
count = len(profiles)
i = 0
for profile in profiles:
i += 1
credentials = getInboundSSOProfileCredentials(ci, profile)
if credentials is None:
continue
if not csvPF:
jcount = len(credentials)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.INBOUND_SSO_PROFILE, profile], jcount, Ent.INBOUND_SSO_CREDENTIALS, i, count)
Ind.Increment()
j = 0
for credential in credentials:
j += 1
_showInboundSSOCredentials(credential, FJQC, j, jcount)
Ind.Decrement()
else:
for credential in credentials:
row = flattenJSON(credential, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': credential['name'],
'JSON': json.dumps(cleanJSON(credential, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Inbound SSO Credentials')
def _getInboundSSOAssignment(ci, name):
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, name]
try:
return callGAPI(ci.inboundSsoAssignments(), 'get',
throwReasons=GAPI.CISSO_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name)
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
return None
def _getInboundSSOAssignments(ci):
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
try:
return callGAPIpages(ci.inboundSsoAssignments(), 'list', 'inboundSsoAssignments',
throwReasons=GAPI.CISSO_LIST_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
filter=f'customer=="{customer}"')
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.INBOUND_SSO_ASSIGNMENT, customer], str(e))
return None
def _getInboundSSOAssignmentName():
name = getString(Cmd.OB_STRING)
if name.startswith('id:') or name.startswith('uid:'):
name = name.split(':', 1)[1]
if not name.startswith('inboundSsoAssignments/'):
name = f'inboundSsoAssignments/{name}'
return name
def _getInboundSSOAssignmentByTarget(ci, cd, target):
targetType = 'name'
if target.startswith('id:') or target.startswith('uid:'):
target = target.split(':', 1)[1]
elif re.match(r'^groups/[^/]+$', target):
targetType = 'targetGroup'
elif re.match(r'^orgUnits/[^/]+$', target):
targetType = 'targetOrgUnit'
elif target.lower().startswith('group:'):
targetType = 'targetGroup'
_, target, _ = convertGroupEmailToCloudID(ci, target[6:])
elif target.lower().startswith('orgunit:'):
targetType = 'targetOrgUnit'
target = getCIOrgunitID(cd, target[8:])
elif not target.startswith('inboundSsoAssignments/'):
target = f'inboundSsoAssignments/{target}'
if targetType == 'name':
return _getInboundSSOAssignment(ci, target)
assignments = _getInboundSSOAssignments(ci)
if assignments is not None:
for assignment in assignments:
if targetType in assignment and assignment[targetType] == target:
return assignment
usageErrorExit(Msg.NO_SSO_PROFILE_ASSIGNED.format(targetType, target))
def _getInboundSSOAssignmentArguments(ci, cd, body):
rank = 0
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'rank':
rank = getInteger(minVal=1)
elif myarg == 'mode':
body['ssoMode'] = getChoice(INBOUNDSSO_MODE_CHOICE_MAP, mapChoice=True)
elif myarg == 'profile':
body['samlSsoInfo'] = {'inboundSamlSsoProfile':
_convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))}
elif myarg == 'neverredirect':
body['signInBehavior'] = {'redirectCondition': 'NEVER'}
elif myarg == 'group':
_, body['targetGroup'], _ = convertGroupEmailToCloudID(ci, getString(Cmd.OB_STRING))
elif myarg in {'ou', 'org', 'orgunit'}:
body['targetOrgUnit'] = getCIOrgunitID(cd, getString(Cmd.OB_ORGUNIT_ITEM))
else:
unknownArgumentExit()
if 'ssoMode' not in body:
missingArgumentExit('mode')
if body['ssoMode'] == 'SAML_SSO' and 'samlSsoInfo' not in body:
missingArgumentExit('profile')
if 'targetGroup' in body:
if 'targetOrgUnit' in body:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('group', 'ou|org|orgunit'))
if not rank:
missingArgumentExit('rank')
body['rank'] = rank
return body
def _updateInboundAssignmentTargetNames(ci, cd, assignment):
if 'targetGroup' in assignment:
_, _, assignment['targetGroupEmail'] = convertGroupCloudIDToEmail(ci, assignment['targetGroup'])
elif 'targetOrgUnit' in assignment:
assignment['targetOrgUnitPath'] = convertOrgUnitIDtoPath(cd, assignment['targetOrgUnit'])
def _showInboundSSOAssignment(assignment, FJQC, ci, cd, i=0, count=0):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(assignment), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.INBOUND_SSO_ASSIGNMENT, assignment['name']], i, count)
_updateInboundAssignmentTargetNames(ci, cd, assignment)
Ind.Increment()
showJSON(None, assignment)
Ind.Decrement()
def _processInboundSSOAssignmentResult(result, kvlist, ci, cd, function):
if result['done']:
if 'error' not in result:
if 'response' in result:
_showInboundSSOAssignment(result['response'], None, ci, cd)
else:
entityActionPerformed(kvlist)
else:
entityActionFailedWarning(kvlist, result['error']['message'])
else:
entityActionPerformedMessage(kvlist, Msg.ACTION_IN_PROGRESS.format(f'{function} inboundssoassignment'))
# gam create inboundssoassignment (group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)
# (mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled) [neverredirect]
def doCreateInboundSSOAssignment():
cd = buildGAPIObject(API.DIRECTORY)
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
body = {'customer': normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])}
body = _getInboundSSOAssignmentArguments(ci, cd, body)
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, body['customer']]
try:
result = callGAPI(ci.inboundSsoAssignments(), 'create',
throwReasons=GAPI.CISSO_CREATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
body=body)
_processInboundSSOAssignmentResult(result, kvlist, ci, cd, 'create')
except (GAPI.failedPrecondition, GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam update inboundssoassignment [(group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)]
# [(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled)] [neverredirect]
def doUpdateInboundSSOAssignment():
cd = buildGAPIObject(API.DIRECTORY)
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
name = _getInboundSSOAssignmentName()
body = _getInboundSSOAssignmentArguments(ci,cd, {})
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, name]
try:
result = callGAPI(ci.inboundSsoAssignments(), 'patch',
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name, updateMask=','.join(list(body.keys())), body=body)
_processInboundSSOAssignmentResult(result, kvlist, ci, cd, 'update')
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam delete inboundssoassignment <SSOAssignmentSelector>
def doDeleteInboundSSOAssignment():
cd = buildGAPIObject(API.DIRECTORY)
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
target = getString(Cmd.OB_STRING)
assignment = _getInboundSSOAssignmentByTarget(ci, cd, target)
if assignment is None:
return
name = assignment['name']
checkForExtraneousArguments()
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, name]
try:
result = callGAPI(ci.inboundSsoAssignments(), 'delete',
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
bailOnInternalError=True,
name=name)
_processInboundSSOAssignmentResult(result, kvlist, None, None, 'delete')
except GAPI.notFound:
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning(kvlist, str(e))
# gam info inboundssoassignment <SSOAssignmentSelector> [formatjson]
def doInfoInboundSSOAssignment():
cd = buildGAPIObject(API.DIRECTORY)
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
target = getString(Cmd.OB_STRING)
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
assignment = _getInboundSSOAssignmentByTarget(ci, cd, target)
if assignment is None:
return
name = assignment.get('samlSsoInfo', {}).get('inboundSamlSsoProfile')
if name:
profile = _getInboundSSOProfile(ci, name)
if profile:
assignment['samlSsoInfo']['inboundSamlSsoProfile'] = profile
_showInboundSSOAssignment(assignment, FJQC, ci, cd)
# gam show inboundssoassignment
# [formatjson]
# gam print inboundssoassignment [todrive <ToDriveAttribute>*]
# [[formatjson [quotechar <Character>]]
def doPrintShowInboundSSOAssignments():
cd = buildGAPIObject(API.DIRECTORY)
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
cfilter = f'customer=="{customer}"'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF:
printGettingAllAccountEntities(Ent.INBOUND_SSO_ASSIGNMENT, cfilter)
assignments = _getInboundSSOAssignments(ci)
if assignments is None:
return
if not csvPF:
count = len(assignments)
if not FJQC.formatJSON:
performActionNumItems(count, Ent.INBOUND_SSO_ASSIGNMENT)
Ind.Increment()
i = 0
for assignment in assignments:
i += 1
_showInboundSSOAssignment(assignment, FJQC, ci, cd, i, count)
Ind.Decrement()
else:
for assignment in assignments:
_updateInboundAssignmentTargetNames(ci, cd, assignment)
row = flattenJSON(assignment)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'name': assignment['name'],
'JSON': json.dumps(cleanJSON(assignment),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Inbound SSO Assignments')
SITEVERIFICATION_METHOD_CHOICE_MAP = {
'cname': 'DNS_CNAME',
'dnscname': 'DNS_CNAME',
'dnstxt': 'DNS_TXT',
'txt': 'DNS_TXT',
'text': 'DNS_TXT',
'file': 'FILE',
'site': 'FILE',
}
# gam create verify|verification <DomainName>
def doCreateSiteVerification():
verif = buildGAPIObject(API.SITEVERIFICATION)
a_domain = getString(Cmd.OB_DOMAIN_NAME)
checkForExtraneousArguments()
txt_record = callGAPI(verif.webResource(), 'getToken',
body={'site': {'type': 'INET_DOMAIN', 'identifier': a_domain},
'verificationMethod': 'DNS_TXT'})
printKeyValueList(['TXT Record Name ', a_domain])
printKeyValueList(['TXT Record Value', txt_record['token']])
printBlankLine()
cname_record = callGAPI(verif.webResource(), 'getToken',
body={'site': {'type': 'INET_DOMAIN', 'identifier': a_domain},
'verificationMethod': 'DNS_CNAME'})
cname_token = cname_record['token']
cname_list = cname_token.split(' ')
cname_subdomain = cname_list[0]
cname_value = cname_list[1]
printKeyValueList(['CNAME Record Name ', f'{cname_subdomain}.{a_domain}'])
printKeyValueList(['CNAME Record Value', cname_value])
printBlankLine()
webserver_file_record = callGAPI(verif.webResource(), 'getToken',
body={'site': {'type': 'SITE', 'identifier': f'http://{a_domain}/'},
'verificationMethod': 'FILE'})
webserver_file_token = webserver_file_record['token']
printKeyValueList(['Saving web server verification file to', webserver_file_token])
writeFile(webserver_file_token, f'google-site-verification: {webserver_file_token}', continueOnError=True)
printKeyValueList(['Verification File URL', f'http://{a_domain}/{webserver_file_token}'])
printBlankLine()
webserver_meta_record = callGAPI(verif.webResource(), 'getToken',
body={'site': {'type': 'SITE', 'identifier': f'http://{a_domain}/'},
'verificationMethod': 'META'})
printKeyValueList(['Meta URL', f'//{a_domain}/'])
printKeyValueList(['Meta HTML Header Data', webserver_meta_record['token']])
printBlankLine()
def _showSiteVerificationInfo(site):
printKeyValueList(['Site', site['site']['identifier']])
Ind.Increment()
printKeyValueList(['ID', unquote(site['id'])])
printKeyValueList(['Type', site['site']['type']])
printKeyValueList(['All Owners', None])
if 'owners' in site:
Ind.Increment()
for owner in sorted(site['owners']):
printKeyValueList([owner])
Ind.Decrement()
Ind.Decrement()
DNS_ERROR_CODES_MAP = {
1: 'DNS Query Format Error',
2: 'Server failed to complete the DNS request',
3: 'Domain name does not exist',
4: 'Function not implemented',
5: 'The server refused to answer for the query',
6: 'Name that should not exist, does exist',
7: 'RRset that should not exist, does exist',
8: 'Server not authoritative for the zone',
9: 'Name not in zone'
}
# gam update verify|verification <DomainName> cname|txt|text|file|site
def doUpdateSiteVerification():
def showDNSrecords():
try:
verify_data = callGAPI(verif.webResource(), 'getToken',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_PARAMETER],
body=body)
except (GAPI.badRequest, GAPI.invalidParameter) as e:
printKeyValueList([ERROR, str(e)])
return
printKeyValueList(['Method', verify_data['method']])
if verify_data['method'] in {'DNS_CNAME', 'DNS_TXT'}:
if verify_data['method'] == 'DNS_CNAME':
cname_subdomain, cname_target = verify_data['token'].split(' ')
query_params = {'name': f'{cname_subdomain}.{a_domain}', 'type': 'cname'}
printKeyValueList(['Expected Record',
f'{query_params["name"]} IN CNAME {cname_target}'])
else:
query_params = {'name': f'{a_domain}', 'type': 'txt'}
printKeyValueList(['Expected Record',
f'{query_params["name"]} IN TXT {verify_data["token"]}'])
_, content = getHttpObj().request('https://dns.google/resolve?' + urlencode(query_params), 'GET')
try:
result = json.loads(content.decode(UTF8))
status = result['Status']
if status == 0 and 'Answer' in result:
if verify_data['method'] == 'DNS_CNAME':
printKeyValueList(['DNS Record',
f'{result["Answer"][0]["name"].rstrip(".")} IN CNAME {result["Answer"][0]["data"]}'])
else:
found = False
for answer in result['Answer']:
answer['data'] = answer['data'].strip('"')
if answer['data'].startswith('google-site-verification'):
found = True
printKeyValueList(['DNS Record',
f'{answer["name"].rstrip(".")} IN TXT {answer["data"]}'])
if not found:
printKeyValueList(['DNS Record', 'No matching record found'])
elif status == 0:
systemErrorExit(NETWORK_ERROR_RC, Msg.DOMAIN_NOT_FOUND_IN_DNS)
else:
systemErrorExit(NETWORK_ERROR_RC, DNS_ERROR_CODES_MAP.get(status, f'Unknown error {status}'))
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
systemErrorExit(INVALID_JSON_RC, Msg.INVALID_JSON_INFORMATION)
verif = buildGAPIObject(API.SITEVERIFICATION)
a_domain = getString(Cmd.OB_DOMAIN_NAME)
verificationMethod = getChoice(SITEVERIFICATION_METHOD_CHOICE_MAP, mapChoice=True)
if verificationMethod in {'DNS_TXT', 'DNS_CNAME'}:
verify_type = 'INET_DOMAIN'
identifier = a_domain
showDNS = True
else:
verify_type = 'SITE'
identifier = f'http://{a_domain}/'
showDNS = False
checkForExtraneousArguments()
body = {'site': {'type': verify_type, 'identifier': identifier},
'verificationMethod': verificationMethod}
try:
verify_result = callGAPI(verif.webResource(), 'insert',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_PARAMETER],
verificationMethod=verificationMethod, body=body)
except GAPI.badRequest as e:
printKeyValueList([ERROR, str(e)])
if showDNS:
showDNSrecords()
return
except GAPI.invalidParameter as e:
printKeyValueList([ERROR, str(e)])
return
printKeyValueList(['Verified!'])
if showDNS:
showDNSrecords()
_showSiteVerificationInfo(verify_result)
printKeyValueList([Msg.YOU_CAN_ADD_DOMAIN_TO_ACCOUNT.format(a_domain, GC.Values[GC.DOMAIN])])
# gam info verify|verification
def doInfoSiteVerification():
verif = buildGAPIObject(API.SITEVERIFICATION)
checkForExtraneousArguments()
sites = callGAPIitems(verif.webResource(), 'list', 'items')
if sites:
for site in sorted(sites, key=lambda k: (k['site']['type'], k['site']['identifier'])):
_showSiteVerificationInfo(site)
else:
printKeyValueList(['No Sites Verified.'])
def checkCourseExists(croom, courseId, i=0, count=0, entityType=Ent.COURSE):
courseId = addCourseIdScope(courseId)
try:
result = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='id,ownerId')
return result
except GAPI.notFound:
entityActionFailedWarning([entityType, removeCourseIdScope(courseId)], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
return None
COURSE_MEMBER_ARGUMENTS = ['none', 'all', 'students', 'teachers']
COURSE_STATE_MAPS = {
Cmd.OB_COURSE_STATE_LIST: {
'active': 'ACTIVE',
'archived': 'ARCHIVED',
'provisioned': 'PROVISIONED',
'declined': 'DECLINED',
'suspended': 'SUSPENDED',
},
Cmd.OB_COURSE_ANNOUNCEMENT_STATE_LIST: {
'draft': 'DRAFT',
'published': 'PUBLISHED',
'deleted': 'DELETED',
},
Cmd.OB_COURSE_WORK_STATE_LIST: {
'draft': 'DRAFT',
'published': 'PUBLISHED',
'deleted': 'DELETED',
},
Cmd.OB_COURSE_MATERIAL_STATE_LIST: {
'draft': 'DRAFT',
'published': 'PUBLISHED',
'deleted': 'DELETED',
},
Cmd.OB_COURSE_SUBMISSION_STATE_LIST: {
'new': 'NEW',
'created': 'CREATED',
'turnedin': 'TURNED_IN',
'returned': 'RETURNED',
'reclaimedbystudent': 'RECLAIMED_BY_STUDENT',
},
}
def _getCourseStates(item, states):
stateMap = COURSE_STATE_MAPS[item]
for state in getString(item).lower().replace(',', ' ').split():
if state == 'all':
states.extend([stateMap[state] for state in stateMap])
elif state in stateMap:
states.append(stateMap[state])
else:
invalidChoiceExit(state, stateMap, True)
def _gettingCourseEntityQuery(entityType, courseWorkStates):
query = ''
if courseWorkStates:
query += f'{Ent.Choose(entityType, len(courseWorkStates))}: {",".join(courseWorkStates)}, '
if query:
query = query[:-2]
return query
class CourseAttributes():
def __init__(self, croom, updateMode):
self.croom = croom
self.ocroom = croom
self.tcroom = None
self.updateMode = updateMode
self.body = {}
self.courseId = None
self.ownerId = None
self.markDraftAsPublished = False
self.markPublishedAsDraft = False
self.removeDueDate = False
self.mapShareModeStudentCopy = None
self.copyMaterialsFiles = False
self.members = 'none'
self.teachers = []
self.students = []
self.announcementStates = []
self.courseAnnouncements = []
self.materialStates = []
self.courseMaterials = []
self.workStates = []
self.courseWorks = []
self.individualStudentAnnouncements = 'copy'
self.individualStudentMaterials = 'copy'
self.individualStudentCourseWork = 'copy'
self.copyTopics = False
self.topicsById = {}
self.reversedTopicIdList = []
self.currDateTime = None
self.csvPF = None
COURSE_ANNOUNCEMENT_READONLY_FIELDS = [
'alternateLink',
'courseId',
'creationTime',
'creatorUserId',
'updateTime',
]
COURSE_MATERIAL_READONLY_FIELDS = [
'alternateLink',
'courseId',
'creationTime',
'creatorUserId',
'updateTime',
]
COURSE_COURSEWORK_READONLY_FIELDS = [
'alternateLink',
'assignment',
'associatedWithDeveloper',
'courseId',
'creationTime',
'creatorUserId',
'updateTime',
]
MAX_TITLE_DISPLAY_LENGTH = 34
def trimTitle(self, title):
if len(title) <= self.MAX_TITLE_DISPLAY_LENGTH:
return title
return title[:self.MAX_TITLE_DISPLAY_LENGTH]+'...'
def CleanMaterials(self, body, entityType, entityId):
if 'materials' not in body:
return
materials = body.pop('materials')
body['materials'] = []
for material in materials:
if 'driveFile' in material:
material['driveFile']['driveFile'].pop('title', None)
material['driveFile']['driveFile'].pop('alternateLink', None)
material['driveFile']['driveFile'].pop('thumbnailUrl', None)
if material['driveFile'].get('shareMode', '') == 'STUDENT_COPY' and self.mapShareModeStudentCopy is not None:
material['driveFile']['shareMode'] = self.mapShareModeStudentCopy
body['materials'].append(material)
if self.csvPF and not self.copyMaterialsFiles:
self.csvPF.WriteRow({'courseId': self.courseId, 'ownerId': self.ownerId, 'fileId': material['driveFile']['driveFile']['id']})
elif 'youtubeVideo' in material:
material['youtubeVideo'].pop('title', None)
material['youtubeVideo'].pop('alternateLink', None)
material['youtubeVideo'].pop('thumbnailUrl', None)
body['materials'].append(material)
elif 'link' in material:
material['link'].pop('title', None)
material['link'].pop('thumbnailUrl', None)
body['materials'].append(material)
elif 'form' in material:
action = Act.Get()
Act.Set(Act.COPY)
entityActionNotPerformedWarning([Ent.COURSE, self.courseId, entityType, entityId,
Ent.COURSE_MATERIAL_FORM, self.trimTitle(material['form'].get('title', UNKNOWN))],
Msg.NOT_COPYABLE)
Act.Set(action)
@staticmethod
def CleanAssignments(body):
if 'assignment' in body and 'studentWorkFolder' in body['assignment']:
body['assignment']['studentWorkFolder'].pop('title', None)
body['assignment']['studentWorkFolder'].pop('alternateLink', None)
COURSE_MATERIAL_SHAREMODE_MAP = {
'edit': 'EDIT',
'none': None,
'view': 'VIEW'
}
COURSE_INDIVIDUAL_STUDENT_OPTIONS = {'copy', 'delete', 'maptoall'}
def GetAttributes(self):
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if not self.updateMode and myarg in {'alias', 'id'}:
self.body['id'] = getCourseAlias()
elif myarg == 'name':
self.body['name'] = getString(Cmd.OB_STRING)
elif myarg == 'section':
self.body['section'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'heading', 'descriptionheading'}:
self.body['descriptionHeading'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'description':
self.body['description'] = getStringWithCRsNLs()
elif myarg == 'room':
self.body['room'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'owner', 'ownerid', 'teacher'}:
self.body['ownerId'] = getEmailAddress()
elif myarg in {'state', 'status', 'coursestate'}:
self.body['courseState'] = getChoice(COURSE_STATE_MAPS[Cmd.OB_COURSE_STATE_LIST], mapChoice=True)
elif myarg == 'guardiansenabled':
self.body['guardiansEnabled'] = getBoolean()
elif myarg == 'copyfrom':
self.courseId = getString(Cmd.OB_COURSE_ID)
elif myarg in {'announcementstate', 'announcementstates'}:
_getCourseStates(Cmd.OB_COURSE_ANNOUNCEMENT_STATE_LIST, self.announcementStates)
elif myarg in {'workstate', 'workstates', 'courseworkstate', 'courseworkstates'}:
_getCourseStates(Cmd.OB_COURSE_WORK_STATE_LIST, self.workStates)
elif myarg in {'materialstate', 'materialstates', 'coursematerialstate', 'coursematerialstates'}:
_getCourseStates(Cmd.OB_COURSE_MATERIAL_STATE_LIST, self.materialStates)
elif myarg == 'individualstudentannouncements':
self.individualStudentAnnouncements = getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
elif myarg == 'individualstudentmaterials':
self.individualStudentMaterials = getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
elif myarg == 'individualstudentcoursework':
self.individualStudentCourseWork = getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
elif myarg == 'individualstudentassignments':
self.individualStudentAnnouncements = self.individualStudentMaterials = self.individualStudentCourseWork =\
getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
elif myarg == 'members':
self.members = getChoice(COURSE_MEMBER_ARGUMENTS)
elif myarg == 'markdraftaspublished':
self.markDraftAsPublished = getBoolean()
elif myarg == 'markpublishedasdraft':
self.markPublishedAsDraft = getBoolean()
elif myarg == 'removeduedate':
self.removeDueDate = getBoolean()
elif myarg == 'mapsharemodestudentcopy':
self.mapShareModeStudentCopy = getChoice(self.COURSE_MATERIAL_SHAREMODE_MAP, mapChoice=True)
elif myarg == 'copymaterialsfiles':
self.copyMaterialsFiles = getBoolean()
elif myarg == 'copytopics':
self.copyTopics = getBoolean()
elif myarg == 'logdrivefileids':
if getBoolean():
self.csvPF = CSVPrintFile(['courseId', 'ownerId', 'fileId'])
else:
self.csvPF = None
else:
unknownArgumentExit()
if not self.updateMode:
if 'ownerId' not in self.body:
missingArgumentExit('teacher <UserItem>')
if 'name' not in self.body:
missingArgumentExit('name <String>')
if self.courseId:
copyFromCourseInfo = checkCourseExists(self.croom, self.courseId, entityType=Ent.COPYFROM_COURSE)
if copyFromCourseInfo is None:
return False
self.ownerId = copyFromCourseInfo['ownerId']
if (self.announcementStates or self.materialStates or self.workStates) and self.copyMaterialsFiles:
self.body['courseState'] = 'ACTIVE'
elif self.members != 'none' or self.announcementStates or self.materialStates or self.workStates or self.copyTopics:
missingArgumentExit('copyfrom <CourseID>')
else:
return True
# ocroom - copyfrom course owner
self.ocroom = self.croom
if GC.Values[GC.USE_COURSE_OWNER_ACCESS]:
if self.announcementStates or self.materialStates or self.workStates or self.copyTopics or self.members != 'none':
_, self.ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{self.ownerId}')
if self.ocroom is None:
return False
if self.members != 'none':
_, self.teachers, self.students = _getCourseAliasesMembers(self.croom, self.ocroom, self.courseId, {'members': self.members},
'nextPageToken,teachers(profile(emailAddress,id))',
'nextPageToken,students(profile(emailAddress))')
if self.announcementStates:
printGettingAllEntityItemsForWhom(Ent.COURSE_ANNOUNCEMENT_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
_gettingCourseEntityQuery(Ent.COURSE_ANNOUNCEMENT_STATE, self.announcementStates))
try:
self.courseAnnouncements = callGAPIpages(self.ocroom.courses().announcements(), 'list', 'announcements',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=self.courseId, announcementStates=self.announcementStates,
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for courseAnnouncement in self.courseAnnouncements:
for field in self.COURSE_ANNOUNCEMENT_READONLY_FIELDS:
courseAnnouncement.pop(field, None)
self.CleanMaterials(courseAnnouncement, Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'])
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
return False
if self.materialStates:
printGettingAllEntityItemsForWhom(Ent.COURSE_MATERIAL_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
_gettingCourseEntityQuery(Ent.COURSE_MATERIAL_STATE, self.materialStates))
try:
self.courseMaterials = callGAPIpages(self.ocroom.courses().courseWorkMaterials(), 'list', 'courseWorkMaterial',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=self.courseId, courseWorkMaterialStates=self.materialStates,
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for courseMaterial in self.courseMaterials:
for field in self.COURSE_MATERIAL_READONLY_FIELDS:
courseMaterial.pop(field, None)
if self.markPublishedAsDraft and courseMaterial['state'] == 'PUBLISHED':
courseMaterial['state'] = 'DRAFT'
elif self.markDraftAsPublished and courseMaterial['state'] == 'DRAFT':
courseMaterial['state'] = 'PUBLISHED'
self.CleanMaterials(courseMaterial, Ent.COURSE_MATERIAL_ID, courseMaterial['id'])
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
return False
if self.workStates:
printGettingAllEntityItemsForWhom(Ent.COURSE_WORK_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
_gettingCourseEntityQuery(Ent.COURSE_WORK_STATE, self.workStates))
try:
self.courseWorks = callGAPIpages(self.ocroom.courses().courseWork(), 'list', 'courseWork',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=self.courseId, courseWorkStates=self.workStates,
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for courseWork in self.courseWorks:
for field in self.COURSE_COURSEWORK_READONLY_FIELDS:
courseWork.pop(field, None)
self.CleanMaterials(courseWork, Ent.COURSE_WORK_ID, courseWork['id'])
self.CleanAssignments(courseWork)
if self.markPublishedAsDraft and courseWork['state'] == 'PUBLISHED':
courseWork['state'] = 'DRAFT'
elif self.markDraftAsPublished and courseWork['state'] == 'DRAFT':
courseWork['state'] = 'PUBLISHED'
if self.removeDueDate:
courseWork.pop('dueDate', None)
courseWork.pop('dueTime', None)
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
return False
if self.copyTopics:
printGettingAllEntityItemsForWhom(Ent.COURSE_TOPIC, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0)
try:
courseTopics = callGAPIpages(self.ocroom.courses().topics(), 'list', 'topic',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=self.courseId, fields='nextPageToken,topic(topicId,name)',
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for topic in courseTopics:
self.topicsById[topic['topicId']] = topic['name']
self.reversedTopicIdList.append(topic['topicId'])
self.reversedTopicIdList.reverse()
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
return False
return True
def CopyMaterials(self, drive, newCourseId, body, entityType, entityId, teacherFolderId):
def _copyMaterialsError(fileId, errMsg):
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, entityType, entityId, Ent.COURSE_MATERIAL_DRIVEFILE, ''], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL_DRIVEFILE, fileId], errMsg)
if 'materials' not in body:
return
action = Act.Get()
Act.Set(Act.COPY)
materials = body.pop('materials')
body['materials'] = []
for material in materials:
if 'driveFile' in material:
fileId = material['driveFile']['driveFile']['id']
try:
source = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId,
fields='name,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
'description,mimeType,modifiedTime,properties,starred,driveId,viewedByMeTime,writersCanShare',
supportsAllDrives=True)
if not source.pop('capabilities')['canCopy']:
_copyMaterialsError(fileId, Msg.NOT_COPYABLE)
continue
source['parents'] = [teacherFolderId]
result = callGAPI(drive.files(), 'copy',
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS,
fileId=fileId, body=source, fields='id', supportsAllDrives=True)
material['driveFile']['driveFile']['id'] = result['id']
body['materials'].append(material)
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, entityType, entityId, Ent.COURSE_MATERIAL_DRIVEFILE, result['id']], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL_DRIVEFILE, fileId])
if self.csvPF:
self.csvPF.WriteRow({'courseId': self.courseId, 'ownerId': self.ownerId, 'fileId': result['id']})
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded) as e:
_copyMaterialsError(fileId, str(e))
except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
_copyMaterialsError(fileId, str(e))
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
_copyMaterialsError(fileId, str(e))
break
else:
body['materials'].append(material)
Act.Set(action)
def checkDueDate(self, body):
if 'dueDate' in body and 'dueTime' in body:
try:
return self.currDateTime < datetime.datetime(body['dueDate']['year'], body['dueDate']['month'], body['dueDate']['day'],
body['dueTime'].get('hours', 0), body['dueTime'].get('minutes', 0), tzinfo=iso8601.UTC)
except ValueError:
pass
return False
def getItemIdTitle(self, body):
itemId = body.pop('id')
return self.trimTitle(body.get('title', itemId))
def checkItemCopyable(self, state, newCourseId, entityType, entityId, body, individualStudentOption, clarg, j, jcount):
if state == 'DELETED':
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, entityType, entityId], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId], Msg.DELETED, j, jcount)
return False
if body['assigneeMode'] == 'INDIVIDUAL_STUDENTS':
if individualStudentOption == 'delete':
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, entityType, entityId], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId], f'{clarg} delete', j, jcount)
return False
if individualStudentOption == 'maptoall':
body['assigneeMode'] = 'ALL_STUDENTS'
body.pop('individualStudentsOptions', None)
else: # individualStudentOption == 'copy':
if 'individualStudentsOptions' not in body:
body['assigneeMode'] = 'ALL_STUDENTS'
return True
def CopyAttributes(self, newCourse, i=0, count=0):
newCourseId = newCourse['id']
ownerId = newCourse['ownerId']
teacherFolderId = newCourse['teacherFolder']['id']
# tcroom - new/update course owner
if self.announcementStates or self.materialStates or self.workStates or self.copyTopics:
_, self.tcroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{ownerId}')
if self.tcroom is None:
return
if (self.announcementStates or self.materialStates or self.workStates) and self.copyMaterialsFiles:
_, tdrive = buildGAPIServiceObject(API.DRIVE3, f'uid:{ownerId}')
if tdrive is None:
return
# Adds are done with domain admin
if self.members in {'all', 'students'}:
addParticipants = [student['profile']['emailAddress'] for student in self.students if 'emailAddress' in student['profile']]
_batchAddItemsToCourse(self.croom, newCourseId, i, count, addParticipants, Ent.STUDENT)
if self.members in {'all', 'teachers'}:
addParticipants = [teacher['profile']['emailAddress'] for teacher in self.teachers if teacher['profile']['id'] != ownerId and 'emailAddress' in teacher['profile']]
_batchAddItemsToCourse(self.croom, newCourseId, i, count, addParticipants, Ent.TEACHER)
if self.copyTopics:
try:
newCourseTopics = callGAPIpages(self.tcroom.courses().topics(), 'list', 'topic',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.FAILED_PRECONDITION, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, fields='nextPageToken,topic(topicId,name)',
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
newTopicsByName = {}
for topic in newCourseTopics:
newTopicsByName[topic['name']] = topic['topicId']
except GAPI.notFound as e:
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
return
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.failedPrecondition, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
jcount = len(self.topicsById)
j = 0
for topicId in self.reversedTopicIdList:
topicName = self.topicsById[topicId]
j += 1
if topicName in newTopicsByName:
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, Ent.COURSE_TOPIC, topicName], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId], Msg.DUPLICATE, j, jcount)
continue
try:
result = callGAPI(self.tcroom.courses().topics(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, body={'name': topicName}, fields='topicId')
newTopicsByName[topicName] = result['topicId']
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_TOPIC, topicName], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId], j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
return
except (GAPI.failedPrecondition, GAPI.invalidArgument, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_TOPIC, topicName], str(e), j, jcount)
if self.courseAnnouncements:
jcount = len(self.courseAnnouncements)
j = 0
for courseAnnouncement in self.courseAnnouncements:
j += 1
body = courseAnnouncement.copy()
courseAnnouncementId = self.getItemIdTitle(body)
if not self.checkItemCopyable(courseAnnouncement['state'], newCourseId, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId,
body, self.individualStudentAnnouncements, 'individualstudentannouncements', j, jcount):
continue
if self.copyMaterialsFiles:
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId, teacherFolderId)
try:
result = callGAPI(self.tcroom.courses().announcements(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR, GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, body=body, fields='id')
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_ANNOUNCEMENT_ID, result['id']], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId], j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
return
except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.backendError, GAPI.internalError,
GAPI.permissionDenied, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId], str(e), j, jcount)
if self.courseMaterials:
jcount = len(self.courseMaterials)
j = 0
for courseMaterial in self.courseMaterials:
j += 1
body = courseMaterial.copy()
courseMaterialId = self.getItemIdTitle(body)
if not self.checkItemCopyable(courseMaterial['state'], newCourseId, Ent.COURSE_MATERIAL, courseMaterialId,
body, self.individualStudentMaterials, 'individualstudentmaterials', j, jcount):
continue
if self.copyMaterialsFiles:
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_MATERIAL, courseMaterialId, teacherFolderId)
topicId = body.pop('topicId', None)
if self.copyTopics:
if topicId:
topicName = self.topicsById.get(topicId)
if topicName:
newTopicId = newTopicsByName.get(topicName)
if newTopicId:
body['topicId'] = newTopicId
try:
result = callGAPI(self.tcroom.courses().courseWorkMaterials(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR, GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, body=body, fields='id')
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_MATERIAL_ID, result['id']], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL, courseMaterialId], j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
return
except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.backendError, GAPI.internalError,
GAPI.permissionDenied, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL, courseMaterialId], str(e), j, jcount)
if self.courseWorks:
jcount = len(self.courseWorks)
j = 0
for courseWork in self.courseWorks:
j += 1
body = courseWork.copy()
courseWorkId = self.getItemIdTitle(body)
if not self.checkItemCopyable(courseWork['state'], newCourseId, Ent.COURSE_WORK, courseWorkId,
body, self.individualStudentCourseWork, 'individualstudentcoursework', j, jcount):
continue
if self.copyMaterialsFiles:
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_WORK, courseWorkId, teacherFolderId)
topicId = body.pop('topicId', None)
if self.copyTopics:
if topicId:
topicName = self.topicsById.get(topicId)
if topicName:
newTopicId = newTopicsByName.get(topicName)
if newTopicId:
body['topicId'] = newTopicId
if not self.removeDueDate and not self.checkDueDate(body):
body.pop('dueDate', None)
body.pop('dueTime', None)
try:
result = callGAPI(self.tcroom.courses().courseWork(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR,
GAPI.INTERNAL_ERROR, GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=newCourseId, body=body, fields='id')
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_WORK_ID, result['id']], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_WORK, courseWorkId], j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
return
except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.backendError, GAPI.internalError, GAPI.invalidArgument,
GAPI.permissionDenied, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
[Ent.COURSE, self.courseId, Ent.COURSE_WORK, courseWorkId], str(e), j, jcount)
def CopyFromCourse(self, newCourse, i=0, count=0):
action = Act.Get()
Act.Set(Act.COPY)
entityPerformActionModifierItemValueList([Ent.COURSE, newCourse['id']], Act.MODIFIER_FROM, [Ent.COURSE, self.courseId], i, count)
Ind.Increment()
if not self.removeDueDate:
self.currDateTime = datetime.datetime.now(iso8601.UTC)
self.CopyAttributes(newCourse, i, count)
if self.csvPF:
self.csvPF.writeCSVfile('Course Drive File IDs')
Ind.Decrement()
Act.Set(action)
# gam create course [id|alias <CourseAlias>] <CourseAttribute>*
# [copyfrom <CourseID>
# [announcementstates <CourseAnnouncementStateList>]
# [individualstudentannouncements copy|delete|maptoall]
# [materialstates <CourseMaterialStateList>]
# [individualstudentmaterials copy|delete|maptoall]
# [workstates <CourseWorkStateList>]
# [individualstudentcoursework copy|delete|maptoall]
# [removeduedate [<Boolean>]]
# [mapsharemodestudentcopy edit|none|view]
# [individualstudentassignments copy|delete|maptoall]
# [copymaterialsfiles [<Boolean>]]
# [copytopics [<Boolean>]]
# [markpublishedasdraft [<Boolean>]] [markdraftaspublished [<Boolean>]]
# [members none|all|students|teachers]]
# [logdrivefileids [<Boolean>]]
def doCreateCourse():
croom = buildGAPIObject(API.CLASSROOM)
courseAttributes = CourseAttributes(croom, False)
if not courseAttributes.GetAttributes():
return
try:
result = callGAPI(croom.courses(), 'create',
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
body=courseAttributes.body, fields='id,name,ownerId,courseState,teacherFolder(id)')
entityActionPerformed([Ent.COURSE_NAME, result['name'], Ent.COURSE, result['id']])
if courseAttributes.courseId:
courseAttributes.CopyFromCourse(result)
except (GAPI.alreadyExists, GAPI.notFound, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.forbidden, GAPI.badRequest, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE_NAME, courseAttributes.body['name'], Ent.TEACHER, courseAttributes.body['ownerId']], str(e))
def _doUpdateCourses(entityList):
croom = buildGAPIObject(API.CLASSROOM)
courseAttributes = CourseAttributes(croom, True)
if not courseAttributes.GetAttributes():
return
i = 0
count = len(entityList)
for course in entityList:
i += 1
courseId = addCourseIdScope(course)
body = courseAttributes.body.copy()
newOwner = body.get('ownerId')
modifier = Act.MODIFIER_WITH_COTEACHER_OWNER
complete = False
while not complete:
complete = True
try:
if body:
result = callGAPI(croom.courses(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION,
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT,
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, body=body, updateMask=','.join(list(body)), fields='id,name,ownerId,courseState,teacherFolder(id)')
else:
result = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='id,name,ownerId,courseState,teacherFolder(id)')
if courseAttributes.body:
if not newOwner:
entityActionPerformed([Ent.COURSE_NAME, result['name'], Ent.COURSE, result['id']], i, count)
else:
entityModifierNewValueActionPerformed([Ent.COURSE_NAME, result['name'], Ent.COURSE, result['id']],
modifier, newOwner, i, count)
if courseAttributes.courseId:
courseAttributes.CopyFromCourse(result, i, count)
except (GAPI.notFound, GAPI.permissionDenied,
GAPI.forbidden, GAPI.badRequest, GAPI.invalidArgument,
GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
except GAPI.failedPrecondition as e:
errMsg = str(e)
if newOwner and '@UserAlreadyOwner Cannot transfer course to the user who is already the owner' in errMsg:
## Handle trying to update current owner to owner, we're done if nothing else was being updated
body.pop('ownerId')
modifier = Act.MODIFIER_WITH_CURRENT_OWNER
complete = False
elif newOwner and '@IneligibleOwner Only a co-teacher can be invited as owner of the course' in errMsg:
## Add new owner as teacher, then go back and do update
action = Act.Get()
Act.Set(Act.ADD)
try:
callGAPI(croom.courses().teachers(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, body={'userId': newOwner}, fields='')
modifier = Act.MODIFIER_WITH_NEW_TEACHER_OWNER
time.sleep(10)
complete = False
except (GAPI.notFound, GAPI.backendError, GAPI.forbidden):
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], getPhraseDNEorSNA(newOwner), i, count)
except GAPI.alreadyExists:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], Msg.DUPLICATE, i, count)
except GAPI.failedPrecondition:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], Msg.NOT_ALLOWED, i, count)
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as ei:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], str(ei), i, count)
Act.Set(action)
else:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
# gam update courses <CourseEntity> <CourseAttribute>+
# [copyfrom <CourseID>
# [announcementstates <CourseAnnouncementStateList>]
# [individualstudentannouncements copy|delete|maptoall]
# [materialstates <CourseMaterialStateList>]
# [individualstudentmaterials copy|delete|maptoall]
# [workstates <CourseWorkStateList>]
# [individualstudentcoursework copy|delete|maptoall]
# [removeduedate [<Boolean>]]
# [mapsharemodestudentcopy edit|none|view]
# [individualstudentassignments copy|delete|maptoall]
# [copymaterialsfiles [<Boolean>]]
# [copytopics [<Boolean>]]
# [markpublishedasdraft [<Boolean>]] [markdraftaspublished [<Boolean>]]
# [members none|all|students|teachers]]
# [logdrivefileids [<Boolean>]]
def doUpdateCourses():
_doUpdateCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
# gam update course <CourseID> <CourseAttribute>+
# [copyfrom <CourseID>
# [announcementstates <CourseAnnouncementStateList>]
# [individualstudentannouncements copy|delete|maptoall]
# [materialstates <CourseMaterialStateList>]
# [individualstudentmaterials copy|delete|maptoall]
# [workstates <CourseWorkStateList>]
# [individualstudentcoursework copy|delete|maptoall]
# [removeduedate [<Boolean>]]
# [mapsharemodestudentcopy edit|none|view]
# [individualstudentassignments copy|delete|maptoall]
# [copymaterialsfiles [<Boolean>]]
# [copytopics [<Boolean>]]
# [markpublishedasdraft [<Boolean>]] [markdraftaspublished [<Boolean>]]
# [members none|all|students|teachers]]
def doUpdateCourse():
_doUpdateCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
def _doDeleteCourses(entityList):
croom = buildGAPIObject(API.CLASSROOM)
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'archive', 'archived'}:
body['courseState'] = 'ARCHIVED'
updateMask = 'courseState'
else:
unknownArgumentExit()
i = 0
count = len(entityList)
for course in entityList:
i += 1
courseId = addCourseIdScope(course)
try:
if body:
callGAPI(croom.courses(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION,
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT,
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, body=body, updateMask=updateMask, fields='')
callGAPI(croom.courses(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION, GAPI.INTERNAL_ERROR],
id=courseId)
entityActionPerformed([Ent.COURSE, removeCourseIdScope(courseId)], i, count)
except (GAPI.notFound, GAPI.permissionDenied, GAPI.failedPrecondition,
GAPI.forbidden, GAPI.badRequest, GAPI.invalidArgument,
GAPI.internalError, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
# gam delete courses <CourseEntity> [archive|archived]
def doDeleteCourses():
_doDeleteCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
# gam delete course <CourseID> [archive|archived]
def doDeleteCourse():
_doDeleteCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
COURSE_FIELDS_CHOICE_MAP = {
'alternatelink': 'alternateLink',
'calendarid': 'calendarId',
'coursegroupemail': 'courseGroupEmail',
'coursematerialsets': 'courseMaterialSets',
'coursestate': 'courseState',
'creationtime': 'creationTime',
'description': 'description',
'descriptionheading': 'descriptionHeading',
'enrollmentcode': 'enrollmentCode',
'gradebooksettings': 'gradebookSettings',
'guardiansenabled': 'guardiansEnabled',
'heading': 'descriptionHeading',
'id': 'id',
'name': 'name',
'owneremail': 'ownerId',
'ownerid': 'ownerId',
'room': 'room',
'section': 'section',
'teacherfolder': 'teacherFolder',
'teachergroupemail': 'teacherGroupEmail',
'updatetime': 'updateTime',
}
COURSE_TIME_OBJECTS = {'creationTime', 'updateTime'}
COURSE_NOLEN_OBJECTS = {'materials'}
COURSE_PROPERTY_PRINT_ORDER = [
'id',
'name',
'Aliases',
'courseState',
'descriptionHeading',
'description',
'section',
'room',
'enrollmentCode',
'guardiansEnabled',
'alternateLink',
'ownerEmail',
'ownerId',
'creationTime',
'updateTime',
'calendarId',
'courseGroupEmail',
'teacherGroupEmail',
'teacherFolder.id',
'teacherFolder.title',
'teacherFolder.alternateLink',
]
def _initCourseShowProperties(fields=None):
return {'aliases': False, 'aliasesInColumns': False, 'ownerEmail': False, 'ownerEmailMatchPattern': None, 'members': 'none', 'countsOnly': False,
'fields': fields if fields is not None else [], 'skips': []}
def _getCourseShowProperties(myarg, courseShowProperties):
if myarg in {'alias', 'aliases'}:
courseShowProperties['aliases'] = True
courseShowProperties['aliasesInColumns'] = False
elif myarg == 'aliasesincolumns':
courseShowProperties['aliases'] = True
courseShowProperties['aliasesInColumns'] = True
elif myarg == 'owneremail':
courseShowProperties['ownerEmail'] = True
elif myarg == 'owneremailmatchpattern':
courseShowProperties['ownerEmail'] = True
courseShowProperties['ownerEmailMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'show':
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
elif myarg == 'countsonly':
courseShowProperties['countsOnly'] = True
elif myarg == 'fields':
for field in _getFieldsList():
if field in {'alias', 'aliases'}:
courseShowProperties['aliases'] = True
courseShowProperties['aliasesInColumns'] = False
elif field == 'aliasesincolumns':
courseShowProperties['aliases'] = True
courseShowProperties['aliasesInColumns'] = True
elif field == 'owneremail':
courseShowProperties['ownerEmail'] = True
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
elif field == 'teachers':
if courseShowProperties['members'] == 'none':
courseShowProperties['members'] = field
elif courseShowProperties['members'] == 'students':
courseShowProperties['members'] = 'all'
elif field == 'students':
if courseShowProperties['members'] == 'none':
courseShowProperties['members'] = field
elif courseShowProperties['members'] == 'teachers':
courseShowProperties['members'] = 'all'
elif field in COURSE_FIELDS_CHOICE_MAP:
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, COURSE_FIELDS_CHOICE_MAP, True)
elif myarg == 'skipfields':
for field in _getFieldsList():
if field in {'alias', 'aliases'}:
courseShowProperties['aliases'] = False
elif field == 'teachers':
if courseShowProperties['members'] == 'all':
courseShowProperties['members'] = 'students'
elif courseShowProperties['members'] == field:
courseShowProperties['members'] = 'none'
elif field == 'students':
if courseShowProperties['members'] == 'all':
courseShowProperties['members'] = 'teachers'
elif courseShowProperties['members'] == field:
courseShowProperties['members'] = 'none'
elif field in COURSE_FIELDS_CHOICE_MAP:
if field != 'id':
courseShowProperties['skips'].append(COURSE_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, COURSE_FIELDS_CHOICE_MAP, True)
else:
return False
return True
def _setCourseFields(courseShowProperties, pagesMode, getOwnerId=False):
if not courseShowProperties['fields']:
return None
courseShowProperties['fields'].append('id')
if courseShowProperties['ownerEmail'] or getOwnerId:
courseShowProperties['fields'].append('ownerId')
if not pagesMode:
return ','.join(set(courseShowProperties['fields']))
return f'nextPageToken,courses({",".join(set(courseShowProperties["fields"]))})'
def _convertCourseUserIdToEmail(croom, userId, emails, entityValueList, i, count):
userEmail = emails.get(userId)
if userEmail is None:
try:
userEmail = callGAPI(croom.userProfiles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userId=userId, fields='emailAddress').get('emailAddress')
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable):
pass
if userEmail is None:
entityDoesNotHaveItemWarning(entityValueList, i, count)
userEmail = 'Unknown user'
emails[userId] = userEmail
return userEmail
def _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess, addCIIdScope=True):
coursesInfo = {}
for courseId in courseIds:
ciCourseId = courseId
courseId = addCourseIdScope(courseId)
if addCIIdScope:
ciCourseId = courseId
if courseId not in coursesInfo:
try:
course = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='name,ownerId')
if useOwnerAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
else:
ocroom = croom
if ocroom is not None:
coursesInfo[ciCourseId] = {'name': course['name'], 'croom': ocroom}
except GAPI.notFound:
entityDoesNotExistWarning(Ent.COURSE, courseId)
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
return 0, len(coursesInfo), coursesInfo
def _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, showGettings=False, i=0, count=0):
aliases = []
teachers = []
students = []
if not showGettings:
pageMessage = None
if courseShowProperties.get('aliases'):
if showGettings:
printGettingEntityItemForWhom(Ent.ALIAS, formatKeyValueList('', [Ent.Singular(Ent.COURSE), courseId], currentCount(i, count)))
pageMessage = getPageMessage()
try:
aliases = callGAPIpages(croom.courses().aliases(), 'list', 'aliases',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE, GAPI.NOT_IMPLEMENTED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.notImplemented):
pass
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
if showGettings:
printGettingEntityItemForWhom(Ent.TEACHER, formatKeyValueList('', [Ent.Singular(Ent.COURSE), courseId], currentCount(i, count)))
pageMessage = getPageMessage()
try:
teachers = callGAPIpages(ocroom.courses().teachers(), 'list', 'teachers',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, fields=teachersFields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
except (GAPI.notFound, GAPI.serviceNotAvailable):
pass
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
if courseShowProperties['members'] != 'teachers':
if showGettings:
printGettingEntityItemForWhom(Ent.STUDENT, formatKeyValueList('', [Ent.Singular(Ent.COURSE), courseId], currentCount(i, count)))
pageMessage = getPageMessage()
try:
students = callGAPIpages(ocroom.courses().students(), 'list', 'students',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, fields=studentsFields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
except (GAPI.notFound, GAPI.serviceNotAvailable):
pass
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
return (aliases, teachers, students)
def _doInfoCourses(courseIdList):
croom = buildGAPIObject(API.CLASSROOM)
courseShowProperties = _initCourseShowProperties()
courseShowProperties['ownerEmail'] = True
ownerEmails = {}
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getCourseShowProperties(myarg, courseShowProperties):
pass
elif myarg in OWNER_ACCESS_OPTIONS:
useOwnerAccess = True
else:
FJQC.GetFormatJSON(myarg)
fields = _setCourseFields(courseShowProperties, False)
if courseShowProperties['members'] != 'none':
if courseShowProperties['countsOnly']:
teachersFields = 'nextPageToken,teachers(profile(id))'
studentsFields = 'nextPageToken,students(profile(id))'
else:
teachersFields = 'nextPageToken,teachers(profile)'
studentsFields = 'nextPageToken,students(profile)'
else:
teachersFields = studentsFields = None
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess)
for courseId, courseInfo in coursesInfo.items():
i += 1
try:
course = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields=fields)
if courseShowProperties['ownerEmail']:
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
[Ent.COURSE, course['id'], Ent.OWNER_ID, course['ownerId']], i, count)
aliases, teachers, students = _getCourseAliasesMembers(croom, courseInfo['croom'], courseId, courseShowProperties, teachersFields, studentsFields)
if FJQC.formatJSON:
if courseShowProperties['aliases']:
course.update({'aliases': list(aliases)})
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
if not courseShowProperties['countsOnly']:
course.update({'teachers': list(teachers)})
else:
course.update({'teachers': len(teachers)})
if courseShowProperties['members'] != 'teachers':
if not courseShowProperties['countsOnly']:
course.update({'students': list(students)})
else:
course.update({'students': len(students)})
printLine(json.dumps(cleanJSON(course, skipObjects=courseShowProperties['skips'],
timeObjects=COURSE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
continue
printEntity([Ent.COURSE, course['id']], i, count)
Ind.Increment()
showJSON(None, course, courseShowProperties['skips'], COURSE_TIME_OBJECTS)
if courseShowProperties['aliases']:
printKeyValueList(['Aliases', len(aliases)])
Ind.Increment()
for alias in aliases:
printKeyValueList([removeCourseAliasScope(alias['alias'])])
Ind.Decrement()
if courseShowProperties['members'] != 'none':
printKeyValueList(['Participants', None])
Ind.Increment()
if courseShowProperties['members'] != 'students':
if teachers:
printKeyValueList(['Teachers', len(teachers)])
if not courseShowProperties['countsOnly']:
Ind.Increment()
for teacher in teachers:
if 'emailAddress' in teacher['profile']:
printKeyValueList([f'{teacher["profile"]["name"]["fullName"]} - {teacher["profile"]["emailAddress"]}'])
else:
printKeyValueList([teacher['profile']['name']['fullName']])
Ind.Decrement()
if courseShowProperties['members'] != 'teachers':
if students:
printKeyValueList(['Students', len(students)])
if not courseShowProperties['countsOnly']:
Ind.Increment()
for student in students:
if 'emailAddress' in student['profile']:
printKeyValueList([f'{student["profile"]["name"]["fullName"]} - {student["profile"]["emailAddress"]}'])
else:
printKeyValueList([student['profile']['name']['fullName']])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
except GAPI.notFound:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
# gam info courses <CourseEntity> [owneraccess]
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
def doInfoCourses():
_doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
# gam info course <CourseID> [owneraccess]
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
def doInfoCourse():
_doInfoCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
def _initCourseSelectionParameters():
return {'courseIds': [], 'teacherId': None, 'studentId': None, 'courseStates': []}
def _getCourseSelectionParameters(myarg, courseSelectionParameters):
if myarg in {'course', 'courses', 'class', 'classes'}:
courseSelectionParameters['courseIds'].extend(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
elif myarg == 'teacher':
courseSelectionParameters['teacherId'] = getEmailAddress()
elif myarg == 'student':
courseSelectionParameters['studentId'] = getEmailAddress()
elif myarg in {'state', 'states', 'status'}:
_getCourseStates(Cmd.OB_COURSE_STATE_LIST, courseSelectionParameters['courseStates'])
else:
return False
return True
COURSE_CU_FILTER_FIELDS_MAP = {'creationtime': 'creationTime', 'updatetime': 'updateTime'}
COURSE_CUS_FILTER_FIELDS_MAP = {'creationtime': 'creationTime', 'updatetime': 'updateTime', 'scheduledtime': 'scheduledTime'}
COURSE_U_FILTER_FIELDS_MAP = {'updatetime': 'updateTime'}
COURSE_START_ARGUMENTS = ['start', 'startdate', 'oldestdate']
COURSE_END_ARGUMENTS = ['end', 'enddate']
def _initCourseItemFilter():
return {'timefilter': None, 'startTime': None, 'endTime': None}
def _getCourseItemFilter(myarg, courseItemFilter, courseFilterFields):
if myarg == 'timefilter':
courseItemFilter['timefilter'] = getChoice(courseFilterFields, mapChoice=True)
elif myarg in COURSE_START_ARGUMENTS:
courseItemFilter['startTime'], _, _ = getTimeOrDeltaFromNow(True)
elif myarg in COURSE_END_ARGUMENTS:
courseItemFilter['endTime'], _, _ = getTimeOrDeltaFromNow(True)
else:
return False
return True
def _setApplyCourseItemFilter(courseItemFilter, fieldsList):
if courseItemFilter['timefilter'] and (courseItemFilter['startTime'] or courseItemFilter['endTime']):
if fieldsList:
fieldsList.append(courseItemFilter['timefilter'])
return True
return False
def _courseItemPassesFilter(item, courseItemFilter):
timeStr = item.get(courseItemFilter['timefilter'])
if not timeStr:
return False
startTime = courseItemFilter['startTime']
endTime = courseItemFilter['endTime']
timeValue, _ = iso8601.parse_date(timeStr)
return ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime))
def _gettingCoursesQuery(courseSelectionParameters):
query = ''
if courseSelectionParameters['teacherId']:
query += f'{Ent.Singular(Ent.TEACHER)}: {courseSelectionParameters["teacherId"]}, '
if courseSelectionParameters['studentId']:
query += f'{Ent.Singular(Ent.STUDENT)}: {courseSelectionParameters["studentId"]}, '
if courseSelectionParameters['courseStates']:
query += f'{Ent.Choose(Ent.COURSE_STATE, len(courseSelectionParameters["courseStates"]))}: {",".join(courseSelectionParameters["courseStates"])}, '
if query:
query = query[:-2]
return query
def _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getOwnerId=False):
if not courseSelectionParameters['courseIds']:
fields = _setCourseFields(courseShowProperties, True, getOwnerId)
printGettingAllAccountEntities(Ent.COURSE, _gettingCoursesQuery(courseSelectionParameters))
try:
return callGAPIpages(croom.courses(), 'list', 'courses',
pageMessage=getPageMessage(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
teacherId=courseSelectionParameters['teacherId'],
studentId=courseSelectionParameters['studentId'],
courseStates=courseSelectionParameters['courseStates'],
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
except (GAPI.invalid, GAPI.notFound):
if (not courseSelectionParameters['studentId']) and courseSelectionParameters['teacherId']:
entityUnknownWarning(Ent.TEACHER, courseSelectionParameters['teacherId'])
elif (not courseSelectionParameters['teacherId']) and courseSelectionParameters['studentId']:
entityUnknownWarning(Ent.STUDENT, courseSelectionParameters['studentId'])
elif courseSelectionParameters['studentId'] and courseSelectionParameters['teacherId']:
entityOrEntityUnknownWarning(Ent.TEACHER, courseSelectionParameters['teacherId'], Ent.STUDENT, courseSelectionParameters['studentId'])
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.badRequest, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, None], str(e))
return None
fields = _setCourseFields(courseShowProperties, False, getOwnerId)
coursesInfo = []
for courseId in courseSelectionParameters['courseIds']:
courseId = addCourseIdScope(courseId)
try:
info = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields=fields)
coursesInfo.append(info)
except GAPI.notFound:
entityDoesNotExistWarning(Ent.COURSE, courseId)
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
except GAPI.forbidden:
ClientAPIAccessDeniedExit()
return coursesInfo
# gam print courses [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
# [owneremail] [owneremailmatchpattern <REMatchPattern>]
# [alias|aliases|aliasesincolumns [delimiter <Character>]]
# [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [showitemcountonly] [formatjson [quotechar <Character>]]
def doPrintCourses():
def _saveParticipants(course, participants, role, rtitles):
jcount = len(participants)
course[role] = jcount
if courseShowProperties['countsOnly']:
return
j = 0
for member in participants:
memberTitles = []
prefix = f'{role}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}.'
profile = member['profile']
emailAddress = profile.get('emailAddress')
if emailAddress:
memberTitle = prefix+'emailAddress'
course[memberTitle] = emailAddress
memberTitles.append(memberTitle)
memberId = profile.get('id')
if memberId:
memberTitle = prefix+'id'
course[memberTitle] = memberId
memberTitles.append(memberTitle)
fullName = profile.get('name', {}).get('fullName')
if fullName:
memberTitle = prefix+'name.fullName'
course[memberTitle] = fullName
memberTitles.append(memberTitle)
for title in memberTitles:
if title not in rtitles['set']:
rtitles['set'].add(title)
rtitles['list'].append(title)
j += 1
croom = buildGAPIObject(API.CLASSROOM)
csvPF = CSVPrintFile('id')
FJQC = FormatJSONQuoteChar(csvPF)
courseSelectionParameters = _initCourseSelectionParameters()
courseItemFilter = _initCourseItemFilter()
courseShowProperties = _initCourseShowProperties()
ownerEmails = {}
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showItemCountOnly = False
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CU_FILTER_FIELDS_MAP):
pass
elif myarg == 'delimiter':
delimiter = getCharacter()
elif _getCourseShowProperties(myarg, courseShowProperties):
pass
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, None)
if applyCourseItemFilter:
if courseShowProperties['fields']:
courseShowProperties['fields'].append(courseItemFilter['timefilter'])
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
if coursesInfo is None:
if showItemCountOnly:
writeStdout('0\n')
return
if courseShowProperties['aliases']:
if FJQC.formatJSON:
csvPF.AddJSONTitles('JSON-aliases')
if courseShowProperties['members'] != 'none':
ttitles = {'set': set(), 'list': []}
stitles = {'set': set(), 'list': []}
if courseShowProperties['members'] != 'students':
ttitles['set'].add('teachers')
ttitles['list'].append('teachers')
if FJQC.formatJSON:
csvPF.AddJSONTitles('JSON-teachers')
if courseShowProperties['members'] != 'teachers':
stitles['set'].add('students')
stitles['list'].append('students')
if FJQC.formatJSON:
csvPF.AddJSONTitles('JSON-students')
if courseShowProperties['countsOnly']:
teachersFields = 'nextPageToken,teachers(profile(id))'
studentsFields = 'nextPageToken,students(profile(id))'
else:
teachersFields = 'nextPageToken,teachers(profile)'
studentsFields = 'nextPageToken,students(profile)'
else:
teachersFields = studentsFields = None
itemCount = 0
count = len(coursesInfo)
i = 0
for course in coursesInfo:
i += 1
if applyCourseItemFilter and not _courseItemPassesFilter(course, courseItemFilter):
continue
for field in courseShowProperties['skips']:
course.pop(field, None)
courseId = course['id']
if useOwnerAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
if not ocroom:
continue
else:
ocroom = croom
if courseShowProperties['ownerEmail']:
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
[Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count)
if courseShowProperties['ownerEmailMatchPattern'] and not courseShowProperties['ownerEmailMatchPattern'].match(course['ownerEmail']):
continue
if showItemCountOnly:
itemCount += 1
continue
aliases, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
if courseShowProperties['aliases']:
if not courseShowProperties['aliasesInColumns']:
course['Aliases'] = delimiter.join([removeCourseAliasScope(alias['alias']) for alias in aliases])
else:
course['Aliases'] = [removeCourseAliasScope(alias['alias']) for alias in aliases]
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
_saveParticipants(course, teachers, 'teachers', ttitles)
if courseShowProperties['members'] != 'teachers':
_saveParticipants(course, students, 'students', stitles)
row = flattenJSON(course, timeObjects=COURSE_TIME_OBJECTS, noLenObjects=COURSE_NOLEN_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'id': courseId, 'JSON': json.dumps(cleanJSON(course, timeObjects=COURSE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)}
if courseShowProperties['aliases']:
row['JSON-aliases'] = json.dumps(list(aliases))
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
if not courseShowProperties['countsOnly']:
row['JSON-teachers'] = json.dumps(list(teachers))
else:
row['JSON-teachers'] = json.dumps(len(teachers))
if courseShowProperties['members'] != 'teachers':
if not courseShowProperties['countsOnly']:
row['JSON-students'] = json.dumps(list(students))
else:
row['JSON-students'] = json.dumps(len(students))
csvPF.WriteRowNoFilter(row)
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
if not FJQC.formatJSON:
if courseShowProperties['aliases']:
csvPF.AddTitles('Aliases')
csvPF.SetSortTitles(COURSE_PROPERTY_PRINT_ORDER)
if courseShowProperties['aliases'] and courseShowProperties['aliasesInColumns']:
csvPF.FixCourseAliasesTitles()
csvPF.SortTitles()
csvPF.SetSortTitles([])
if courseShowProperties['members'] != 'none':
csvPF.RearrangeCourseTitles(ttitles, stitles)
csvPF.writeCSVfile('Courses')
def _printCourseItemCount(course, results, title, applyCourseItemFilter, courseItemFilter, csvPF):
if applyCourseItemFilter:
numItems = 0
for item in results:
if _courseItemPassesFilter(item, courseItemFilter):
numItems += 1
else:
numItems = len(results)
if csvPF:
csvPF.WriteRowTitles({'courseId': course['id'], 'courseName': course['name'], title: numItems})
return numItems
COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP = {
'alternatelink': 'alternateLink',
'announcementid': 'id',
'assigneemode': 'assigneeMode',
'courseid': 'courseId',
'courseannouncementid': 'id',
'creationtime': 'creationTime',
'creator': 'creatorUserId',
'creatoruserid': 'creatorUserId',
'id': 'id',
'individualstudentsoptions': 'individualStudentsOptions',
'materials': 'materials',
'scheduledtime': 'scheduledTime',
'state': 'state',
'text': 'text',
'updatetime': 'updateTime',
}
COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP = {
'updatetime': 'updateTime',
'updatedate': 'updateTime',
}
COURSE_ANNOUNCEMENTS_TIME_OBJECTS = {'creationTime', 'scheduledTime', 'updateTime'}
COURSE_ANNOUNCEMENTS_SORT_TITLES = ['courseId', 'courseName', 'id', 'text', 'state']
COURSE_ANNOUNCEMENTS_INDEXED_TITLES = ['materials']
# gam print course-announcements [todrive <ToDriveAttribute>*]
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# (announcementids <CourseAnnouncementIDEntity>)|((announcementstates <CourseAnnouncementStateList>)*
# (orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
# [showcreatoremails|creatoremail] [fields <CourseAnnouncementFieldNameList>]
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [countsonly] [formatjson [quotechar <Character>]]
def doPrintCourseAnnouncements():
def _printCourseAnnouncement(course, courseAnnouncement, i, count):
if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter):
return
if showCreatorEmail:
courseAnnouncement['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseAnnouncement['creatorUserId'], creatorEmails,
[Ent.COURSE, course['id'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'],
Ent.CREATOR_ID, courseAnnouncement['creatorUserId']], i, count)
row = flattenJSON(courseAnnouncement, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
'JSON': json.dumps(cleanJSON(courseAnnouncement, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
croom = buildGAPIObject(API.CLASSROOM)
csvPF = CSVPrintFile(['courseId', 'courseName'], COURSE_ANNOUNCEMENTS_SORT_TITLES, COURSE_ANNOUNCEMENTS_INDEXED_TITLES)
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
courseSelectionParameters = _initCourseSelectionParameters()
courseItemFilter = _initCourseItemFilter()
courseShowProperties = _initCourseShowProperties(['name'])
courseAnnouncementIds = []
courseAnnouncementStates = []
OBY = OrderBy(COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP)
creatorEmails = {}
countsOnly = showCreatorEmail = False
items = 'courseAnnouncements'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CUS_FILTER_FIELDS_MAP):
pass
elif myarg in {'announcementid', 'announcementids'}:
courseAnnouncementIds = getEntityList(Cmd.OB_COURSE_ANNOUNCEMENT_ID_ENTITY)
elif myarg in {'announcementstate', 'announcementstates'}:
_getCourseStates(Cmd.OB_COURSE_ANNOUNCEMENT_STATE_LIST, courseAnnouncementStates)
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg in {'showcreatoremails', 'creatoremail'}:
showCreatorEmail = True
elif getFieldsList(myarg, COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
elif myarg == 'countsonly':
countsOnly = True
csvPF.AddTitles(items)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
if coursesInfo is None:
return
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
if showCreatorEmail and fieldsList:
fieldsList.append('creatorUserId')
courseAnnouncementIdsLists = courseAnnouncementIds if isinstance(courseAnnouncementIds, dict) else None
i = 0
count = len(coursesInfo)
for course in coursesInfo:
i += 1
courseId = course['id']
if courseAnnouncementIdsLists:
courseAnnouncementIds = courseAnnouncementIdsLists[courseId]
if not courseAnnouncementIds:
fields = getItemFieldsFromFieldsList('announcements', fieldsList)
printGettingAllEntityItemsForWhom(Ent.COURSE_ANNOUNCEMENT_ID, Ent.TypeName(Ent.COURSE, courseId), i, count,
_gettingCourseEntityQuery(Ent.COURSE_ANNOUNCEMENT_STATE, courseAnnouncementStates))
try:
results = callGAPIpages(croom.courses().announcements(), 'list', 'announcements',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, announcementStates=courseAnnouncementStates, orderBy=OBY.orderBy,
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
if not countsOnly:
for courseAnnouncement in results:
_printCourseAnnouncement(course, courseAnnouncement, i, count)
else:
_printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, csvPF)
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
else:
jcount = len(courseAnnouncementIds)
if jcount == 0:
continue
fields = f'{",".join(set(fieldsList))}' if fieldsList else None
j = 0
for courseAnnouncementId in courseAnnouncementIds:
j += 1
try:
courseAnnouncement = callGAPI(croom.courses().announcements(), 'get',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, id=courseAnnouncementId, fields=fields)
_printCourseAnnouncement(course, courseAnnouncement, i, count)
except GAPI.notFound:
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncementId], j, jcount)
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncementId], str(e), j, jcount)
csvPF.writeCSVfile('Course Announcements')
COURSE_TOPICS_TIME_OBJECTS = {'updateTime'}
COURSE_TOPICS_SORT_TITLES = ['courseId', 'courseName', 'topicId', 'name', 'updateTime']
# gam print course-topics [todrive <ToDriveAttribute>*]
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# [topicids <CourseTopicIDEntity>]
# [timefilter updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [countsonly] [formatjson [quotechar <Character>]]
def doPrintCourseTopics():
def _printCourseTopic(course, courseTopic):
if applyCourseItemFilter and not _courseItemPassesFilter(courseTopic, courseItemFilter):
return
row = flattenJSON(courseTopic, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_TOPICS_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
'JSON': json.dumps(cleanJSON(courseTopic, timeObjects=COURSE_TOPICS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
croom = buildGAPIObject(API.CLASSROOM)
csvPF = CSVPrintFile(['courseId', 'courseName'], COURSE_TOPICS_SORT_TITLES)
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = ['topicId', 'name', 'updateTime']
courseSelectionParameters = _initCourseSelectionParameters()
courseItemFilter = _initCourseItemFilter()
courseShowProperties = _initCourseShowProperties(['name'])
courseTopicIds = []
countsOnly = False
items = 'courseTopics'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_U_FILTER_FIELDS_MAP):
pass
elif myarg in {'topicid', 'topicids'}:
courseTopicIds = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY)
elif myarg == 'countsonly':
countsOnly = True
csvPF.AddTitles(items)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
if coursesInfo is None:
return
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
courseTopicIdsLists = courseTopicIds if isinstance(courseTopicIds, dict) else None
i = 0
count = len(coursesInfo)
for course in coursesInfo:
i += 1
courseId = course['id']
if courseTopicIdsLists:
courseTopicIds = courseTopicIdsLists[courseId]
if not courseTopicIds:
fields = getItemFieldsFromFieldsList('topic', fieldsList)
printGettingAllEntityItemsForWhom(Ent.COURSE_TOPIC, Ent.TypeName(Ent.COURSE, courseId), i, count)
try:
results = callGAPIpages(croom.courses().topics(), 'list', 'topic',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId,
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
if not countsOnly:
for courseTopic in results:
_printCourseTopic(course, courseTopic)
else:
_printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, csvPF)
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
else:
jcount = len(courseTopicIds)
if jcount == 0:
continue
fields = getFieldsFromFieldsList(fieldsList)
j = 0
for courseTopicId in courseTopicIds:
j += 1
try:
courseTopic = callGAPI(croom.courses().topics(), 'get',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, id=courseTopicId, fields=fields)
_printCourseTopic(course, courseTopic)
except GAPI.notFound:
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_TOPIC_ID, courseTopicId], j, jcount)
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_TOPIC_ID, courseTopicId], str(e), j, jcount)
csvPF.writeCSVfile('Course Topics')
def _initCourseWMSelectionParameters():
return {'courseWMIds': [], 'courseWMStates': []}
def _getCourseWMSelectionParameters(myarg, courseWMSelectionParameters,
IDArguments, OBIDEntity,
StateArguments, OBStateList):
if myarg in IDArguments:
courseWMSelectionParameters['courseWMIds'] = getEntityList(OBIDEntity)
elif myarg in StateArguments:
_getCourseStates(OBStateList, courseWMSelectionParameters['courseWMStates'])
else:
return False
return True
COURSE_MATERIAL_ID_ARGUMENTS = {'materialid', 'materialids', 'coursematerialid', 'coursematerialids'}
COURSE_MATERIAL_STATE_ARGUMENTS = {'materialstate', 'materialstates', 'coursematerialstate', 'coursematerialstates'}
COURSE_MATERIAL_FIELDS_CHOICE_MAP = {
'alternatelink': 'alternateLink',
'assigneemode': 'assigneeMode',
'courseid': 'courseId',
'coursematerialid': 'id',
'creationtime': 'creationTime',
'creator': 'creatorUserId',
'creatoruserid': 'creatorUserId',
'description': 'description',
'id': 'id',
'individualstudentsoptions': 'individualStudentsOptions',
'materialid': 'id',
'materials': 'materials',
'scheduledtime': 'scheduledTime',
'state': 'state',
'title': 'title',
'topicid': 'topicId',
'updatetime': 'updateTime',
}
COURSE_MATERIAL_ORDERBY_CHOICE_MAP = {'updatetime': 'updateTime', 'updatedate': 'updateTime'}
COURSE_MATERIAL_TIME_OBJECTS = {'creationTime', 'scheduledTime', 'updateTime'}
COURSE_MATERIAL_SORT_TITLES = ['courseId', 'courseName', 'id', 'title', 'description', 'state']
COURSE_MATERIAL_INDEXED_TITLES = ['materials']
COURSE_WORK_ID_ARGUMENTS = {'workid', 'workids', 'courseworkid', 'courseworkids'}
COURSE_WORK_STATE_ARGUMENTS = {'workstate', 'workstates', 'courseworkstate', 'courseworkstates'}
COURSE_WORK_FIELDS_CHOICE_MAP = {
'alternatelink': 'alternateLink',
'assigneemode': 'assigneeMode',
'courseid': 'courseId',
'courseworkid': 'id',
'courseworktype': 'workType',
'creationtime': 'creationTime',
'creator': 'creatorUserId',
'creatoruserid': 'creatorUserId',
'description': 'description',
'duedate': 'dueDate',
'duetime': 'dueTime',
'id': 'id',
'individualstudentsoptions': 'individualStudentsOptions',
'materials': 'materials',
'maxpoints': 'maxPoints',
'scheduledtime': 'scheduledTime',
'state': 'state',
'submissionmodificationmode': 'submissionModificationMode',
'title': 'title',
'topicid': 'topicId',
'updatetime': 'updateTime',
'workid': 'id',
'worktype': 'workType',
}
COURSE_WORK_ORDERBY_CHOICE_MAP = {'duedate': 'dueDate', 'updatetime': 'updateTime', 'updatedate': 'updateTime'}
COURSE_WORK_TIME_OBJECTS = {'creationTime', 'scheduledTime', 'updateTime'}
COURSE_WORK_SORT_TITLES = ['courseId', 'courseName', 'id', 'title', 'description', 'state']
COURSE_WORK_INDEXED_TITLES = ['materials']
def doPrintCourseWM(entityIDType, entityStateType):
def _getTopicNames(croom, courseId):
topicNames = {}
try:
results = callGAPIpages(croom.courses().topics(), 'list', 'topic',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId,
fields='nextPageToken,topic(topicId,name)', pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
for courseTopic in results:
topicNames[courseTopic['topicId']] = courseTopic['name']
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable):
pass
return topicNames
def _printCourseWM(course, courseWM, i, count):
if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter):
return
if showCreatorEmail:
courseWM['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseWM['creatorUserId'], creatorEmails,
[Ent.COURSE, course['id'], entityIDType, courseWM['id'],
Ent.CREATOR_ID, courseWM['creatorUserId']], i, count)
if showTopicNames:
topicId = courseWM.get('topicId')
if topicId:
courseWM['topicName'] = topicNames.get(topicId, topicId)
row = flattenJSON(courseWM, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=TimeObjects,
simpleLists=['studentIds'] if showStudentsAsList else None, delimiter=delimiter)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
'JSON': json.dumps(cleanJSON(courseWM, timeObjects=TimeObjects),
ensure_ascii=False, sort_keys=True)})
croom = buildGAPIObject(API.CLASSROOM)
if entityIDType == Ent.COURSE_WORK_ID:
SortTitles = COURSE_WORK_SORT_TITLES
IndexedTitles = COURSE_WORK_INDEXED_TITLES
TimeObjects = COURSE_WORK_TIME_OBJECTS
OrderbyChoiceMap = COURSE_WORK_ORDERBY_CHOICE_MAP
FieldsChoiceMap = COURSE_WORK_FIELDS_CHOICE_MAP
IDArguments = COURSE_WORK_ID_ARGUMENTS
OBIDEntity = Cmd.OB_COURSE_WORK_ID_ENTITY
StateArguments = COURSE_WORK_STATE_ARGUMENTS
OBStateList = Cmd.OB_COURSE_WORK_STATE_LIST
CSVTitle = 'Course Work'
service = croom.courses().courseWork()
items = 'courseWork'
else:
SortTitles = COURSE_MATERIAL_SORT_TITLES
IndexedTitles = COURSE_MATERIAL_INDEXED_TITLES
TimeObjects = COURSE_MATERIAL_TIME_OBJECTS
OrderbyChoiceMap = COURSE_MATERIAL_ORDERBY_CHOICE_MAP
FieldsChoiceMap = COURSE_MATERIAL_FIELDS_CHOICE_MAP
IDArguments = COURSE_MATERIAL_ID_ARGUMENTS
OBIDEntity = Cmd.OB_COURSE_MATERIAL_ID_ENTITY
StateArguments = COURSE_MATERIAL_STATE_ARGUMENTS
OBStateList = Cmd.OB_COURSE_MATERIAL_STATE_LIST
CSVTitle = 'Course Work Material'
service = croom.courses().courseWorkMaterials()
items = 'courseWorkMaterial'
csvPF = CSVPrintFile(['courseId', 'courseName'], SortTitles, IndexedTitles)
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
courseSelectionParameters = _initCourseSelectionParameters()
courseWMSelectionParameters = _initCourseWMSelectionParameters()
courseItemFilter = _initCourseItemFilter()
courseShowProperties = _initCourseShowProperties(['name'])
OBY = OrderBy(OrderbyChoiceMap)
creatorEmails = {}
showCreatorEmail = showTopicNames = False
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
countsOnly = showStudentsAsList = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif _getCourseWMSelectionParameters(myarg, courseWMSelectionParameters,
IDArguments, OBIDEntity,
StateArguments, OBStateList):
pass
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CUS_FILTER_FIELDS_MAP):
pass
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg in {'showcreatoremails', 'creatoremail'}:
showCreatorEmail = True
elif myarg == 'showtopicnames':
showTopicNames = True
elif getFieldsList(myarg, FieldsChoiceMap, fieldsList, initialField='id'):
pass
elif myarg == 'showstudentsaslist':
showStudentsAsList = getBoolean()
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'countsonly':
countsOnly = True
csvPF.AddTitles(items)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if showCreatorEmail and fieldsList:
fieldsList.append('creatorUserId')
if showTopicNames and fieldsList:
fieldsList.append('topicId')
if entityIDType == Ent.COURSE_WORK_ID:
kwargs = {'courseWorkStates': courseWMSelectionParameters['courseWMStates']}
else:
kwargs = {'courseWorkMaterialStates': courseWMSelectionParameters['courseWMStates']}
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
if coursesInfo is None:
return
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
courseWMIds = courseWMSelectionParameters['courseWMIds']
courseWMIdsLists = courseWMIds if isinstance(courseWMIds, dict) else {}
i = 0
count = len(coursesInfo)
for course in coursesInfo:
i += 1
courseId = course['id']
if showTopicNames:
topicNames = _getTopicNames(croom, courseId)
if courseWMIdsLists:
courseWMIds = courseWMIdsLists[courseId]
if not courseWMIds:
fields = getItemFieldsFromFieldsList(items, fieldsList)
printGettingAllEntityItemsForWhom(entityIDType, Ent.TypeName(Ent.COURSE, courseId), i, count,
_gettingCourseEntityQuery(entityStateType, courseWMSelectionParameters['courseWMStates']))
try:
results = callGAPIpages(service, 'list', items,
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS,
courseId=courseId, orderBy=OBY.orderBy,
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS], **kwargs)
if not countsOnly:
for courseWM in results:
_printCourseWM(course, courseWM, i, count)
else:
_printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, csvPF)
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
else:
jcount = len(courseWMIds)
if jcount == 0:
continue
fields = getFieldsFromFieldsList(fieldsList)
j = 0
for courseWMId in courseWMIds:
j += 1
try:
courseWM = callGAPI(service, 'get',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS,
courseId=courseId, id=courseWMId, fields=fields)
_printCourseWM(course, courseWM, i, count)
except GAPI.notFound:
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], entityIDType, courseWMId], j, jcount)
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], entityIDType, courseWMId], str(e), j, jcount)
csvPF.writeCSVfile(CSVTitle)
# gam print course-materials [todrive <ToDriveAttribute>*]
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
# (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*)
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>]
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [countsonly] [formatjson [quotechar <Character>]]
def doPrintCourseMaterials():
doPrintCourseWM(Ent.COURSE_MATERIAL_ID, Ent.COURSE_MATERIAL_STATE)
# gam print course-work [todrive <ToDriveAttribute>*]
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>]
# [showstudentsaslist [<Boolean>]] [delimiter <Character>]
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [countsonly] [formatjson [quotechar <Character>]]
def doPrintCourseWork():
doPrintCourseWM(Ent.COURSE_WORK_ID, Ent.COURSE_WORK_STATE)
COURSE_SUBMISSION_FIELDS_CHOICE_MAP = {
'alternatelink': 'alternateLink',
'assignedgrade': 'assignedGrade',
'courseid': 'courseId',
'coursesubmissionid': 'id',
'courseworkid': 'courseWorkId',
'courseworktype': 'courseWorkType',
'creationtime': 'creationTime',
'draftgrade': 'draftGrade',
'id': 'id',
'late': 'late',
'state': 'state',
'submissionhistory': 'submissionHistory',
'submissionid': 'id',
'updatetime': 'updateTime',
'userid': 'userId',
'workid': 'courseWorkId',
'worktype': 'courseWorkType',
}
COURSE_SUBMISSION_TIME_OBJECTS = {'creationTime', 'updateTime', 'gradeTimestamp', 'stateTimestamp'}
def _gettingCourseSubmissionQuery(courseSubmissionStates, late, userId):
query = ''
if courseSubmissionStates:
query += f'{Ent.Choose(Ent.COURSE_SUBMISSION_STATE, len(courseSubmissionStates))}: {",".join(courseSubmissionStates)}, '
if late:
query += f'{late}, '
if userId:
query += f'{Ent.Singular(Ent.USER)}: {userId}, '
if query:
query = query[:-2]
return query
# gam print course-submissions [todrive <ToDriveAttribute>*]
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
# (submissionids <CourseSubmissionIDEntity>)|((submissionstates <CourseSubmissionStateList>)*) [late|notlate]
# [fields <CourseSubmissionFieldNameList>] [showuserprofile]
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [countsonly] [formatjson [quotechar <Character>]]
def doPrintCourseSubmissions():
def _printCourseSubmission(course, courseSubmission):
if applyCourseItemFilter and not _courseItemPassesFilter(courseSubmission, courseItemFilter):
return
if showUserProfile:
userId = courseSubmission.get('userId')
if userId:
if userId not in userProfiles:
try:
userProfile = callGAPI(tcroom.userProfiles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userId=userId, fields='emailAddress,name')
userProfiles[userId] = {'profile': {'emailAddress': userProfile.get('emailAddress', ''), 'name': userProfile['name']}}
except (GAPI.notFound, GAPI.permissionDenied, GAPI.serviceNotAvailable):
userProfiles[userId] = {'profile': {'emailAddress': '', 'name': {'givenName': '', 'familyName': '', 'fullName': ''}}}
courseSubmission.update(userProfiles[userId])
row = flattenJSON(courseSubmission, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_SUBMISSION_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
'JSON': json.dumps(cleanJSON(courseSubmission, timeObjects=COURSE_SUBMISSION_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
croom = buildGAPIObject(API.CLASSROOM)
csvPF = CSVPrintFile(['courseId', 'courseName'],
['courseId', 'courseName', 'courseWorkId', 'id', 'userId',
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}emailAddress',
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}givenName',
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}familyName',
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}fullName',
'state'],
['submissionHistory'])
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
courseSelectionParameters = _initCourseSelectionParameters()
courseWMSelectionParameters = _initCourseWMSelectionParameters()
courseItemFilter = _initCourseItemFilter()
courseShowProperties = _initCourseShowProperties(['name'])
courseSubmissionStates = []
courseSubmissionIds = []
OBY = OrderBy(COURSE_WORK_ORDERBY_CHOICE_MAP)
late = None
userProfiles = {}
countsOnly = showUserProfile = False
items = 'courseSubmissions'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif _getCourseWMSelectionParameters(myarg, courseWMSelectionParameters,
COURSE_WORK_ID_ARGUMENTS, Cmd.OB_COURSE_WORK_ID_ENTITY,
COURSE_WORK_STATE_ARGUMENTS, Cmd.OB_COURSE_WORK_STATE_LIST):
pass
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CU_FILTER_FIELDS_MAP):
pass
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg in {'submissionid', 'submissionids', 'coursesubmissionid', 'coursesubmissionids'}:
courseSubmissionIds = getEntityList(Cmd.OB_COURSE_SUBMISSION_ID_ENTITY)
elif myarg in {'submissionstate', 'submissionstates', 'coursesubmissionstate', 'coursesubmissionstates'}:
_getCourseStates(Cmd.OB_COURSE_SUBMISSION_STATE_LIST, courseSubmissionStates)
elif myarg == 'late':
late = 'LATE_ONLY'
elif myarg == 'notlate':
late = 'NOT_LATE_ONLY'
elif myarg == 'showuserprofile':
showUserProfile = True
elif getFieldsList(myarg, COURSE_SUBMISSION_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
elif myarg == 'countsonly':
countsOnly = True
csvPF.AddTitles(items)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getOwnerId=True)
if coursesInfo is None:
return
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
courseWorkIds = courseWMSelectionParameters['courseWMIds']
courseWorkIdsLists = courseWorkIds if isinstance(courseWorkIds, dict) else {}
courseSubmissionIdsLists = courseSubmissionIds if isinstance(courseSubmissionIds, dict) else {}
i = 0
count = len(coursesInfo)
for course in coursesInfo:
i += 1
_, tcroom = buildGAPIServiceObject(API.CLASSROOM, f"uid:{course['ownerId']}")
if tcroom is None:
continue
submissionsCount = 0
courseId = course['id']
if courseWorkIdsLists:
courseWorkIds = courseWorkIdsLists[courseId]
if not courseWorkIds:
printGettingAllEntityItemsForWhom(Ent.COURSE_WORK_ID, Ent.TypeName(Ent.COURSE, courseId), i, count,
_gettingCourseEntityQuery(Ent.COURSE_WORK_STATE, courseWMSelectionParameters['courseWMStates']))
try:
results = callGAPIpages(croom.courses().courseWork(), 'list', 'courseWork',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, courseWorkStates=courseWMSelectionParameters['courseWMStates'], orderBy=OBY.orderBy,
fields='nextPageToken,courseWork(id)', pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
courseWorkIdsForCourse = [courseWork['id'] for courseWork in results]
except GAPI.notFound:
continue
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
continue
else:
courseWorkIdsForCourse = courseWorkIds
jcount = len(courseWorkIdsForCourse)
if jcount == 0:
continue
j = 0
for courseWorkId in courseWorkIdsForCourse:
j += 1
if not courseSubmissionIds:
fields = getItemFieldsFromFieldsList('studentSubmissions', fieldsList)
printGettingAllEntityItemsForWhom(Ent.COURSE_SUBMISSION_ID, Ent.TypeName(Ent.COURSE_WORK_ID, courseWorkId), j, jcount,
_gettingCourseSubmissionQuery(courseSubmissionStates, late, courseSelectionParameters['studentId']))
try:
results = callGAPIpages(croom.courses().courseWork().studentSubmissions(), 'list', 'studentSubmissions',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, courseWorkId=courseWorkId, states=courseSubmissionStates, late=late, userId=courseSelectionParameters['studentId'],
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
if not countsOnly:
for submission in results:
_printCourseSubmission(course, submission)
else:
submissionsCount += _printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, None)
except GAPI.notFound:
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId], j, jcount)
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId], str(e), j, jcount)
else:
if courseSubmissionIdsLists:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
courseSubmissionIds = courseSubmissionIdsLists[courseWorkId]
else:
courseSubmissionIds = courseSubmissionIdsLists[courseId][courseWorkId]
kcount = len(courseSubmissionIds)
if kcount == 0:
continue
fields = f'{",".join(set(fieldsList))}' if fieldsList else None
k = 0
for courseSubmissionId in courseSubmissionIds:
k += 1
try:
submission = callGAPI(croom.courses().courseWork().studentSubmissions(), 'get',
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
courseId=courseId, courseWorkId=courseWorkId, id=courseSubmissionId,
fields=fields)
_printCourseSubmission(course, submission)
except GAPI.notFound:
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId, Ent.COURSE_SUBMISSION_ID, courseSubmissionId], k, kcount)
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId, Ent.COURSE_SUBMISSION_ID, courseSubmissionId], str(e), k, kcount)
if countsOnly:
csvPF.WriteRowTitles({'courseId': course['id'], 'courseName': course['name'], items: submissionsCount})
csvPF.writeCSVfile('Course Submissions')
COURSE_PARTICIPANTS_SORT_TITLES = ['courseId', 'courseName', 'userRole', 'userId']
# gam print course-participants [todrive <ToDriveAttribute>*]
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
# [show all|students|teachers]
# [showitemcountonly] [formatjson [quotechar <Character>]]
def doPrintCourseParticipants():
croom = buildGAPIObject(API.CLASSROOM)
csvPF = CSVPrintFile(['courseId', 'courseName'])
FJQC = FormatJSONQuoteChar(csvPF)
courseSelectionParameters = _initCourseSelectionParameters()
courseShowProperties = _initCourseShowProperties(['name'])
courseShowProperties['members'] = 'all'
showItemCountOnly = False
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif myarg == 'show':
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
elif myarg == 'showitemcountonly':
showItemCountOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
if coursesInfo is None:
if showItemCountOnly:
writeStdout('0\n')
return
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
if FJQC.formatJSON:
csvPF.AddJSONTitles('JSON-teachers')
if courseShowProperties['members'] != 'teachers':
if FJQC.formatJSON:
csvPF.AddJSONTitles('JSON-students')
teachersFields = 'nextPageToken,teachers(userId,profile)'
studentsFields = 'nextPageToken,students(userId,profile)'
else:
teachersFields = studentsFields = None
itemCount = 0
count = len(coursesInfo)
i = 0
for course in coursesInfo:
i += 1
courseId = course['id']
if useOwnerAccess:
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
if not ocroom:
continue
else:
ocroom = croom
_, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
if showItemCountOnly:
if courseShowProperties['members'] != 'students':
itemCount += len(teachers)
if courseShowProperties['members'] != 'teachers':
itemCount += len(students)
continue
if not FJQC.formatJSON:
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
for member in teachers:
csvPF.WriteRowTitles(flattenJSON(member, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'TEACHER'}))
if courseShowProperties['members'] != 'teachers':
for member in students:
csvPF.WriteRowTitles(flattenJSON(member, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'STUDENT'}))
else:
row = {'courseId': courseId, 'courseName': course['name']}
if courseShowProperties['members'] != 'none':
if courseShowProperties['members'] != 'students':
row['JSON-teachers'] = json.dumps(list(teachers))
if courseShowProperties['members'] != 'teachers':
row['JSON-students'] = json.dumps(list(students))
csvPF.WriteRowNoFilter(row)
if showItemCountOnly:
writeStdout(f'{itemCount}\n')
return
if not FJQC.formatJSON:
csvPF.SetSortTitles(COURSE_PARTICIPANTS_SORT_TITLES)
csvPF.writeCSVfile('Course Participants')
def _batchAddItemsToCourse(croom, courseId, i, count, addParticipants, role):
_ADD_PART_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.ALREADY_EXISTS: Msg.DUPLICATE,
GAPI.FAILED_PRECONDITION: Msg.NOT_ALLOWED}
def _callbackAddItemsToCourse(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if (reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}) and ((reason != GAPI.NOT_FOUND) or (ri[RI_ROLE] == Ent.COURSE_ALIAS)):
if reason in [GAPI.FORBIDDEN, GAPI.BACKEND_ERROR]:
errMsg = getPhraseDNEorSNA(ri[RI_ITEM])
else:
errMsg = getHTTPError(_ADD_PART_REASON_TO_MESSAGE_MAP, http_status, reason, message)
if (reason == GAPI.PERMISSION_DENIED) and (ri[RI_ROLE] in {Ent.STUDENT, Ent.TEACHER}) and ('CannotDirectAddUser' in errMsg):
errMsg += f' Add external user with: gam user {ri[RI_ITEM]} create classroominvitation courses {ri[RI_ENTITY]} role {Ent.Singular(ri[RI_ROLE])}'
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
return
waitOnFailure(1, 10, reason, message)
try:
callGAPI(service, 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]),
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
fields='')
except (GAPI.notFound, GAPI.backendError, GAPI.forbidden):
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], getPhraseDNEorSNA(ri[RI_ITEM]), int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.alreadyExists:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.DUPLICATE, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.failedPrecondition:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.NOT_ALLOWED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
if role == Ent.STUDENT:
service = croom.courses().students()
attribute = 'userId'
elif role == Ent.TEACHER:
service = croom.courses().teachers()
attribute = 'userId'
elif role == Ent.COURSE_ALIAS:
service = croom.courses().aliases()
attribute = 'alias'
else: # role == Ent.COURSE_TOPIC:
service = croom.courses().topics()
attribute = 'name'
method = getattr(service, 'create')
Act.Set(Act.ADD)
jcount = len(addParticipants)
noScopeCourseId = removeCourseIdScope(courseId)
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, role, i, count)
Ind.Increment()
svcargs = dict([('courseId', courseId), ('body', {attribute: None}), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
dbatch = croom.new_batch_http_request(callback=_callbackAddItemsToCourse)
bcount = 0
j = 0
for participant in addParticipants:
j += 1
svcparms = svcargs.copy()
if role in {Ent.STUDENT, Ent.TEACHER}:
svcparms['body'][attribute] = cleanItem = normalizeEmailAddressOrUID(participant)
elif role == Ent.COURSE_ALIAS:
svcparms['body'][attribute] = addCourseAliasScope(participant)
cleanItem = removeCourseAliasScope(svcparms['body'][attribute])
else: # role == Ent.COURSE_TOPIC:
svcparms['body'][attribute] = cleanItem = participant
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, role))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = croom.new_batch_http_request(callback=_callbackAddItemsToCourse)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
def _batchRemoveItemsFromCourse(croom, courseId, i, count, removeParticipants, role):
_REMOVE_PART_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.FORBIDDEN: Msg.FORBIDDEN,
GAPI.PERMISSION_DENIED: Msg.PERMISSION_DENIED}
def _callbackRemoveItemsFromCourse(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}:
if reason == GAPI.NOT_FOUND and ri[RI_ROLE] != Ent.COURSE_ALIAS:
errMsg = f'{Msg.NOT_A} {Ent.Singular(ri[RI_ROLE])}'
else:
errMsg = getHTTPError(_REMOVE_PART_REASON_TO_MESSAGE_MAP, http_status, reason, message)
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
return
waitOnFailure(1, 10, reason, message)
try:
callGAPI(service, 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED,
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
courseId=addCourseIdScope(ri[RI_ENTITY]),
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
fields='')
except GAPI.notFound:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.DOES_NOT_EXIST, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.forbidden:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.FORBIDDEN, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.permissionDenied:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.PERMISSION_DENIED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
if role == Ent.STUDENT:
service = croom.courses().students()
attribute = 'userId'
elif role == Ent.TEACHER:
service = croom.courses().teachers()
attribute = 'userId'
elif role == Ent.COURSE_ALIAS:
service = croom.courses().aliases()
attribute = 'alias'
else: # role == Ent.COURSE_TOPIC:
service = croom.courses().topics()
attribute = 'id'
method = getattr(service, 'delete')
Act.Set(Act.REMOVE)
jcount = len(removeParticipants)
noScopeCourseId = removeCourseIdScope(courseId)
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, role, i, count)
Ind.Increment()
svcargs = dict([('courseId', courseId), ('fields', ''), (attribute, None)]+GM.Globals[GM.EXTRA_ARGS_LIST])
dbatch = croom.new_batch_http_request(callback=_callbackRemoveItemsFromCourse)
bcount = 0
j = 0
for participant in removeParticipants:
j += 1
svcparms = svcargs.copy()
if role in {Ent.STUDENT, Ent.TEACHER}:
svcparms[attribute] = cleanItem = normalizeEmailAddressOrUID(participant)
elif role == Ent.COURSE_ALIAS:
svcparms[attribute] = addCourseAliasScope(participant)
cleanItem = removeCourseAliasScope(svcparms[attribute])
else: # role == Ent.COURSE_TOPIC:
svcparms[attribute] = cleanItem = participant
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, role))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = croom.new_batch_http_request(callback=_callbackRemoveItemsFromCourse)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
def _updateCourseOwner(croom, courseId, owner, i, count):
action = Act.Get()
Act.Set(Act.UPDATE_OWNER)
try:
callGAPI(croom.courses(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION,
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT,
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
id=courseId, body={'ownerId': owner}, updateMask='ownerId', fields='ownerId')
entityActionPerformed([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], i, count)
except (GAPI.notFound, GAPI.permissionDenied, GAPI.failedPrecondition,
GAPI.forbidden, GAPI.badRequest, GAPI.invalidArgument,
GAPI.internalError, GAPI.serviceNotAvailable) as e:
errMsg = str(e)
if '@UserAlreadyOwner' not in errMsg:
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], errMsg, i, count)
else:
entityActionPerformedMessage([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], Msg.ALREADY_WAS_OWNER, i, count)
Act.Set(action)
ADD_REMOVE_PARTICIPANT_TYPES_MAP = {
'alias': Ent.COURSE_ALIAS,
'aliases': Ent.COURSE_ALIAS,
'student': Ent.STUDENT,
'students': Ent.STUDENT,
'teacher': Ent.TEACHER,
'teachers': Ent.TEACHER,
'topic': Ent.COURSE_TOPIC,
'topics': Ent.COURSE_TOPIC,
}
CLEAR_SYNC_PARTICIPANT_TYPES_MAP = {
'student': Ent.STUDENT,
'students': Ent.STUDENT,
'teacher': Ent.TEACHER,
'teachers': Ent.TEACHER,
}
PARTICIPANT_EN_MAP = {
Ent.STUDENT: Cmd.ENTITY_STUDENTS,
Ent.TEACHER: Cmd.ENTITY_TEACHERS,
}
# gam courses <CourseEntity> create alias <CourseAliasEntity>
# gam course <CourseID> create alias <CourseAlias>
# gam courses <CourseEntity> create topic <CourseTopicEntity>
# gam course <CourseID> create topic <CourseTopic>
# gam courses <CourseEntity> create students <UserTypeEntity>
# gam course <CourseID> create student <EmailAddress>
# gam courses <CourseEntity> create teachers [makefirstteacherowner] <UserTypeEntity>
# gam course <CourseID> create teacher [makefirstteacherowner] <EmailAddress>
def doCourseAddItems(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
if role == Ent.TEACHER:
makeFirstTeacherOwner = checkArgumentPresent(['makefirstteacherowner'])
else:
makeFirstTeacherOwner = False
if not getEntityListArg:
if role in {Ent.STUDENT, Ent.TEACHER}:
addItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
elif role == Ent.COURSE_ALIAS:
addItems = getStringReturnInList(Cmd.OB_COURSE_ALIAS)
else: # role == Ent.COURSE_TOPIC:
addItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC)
courseParticipantLists = None
else:
if role in {Ent.STUDENT, Ent.TEACHER}:
_, addItems = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]},
isSuspended=False, isArchived=False)
elif role == Ent.COURSE_ALIAS:
addItems = getEntityList(Cmd.OB_COURSE_ALIAS_ENTITY, shlexSplit=True)
else: # role == Ent.COURSE_TOPIC:
addItems = getEntityList(Cmd.OB_COURSE_TOPIC_ENTITY, shlexSplit=True)
courseParticipantLists = addItems if isinstance(addItems, dict) else None
if courseParticipantLists is None:
firstTeacher = None
if makeFirstTeacherOwner and addItems:
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
checkForExtraneousArguments()
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, role == Ent.COURSE_TOPIC,
addCIIdScope=courseParticipantLists is None)
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseParticipantLists:
addItems = courseParticipantLists[courseId]
firstTeacher = None
if makeFirstTeacherOwner and addItems:
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
courseId = addCourseIdScope(courseId)
_batchAddItemsToCourse(courseInfo['croom'], courseId, i, count, addItems, role)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(courseInfo['croom'], courseId, firstTeacher, i, count)
# gam courses <CourseEntity> remove alias <CourseAliasEntity>
# gam course <CourseID> remove alias <CourseAlias>
# gam courses <CourseEntity> remove topic <CourseTopicIDEntity>
# gam course <CourseID> remove topic <CourseTopicID>
# gam courses <CourseEntity> remove teachers|students [owneracccess] <UserTypeEntity>
# gam course <CourseID> remove teacher|student [owneracccess] <EmailAddress>
def doCourseRemoveItems(courseIdList, getEntityListArg):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
if not getEntityListArg:
if role in {Ent.STUDENT, Ent.TEACHER}:
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
if checkArgumentPresent(OWNER_ACCESS_OPTIONS):
useOwnerAccess = True
removeItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
elif role == Ent.COURSE_ALIAS:
useOwnerAccess = False
removeItems = getStringReturnInList(Cmd.OB_COURSE_ALIAS)
else: # role == Ent.COURSE_TOPIC:
useOwnerAccess = True
removeItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC_ID)
courseParticipantLists = None
else:
if role in {Ent.STUDENT, Ent.TEACHER}:
useOwnerAccess = checkArgumentPresent(OWNER_ACCESS_OPTIONS)
_, removeItems = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]})
elif role == Ent.COURSE_ALIAS:
useOwnerAccess = False
removeItems = getEntityList(Cmd.OB_COURSE_ALIAS_ENTITY, shlexSplit=True)
else: # role == Ent.COURSE_TOPIC:
useOwnerAccess = True
removeItems = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY, shlexSplit=True)
courseParticipantLists = removeItems if isinstance(removeItems, dict) else None
checkForExtraneousArguments()
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess,
addCIIdScope=courseParticipantLists is None)
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseParticipantLists:
removeItems = courseParticipantLists[courseId]
courseId = addCourseIdScope(courseId)
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, role)
# gam courses <CourseEntity> clear teachers|students
# gam course <CourseID> clear teacher|student
def doCourseClearParticipants(courseIdList, _):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(CLEAR_SYNC_PARTICIPANT_TYPES_MAP, mapChoice=True)
checkForExtraneousArguments()
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
for courseId, courseInfo in coursesInfo.items():
i += 1
removeParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
continue
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeParticipants, role)
# gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
# gam course <CourseID> sync students [addonly|removeonly] <UserTypeEntity>
# gam courses <CourseEntity> sync teachers [addonly|removeonly] [makefirstteacherowner] <UserTypeEntity>
# gam course <CourseID> sync teachers [addonly|removeonly] [makefirstteacherowner] <UserTypeEntity>
def doCourseSyncParticipants(courseIdList, _):
croom = buildGAPIObject(API.CLASSROOM)
role = getChoice(CLEAR_SYNC_PARTICIPANT_TYPES_MAP, mapChoice=True)
if role == Ent.TEACHER:
makeFirstTeacherOwner = checkArgumentPresent(['makefirstteacherowner'])
else:
makeFirstTeacherOwner = False
syncOperation = getSyncOperation()
_, syncParticipants = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]},
isSuspended=False, isArchived=False)
checkForExtraneousArguments()
courseParticipantLists = syncParticipants if isinstance(syncParticipants, dict) else None
if courseParticipantLists is None:
syncParticipantsSet = set()
firstTeacher = None
if syncParticipants:
for user in syncParticipants:
syncParticipantsSet.add(normalizeEmailAddressOrUID(user))
if makeFirstTeacherOwner:
firstTeacher = normalizeEmailAddressOrUID(syncParticipants[0])
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS],
addCIIdScope=courseParticipantLists is None)
for courseId, courseInfo in coursesInfo.items():
i += 1
if courseParticipantLists:
syncParticipantsSet = set()
firstTeacher = None
if courseParticipantLists[courseId]:
for user in courseParticipantLists[courseId]:
syncParticipantsSet.add(normalizeEmailAddressOrUID(user))
if makeFirstTeacherOwner:
firstTeacher = normalizeEmailAddressOrUID(courseParticipantLists[courseId][0])
courseId = addCourseIdScope(courseId)
currentParticipantsSet = set()
currentParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
continue
for user in currentParticipants:
currentParticipantsSet.add(normalizeEmailAddressOrUID(user))
if syncOperation != 'removeonly':
_batchAddItemsToCourse(croom, courseId, i, count, list(syncParticipantsSet-currentParticipantsSet), role)
if makeFirstTeacherOwner and firstTeacher:
_updateCourseOwner(croom, courseId, firstTeacher, i, count)
if syncOperation != 'addonly':
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, list(currentParticipantsSet-syncParticipantsSet), role)
def studentUnknownWarning(studentId, errMsg, i, count):
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
writeStderr(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(Ent.STUDENT), studentId, f'{Msg.SERVICE_NOT_APPLICABLE}: {errMsg}'],
currentCountNL(i, count)))
def getGuardianEntity():
guardians = getEntityList(Cmd.OB_GUARDIAN_ENTITY)
if isinstance(guardians, dict):
return (guardians, guardians)
return ([normalizeEmailAddressOrUID(guardian) for guardian in guardians], None)
def getGuardianEmails(user, guardianEntity, guardianEntityList):
studentId = normalizeStudentGuardianEmailAddressOrUID(user)
if guardianEntityList:
guardianEmails = [normalizeEmailAddressOrUID(guardian) for guardian in guardianEntityList[user]]
else:
guardianEmails = guardianEntity
return (studentId, guardianEmails, len(guardianEmails))
def getGuardianInvitationEntity():
invitations = getEntityList(Cmd.OB_GUARDIAN_INVITATION_ID_ENTITY)
if isinstance(invitations, dict):
return (invitations, invitations)
return (invitations, None)
def getGuardianInvitationIds(user, invitationEntity, invitationEntityList):
studentId = normalizeStudentGuardianEmailAddressOrUID(user)
if invitationEntityList:
invitationIds = invitationEntityList[user]
else:
invitationIds = invitationEntity
return (studentId, invitationIds, len(invitationIds))
GUARDIAN_CLASS_UNDEFINED = 0
GUARDIAN_CLASS_ACCEPTED = 1
GUARDIAN_CLASS_INVITATIONS = 2
GUARDIAN_CLASS_ALL = 3
GUARDIAN_CLASS_MAP = {
'all': GUARDIAN_CLASS_ALL,
'accepted': GUARDIAN_CLASS_ACCEPTED,
'invitation': GUARDIAN_CLASS_INVITATIONS,
'invitations': GUARDIAN_CLASS_INVITATIONS,
}
GUARDIAN_CLASS_ENTITY = {
GUARDIAN_CLASS_ALL: Ent.GUARDIAN_AND_INVITATION,
GUARDIAN_CLASS_ACCEPTED: Ent.GUARDIAN,
GUARDIAN_CLASS_INVITATIONS: Ent.GUARDIAN_INVITATION,
}
def _inviteGuardian(croom, studentId, guardianEmail, i=0, count=0, j=0, jcount=0):
body = {'invitedEmailAddress': guardianEmail}
try:
result = callGAPI(croom.userProfiles().guardianInvitations(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS,
GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN,
GAPI.PERMISSION_DENIED, GAPI.RESOURCE_EXHAUSTED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, body=body, fields='invitationId')
entityActionPerformed([Ent.STUDENT, studentId, Ent.GUARDIAN, body['invitedEmailAddress'], Ent.GUARDIAN_INVITATION, result['invitationId']], j, jcount)
return 1
except GAPI.notFound:
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
return -1
except GAPI.alreadyExists:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, body['invitedEmailAddress']], Msg.DUPLICATE, j, jcount)
return 0
except GAPI.resourceExhausted as e:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, body['invitedEmailAddress']], str(e), j, jcount)
return -1
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
studentUnknownWarning(studentId, str(e), i, count)
return -1
# gam create guardian|guardianinvite|inviteguardian <EmailAddress> <StudentItem>
def doInviteGuardian():
croom = buildGAPIObject(API.CLASSROOM)
guardianEmail = getEmailAddress()
studentId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_STUDENT_ITEM))
checkForExtraneousArguments()
_inviteGuardian(croom, studentId, guardianEmail)
# gam <UserTypeEntity> create guardian|guardianinvite|inviteguardian <GuardianEntity>
def inviteGuardians(users):
croom = buildGAPIObject(API.CLASSROOM)
guardianEntity, guardianEntityList = getGuardianEntity()
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
studentId, guardianEmails, jcount = getGuardianEmails(user, guardianEntity, guardianEntityList)
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN_INVITATION, i, count)
Ind.Increment()
j = 0
for guardianEmail in guardianEmails:
j += 1
if _inviteGuardian(croom, studentId, guardianEmail, i, count, j, jcount) < 0:
break
Ind.Decrement()
def _cancelGuardianInvitation(croom, studentId, invitationId, i=0, count=0, j=0, jcount=0):
try:
result = callGAPI(croom.userProfiles().guardianInvitations(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION,
GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN,
GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, invitationId=invitationId, updateMask='state', body={'state': 'COMPLETE'}, fields='invitedEmailAddress')
entityActionPerformed([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, result['invitedEmailAddress']], j, jcount)
return 1
except GAPI.notFound:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, invitationId], Msg.NOT_FOUND, j, jcount)
return 0
except GAPI.failedPrecondition:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, invitationId], Msg.GUARDIAN_INVITATION_STATUS_NOT_PENDING, j, jcount)
return 1
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, invitationId], str(e), j, jcount)
return -1
except (GAPI.forbidden, GAPI.permissionDenied) as e:
studentUnknownWarning(studentId, str(e), i, count)
return -1
# gam cancel guardianinvitation|guardianinvitations <GuardianInvitationID> <StudentItem>
def doCancelGuardianInvitation():
croom = buildGAPIObject(API.CLASSROOM)
invitationId = getString(Cmd.OB_GUARDIAN_INVITATION_ID)
studentId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_STUDENT_ITEM))
checkForExtraneousArguments()
_cancelGuardianInvitation(croom, studentId, invitationId)
# gam <UserTypeEntity> cancel guardianinvitation|guardianinvitations <GuardianInvitationIDEntity>
def cancelGuardianInvitations(users):
croom = buildGAPIObject(API.CLASSROOM)
invitationEntity, invitationEntityList = getGuardianInvitationEntity()
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
studentId, invitationIds, jcount = getGuardianInvitationIds(user, invitationEntity, invitationEntityList)
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN_INVITATION, i, count)
Ind.Increment()
j = 0
for invitationId in invitationIds:
j += 1
if _cancelGuardianInvitation(croom, studentId, invitationId, i, count, j, jcount) == -1:
break
Ind.Decrement()
def _deleteGuardian(croom, studentId, guardianId, guardianEmail, i, count, j, jcount):
try:
callGAPI(croom.userProfiles().guardians(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, guardianId=guardianId)
entityActionPerformed([Ent.STUDENT, studentId, Ent.GUARDIAN, guardianEmail], j, jcount)
return 1
except GAPI.notFound:
if guardianId == guardianEmail:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, guardianEmail], Msg.NOT_FOUND, j, jcount)
return 0
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, guardianEmail], str(e), j, jcount)
return -1
except (GAPI.forbidden, GAPI.permissionDenied) as e:
studentUnknownWarning(studentId, str(e), i, count)
return -1
def _doDeleteGuardian(croom, studentId, guardianId, guardianClass, i=0, count=0, j=0, jcount=0):
guardianIdIsEmail = guardianId.find('@') != -1
guardianFound = False
try:
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
Act.Set(Act.CANCEL)
if guardianIdIsEmail:
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, invitedEmailAddress=guardianId, states=['PENDING'],
fields='nextPageToken,guardianInvitations(studentId,invitationId)')
for invitation in invitations:
result = _cancelGuardianInvitation(croom, invitation['studentId'], invitation['invitationId'], i, count, j, jcount)
if result < 0:
return result
if result > 0:
guardianFound = True
else:
result = _cancelGuardianInvitation(croom, studentId, guardianId, i, count, j, jcount)
if result != 0:
return result
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
Act.Set(Act.DELETE)
if guardianIdIsEmail:
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, invitedEmailAddress=guardianId,
fields='nextPageToken,guardians(studentId,guardianId)')
for guardian in guardians:
result = _deleteGuardian(croom, guardian['studentId'], guardian['guardianId'], guardianId, i, count, j, jcount)
if result < 0:
return result
if result > 0:
guardianFound = True
else:
result = _deleteGuardian(croom, studentId, guardianId, guardianId, i, count, j, jcount)
if result != 0:
return result
except GAPI.notFound:
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
return -1
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
studentUnknownWarning(studentId, str(e), i, count)
return -1
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.STUDENT, studentId, GUARDIAN_CLASS_ENTITY[guardianClass], guardianId], str(e))
return -1
if not guardianFound:
Act.Set(Act.DELETE)
entityActionFailedWarning([Ent.STUDENT, studentId, GUARDIAN_CLASS_ENTITY[guardianClass], guardianId], Msg.NOT_FOUND)
return 0
return 1
# gam delete guardian|guardians <GuardianItem> <StudentItem> [accepted|invitations|all]
def doDeleteGuardian():
croom = buildGAPIObject(API.CLASSROOM)
guardianId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_GUARDIAN_ITEM))
studentId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_STUDENT_ITEM), allowDash=True)
guardianClass = getChoice(GUARDIAN_CLASS_MAP, mapChoice=True, defaultChoice=GUARDIAN_CLASS_ALL)
checkForExtraneousArguments()
_doDeleteGuardian(croom, studentId, guardianId, guardianClass)
# gam <UserTypeEntity> delete guardian|guardians <GuardianEntity> [accepted|invitations|all]
def deleteGuardians(users):
croom = buildGAPIObject(API.CLASSROOM)
guardianEntity, guardianEntityList = getGuardianEntity()
guardianClass = getChoice(GUARDIAN_CLASS_MAP, mapChoice=True, defaultChoice=GUARDIAN_CLASS_ALL)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
studentId, guardianEmails, jcount = getGuardianEmails(user, guardianEntity, guardianEntityList)
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, GUARDIAN_CLASS_ENTITY[guardianClass], i, count)
Ind.Increment()
j = 0
for guardianEmail in guardianEmails:
j += 1
if _doDeleteGuardian(croom, studentId, guardianEmail, guardianClass, i, count, j, jcount) < 0:
break
Ind.Decrement()
# gam <UserTypeEntity> clear guardian|guardians [accepted|invitations|all]
def clearGuardians(users):
croom = buildGAPIObject(API.CLASSROOM)
guardianClass = getChoice(GUARDIAN_CLASS_MAP, mapChoice=True, defaultChoice=GUARDIAN_CLASS_ALL)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
studentId = normalizeStudentGuardianEmailAddressOrUID(user)
try:
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, states=['PENDING'], fields='nextPageToken,guardianInvitations(invitationId)')
Act.Set(Act.CANCEL)
jcount = len(invitations)
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN_INVITATION, i, count)
Ind.Increment()
j = 0
for invitation in invitations:
j += 1
_cancelGuardianInvitation(croom, studentId, invitation['invitationId'], i, count, j, jcount)
Ind.Decrement()
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
studentId=studentId, fields='nextPageToken,guardians(guardianId,invitedEmailAddress)')
Act.Set(Act.DELETE)
jcount = len(guardians)
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN, i, count)
Ind.Increment()
j = 0
for guardian in guardians:
j += 1
_deleteGuardian(croom, studentId, guardian['guardianId'], guardian['invitedEmailAddress'], i, count, j, jcount)
Ind.Decrement()
except GAPI.notFound:
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.STUDENT, studentId], str(e))
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
studentUnknownWarning(studentId, str(e), i, count)
# gam <UserTypeEntity> sync guardian|guardians <GuardianEntity>
def syncGuardians(users):
croom = buildGAPIObject(API.CLASSROOM)
guardianEntity, guardianEntityList = getGuardianEntity()
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
Act.Set(Act.SYNC)
studentId, guardianEmails, jcount = getGuardianEmails(user, guardianEntity, guardianEntityList)
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN, i, count)
try:
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, states=['PENDING'], fields='nextPageToken,guardianInvitations(invitationId,invitedEmailAddress)')
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, fields='nextPageToken,guardians(guardianId,invitedEmailAddress)')
except GAPI.notFound:
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
continue
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.STUDENT, studentId], str(e), i, count)
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
studentUnknownWarning(studentId, str(e), i, count)
continue
Ind.Increment()
Act.Set(Act.CANCEL)
jcount = len(invitations)
j = 0
for invitation in invitations:
j += 1
if invitation['invitedEmailAddress'] not in guardianEmails:
_cancelGuardianInvitation(croom, studentId, invitation['invitationId'], i, count, j, jcount)
Act.Set(Act.DELETE)
jcount = len(guardians)
j = 0
for guardian in guardians:
j += 1
if guardian['invitedEmailAddress'] not in guardianEmails:
_deleteGuardian(croom, studentId, guardian['guardianId'], guardian['invitedEmailAddress'], i, count, j, jcount)
Act.Set(Act.CREATE)
for guardianEmail in guardianEmails:
for guardian in guardians:
if guardianEmail == guardian['invitedEmailAddress']:
break
else:
for invitation in invitations:
if guardianEmail == invitation['invitedEmailAddress']:
break
else:
_inviteGuardian(croom, studentId, guardianEmail, i, count)
Ind.Decrement()
def _getCourseName(croom, courseNames, courseId):
courseName = courseNames.get(courseId)
if courseName is None:
try:
courseName = callGAPI(croom.courses(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields='name')['name']
except (GAPI.notFound, GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable):
pass
if courseName is None:
courseName = courseId
courseNames[courseId] = courseName
return courseName
def _getClassroomEmail(croom, classroomEmails, userId, user):
if user.find('@') != -1:
return user
userEmail = classroomEmails.get(userId)
if userEmail is None:
try:
userEmail = callGAPI(croom.userProfiles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN,
GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userId=userId, fields='emailAddress').get('emailAddress')
except (GAPI.notFound, GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable):
pass
if userEmail is None:
userEmail = userId
classroomEmails[userId] = userEmail
return userEmail
GUARDIAN_TIME_OBJECTS = {'creationTime'}
GUARDIAN_STATES = ['complete', 'pending']
def _printShowGuardians(entityList=None):
croom = buildGAPIObject(API.CLASSROOM)
if entityList is None:
studentIds = ['-']
allStudents = True
else:
studentIds = entityList
allStudents = False
showStudentEmails = False
classroomEmails = {}
invitedEmailAddress = None
states = []
guardianClass = GUARDIAN_CLASS_ACCEPTED
csvPF = CSVPrintFile() if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'invitedguardian':
invitedEmailAddress = getEmailAddress()
elif myarg in {'invitation', 'invitations'}:
guardianClass = GUARDIAN_CLASS_INVITATIONS
if not states:
states = [state.upper() for state in GUARDIAN_STATES]
elif myarg == 'accepted':
guardianClass = GUARDIAN_CLASS_ACCEPTED
elif myarg == 'all':
guardianClass = GUARDIAN_CLASS_ALL
if not states:
states.append('PENDING')
elif myarg in {'state', 'states', 'status'}:
statesList = getString(Cmd.OB_GUARDIAN_STATE_LIST).lower().split(',')
states = []
for state in statesList:
if state in GUARDIAN_STATES:
states.append(state.upper())
else:
invalidChoiceExit(state, GUARDIAN_STATES, True)
elif myarg == 'showstudentemails':
showStudentEmails = True
elif entityList is None and myarg == 'student':
studentIds = [getString(Cmd.OB_STUDENT_ITEM)]
allStudents = studentIds[0] == '-'
elif FJQC.GetFormatJSONQuoteChar(myarg, False, True):
pass
elif entityList is None:
Cmd.Backup()
_, studentIds = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
allStudents = False
else:
unknownArgumentExit()
if csvPF:
if not FJQC.formatJSON:
sortTitles = ['studentEmail', 'studentId', 'invitedEmailAddress']
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
sortTitles.extend(['invitationId', 'creationTime', 'state'])
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
sortTitles.append('guardianId')
csvPF.SetTitles(sortTitles)
csvPF.SetSortAllTitles()
else:
csvPF.SetJSONTitles(['studentEmail', 'studentId', 'JSON'])
i, count, studentIds = getEntityArgument(studentIds)
for studentId in studentIds:
i += 1
if not allStudents:
studentId = normalizeStudentGuardianEmailAddressOrUID(studentId)
if showStudentEmails:
studentId = _getClassroomEmail(croom, classroomEmails, studentId, studentId)
try:
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
if csvPF:
if states:
qualifier = f' ({",".join(states)})'
else:
qualifier = ''
if not allStudents:
printGettingAllEntityItemsForWhom(Ent.GUARDIAN_INVITATION, studentId, i, count, qualifier=qualifier)
else:
printGettingAllEntityItemsForWhom(Ent.GUARDIAN_INVITATION, 'All students', qualifier=qualifier)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, invitedEmailAddress=invitedEmailAddress, states=states)
jcount = len(invitations)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.STUDENT, studentId if not allStudents else 'All'], jcount, Ent.GUARDIAN_INVITATION, i, count)
Ind.Increment()
j = 0
for invitation in invitations:
j += 1
printKeyValueListWithCount(['invitedEmailAddress', invitation['invitedEmailAddress']], j, jcount)
Ind.Increment()
if showStudentEmails:
invitation['studentEmail'] = _getClassroomEmail(croom, classroomEmails, invitation['studentId'], studentId)
showJSON(None, invitation, ['invitedEmailAddress'], GUARDIAN_TIME_OBJECTS)
Ind.Decrement()
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(invitations, timeObjects=GUARDIAN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
else:
if not FJQC.formatJSON:
for invitation in invitations:
if showStudentEmails:
invitation['studentEmail'] = _getClassroomEmail(croom, classroomEmails, invitation['studentId'], studentId)
else:
invitation['studentEmail'] = studentId
csvPF.WriteRowTitles(flattenJSON(invitation, timeObjects=GUARDIAN_TIME_OBJECTS))
else:
csvPF.WriteRow({'studentId': studentId, 'studentEmail': _getClassroomEmail(croom, classroomEmails, studentId, studentId),
'JSON': json.dumps(cleanJSON(invitations, timeObjects=GUARDIAN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
if csvPF:
if not allStudents:
printGettingAllEntityItemsForWhom(Ent.GUARDIAN, studentId, i, count)
else:
printGettingAllEntityItemsForWhom(Ent.GUARDIAN, 'All students')
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
pageMessage=pageMessage,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
studentId=studentId, invitedEmailAddress=invitedEmailAddress)
jcount = len(guardians)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.STUDENT, studentId if not allStudents else 'All'], jcount, Ent.GUARDIAN, i, count)
Ind.Increment()
j = 0
for guardian in guardians:
j += 1
printKeyValueListWithCount(['invitedEmailAddress', guardian['invitedEmailAddress']], j, jcount)
Ind.Increment()
if showStudentEmails:
guardian['studentEmail'] = _getClassroomEmail(croom, classroomEmails, guardian['studentId'], studentId)
showJSON(None, guardian, ['invitedEmailAddress'])
Ind.Decrement()
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(guardians), ensure_ascii=False, sort_keys=True))
else:
if not FJQC.formatJSON:
for guardian in guardians:
if showStudentEmails:
guardian['studentEmail'] = _getClassroomEmail(croom, classroomEmails, guardian['studentId'], studentId)
else:
guardian['studentEmail'] = studentId
csvPF.WriteRowTitles(flattenJSON(guardian))
else:
csvPF.WriteRowNoFilter({'studentId': studentId, 'studentEmail': _getClassroomEmail(croom, classroomEmails, studentId, studentId),
'JSON': json.dumps(cleanJSON(guardians), ensure_ascii=False, sort_keys=True)})
except GAPI.notFound:
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
studentUnknownWarning(studentId, str(e), i, count)
except GAPI.serviceNotAvailable as e:
entityActionFailedWarning([Ent.STUDENT, studentId], str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Guardians')
# gam show guardian|guardians [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
# [student <StudentItem>] [<UserTypeEntity>]
# [showstudentemails] [formatjson]
# gam print guardian|guardians [todrive <ToDriveAttribute>*] [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
# [student <StudentItem>] [<UserTypeEntity>]
# [showstudentemails] [formatjson [quotechar <Character>]]
def doPrintShowGuardians():
_printShowGuardians()
# gam <UserTypeEntity> show guardian|guardians [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
# [showstudentemails] [formatjson]
# gam <UserTypeEntity> print guardian|guardians [todrive <ToDriveAttribute>*] [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
# [showstudentemails] [formatjson [quotechar <Character>]]
def printShowGuardians(users):
_printShowGuardians(users)
CLASSROOM_ROLE_ALL = 'ALL'
CLASSROOM_ROLE_OWNER = 'OWNER'
CLASSROOM_ROLE_STUDENT = 'STUDENT'
CLASSROOM_ROLE_TEACHER = 'TEACHER'
def _getClassroomInvitations(croom, userId, courseId, role, i, count, j=0, jcount=0):
try:
invitations = callGAPIpages(croom.invitations(), 'list', 'invitations',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userId=userId, courseId=courseId)
except GAPI.notFound:
if userId is not None:
entityUnknownWarning(Ent.USER, userId, i, count)
return (-1, None)
entityUnknownWarning(Ent.COURSE, courseId, j, jcount)
return (0, [])
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
if userId is not None:
entityActionFailedWarning([Ent.USER, userId], str(e), i, count)
return (-1, None)
entityActionFailedWarning([Ent.COURSE, courseId], str(e), j, jcount)
return (0, [])
if role == CLASSROOM_ROLE_ALL:
return (1, invitations)
return (1, [invitation for invitation in invitations if invitation['role'] == role])
def _getClassroomInvitationIds(croom, userId, courseIds, role, i, count):
invitationIds = []
if courseIds is not None:
jcount = len(courseIds)
j = 0
for courseId in courseIds:
j += 1
courseId = addCourseIdScope(courseId)
status, invitations = _getClassroomInvitations(croom, userId, courseId, role, i, count, j, jcount)
if status < 0:
return (status, None)
if status > 0:
invitationIds.extend([invitation['id'] for invitation in invitations])
else:
status, invitations = _getClassroomInvitations(croom, userId, None, role, i, count)
if status < 0:
return (status, None)
if status > 0:
invitationIds.extend([invitation['id'] for invitation in invitations])
return (1, invitationIds)
CLASSROOM_CREATE_ROLE_MAP = {
'owner': CLASSROOM_ROLE_OWNER,
'student': CLASSROOM_ROLE_STUDENT,
'teacher': CLASSROOM_ROLE_TEACHER,
}
CLASSROOM_ROLE_MAP = {
'all': CLASSROOM_ROLE_ALL,
'owner': CLASSROOM_ROLE_OWNER,
'student': CLASSROOM_ROLE_STUDENT,
'teacher': CLASSROOM_ROLE_TEACHER,
}
CLASSROOM_ROLE_ENTITY_MAP = {
CLASSROOM_ROLE_ALL: Ent.CLASSROOM_INVITATION,
CLASSROOM_ROLE_OWNER: Ent.CLASSROOM_INVITATION_OWNER,
CLASSROOM_ROLE_STUDENT: Ent.CLASSROOM_INVITATION_STUDENT,
CLASSROOM_ROLE_TEACHER: Ent.CLASSROOM_INVITATION_TEACHER,
}
# gam <UserTypeEntity> create classroominvitation courses <CourseEntity> [role owner|student|teacher]
# [asadmin] [csv|csvformat] [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]
def createClassroomInvitations(users):
croom = buildGAPIObject(API.CLASSROOM)
classroomEmails = {}
courseIds = None
role = CLASSROOM_ROLE_STUDENT
useOwnerAccess = True
csvPF = None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'course', 'courses', 'class', 'classes'}:
courseIds = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
elif myarg == 'role':
role = getChoice(CLASSROOM_CREATE_ROLE_MAP, mapChoice=True)
elif myarg in {'csv', 'csvformat'}:
csvPF = CSVPrintFile()
FJQC.SetCsvPF(csvPF)
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ADMIN_ACCESS_OPTIONS:
useOwnerAccess = False
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if courseIds is None:
missingArgumentExit('courses <CourseEntity>')
if csvPF:
if FJQC.formatJSON:
csvPF.SetJSONTitles(['userEmail', 'JSON'])
else:
csvPF.SetTitles(['userEmail', 'courseId', 'courseName', 'id', 'role'])
csvPF.SetSortAllTitles()
courseIdsLists = courseIds if isinstance(courseIds, dict) else None
if courseIdsLists is None:
j, jcount, coursesInfo = _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess)
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
i, count, users = getEntityArgument(users)
for user in users:
i += 1
userId = normalizeEmailAddressOrUID(user)
userEmail = _getClassroomEmail(croom, classroomEmails, userId, userId)
if courseIdsLists:
j, jcount, coursesInfo = _getCoursesOwnerInfo(croom, courseIdsLists[user], useOwnerAccess)
if csvPF or not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
if jcount == 0:
continue
for courseId, courseInfo in coursesInfo.items():
j += 1
courseNameId = f'{courseInfo["name"]} ({courseId})'
try:
invitation = callGAPI(courseInfo['croom'].invitations(), 'create',
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.ALREADY_EXISTS, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
body={'userId': userId, 'courseId': courseId, 'role': role})
invitation['courseName'] = courseInfo['name']
invitation['userEmail'] = userEmail
if not csvPF:
if not FJQC.formatJSON:
Ind.Increment()
entityActionPerformed([Ent.USER, userEmail, Ent.COURSE, courseNameId, entityType, invitation['id']], j, jcount)
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(invitation), ensure_ascii=False, sort_keys=True))
else:
if not FJQC.formatJSON:
csvPF.WriteRow(invitation)
else:
csvPF.WriteRowNoFilter({'userEmail': userEmail,
'JSON': json.dumps(cleanJSON(invitation), ensure_ascii=False, sort_keys=True)})
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, userId, Ent.COURSE, courseNameId, entityType, None], str(e), j, jcount)
break
except GAPI.notFound:
entityUnknownWarning(Ent.COURSE, courseNameId, j, jcount)
except (GAPI.failedPrecondition, GAPI.alreadyExists, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, userId, Ent.COURSE, courseNameId, entityType, None], str(e), j, jcount)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('ClassroomInvitations')
def acceptDeleteClassroomInvitations(users, function):
croom = buildGAPIObject(API.CLASSROOM)
if function == 'delete':
ucroom = croom
courseIds = invitationIds = None
role = CLASSROOM_ROLE_ALL
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'id', 'ids'}:
invitationIds = getEntityList(Cmd.OB_CLASSROOM_INVITATION_ID_ENTITY)
courseIds = None
elif myarg in {'course', 'courses', 'class', 'classes'}:
courseIds = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
invitationIds = None
elif myarg == 'role':
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
else:
unknownArgumentExit()
courseIdsLists = courseIds if isinstance(courseIds, dict) else None
invitationIdsLists = invitationIds if isinstance(invitationIds, dict) else None
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if function == 'delete':
userId = normalizeEmailAddressOrUID(user)
else:
userId, ucroom = buildGAPIServiceObject(API.CLASSROOM, user, i, count)
if not ucroom:
continue
if invitationIdsLists:
userInvitationIds = invitationIdsLists[user]
elif invitationIds is not None:
userInvitationIds = invitationIds
else:
if courseIdsLists:
courseIds = courseIdsLists[user]
status, userInvitationIds = _getClassroomInvitationIds(croom, userId, courseIds, role, i, count)
if status < 0:
continue
jcount = len(userInvitationIds)
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
Ind.Increment()
j = 0
for invitationId in userInvitationIds:
j += 1
try:
callGAPI(ucroom.invitations(), function,
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
id=invitationId)
entityActionPerformed([Ent.USER, userId, entityType, invitationId], j, jcount)
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, userId, entityType, invitationId], str(e), j, jcount)
Ind.Decrement()
# gam <UserTypeEntity> accept classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
def acceptClassroomInvitations(users):
acceptDeleteClassroomInvitations(users, 'accept')
# gam <UserTypeEntity> delete classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
def deleteClassroomInvitations(users):
acceptDeleteClassroomInvitations(users, 'delete')
# gam <UserTypeEntity> show classroominvitations [role all|owner|student|teacher]
# [formatjson]
# gam <UserTypeEntity> print classroominvitations [todrive <ToDriveAttribute>*] [role all|owner|student|teacher]
# [formatjson [quotechar <Character>]]
def printShowClassroomInvitations(users):
croom = buildGAPIObject(API.CLASSROOM)
classroomEmails = {}
courseNames = {}
role = CLASSROOM_ROLE_ALL
csvPF = CSVPrintFile() if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'role':
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if csvPF:
if FJQC.formatJSON:
csvPF.SetJSONTitles(['userEmail', 'JSON'])
else:
csvPF.SetTitles(['userId', 'userEmail', 'courseId', 'courseName', 'id', 'role'])
csvPF.SetSortAllTitles()
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
i, count, users = getEntityArgument(users)
for user in users:
i += 1
userId = normalizeEmailAddressOrUID(user)
userEmail = _getClassroomEmail(croom, classroomEmails, userId, userId)
if csvPF:
printGettingAllEntityItemsForWhom(entityType, userId, i, count)
status, invitations = _getClassroomInvitations(croom, userId, None, role, i, count)
if status > 0:
jcount = len(invitations)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
if not csvPF:
if not FJQC.formatJSON:
Ind.Increment()
j = 0
for invitation in invitations:
j += 1
courseId = invitation['courseId']
courseName = _getCourseName(croom, courseNames, courseId)
printKeyValueListWithCount([Ent.Singular(Ent.COURSE), f'{courseName} ({courseId})'], j, jcount)
Ind.Increment()
printKeyValueList(['id', invitation['id']])
printKeyValueList(['role', invitation['role']])
printKeyValueList(['userid', invitation['userId']])
printKeyValueList(['userEmail', userEmail])
Ind.Decrement()
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True))
else:
if not FJQC.formatJSON:
for invitation in invitations:
invitation['courseName'] = _getCourseName(croom, courseNames, invitation['courseId'])
invitation['userEmail'] = userEmail
csvPF.WriteRow(invitation)
else:
csvPF.WriteRowNoFilter({'userEmail': userEmail,
'JSON': json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('ClassroomInvitations')
# gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
def doDeleteClassroomInvitations():
croom = buildGAPIObject(API.CLASSROOM)
courseIdList = invitationIds = None
role = CLASSROOM_ROLE_ALL
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'course', 'courses', 'class', 'classes'}:
courseIdList = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
elif myarg in {'id', 'ids'}:
invitationIds = getEntityList(Cmd.OB_CLASSROOM_INVITATION_ID_ENTITY)
elif myarg == 'role':
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
else:
unknownArgumentExit()
if courseIdList is None:
missingArgumentExit('courses <CourseEntity>')
entityType = Ent.CLASSROOM_INVITATION
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, True)
for courseId, courseInfo in coursesInfo.items():
i += 1
courseNameId = f'{courseInfo["name"]} ({courseId})'
if invitationIds is not None:
userInvitationIds = invitationIds
else:
status, userInvitationIds = _getClassroomInvitationIds(courseInfo['croom'], None, [courseId], role, i, count)
if status < 0:
continue
jcount = len(userInvitationIds)
entityPerformActionNumItems([Ent.COURSE, courseNameId], jcount, entityType, i, count)
Ind.Increment()
j = 0
for invitationId in userInvitationIds:
j += 1
try:
callGAPI(courseInfo['croom'].invitations(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
id=invitationId)
entityActionPerformed([Ent.COURSE, courseNameId, entityType, invitationId], j, jcount)
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.COURSE, courseNameId, entityType, invitationId], str(e), j, jcount)
Ind.Decrement()
# gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
# [role all|owner|student|teacher] [formatjson]
# gam print classroominvitations [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
# [role all|owner|student|teacher] [formatjson [quotechar <Character>]]
def doPrintShowClassroomInvitations():
croom = buildGAPIObject(API.CLASSROOM)
classroomEmails = {}
courseSelectionParameters = _initCourseSelectionParameters()
courseShowProperties = _initCourseShowProperties(['id', 'name'])
role = CLASSROOM_ROLE_ALL
csvPF = CSVPrintFile(['courseId', 'courseName']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
pass
elif myarg == 'role':
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
if coursesInfo is None:
return
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(['id', 'role', 'userId', 'userEmail'])
csvPF.SetSortAllTitles()
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
i = 0
count = len(coursesInfo)
for course in coursesInfo:
i += 1
courseId = course['id']
courseName = course['name']
status, invitations = _getClassroomInvitations(croom, None, courseId, role, i, count)
if status > 0:
jcount = len(invitations)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.COURSE, f'{courseName} ({courseId})'], jcount, entityType, i, count)
if not csvPF:
if not FJQC.formatJSON:
Ind.Increment()
j = 0
for invitation in invitations:
j += 1
printKeyValueListWithCount(['id', invitation['id']], j, jcount)
Ind.Increment()
printKeyValueList(['role', invitation['role']])
userId = invitation.get('userId')
if userId is not None:
printKeyValueList(['userid', userId])
printKeyValueList(['userEmail', _getClassroomEmail(croom, classroomEmails, userId, userId)])
Ind.Decrement()
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True))
else:
if not FJQC.formatJSON:
for invitation in invitations:
invitation['courseName'] = courseName
userId = invitation.get('userId')
if userId is not None:
invitation['userEmail'] = _getClassroomEmail(croom, classroomEmails, userId, userId)
csvPF.WriteRowNoFilter(invitation)
else:
csvPF.WriteRowNoFilter({'courseId': courseId, 'courseName': courseName,
'JSON': json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('ClassroomInvitations')
# gam <UserTypeEntity> print classroomprofile [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show classroomprofile
def printShowClassroomProfile(users):
croom = buildGAPIObject(API.CLASSROOM)
csvPF = CSVPrintFile(['emailAddress', 'id',
f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}givenName',
f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}familyName',
f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}fullName',
'photoUrl'], indexedTitles=['permissions']) if Act.csvFormat() else None
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
userId = normalizeEmailAddressOrUID(user)
if csvPF:
printGettingEntityItemForWhom(Ent.CLASSROOM_USER_PROFILE, user, i, count)
try:
result = callGAPI(croom.userProfiles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userId=userId, fields='*')
result.setdefault('verifiedTeacher', False)
if not csvPF:
printEntity([Ent.USER, userId], i, count)
Ind.Increment()
printKeyValueList(['email', result['emailAddress']])
printKeyValueList([UProp.PROPERTIES['id'][UProp.TITLE], result['id']])
for up in USER_NAME_PROPERTY_PRINT_ORDER:
if up in result['name']:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], result['name'][up]])
printKeyValueList([UProp.PROPERTIES['thumbnailPhotoUrl'][UProp.TITLE], result['photoUrl']])
printKeyValueList(['Permissions', ','.join([permission['permission'] for permission in result.get('permissions', [])])])
printKeyValueList(['Verified Teacher', result['verifiedTeacher']])
Ind.Decrement()
else:
csvPF.WriteRowTitles(flattenJSON(result))
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, userId], str(e))
if csvPF:
csvPF.writeCSVfile('Classroom User Profiles')
def _showASPs(user, asps, i=0, count=0):
Act.Set(Act.SHOW)
jcount = len(asps)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.APPLICATION_SPECIFIC_PASSWORD, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
for asp in asps:
if asp['creationTime'] == '0':
created_date = UNKNOWN
else:
created_date = formatLocalTimestamp(asp['creationTime'])
if asp['lastTimeUsed'] == '0':
used_date = GC.NEVER
else:
used_date = formatLocalTimestamp(asp['lastTimeUsed'])
printKeyValueList(['ID', asp['codeId']])
Ind.Increment()
printKeyValueList(['Name', asp['name']])
printKeyValueList(['Created', created_date])
printKeyValueList(['Last Used', used_date])
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> delete asps|applicationspecificpasswords all|<AspIDList>
def deleteASP(users):
cd = buildGAPIObject(API.DIRECTORY)
codeIdList = getString(Cmd.OB_ASP_ID_LIST).lower()
if codeIdList == 'all':
allCodeIds = True
else:
allCodeIds = False
codeIds = codeIdList.replace(',', ' ').split()
for codeId in codeIds:
if not codeId.isdigit():
Cmd.Backup()
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(Ent.APPLICATION_SPECIFIC_PASSWORD), Msg.MUST_BE_NUMERIC))
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
if allCodeIds:
try:
asps = callGAPIitems(cd.asps(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user, fields='items(codeId)')
codeIds = [asp['codeId'] for asp in asps]
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
continue
jcount = len(codeIds)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.APPLICATION_SPECIFIC_PASSWORD, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
Ind.Increment()
j = 0
for codeId in codeIds:
j += 1
try:
callGAPI(cd.asps(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER, GAPI.FORBIDDEN],
userKey=user, codeId=codeId)
entityActionPerformed([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], j, jcount)
except (GAPI.invalid, GAPI.invalidParameter, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], str(e), j, jcount)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> print asps|applicationspecificpasswords [todrive <ToDriveAttribute>*]
# [oneitemperrow]
# gam <UserTypeEntity> show asps|applicationspecificpasswords
def printShowASPs(users):
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User']) if Act.csvFormat() else None
oneItemPerRow = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'oneitemperrow':
oneItemPerRow = True
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
if csvPF:
printGettingEntityItemForWhom(Ent.APPLICATION_SPECIFIC_PASSWORD, user, i, count)
try:
asps = callGAPIitems(cd.asps(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user)
if not csvPF:
_showASPs(user, asps, i, count)
else:
for asp in asps:
asp.pop('userKey', None)
if asp['creationTime'] == '0':
asp['creationTime'] = UNKNOWN
else:
asp['creationTime'] = formatLocalTimestamp(asp['creationTime'])
if asp['lastTimeUsed'] == '0':
asp['lastTimeUsed'] = GC.NEVER
else:
asp['lastTimeUsed'] = formatLocalTimestamp(asp['lastTimeUsed'])
if not oneItemPerRow:
csvPF.WriteRowTitles(flattenJSON({'asps': asps}, flattened={'User': user}))
else:
for asp in asps:
csvPF.WriteRowTitles(flattenJSON({'asp': asp}, flattened={'User': user}))
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
if csvPF:
csvPF.writeCSVfile('Application Specific Passwords')
def _showBackupCodes(user, codes, i, count):
Act.Set(Act.SHOW)
jcount = 0
for code in codes:
if code.get('verificationCode'):
jcount += 1
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.BACKUP_VERIFICATION_CODES, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return
Ind.Increment()
j = 0
for code in codes:
j += 1
printKeyValueList([f'{j:2}', code.get('verificationCode')])
Ind.Decrement()
# gam <UserTypeEntity> update backupcodes|verificationcodes
def updateBackupCodes(users):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
userSuspended = checkUserSuspended(cd, user, Ent.USER, i, count)
if userSuspended is None:
continue
if not userSuspended:
try:
callGAPI(cd.verificationCodes(), 'generate',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT],
userKey=user)
codes = callGAPIitems(cd.verificationCodes(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user, fields='items(verificationCode)')
_showBackupCodes(user, codes, i, count)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
except (GAPI.invalid, GAPI.invalidInput) as e:
entityActionFailedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], str(e), i, count)
else:
entityActionNotPerformedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None],
Msg.IS_SUSPENDED_NO_BACKUPCODES, i, count)
# gam <UserTypeEntity> delete backupcodes|verificationcodes
def deleteBackupCodes(users):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
userSuspended = checkUserSuspended(cd, user, Ent.USER, i, count)
if userSuspended is None:
continue
if not userSuspended:
try:
callGAPI(cd.verificationCodes(), 'invalidate',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT],
userKey=user)
printEntityKVList([Ent.USER, user], [Ent.Plural(Ent.BACKUP_VERIFICATION_CODES), '', 'Invalidated'], i, count)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
except (GAPI.invalid, GAPI.invalidInput) as e:
entityActionFailedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], str(e), i, count)
else:
entityActionNotPerformedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None],
Msg.IS_SUSPENDED_NO_BACKUPCODES, i, count)
# gam <UserTypeEntity> print backupcodes|verificationcodes [todrive <ToDriveAttribute>*]
# [delimiter <Character>] [countsonly]
# gam <UserTypeEntity> show backupcodes|verificationcodes
def printShowBackupCodes(users):
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'verificationCodesCount', 'verificationCodes']) if Act.csvFormat() else None
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
counts_only = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'countsonly':
counts_only = True
else:
unknownArgumentExit()
# if we're only getting counts, we don't want actual codes pulled down
if counts_only:
csvPF.RemoveTitles('verificationCodes')
fields = 'items(etag)'
else:
fields = 'items(verificationCode)'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
if csvPF:
printGettingEntityItemForWhom(Ent.BACKUP_VERIFICATION_CODES, user, i, count)
try:
codes = callGAPIitems(cd.verificationCodes(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user, fields=fields)
if not csvPF:
_showBackupCodes(user, codes, i, count)
elif counts_only:
csvPF.WriteRow({'User': user, 'verificationCodesCount': len(codes)})
else:
csvPF.WriteRow({'User': user,
'verificationCodesCount': len(codes),
'verificationCodes': delimiter.join([code['verificationCode'] for code in codes if 'verificationCode' in code])})
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
if csvPF:
csvPF.writeCSVfile('Backup Verification Codes')
def _getCalendarSelectProperty(myarg, kwargs):
if myarg == 'minaccessrole':
kwargs['minAccessRole'] = getChoice(CALENDAR_ACL_ROLES_MAP, mapChoice=True)
elif myarg == 'showdeleted':
kwargs['showDeleted'] = True
elif myarg == 'showhidden':
kwargs['showHidden'] = True
else:
return False
return True
def initUserCalendarEntity():
return {'list': [], 'kwargs': {}, 'dict': None, 'all': False, 'primary': False, 'resourceIds': []}
def getUserCalendarEntity(default='primary', noSelectionKwargs=None):
def _initCourseCalendarSelectionParameters():
return {'courseIds': [], 'teacherId': None, 'myCoursesAsTeacher': False,
'studentId': None, 'myCoursesAsStudent': False, 'courseStates': []}
def _getCourseCalendarSelectionParameters(myarg):
if myarg in {'course', 'courses', 'class', 'classes'}:
courseSelectionParameters['courseIds'].extend(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
elif myarg == 'courseswithteacher':
courseSelectionParameters['teacherId'] = getEmailAddress()
courseSelectionParameters['myCoursesAsTeacher'] = False
elif myarg == 'mycoursesasteacher':
courseSelectionParameters['myCoursesAsTeacher'] = True
courseSelectionParameters['teacherId'] = None
elif myarg == 'courseswithstudent':
courseSelectionParameters['studentId'] = getEmailAddress()
courseSelectionParameters['myCoursesAsStudent'] = False
elif myarg == 'mycoursesasstudent':
courseSelectionParameters['myCoursesAsStudent'] = True
courseSelectionParameters['studentId'] = None
elif myarg in {'coursestate', 'coursestates', 'coursestatus'}:
_getCourseStates(Cmd.OB_COURSE_STATE_LIST, courseSelectionParameters['courseStates'])
else:
return False
return True
def _noSelectionMade():
return (not calendarEntity['list'] and not calendarEntity['kwargs'] and calendarEntity['dict'] is None and
not calendarEntity['all'] and not calendarEntity['primary'] and not calendarEntity['resourceIds'] and
not courseCalendarSelected)
calendarEntity = initUserCalendarEntity()
courseSelectionParameters = _initCourseCalendarSelectionParameters()
courseCalendarSelected = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'calendar', 'calendars'}:
entitySelector = getEntitySelector()
if entitySelector:
entityList = getEntitySelection(entitySelector, False)
if isinstance(entityList, dict):
calendarEntity['dict'] = entityList
else:
calendarEntity['list'] = entityList
else:
calendarEntity['list'].extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST)))
elif myarg == 'allcalendars':
calendarEntity['all'] = True
elif myarg == 'primary':
calendarEntity['primary'] = True
elif _getCalendarSelectProperty(myarg, calendarEntity['kwargs']):
pass
elif myarg == 'resource':
calendarEntity['resourceIds'].append(getString(Cmd.OB_RESOURCE_ID))
elif myarg == 'resources':
calendarEntity['resourceIds'].extend(convertEntityToList(getString(Cmd.OB_RESOURCE_ID, minLen=0), shlexSplit=True))
elif _getCourseCalendarSelectionParameters(myarg):
courseCalendarSelected = True
elif _noSelectionMade() and (myarg.find('@') != -1 or myarg.find('id:') != -1):
Cmd.Backup()
calendarEntity['list'].append(getEmailAddress())
else:
Cmd.Backup()
break
if _noSelectionMade():
if not noSelectionKwargs:
calendarEntity[default] = True
else:
calendarEntity['all'] = True
calendarEntity['kwargs'].update(noSelectionKwargs)
elif (courseCalendarSelected and
(courseSelectionParameters['courseIds'] or
courseSelectionParameters['teacherId'] or courseSelectionParameters['myCoursesAsTeacher'] or
courseSelectionParameters['studentId'] or courseSelectionParameters['myCoursesAsStudent'])):
calendarEntity['courseSelectionParameters'] = courseSelectionParameters
calendarEntity['courseShowProperties'] = _initCourseShowProperties(['calendarId'])
calendarEntity['croom'] = buildGAPIObject(API.CLASSROOM)
return calendarEntity
def _validateUserGetCalendarIds(user, i, count, calendarEntity,
itemType=None, modifier=None, showAction=True, setRC=True, newCalId=None, secondaryCalendarsOnly=False):
if user and calendarEntity['dict']:
calIds = calendarEntity['dict'][user][:]
else:
calIds = calendarEntity['list'][:]
user, cal = validateCalendar(user, i, count, noClientAccess=True)
if not cal:
return (user, None, None, 0)
if calendarEntity['resourceIds']:
cd = buildGAPIObject(API.DIRECTORY)
for resourceId in calendarEntity['resourceIds']:
try:
calIds.append(callGAPI(cd.resources().calendars(), 'get',
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId,
fields='resourceEmail')['resourceEmail'])
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
return (user, None, None, 0)
courseSelectionParameters = calendarEntity.get('courseSelectionParameters')
if courseSelectionParameters is not None:
if courseSelectionParameters['myCoursesAsTeacher']:
courseSelectionParameters['teacherId'] = user
if courseSelectionParameters['myCoursesAsStudent']:
courseSelectionParameters['studentId'] = user
coursesInfo = _getCoursesInfo(calendarEntity['croom'], courseSelectionParameters,
calendarEntity['courseShowProperties'])
if coursesInfo is None:
return (user, None, None, 0)
calIds.extend([course['calendarId'] for course in coursesInfo if 'calendarId' in course])
if calendarEntity['primary']:
calIds.append(user)
try:
if calendarEntity['kwargs'] or calendarEntity['all']:
result = callGAPIpages(cal.calendarList(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS,
fields='nextPageToken,items/id', **calendarEntity['kwargs'])
calIds.extend([calId['id'] for calId in result if not secondaryCalendarsOnly or calId['id'].find('@group.calendar.google.com') != -1])
else:
callGAPI(cal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS,
calendarId='primary', fields='')
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
return (user, None, None, 0)
if newCalId:
newcal = buildGAPIObject(API.CALENDAR)
if not checkCalendarExists(newcal, newCalId, i, count):
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, newCalId], Msg.DOES_NOT_EXIST, i, count)
return (user, None, None, 0)
jcount = len(calIds)
if setRC and jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if showAction:
if not itemType:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CALENDAR, i, count)
elif not newCalId:
entityPerformActionSubItemModifierNumItems([Ent.USER, user], itemType, modifier, jcount, Ent.CALENDAR, i, count)
else:
entityPerformActionSubItemModifierNumItemsModifierNewValue([Ent.USER, user], itemType, modifier, jcount, Ent.CALENDAR, Act.MODIFIER_TO, newCalId, i, count)
return (user, cal, calIds, jcount)
CALENDAR_NOTIFICATION_METHODS = ['email']
CALENDAR_NOTIFICATION_TYPES_MAP = {
'eventcreation': 'eventCreation',
'eventchange': 'eventChange',
'eventcancellation': 'eventCancellation',
'eventresponse': 'eventResponse',
'agenda': 'agenda',
}
def _getCalendarAttributes(body, returnOnUnknownArgument=False):
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'selected':
body['selected'] = getBoolean()
elif myarg == 'hidden':
body['hidden'] = getBoolean()
elif myarg == 'summary':
body['summaryOverride'] = getString(Cmd.OB_STRING)
elif myarg in {'color', 'colour'}:
body['colorId'] = getChoice(CALENDAR_COLOR_MAP, mapChoice=True)
elif myarg in {'colorindex', 'colorid', 'colourindex', 'colourid'}:
body['colorId'] = getInteger(minVal=CALENDAR_MIN_COLOR_INDEX, maxVal=CALENDAR_MAX_COLOR_INDEX)
elif myarg in {'backgroundcolor', 'backgroundcolour'}:
body['backgroundColor'] = getColor()
body.setdefault('foregroundColor', '#000000')
elif myarg in {'foregroundcolor', 'foregroundcolour'}:
body['foregroundColor'] = getColor()
elif myarg == 'reminder':
body.setdefault('defaultReminders', [])
if not checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
body['defaultReminders'].append(getCalendarReminder(True))
elif myarg == 'notification':
body.setdefault('notificationSettings', {'notifications': []})
method = getChoice(CALENDAR_NOTIFICATION_METHODS+Cmd.CLEAR_NONE_ARGUMENT)
if method not in Cmd.CLEAR_NONE_ARGUMENT:
for ntype in _getFieldsList():
if ntype in CALENDAR_NOTIFICATION_TYPES_MAP:
body['notificationSettings']['notifications'].append({'method': method,
'type': CALENDAR_NOTIFICATION_TYPES_MAP[ntype]})
else:
invalidChoiceExit(ntype, CALENDAR_NOTIFICATION_TYPES_MAP, True)
else:
body['notificationSettings']['notifications'] = []
elif returnOnUnknownArgument:
Cmd.Backup()
return
else:
unknownArgumentExit()
def _showCalendar(calendar, j, jcount, FJQC, acls=None):
if FJQC.formatJSON:
if acls:
calendar['acls'] = [{'id': rule['id'], 'role': rule['role']} for rule in acls]
printLine(json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True))
return
_showCalendarSettings(calendar, j, jcount)
Ind.Increment()
if 'primary' in calendar:
printKeyValueList(['Primary', calendar['primary']])
if 'accessRole' in calendar:
printKeyValueList(['Access Level', calendar['accessRole']])
if 'deleted' in calendar:
printKeyValueList(['Deleted', calendar['deleted']])
if 'hidden' in calendar:
printKeyValueList(['Hidden', calendar['hidden']])
if 'selected' in calendar:
printKeyValueList(['Selected', calendar['selected']])
if 'colorId' in calendar:
printKeyValueList(['Color ID', calendar['colorId'], 'Background Color', calendar['backgroundColor'], 'Foreground Color', calendar['foregroundColor']])
if 'defaultReminders' in calendar:
printKeyValueList(['Default Reminders', None])
Ind.Increment()
for reminder in calendar['defaultReminders']:
printKeyValueList(['Method', reminder['method'], 'Minutes', reminder['minutes']])
Ind.Decrement()
if 'notificationSettings' in calendar:
printKeyValueList(['Notifications', None])
Ind.Increment()
for notification in calendar['notificationSettings'].get('notifications', []):
printKeyValueList(['Method', notification['method'], 'Type', notification['type']])
Ind.Decrement()
if acls:
j = 0
jcount = len(acls)
printEntitiesCount(Ent.CALENDAR_ACL, acls)
Ind.Increment()
for rule in acls:
j += 1
printKeyValueListWithCount(ACLRuleKeyValueList(rule), j, jcount)
Ind.Decrement()
Ind.Decrement()
# Process CalendarList functions
def _processCalendarList(user, i, count, calId, j, jcount, cal, function, **kwargs):
try:
callGAPI(cal.calendarList(), function,
throwReasons=[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR,
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION],
**kwargs)
entityActionPerformed([Ent.USER, user, Ent.CALENDAR, calId], j, jcount)
except (GAPI.notFound, GAPI.duplicate, GAPI.unknownError, GAPI.serviceNotAvailable,
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> add calendars <UserCalendarAddEntity> <CalendarAttribute>*
def addCalendars(users):
calendarEntity = getUserCalendarEntity()
body = {'selected': True, 'hidden': False}
_getCalendarAttributes(body)
colorRgbFormat = 'backgroundColor' in body or 'foregroundColor' in body
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for calId in calIds:
j += 1
body['id'] = calId = normalizeCalendarId(calId, user)
_processCalendarList(user, i, count, calId, j, jcount, cal, 'insert',
body=body, colorRgbFormat=colorRgbFormat, fields='')
Ind.Decrement()
def _updateDeleteCalendars(users, calendarEntity, function, **kwargs):
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for calId in calIds:
j += 1
calId = normalizeCalendarId(calId, user)
_processCalendarList(user, i, count, calId, j, jcount, cal, function,
calendarId=calId, **kwargs)
Ind.Decrement()
# gam <UserTypeEntity> update calendars <UserCalendarEntity> <CalendarAttribute>+
def updateCalendars(users):
calendarEntity = getUserCalendarEntity()
body = {}
_getCalendarAttributes(body)
colorRgbFormat = 'backgroundColor' in body or 'foregroundColor' in body
_updateDeleteCalendars(users, calendarEntity, 'patch', body=body, colorRgbFormat=colorRgbFormat, fields='')
# gam <UserTypeEntity> delete calendars <UserCalendarEntity>
def deleteCalendars(users):
calendarEntity = getUserCalendarEntity()
checkForExtraneousArguments()
_updateDeleteCalendars(users, calendarEntity, 'delete')
# gam <UserTypeEntity> create calendars <CalendarSettings>
def createCalendar(users):
calendarEntity = initUserCalendarEntity()
body = getCalendarSettings(summaryRequired=True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, _, _ = _validateUserGetCalendarIds(user, i, count, calendarEntity, showAction=False, setRC=False)
if not cal:
continue
try:
calId = callGAPI(cal.calendars(), 'insert',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.FORBIDDEN],
body=body, fields='id')['id']
entityActionPerformed([Ent.USER, user, Ent.CALENDAR, calId], i, count)
except GAPI.forbidden as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
def addCreateCalendars(users):
if Act.Get() == Act.ADD:
addCalendars(users)
else:
createCalendar(users)
def _modifyRemoveCalendars(users, calendarEntity, function, **kwargs):
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for calId in calIds:
j += 1
calId = normalizeCalendarId(calId, user)
try:
callGAPI(cal.calendars(), function,
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.CANNOT_DELETE_PRIMARY_CALENDAR,
GAPI.FORBIDDEN, GAPI.INVALID, GAPI.REQUIRED_ACCESS_LEVEL],
calendarId=calId, **kwargs)
entityActionPerformed([Ent.USER, user, Ent.CALENDAR, calId], j, jcount)
except (GAPI.notFound, GAPI.cannotDeletePrimaryCalendar, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel) as e:
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> modify calendars <UserCalendarEntity> <CalendarSettings>
def modifyCalendars(users):
calendarEntity = getUserCalendarEntity()
body = getCalendarSettings(summaryRequired=False)
_modifyRemoveCalendars(users, calendarEntity, 'patch', body=body)
# gam <UserTypeEntity> remove calendars <UserCalendarEntity>
def removeCalendars(users):
calendarEntity = getUserCalendarEntity()
checkForExtraneousArguments()
_modifyRemoveCalendars(users, calendarEntity, 'delete')
def _getCalendarPermissions(cal, calendar):
if calendar['accessRole'] == 'owner':
try:
return callGAPIpages(cal.acl(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
calendarId=calendar['id'], fields='nextPageToken,items(id,role,scope)')
except (GAPI.notACalendarUser, GAPI.notFound):
pass
return []
CALENDAR_LIST_FIELDS_CHOICE_MAP = {
"accessrole": 'accessRole',
"backgroundcolor": 'backgroundColor',
"backgroundcolour": 'backgroundColor',
"colorid": 'colorId',
"conferenceproperties": 'conferenceProperties',
"defaultreminders": 'defaultReminders',
"deleted": 'deleted',
"description": 'description',
"foregroundcolor": 'foregroundColor',
"foregroundcolour": 'foregroundColor',
"hidden": 'hidden',
"id": 'id',
"location": 'location',
"notificationsettings": 'notificationSettings',
"primary": 'primary',
"selected": 'selected',
"summary": ['summary', 'summaryOverride'],
"summaryoverride": ['summary', 'summaryOverride'],
"timezone": 'timeZone',
}
# gam <UserTypeEntity> info calendars <UserCalendarEntity>
# [fields <CalendarFieldList>] [permissions]
# [formatjson]
def infoCalendars(users):
calendarEntity = getUserCalendarEntity()
FJQC = FormatJSONQuoteChar()
fieldsList = []
acls = []
getCalPermissions = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
getCalPermissions = True
elif getFieldsList(myarg, CALENDAR_LIST_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
else:
FJQC.GetFormatJSON(myarg)
if fieldsList:
if getCalPermissions:
fieldsList.append('accessRole')
fields = getFieldsFromFieldsList(fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for calId in calIds:
j += 1
calId = normalizeCalendarId(calId, user)
try:
result = callGAPI(cal.calendarList(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
calendarId=calId, fields=fields)
if getCalPermissions:
acls = _getCalendarPermissions(cal, result)
_showCalendar(result, j, jcount, FJQC, acls)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
CALENDAR_SIMPLE_LISTS = {'allowedConferenceSolutionTypes'}
CALENDAR_EXCLUDE_OPTIONS = {'noprimary', 'nogroups', 'noresources', 'nosystem', 'nousers'}
CALENDAR_EXCLUDE_DOMAINS = {
'nogroups': 'group.calendar.google.com',
'noresources': 'resource.calendar.google.com',
'nosystem': 'group.v.calendar.google.com',
}
# gam <UserTypeEntity> print calendars <UserCalendarEntity> [todrive <ToDriveAttribute>*]
# [primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
# [fields <CalendarFieldList>] [permissions]
# [formatjson [quotechar <Character>]] [delimiter <Character>]
# gam <UserTypeEntity> show calendars <UserCalendarEntity>
# [primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
# [fields <CalendarFieldList>] [permissions]
# [formatjson]
def printShowCalendars(users):
acls = []
getCalPermissions = noPrimary = primaryOnly = False
excludes = set()
excludeDomains = set()
csvPF = CSVPrintFile(['primaryEmail', 'calendarId'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {}
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
getCalPermissions = True
elif myarg == 'allcalendars':
pass
elif myarg == 'primary':
primaryOnly = True
elif _getCalendarSelectProperty(myarg, kwargs):
pass
elif myarg in CALENDAR_EXCLUDE_OPTIONS:
excludes.add(myarg)
elif myarg == 'delimiter':
delimiter = getCharacter()
elif getFieldsList(myarg, CALENDAR_LIST_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
for exclude in excludes:
if exclude == 'noprimary':
noPrimary = True
elif exclude == 'nousers':
excludeDomains.add(GC.Values[GC.DOMAIN])
else:
excludeDomains.add(CALENDAR_EXCLUDE_DOMAINS[exclude])
if fieldsList:
if getCalPermissions:
fieldsList.append('accessRole')
if noPrimary or primaryOnly:
fieldsList.append('primary')
fields = getItemFieldsFromFieldsList('items', fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal = validateCalendar(user, i, count, noClientAccess=True)
if not cal:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.CALENDAR, user, i, count)
try:
calendars = callGAPIpages(cal.calendarList(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS,
fields=fields, **kwargs)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
continue
if primaryOnly:
for calendar in calendars:
if calendar.get('primary', False):
calendars = [calendar]
break
else:
calendars = []
elif noPrimary or excludeDomains:
allCalendars = calendars[:]
calendars = []
for calendar in allCalendars:
primary = calendar.get('primary', False)
if noPrimary and primary:
continue
if not primary and excludeDomains:
_, domain = splitEmailAddress(calendar['id'])
if domain in excludeDomains:
continue
calendars.append(calendar)
jcount = len(calendars)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CALENDAR, i, count)
Ind.Increment()
j = 0
for calendar in calendars:
j += 1
if getCalPermissions:
acls = _getCalendarPermissions(cal, calendar)
_showCalendar(calendar, j, jcount, FJQC, acls)
Ind.Decrement()
else:
if calendars:
for calendar in calendars:
row = {'primaryEmail': user, 'calendarId': calendar['id']}
if getCalPermissions:
flattenJSON({'permissions': _getCalendarPermissions(cal, calendar)}, flattened=row)
flattenJSON(calendar, flattened=row, simpleLists=CALENDAR_SIMPLE_LISTS, delimiter=delimiter)
if not FJQC.formatJSON:
row.pop('id')
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'primaryEmail': user, 'calendarId': calendar['id'],
'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'primaryEmail': user})
if csvPF:
csvPF.writeCSVfile('Calendars')
USER_CALENDAR_SETTINGS_FIELDS_CHOICE_MAP = {
'autoaddhangouts': 'autoAddHangouts',
'datefieldorder': 'dateFieldOrder',
'defaulteventlength': 'defaultEventLength',
'format24hourtime': 'format24HourTime',
'hideinvitations': 'hideInvitations',
'hideweekends': 'hideWeekends',
'locale': 'locale',
'remindonrespondedeventsonly': 'remindOnRespondedEventsOnly',
'showdeclinedevents': 'showDeclinedEvents',
'timezone': 'timezone',
'usekeyboardshortcuts': 'useKeyboardShortcuts',
'weekstart': 'weekStart'
}
# gam <UserTypeEntity> print calsettings [todrive <ToDriveAttribute>*]
# [fields <UserCalendarSettingsFieldList>]
# [formatjson] [quotechar <Character>}
# gam <UserTypeEntity> show calsettings
# [formatjson]
def printShowCalSettings(users):
csvPF = CSVPrintFile(['User'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getFieldsList(myarg, USER_CALENDAR_SETTINGS_FIELDS_CHOICE_MAP, fieldsList):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
fields = set(fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal = validateCalendar(user, i, count, noClientAccess=True)
if not cal:
continue
try:
feed = callGAPIpages(cal.settings(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
continue
settings = {}
for setting in feed:
if not fields or setting['id'] in fields:
settings[setting['id']] = setting['value']
if not csvPF:
if not FJQC.formatJSON:
printEntityKVList([Ent.USER, user], [Ent.Plural(Ent.CALENDAR_SETTINGS), None], i, count)
Ind.Increment()
for attr in sorted(settings):
printKeyValueList([attr, settings[attr]])
Ind.Decrement()
else:
printLine(json.dumps({'User': user, 'settings': settings}, ensure_ascii=False, sort_keys=True))
else:
row = flattenJSON(settings, flattened={'User': user})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'User': user, 'JSON': json.dumps(settings, ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile('Calendar Settings')
# gam <UserTypeEntity> create calendaracls <UserCalendarEntity> <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
def createCalendarACLs(users):
calendarEntity = getUserCalendarEntity()
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, Act.MODIFIER_TO)
if jcount == 0:
continue
Ind.Increment()
_doCalendarsCreateACLs(origUser, user, cal, calIds, jcount, role, ACLScopeEntity, sendNotifications)
Ind.Decrement()
def updateDeleteCalendarACLs(users, calendarEntity, function, modifier, ACLScopeEntity, role, sendNotifications):
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, modifier)
if jcount == 0:
continue
Ind.Increment()
_doUpdateDeleteCalendarACLs(origUser, user, cal, function, calIds, jcount, ACLScopeEntity, role, sendNotifications)
Ind.Decrement()
# gam <UserTypeEntity> update calendaracls <UserCalendarEntity> <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
def updateCalendarACLs(users):
calendarEntity = getUserCalendarEntity()
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
updateDeleteCalendarACLs(users, calendarEntity, 'patch', Act.MODIFIER_IN, ACLScopeEntity, role, sendNotifications)
# gam <UserTypeEntity> delete calendaracls <UserCalendarEntity> [<CalendarACLRole>] <CalendarACLScopeEntity>
def deleteCalendarACLs(users):
calendarEntity = getUserCalendarEntity()
role, ACLScopeEntity = getCalendarDeleteACLsOptions(True)
updateDeleteCalendarACLs(users, calendarEntity, 'delete', Act.MODIFIER_FROM, ACLScopeEntity, role, False)
# gam <UserTypeEntity> info calendaracls <UserCalendarEntity> <CalendarACLScopeEntity>
# [formatjson]
def infoCalendarACLs(users):
calendarEntity = getUserCalendarEntity()
ACLScopeEntity = getCalendarSiteACLScopeEntity()
FJQC = _getCalendarInfoACLOptions()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, Act.MODIFIER_FROM, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
_doInfoCalendarACLs(origUser, user, cal, calIds, jcount, ACLScopeEntity, FJQC)
Ind.Decrement()
# gam <UserTypeEntity> print calendaracls <UserCalendarEntity> [todrive <ToDriveAttribute>*]
# [noselfowner] (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show calendaracls <UserCalendarEntity>
# [noselfowner]
# [formatjson]
def printShowCalendarACLs(users):
calendarEntity = getUserCalendarEntity(default='all')
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['primaryEmail', 'calendarId'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, Act.MODIFIER_FROM, showAction=not csvPF and not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for calId in calIds:
j += 1
calId = convertUIDtoEmailAddress(calId)
_printShowCalendarACLs(cal, user, Ent.CALENDAR, calId, j, jcount, csvPF, FJQC, noSelfOwner, addCSVData)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Calendar ACLs')
TRANSFER_CALENDAR_APPEND_FIELDS = ['description', 'location', 'summary']
# gam <UserTypeEntity> transfer calendars|seccals <UserItem> [<UserCalendarEntity>]
# [keepuser | (retainrole <CalendarACLRole>)] [sendnotifications <Boolean>] [noretentionmessages]
# <CalendarSettings>* [append description|location|summary] [noupdatemessages]
# [deletefromoldowner] [addtonewowner <CalendarAttribute>*] [nolistmessages]
def transferCalendars(users):
targetUser = getEmailAddress()
calendarEntity = getUserCalendarEntity(noSelectionKwargs={'minAccessRole': 'owner', 'showHidden': True})
notAllowedForbidden = Msg.FORBIDDEN if (not calendarEntity['all']) and (not calendarEntity.get('kwargs', {}).get('minAccessRole', '') == 'owner') else Msg.NOT_ALLOWED
retainRoleBody = {'role': 'none'}
sendNotifications = showListMessages = showRetentionMessages = showUpdateMessages = True
updateBody = {}
appendFieldsList = []
addToNewOwner = deleteFromOldOwner = False
targetListBody = {'selected': True, 'hidden': False}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'keepuser':
retainRoleBody['role'] = 'owner'
elif myarg == 'retainrole':
retainRoleBody['role'] = getChoice(CALENDAR_ACL_ROLES_MAP, mapChoice=True)
elif myarg == 'sendnotifications':
sendNotifications = getBoolean()
elif _getCalendarSetting(myarg, updateBody):
pass
elif myarg == 'append':
for field in _getFieldsList():
if field in TRANSFER_CALENDAR_APPEND_FIELDS:
appendFieldsList.append(field)
else:
invalidChoiceExit(field, TRANSFER_CALENDAR_APPEND_FIELDS, True)
elif myarg == 'nolistmessages':
showListMessages = False
elif myarg == 'noretentionmessages':
showRetentionMessages = False
elif myarg == 'noupdatemessages':
showUpdateMessages = False
elif myarg == 'deletefromoldowner':
deleteFromOldOwner = True
elif myarg == 'addtonewowner':
addToNewOwner = True
_getCalendarAttributes(targetListBody, returnOnUnknownArgument=True)
else:
unknownArgumentExit()
targetUser, targetCal = validateCalendar(targetUser, noClientAccess=True)
if not targetCal:
return
colorRgbFormat = 'backgroundColor' in targetListBody or 'foregroundColor' in targetListBody
if 'summaryOverride' in targetListBody and 'summary' not in updateBody:
updateBody['summary'] = targetListBody.pop('summaryOverride')
if updateBody:
timestamp = currentISOformatTimeStamp('seconds')
appendFields = ','.join(set(appendFieldsList))
targetRoleBody = {'role': 'owner', 'scope': {'type': 'user', 'value': targetUser}}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
Act.Set(Act.TRANSFER_OWNERSHIP)
user, sourceCal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, secondaryCalendarsOnly=True)
if jcount == 0:
continue
if updateBody:
userName, domain = splitEmailAddress(user)
for field in updateBody:
updateBody[field] = _substituteForUser(updateBody[field], user, userName)
updateBody[field] = updateBody[field].replace('#domain#', domain)
updateBody[field] = updateBody[field].replace('#timestamp#', timestamp)
sourceRuleId = f'user:{user}'
Ind.Increment()
j = 0
for calId in calIds:
j += 1
Act.Set(Act.TRANSFER_OWNERSHIP)
if calId.find('@group.calendar.google.com') == -1:
entityActionNotPerformedWarning([Ent.CALENDAR, calId], notAllowedForbidden, j, jcount)
continue
try:
callGAPI(sourceCal.acl(), 'insert',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.REQUIRED_ACCESS_LEVEL],
calendarId=calId, body=targetRoleBody, sendNotifications=sendNotifications, fields='')
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
except (GAPI.forbidden, GAPI.requiredAccessLevel) as e:
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
continue
except (GAPI.notFound, GAPI.invalid):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
continue
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
if updateBody:
Act.Set(Act.UPDATE)
try:
if appendFields:
body = callGAPI(targetCal.calendars(), 'get',
throwReasons=GAPI.CALENDAR_THROW_REASONS+GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
calendarId=calId, fields=appendFields)
for field in appendFieldsList:
if field in updateBody:
if field in body:
body[field] += updateBody[field]
else:
body[field] = updateBody[field]
else:
body = {}
for field, updateField in iter(updateBody.items()):
if field not in appendFieldsList:
body[field] = updateField
callGAPI(targetCal.calendars(), 'patch',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
calendarId=calId, body=body)
if showUpdateMessages:
entityActionPerformed([Ent.CALENDAR, calId], j, jcount)
except (GAPI.notFound) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(targetUser, i, count)
break
if addToNewOwner:
Act.Set(Act.ADD)
targetListBody['id'] = calId
try:
callGAPI(targetCal.calendarList(), 'insert',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR,
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION],
body=targetListBody, colorRgbFormat=colorRgbFormat, fields='')
if showListMessages:
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
except (GAPI.notFound, GAPI.duplicate, GAPI.unknownError,
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(targetUser, i, count)
break
Act.Set(Act.RETAIN)
if retainRoleBody['role'] == 'owner':
if showRetentionMessages:
entityActionPerformed([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], j, jcount)
elif retainRoleBody['role'] != 'none':
try:
callGAPI(targetCal.acl(), 'patch',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER,
GAPI.INVALID_SCOPE_VALUE, GAPI.ILLEGAL_ACCESS_ROLE_FOR_DEFAULT,
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWNER_ACL, GAPI.FORBIDDEN],
calendarId=calId, ruleId=sourceRuleId, body=retainRoleBody, sendNotifications=sendNotifications, fields='')
if showRetentionMessages:
entityActionPerformed([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], j, jcount)
except GAPI.notFound as e:
if not checkCalendarExists(targetCal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
else:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], str(e), j, jcount)
except (GAPI.invalid, GAPI.invalidParameter, GAPI.invalidScopeValue, GAPI.illegalAccessRoleForDefault, GAPI.forbidden, GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnerAcl) as e:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(targetUser, i, count)
break
else:
try:
callGAPI(targetCal.acl(), 'delete',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID],
calendarId=calId, ruleId=sourceRuleId)
if showRetentionMessages:
entityActionPerformed([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], j, jcount)
except (GAPI.notFound, GAPI.invalid):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(targetUser, i, count)
break
if deleteFromOldOwner:
Act.Set(Act.DELETE)
try:
callGAPI(sourceCal.calendarList(), 'delete',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR,
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION],
calendarId=calId)
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_FROM, None, [Ent.USER, user], j, jcount)
except GAPI.notFound as e:
if retainRoleBody['role'] == 'none':
if showListMessages:
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_FROM, None, [Ent.USER, user], j, jcount)
else:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
except (GAPI.duplicate, GAPI.unknownError,
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
def _createImportCalendarEvent(users, function):
calendarEntity = getUserCalendarEntity()
body, parameters = _getCalendarCreateImportUpdateEventOptions(function, Ent.USER)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_TO)
if jcount == 0:
continue
Ind.Increment()
_createCalendarEvents(user, cal, function, calIds, jcount, body, parameters)
Ind.Decrement()
# gam <UserTypeEntity> create event <UserCalendarEntity> [id <String>] <EventAddAttribute>+
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def createCalendarEvent(users):
_createImportCalendarEvent(users, 'insert')
# gam <UserTypeEntity> import event <UserCalendarEntity> icaluid <iCalUID> <EventImportAttribute>+
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def importCalendarEvent(users):
_createImportCalendarEvent(users, 'import')
# gam <UserTypeEntity> update events <UserCalendarEntity> [<EventEntity>] [replacemode] <EventUpdateAttribute>+ [<EventNotificationAttribute>]
# [showdayofweek]
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
def updateCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
body, parameters = _getCalendarCreateImportUpdateEventOptions('update', Ent.USER)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_IN)
if jcount == 0:
continue
Ind.Increment()
_updateCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, body, parameters)
Ind.Decrement()
# gam <UserTypeEntity> delete events <UserCalendarEntity> <EventEntity> [doit] [<EventNotificationAttribute>]
def deleteCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
parameters = _getCalendarDeleteEventOptions()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM)
if jcount == 0:
continue
Ind.Increment()
_deleteCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, parameters)
Ind.Decrement()
# gam <UserTypeEntity> purge events <UserCalendarEntity> <EventEntity> [doit] [<EventNotificationAttribute>]
def purgeCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
parameters = _getCalendarDeleteEventOptions()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM)
if jcount == 0:
continue
Ind.Increment()
_purgeCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, parameters, False)
Ind.Decrement()
# gam <UserTypeEntity> wipe events <UserCalendarEntity>
def wipeCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM)
if jcount == 0:
continue
Ind.Increment()
_wipeCalendarEvents(user, cal, calIds, jcount)
Ind.Decrement()
# gam <UserTypeEntity> move events <UserCalendarEntity> <EventEntity> to|destination <CalendarItem> [<EventNotificationAttribute>]
def moveCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
checkArgumentPresent(['to', 'destination'])
newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM))
parameters, _ = _getCalendarMoveEventsOptions()
if not checkCalendarExists(None, newCalId, 0, 0, True):
return
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM, newCalId=newCalId)
if jcount == 0:
continue
Ind.Increment()
_moveCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, newCalId, parameters)
Ind.Decrement()
# gam <UserTypeEntity> empty calendartrash <UserCalendarEntity>
def emptyCalendarTrash(users):
calendarEntity = getUserCalendarEntity()
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
Act.Set(Act.PURGE)
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.TRASHED_EVENT, Act.MODIFIER_FROM)
if jcount == 0:
continue
Ind.Increment()
_emptyCalendarTrash(user, cal, calIds, jcount)
Ind.Decrement()
# gam <UserTypeEntity> update calattendees <UserCalendarEntity> <EventEntity> [anyorganizer]
# [<EventNotificationAttribute>] [splitupdate] [doit]
# (<CSVFileSelector>)
# (delete <EmailAddress>)*
# (deleteentity <EmailAddressEntity>)*
# (add <EmailAddress>)*
# (addentity <EmailAddressEntity>)*
# (addstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddress>)*
# (addentitystatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)*
# (replace <EmailAddress> <EmailAddress>)*
# (replacestatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddress> <EmailAddress>)*
# (updatestatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddress>)*
# (updateentitystatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)*
def updateCalendarAttendees(users):
def getStatus(option):
if option.endswith('status'):
return(getChoice(CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP, defaultChoice=None, mapChoice=True),
getChoice(CALENDAR_ATTENDEE_STATUS_CHOICE_MAP, defaultChoice=None, mapChoice=True))
return (None, None)
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
anyOrganizer = doIt = splitUpdate = False
parameters = {'sendUpdates': 'none'}
attendeeMap = {}
errors = 0
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'csv', 'csvfile'}:
errors = 0
f, csvFile, _ = openCSVFileReader(getString(Cmd.OB_FILE_NAME), fieldnames=['addr', 'op', 'optional', 'status'])
for row in csvFile:
updAddr = row['addr']
updOp = row['op'].lower()
updOptional = row['optional']
updStatus = row['status']
if not updAddr and not updOp:
continue
if updOptional:
updOptional = updOptional.lower()
if updStatus:
updStatus = updStatus.lower()
if (not updAddr or not updOp or
(updOptional and updOptional not in CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP) or
(updStatus and updStatus not in CALENDAR_ATTENDEE_STATUS_CHOICE_MAP)):
stderrErrorMsg(Msg.INVALID_ATTENDEE_CHANGE.format(','.join([updAddr, updOp, updStatus, updOptional])))
errors += 1
continue
updAddr = normalizeEmailAddressOrUID(updAddr, noUid=True)
if updOp == 'delete':
attendeeMap[updAddr] = {'op': updOp, 'done': False}
else:
updOptional = CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP[updOptional] if updOptional else None
updStatus = CALENDAR_ATTENDEE_STATUS_CHOICE_MAP[updStatus] if updStatus else None
if updOp == 'add':
attendeeMap[updAddr] = {'op': updOp, 'status': updStatus, 'optional': updOptional, 'done': False}
elif updOp == 'update':
attendeeMap[updAddr] = {'op': updOp, 'status': updStatus, 'optional': updOptional, 'done': False}
else: #replace
attendeeMap[updAddr] = {'op': 'replace', 'status': updStatus, 'optional': updOptional, 'email': normalizeEmailAddressOrUID(updOp, noUid=True), 'done': False}
closeFile(f)
elif myarg == 'delete':
updAddr = getEmailAddress(noUid=True)
attendeeMap[updAddr] = {'op': 'delete'}
elif myarg == 'deleteentity':
for updAddr in getNormalizedEmailAddressEntity(noUid=True):
attendeeMap[updAddr] = {'op': 'delete'}
elif myarg in {'add', 'addstatus'}:
updOptional, updStatus = getStatus(myarg)
updAddr = getEmailAddress(noUid=True)
attendeeMap[updAddr] = {'op': 'add', 'status': updStatus, 'optional': updOptional, 'done': False}
elif myarg in {'addentity', 'addentitystatus'}:
updOptional, updStatus = getStatus(myarg)
for updAddr in getNormalizedEmailAddressEntity(noUid=True):
attendeeMap[updAddr] = {'op': 'add', 'status': updStatus, 'optional': updOptional, 'done': False}
elif myarg in {'update', 'updatestatus'}:
updOptional, updStatus = getStatus(myarg)
updAddr = getEmailAddress(noUid=True)
attendeeMap[updAddr] = {'op': 'update', 'status': updStatus, 'optional': updOptional, 'done': False}
elif myarg in {'updateentity', 'updateentitystatus'}:
updOptional, updStatus = getStatus(myarg)
for updAddr in getNormalizedEmailAddressEntity(noUid=True):
attendeeMap[updAddr] = {'op': 'update', 'status': updStatus, 'optional': updOptional, 'done': False}
elif myarg in {'replace', 'replacestatus'}:
updOptional, updStatus = getStatus(myarg)
updAddr = getEmailAddress(noUid=True)
newAddr = getEmailAddress(noUid=True)
attendeeMap[updAddr] = {'op': 'replace', 'status': updStatus, 'optional': updOptional, 'email': newAddr, 'done': False}
elif myarg in {'anyorganizer', 'allevents'}:
anyOrganizer = True
elif _getCalendarSendUpdates(myarg, parameters):
pass
elif myarg == 'doit':
doIt = True
elif myarg == 'dryrun':
doIt = False
elif myarg == 'splitupdate':
splitUpdate = True
else:
unknownArgumentExit()
if not attendeeMap:
missingArgumentExit(Msg.UPDATE_ATTENDEE_CHANGES)
ucount = len(attendeeMap)
if errors:
systemErrorExit(USAGE_ERROR_RC, '')
removeMessage = Msg.ATTENDEES_REMOVE
addMessage = Msg.ATTENDEES_ADD_REMOVE if not splitUpdate else Msg.ATTENDEES_ADD
fieldsList = ['attendees', 'id', 'organizer', 'status', 'summary']
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for calId in calIds:
j += 1
Act.Set(Act.UPDATE)
calId, cal, events, kcount = _validateCalendarGetEvents(origUser, user, cal, calId, j, jcount, calendarEventEntity,
fieldsList, True)
if kcount == 0:
continue
Ind.Increment()
k = 0
for event in events:
k += 1
eventSummary = event.get('summary', event['id'])
if event['status'] == 'cancelled':
entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.EVENT_IS_CANCELED, k, kcount)
continue
if not anyOrganizer and not event.get('organizer', {}).get('self'):
entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.USER_IS_NOT_ORGANIZER, k, kcount)
continue
needsUpdate = False
for _, v in sorted(iter(attendeeMap.items())):
v['done'] = False
updatedAttendeesAdd = []
updatedAttendeesRemove = []
entityPerformActionNumItems([Ent.EVENT, eventSummary], ucount, Ent.ATTENDEE, k, kcount)
Ind.Increment()
u = 0
for attendee in event.get('attendees', []):
oldAddr = attendee.get('email', '').lower()
if not oldAddr:
updatedAttendeesAdd.append(attendee)
if splitUpdate:
updatedAttendeesRemove.append(attendee)
continue
update = attendeeMap.get(oldAddr)
if not update:
updatedAttendeesAdd.append(attendee)
if splitUpdate:
updatedAttendeesRemove.append(attendee)
continue
updOp = update['op']
if updOp == 'delete':
u += 1
update['done'] = True
Act.Set(Act.DELETE)
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount)
needsUpdate = True
else:
oldStatus = attendee.get('responseStatus')
oldOptional = attendee.get('optional', False)
updStatus = update['status']
updOptional = update['optional']
if updOp in {'add', 'update'}:
u += 1
update['done'] = True
if ((updStatus is not None and updStatus != oldStatus) or
(updOptional is not None and updOptional != oldOptional)):
attendee['responseStatus'] = updStatus if updStatus is not None else oldStatus
attendee['optional'] = updOptional if updOptional is not None else oldOptional
Act.Set(Act.UPDATE)
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount)
needsUpdate = True
else:
Act.Set(Act.SKIP)
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount)
updatedAttendeesAdd.append(attendee)
else: #replace
u += 1
update['done'] = True
attendee['email'] = update['email']
attendee['responseStatus'] = updStatus if updStatus is not None else oldStatus
attendee['optional'] = updOptional if updOptional is not None else oldOptional
Act.Set(Act.REPLACE)
entityPerformActionModifierNewValue([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], Act.MODIFIER_WITH, update['email'], u, ucount)
updatedAttendeesAdd.append(attendee)
needsUpdate = True
for newAddr, v in sorted(iter(attendeeMap.items())):
if v['op'] == 'add' and not v['done']:
u += 1
v['done'] = True
attendee = {'email': newAddr}
if v['status'] is not None:
attendee['responseStatus'] = v['status']
if v['optional'] is not None:
attendee['optional'] = v['optional']
Act.Set(Act.ADD)
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, newAddr], u, ucount)
updatedAttendeesAdd.append(attendee)
needsUpdate = True
for newAddr, v in sorted(iter(attendeeMap.items())):
if not v['done']:
u += 1
Act.Set(Act.SKIP)
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, newAddr], u, ucount)
Ind.Decrement()
if needsUpdate:
Act.Set(Act.UPDATE)
if doIt:
status = True
if splitUpdate:
try:
callGAPI(cal.events(), 'patch',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendeesRemove},
sendUpdates=parameters['sendUpdates'], fields='')
entityActionPerformedMessage([Ent.EVENT, eventSummary], removeMessage, j, jcount)
except GAPI.notFound as e:
if not checkCalendarExists(cal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
break
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount)
status = False
except (GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
if status:
try:
callGAPI(cal.events(), 'patch',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendeesAdd},
sendUpdates=parameters['sendUpdates'], fields='')
entityActionPerformedMessage([Ent.EVENT, eventSummary], addMessage, jcount)
except GAPI.notFound as e:
if not checkCalendarExists(cal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
break
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount)
except (GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
else:
entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> info events <UserCalendarEntity> <EventEntity> [maxinstances <Number>]
# [fields <EventFieldNameList>] [showdayofweek]
# [formatjson]
def infoCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
FJQC, fieldsList = _getCalendarInfoEventOptions(calendarEventEntity)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_IN, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
_infoCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, FJQC, fieldsList)
Ind.Decrement()
# gam <UserTypeEntity> print events <UserCalendarEntity> <EventEntity> <EventDisplayProperties>*
# [fields <EventFieldNameList>] [showdayofweek]
# [countsonly] [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show events <UserCalendarEntity> <EventEntity> <EventDisplayProperties>*
# [fields <EventFieldNameList>] [showdayofweek]
# [countsonly] [formatjson]
def printShowCalendarEvents(users):
calendarEntity = getUserCalendarEntity()
calendarEventEntity = getCalendarEventEntity()
csvPF, FJQC, fieldsList = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.USER)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM,
showAction=not csvPF and not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
_printShowCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity,
csvPF, FJQC, fieldsList)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Calendar Events')
def getStatusEventDateTime(dateType, dateList):
if dateType == 'timerange':
startTime = getTimeOrDeltaFromNow(returnDateTime=True)[0]
endTime = getTimeOrDeltaFromNow(returnDateTime=True)[0]
recurrence = []
while checkArgumentPresent(['recurrence']):
recurrence.append(getString(Cmd.OB_RECURRENCE))
dateList.append({'type': dateType, 'first': startTime, 'last': endTime, 'ulast': endTime, 'recurrence': recurrence})
return
firstDate = getYYYYMMDD(minLen=1, returnDateTime=True).replace(tzinfo=GC.Values[GC.TIMEZONE])
if dateType == 'range':
lastDate = getYYYYMMDD(minLen=1, returnDateTime=True).replace(tzinfo=GC.Values[GC.TIMEZONE])
deltaDay = datetime.timedelta(days=1)
deltaWeek = datetime.timedelta(weeks=1)
if dateType in {'date', 'allday'}:
dateList.append({'type': 'date', 'first': firstDate, 'last': firstDate+deltaDay,
'ulast': firstDate+deltaDay, 'udelta': deltaDay})
elif dateType == 'range':
dateList.append({'type': dateType, 'first': firstDate, 'last': lastDate+deltaDay,
'ulast': lastDate, 'udelta': deltaDay})
elif dateType == 'daily':
argRepeat = getInteger(minVal=1, maxVal=366)
dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate+datetime.timedelta(days=argRepeat),
'ulast': firstDate+datetime.timedelta(days=argRepeat), 'udelta': deltaDay})
else: #weekly
argRepeat = getInteger(minVal=1, maxVal=52)
dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate+deltaDay, 'pdelta': deltaWeek, 'repeats': argRepeat,
'ulast': firstDate+datetime.timedelta(weeks=argRepeat), 'udelta': deltaWeek})
def _showCalendarStatusEvent(primaryEmail, calId, eventEntityType, event, k, kcount, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON({'primaryEmail': primaryEmail, 'calendarId': calId, 'event': event},
timeObjects=EVENT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([eventEntityType, event['id']], k, kcount)
skipObjects = {'id'}
Ind.Increment()
showJSON(None, event, skipObjects, EVENT_TIME_OBJECTS)
Ind.Decrement()
EVENT_AUTO_DECLINE_MODE_CHOICE_MAP = {
'declinenone': 'declineNone',
'declineallconflictinginvitations': 'declineAllConflictingInvitations',
'declineonlynewconflictinginvitations': 'declineOnlyNewConflictingInvitations',
'none': 'declineNone',
'all': 'declineAllConflictingInvitations',
'new': 'declineOnlyNewConflictingInvitations',
}
EVENT_CHAT_STATUS_CHOICE_MAP = {
'available': 'available',
'dnd': 'doNotDisturb',
'donotdisturb': 'doNotDisturb',
}
def getFocusTimeProperties(body, parameters, dateList):
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[EVENT_TYPE_FOCUSTIME]
body.update({'eventType': EVENT_TYPE_FOCUSTIME, 'summary': 'Focus time',
eventProperties: {'autoDeclineMode': 'declineNone', 'chatStatus': 'available', 'declineMessage': 'Declined'},
'transparency':'opaque'})
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'summary':
body['summary'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'declinemode':
body[eventProperties]['autoDeclineMode'] = getChoice(EVENT_AUTO_DECLINE_MODE_CHOICE_MAP, mapChoice=True)
elif myarg == 'declinemessage':
body[eventProperties]['declineMessage'] = getString(Cmd.OB_STRING)
elif myarg == 'chatstatus':
body[eventProperties]['chatStatus'] = getChoice(EVENT_CHAT_STATUS_CHOICE_MAP, mapChoice=True)
elif myarg == 'timerange':
getStatusEventDateTime(myarg, dateList)
elif myarg == 'timezone':
parameters['timeZone'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
def getOutOfOfficeProperties(body, parameters, dateList):
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[EVENT_TYPE_OUTOFOFFICE]
body.update({'eventType': EVENT_TYPE_OUTOFOFFICE, 'summary': 'Out of office',
eventProperties: {'autoDeclineMode': 'declineNone', 'declineMessage': 'Declined'},
'transparency':'opaque'})
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'summary':
body['summary'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'declinemode':
body[eventProperties]['autoDeclineMode'] = getChoice(EVENT_AUTO_DECLINE_MODE_CHOICE_MAP, mapChoice=True)
elif myarg == 'declinemessage':
body[eventProperties]['declineMessage'] = getString(Cmd.OB_STRING)
elif myarg == 'timerange':
getStatusEventDateTime(myarg, dateList)
elif myarg == 'timezone':
parameters['timeZone'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
STATUS_EVENTS_DATETIME_CHOICES = {'date', 'allday', 'range', 'daily', 'weekly', 'timerange'}
WORKING_LOCATION_CHOICE_MAP = {
'custom': 'customLocation',
'home': 'homeOffice',
'office': 'officeLocation',
}
def getWorkingLocationProperties(body, parameters, dateList):
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[EVENT_TYPE_WORKINGLOCATION]
body.update({'eventType': EVENT_TYPE_WORKINGLOCATION, eventProperties: {},
'visibility': 'public', 'transparency':'transparent'})
location = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in WORKING_LOCATION_CHOICE_MAP:
location = WORKING_LOCATION_CHOICE_MAP[myarg]
body[eventProperties]['type'] = location
if location == 'homeOffice':
pass
elif location == 'customLocation':
body[eventProperties][location] = {'label': getString(Cmd.OB_STRING)}
else: #officeLocation
body[eventProperties][location] = {'label': getString(Cmd.OB_STRING)}
entry = body[eventProperties][location]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'building', 'buildingid'}:
entry['buildingId'] = _getBuildingByNameOrId(None)
elif myarg in {'floor', 'floorname'}:
entry['floorId'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'section', 'floorsection'}:
entry['floorSectionId'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in {'desk', 'deskcode'}:
entry['deskId'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'endlocation':
break
else:
Cmd.Backup()
break
elif myarg in STATUS_EVENTS_DATETIME_CHOICES:
getStatusEventDateTime(myarg, dateList)
elif myarg == 'timezone':
parameters['timeZone'] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
return location
# gam <UserTypeEntity> create focustime
# [chatstatus available|donotdisturb]
# [declinemode none|all|new]
# [declinemessage <String>]
# [summary <String>]
# (timerange <Time> <Time> [recurrence <String>])+
# [timezone <String>]
# gam <UserTypeEntity> create outofoffice
# [declinemode none|all|new]
# [declinemessage <String>]
# [summary <String>]
# (timerange <Time> <Time> [recurrence <String>])+
# [timezone <String>]
# gam <UserTypeEntity> create workinglocation
# (home|
# (custom <String>)|
# (office <String> [building|buildingid <String>] [floor|floorname <String>]
# [section|floorsection <String>] [desk|deskcode <String>]))
# ((date yyyy-mm-dd)|
# (range yyyy-mm-dd yyyy-mm-dd)|
# (daily yyyy-mm-dd N)|
# (weekly yyyy-mm-dd N)|
# (timerange <Time> <Time>))+
# [timezone <String>]
def createStatusEvent(users, eventType):
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[eventType]
entityType = EVENT_TYPE_ENTITY_MAP[eventType]
body = {'start': {}, 'end': {}, 'recurrence': None}
calId = 'primary'
parameters = {}
dateList = []
if eventType == EVENT_TYPE_WORKINGLOCATION:
location = getWorkingLocationProperties(body, parameters, dateList)
if not location:
missingArgumentExit('|'.join(WORKING_LOCATION_CHOICE_MAP))
elif eventType == EVENT_TYPE_OUTOFOFFICE:
getOutOfOfficeProperties(body, parameters, dateList)
else: # elif eventType == EVENT_TYPE_FOCUSTIME:
getFocusTimeProperties(body, parameters, dateList)
if not dateList:
missingChoiceExit(STATUS_EVENTS_DATETIME_CHOICES)
datekvList = [Ent.CALENDAR, '', Ent.EVENT, '', Ent.DATE, '']
timekvList = [Ent.CALENDAR, '', Ent.EVENT, '', Ent.START_TIME, '', Ent.END_TIME, '']
if eventType == EVENT_TYPE_WORKINGLOCATION:
location = body[eventProperties]['type']
if location in body[eventProperties] and 'label' in body[eventProperties][location]:
location += f"/{body[eventProperties][location]['label']}"
datekvList.extend([Ent.LOCATION, location])
timekvList.extend([Ent.LOCATION, location])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal = buildGAPIServiceObject(API.CALENDAR, user, i, count)
if not cal:
continue
jcount = len(dateList)
entityPerformAction([Ent.CALENDAR, user, entityType, None], i, count)
Ind.Increment()
j = 0
for wlDate in dateList:
j += 1
first = wlDate['first']
last = wlDate['ulast']
kvList = datekvList if wlDate['type'] != 'timerange' else timekvList
kvList[1] = user
while first < last:
body.pop('recurrence', None)
if wlDate['type'] != 'timerange':
body['start']['date'] = first.strftime(YYYYMMDD_FORMAT)
kvList[5] = body['start']['date']
body['end']['date'] = (first+datetime.timedelta(days=1)).strftime(YYYYMMDD_FORMAT)
else:
body['start']['dateTime'] = ISOformatTimeStamp(first)
kvList[5] = body['start']['dateTime']
body['end']['dateTime'] = ISOformatTimeStamp(last)
kvList[7] = body['end']['dateTime']
if wlDate['recurrence']:
body['recurrence'] = wlDate['recurrence']
if not _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
break
try:
event = callGAPI(cal.events(), 'insert',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.BAD_REQUEST,
GAPI.TIME_RANGE_EMPTY, GAPI.MALFORMED_WORKING_LOCATION_EVENT],
calendarId=calId, body=body, fields='id')
kvList[3] = event['id']
entityActionPerformed(kvList, j, jcount)
if wlDate['type'] == 'timerange':
break
first += wlDate['udelta']
except (GAPI.forbidden, GAPI.invalid) as e:
entityActionFailedWarning([Ent.CALENDAR, user], str(e), i, count)
break
except (GAPI.badRequest, GAPI.timeRangeEmpty, GAPI.malformedWorkingLocationEvent) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
def createFocusTime(users):
createStatusEvent(users, EVENT_TYPE_FOCUSTIME)
def createOutOfOffice(users):
createStatusEvent(users, EVENT_TYPE_OUTOFOFFICE)
def createWorkingLocation(users):
createStatusEvent(users, EVENT_TYPE_WORKINGLOCATION)
# gam <UserTypeEntity> delete focustime|outofoffice|workinglocation
# ((date yyyy-mm-dd)|
# (range yyyy-mm-dd yyyy-mm-dd)|
# (daily yyyy-mm-dd N)|
# (weekly yyyy-mm-dd N)|
# (timerange <Time> <Time>))+
def deleteStatusEvent(users, eventType):
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[eventType]
entityType = EVENT_TYPE_ENTITY_MAP[eventType]
kwargs = {'eventTypes': [eventType], 'showDeleted': False, 'singleEvents': True,
'timeMax': None, 'timeMin': None, 'orderBy': 'startTime'}
calId = 'primary'
dateList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in STATUS_EVENTS_DATETIME_CHOICES:
getStatusEventDateTime(myarg, dateList)
else:
unknownArgumentExit()
if not dateList:
missingChoiceExit(STATUS_EVENTS_DATETIME_CHOICES)
basekvList = [Ent.CALENDAR, '', Ent.EVENT, '']
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal = buildGAPIServiceObject(API.CALENDAR, user, i, count)
if not cal:
continue
jcount = len(dateList)
entityPerformAction([Ent.CALENDAR, user, entityType, None], i, count)
Ind.Increment()
j = 0
for wlDate in dateList:
j += 1
first = wlDate['first']
last = wlDate['last']
basekvList[1] = user
events = []
for _ in range(1, wlDate.get('repeats', 1)+1):
kwargs['timeMin'] = ISOformatTimeStamp(first)
kwargs['timeMax'] = ISOformatTimeStamp(last)
try:
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.BAD_REQUEST],
calendarId=calId, fields=f'nextPageToken,items(id,start,end,{eventProperties})', **kwargs)
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.CALENDAR, user], str(e), j, jcount)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
kcount = len(events)
k = 0
for event in events:
k += 1
eventId = event['id']
basekvList[3] = eventId
kvList = basekvList[:]
if eventType == EVENT_TYPE_WORKINGLOCATION:
location = event[eventProperties]['type']
if location in event[eventProperties] and 'label' in event[eventProperties][location]:
location += f"/{event[eventProperties][location]['label']}"
try:
callGAPI(cal.events(), 'delete',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN,
GAPI.INVALID, GAPI.REQUIRED, GAPI.REQUIRED_ACCESS_LEVEL],
calendarId=calId, eventId=eventId, sendUpdates='none')
if 'date' in event['start']:
kvList.extend([Ent.DATE, event['start']['date']])
if eventType == EVENT_TYPE_WORKINGLOCATION:
kvList.extend([Ent.LOCATION, location])
else:
kvList.extend([Ent.START_TIME, formatLocalTime(event['start']['dateTime']),
Ent.END_TIME, formatLocalTime(event['end']['dateTime'])])
if eventType == EVENT_TYPE_WORKINGLOCATION:
kvList.extend([Ent.LOCATION, location])
entityActionPerformed(kvList, k, kcount)
except (GAPI.notFound, GAPI.deleted) as e:
if not checkCalendarExists(cal, calId, i, count):
entityUnknownWarning(Ent.CALENDAR, calId, k, kcount)
break
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), k, kcount)
except (GAPI.forbidden, GAPI.invalid, GAPI.required, GAPI.requiredAccessLevel) as e:
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), k, kcount)
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
def deleteFocusTime(users):
deleteStatusEvent(users, EVENT_TYPE_FOCUSTIME)
def deleteOutOfOffice(users):
deleteStatusEvent(users, EVENT_TYPE_OUTOFOFFICE)
def deleteWorkingLocation(users):
deleteStatusEvent(users, EVENT_TYPE_WORKINGLOCATION)
# gam <UserTypeEntity> show focustime|outofoffice|workinglocation
# ((date yyyy-mm-dd)|
# (range yyyy-mm-dd yyyy-mm-dd)|
# (daily yyyy-mm-dd N)|
# (weekly yyyy-mm-dd N)|
# (timerange <Time> <Time>))+
# [showdayofweek]
# [formatjson]
# gam <UserTypeEntity> print focustime|outofoffice|workinglocation
# ((date yyyy-mm-dd)|
# (range yyyy-mm-dd yyyy-mm-dd)|
# (daily yyyy-mm-dd N)|
# (weekly yyyy-mm-dd N)|
# (timerange <Time> <Time>))+
# [showdayofweek]
# [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
def printShowStatusEvent(users, eventType):
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[eventType]
entityType = EVENT_TYPE_ENTITY_MAP[eventType]
csvPF = CSVPrintFile(['primaryEmail', 'calendarId', 'id'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {'eventTypes': [eventType], 'showDeleted': False, 'singleEvents': True,
'timeMax': None, 'timeMin': None, 'orderBy': 'startTime'}
calId = 'primary'
showDayOfWeek = False
dateList = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in STATUS_EVENTS_DATETIME_CHOICES:
getStatusEventDateTime(myarg, dateList)
elif myarg == 'showdayofweek':
showDayOfWeek = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not dateList:
missingChoiceExit(STATUS_EVENTS_DATETIME_CHOICES)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, cal = buildGAPIServiceObject(API.CALENDAR, user, i, count)
if not cal:
continue
jcount = len(dateList)
if not csvPF and not FJQC.formatJSON:
entityPerformActionNumItems([Ent.CALENDAR, user], jcount, Ent.DATE, i, count)
j = 0
for wlDate in dateList:
j += 1
first = wlDate['first']
last = wlDate['last']
for _ in range(1, wlDate.get('repeats', 1)+1):
kwargs['timeMin'] = ISOformatTimeStamp(first)
kwargs['timeMax'] = ISOformatTimeStamp(last)
try:
events = callGAPIpages(cal.events(), 'list', 'items',
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.BAD_REQUEST],
calendarId=calId, fields=f'nextPageToken,items(id,start,end,eventType,{eventProperties},transparency,visibility)',
**kwargs)
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.CALENDAR, user], str(e), j, jcount)
break
except GAPI.notACalendarUser:
userCalServiceNotEnabledWarning(user, i, count)
break
if not csvPF:
kcount = len(events)
Ind.Increment()
k = 0
for event in events:
k += 1
if showDayOfWeek:
_getEventDaysOfWeek(event)
_showCalendarStatusEvent(user, calId, Ent.EVENT, event, k, kcount, FJQC)
Ind.Decrement()
else:
for event in events:
if showDayOfWeek:
_getEventDaysOfWeek(event)
_printCalendarEvent(user, calId, event, csvPF, FJQC)
if 'pdelta' in wlDate:
first += wlDate['pdelta']
last += wlDate['pdelta']
if csvPF:
csvPF.writeCSVfile(f'Calendar {Ent.Plural(entityType)}')
def printShowFocusTime(users):
printShowStatusEvent(users, EVENT_TYPE_FOCUSTIME)
def printShowOutOfOffice(users):
printShowStatusEvent(users, EVENT_TYPE_OUTOFOFFICE)
def printShowWorkingLocation(users):
printShowStatusEvent(users, EVENT_TYPE_WORKINGLOCATION)
YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP = {
'brandingsettings': 'brandingSettings',
'contentdetails': 'contentDetails',
'contentownerdetails': 'contentOwnerDetails',
'id': 'id',
'localizations': 'localizations',
'snippet': 'snippet',
'statistics': 'statistics',
'status': 'status',
'topicdetails': 'topicDetails',
}
YOUTUBE_CHANNEL_TIME_OBJECTS = {'publishedAt'}
# gam <UserTypeEntity> show youtubechannels
# (mine|
# (ids|channels <YouTubeChannelIDList>)|
# (forusername <String>)|
# (managedbyme <String>))
# [languagecode <BCP47LanguageCode>]
# [allfields|(fields <YouTubeChannelFieldNameList>)]
# [formatjson]
# gam <UserTypeEntity> print youtubechannels [todrive <ToDriveAttribute>*]
# (mine|
# (ids|channels <YouTubeChannelIDList>)|
# (forusername <String>)|
# (managedbyme <String>))
# [languagecode <BCP47LanguageCode>]
# [allfields|(fields <YouTubeChannelFieldNameList>)]
# [formatjson [quotechar <Character>]]
def printShowYouTubeChannel(users):
csvPF = CSVPrintFile(['User', 'id'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {'mine': True}
languageCode = ''
fieldsList = ['id']
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'mine':
kwargs = {'mine': True}
elif myarg in {'id', 'ids', 'channel', 'channels'}:
kwargs = {'id': ','.join(getEntityList(Cmd.OB_YOUTUBE_CHANNEL_ID_LIST))}
elif myarg == 'forusername':
kwargs = {'forUsername': getString(Cmd.OB_USER_NAME)}
elif myarg == 'managedbyme':
kwargs = {'managedByMe': True, 'onBehalfOfContentOwner': getString(Cmd.OB_USER_NAME)}
elif getFieldsList(myarg, YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'allfields':
for field in YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP, fieldsList)
elif myarg in {'languagecode', 'hl'}:
languageCode = getLanguageCode(BCP47_LANGUAGE_CODES_MAP)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
kwargs['part'] = ','.join(set(fieldsList))
if languageCode:
kwargs['hl'] = languageCode
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, yt = buildGAPIServiceObject(API.YOUTUBE, user, i, count)
if not yt:
continue
try:
channels = callGAPIpages(yt.channels(), 'list', 'items',
throwReasons=GAPI.YOUTUBE_THROW_REASONS,
fields='nextPageToken,items', **kwargs)
except (GAPI.unsupportedSupervisedAccount, GAPI.unsupportedLanguageCode) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.contentOwnerAccountNotFound as e:
if 'managedByMe' in kwargs:
entityActionFailedWarning([Ent.USER, user, Ent.OWNER, kwargs['onBehalfOfContentOwner']], str(e), i, count)
else:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.authError):
userYouTubeServiceNotEnabledWarning(user, i, count)
continue
if not csvPF:
jcount = len(channels)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.YOUTUBE_CHANNEL, i, count)
Ind.Increment()
j = 0
for channel in channels:
j += 1
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(channel, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True))
break
printEntity([Ent.YOUTUBE_CHANNEL, channel['id']], j, jcount)
Ind.Increment()
showJSON(None, channel, skipObjects={'id'}, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS)
Ind.Decrement()
Ind.Decrement()
else:
for channel in channels:
row = {'User': user, 'id': channel['id']}
flattenJSON(channel, flattened=row, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user, 'id': channel['id'],
'JSON': json.dumps(cleanJSON(channel, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)}
csvPF.WriteRowNoFilter(row)
if csvPF:
csvPF.writeCSVfile('YouTube Channels')
def _getEntityMimeType(fileEntry):
if fileEntry['mimeType'] == MIMETYPE_GA_FOLDER:
return Ent.DRIVE_FOLDER
if fileEntry['mimeType'].startswith(MIMETYPE_GA_3P_SHORTCUT):
return Ent.DRIVE_3PSHORTCUT
if fileEntry['mimeType'] != MIMETYPE_GA_SHORTCUT:
return Ent.DRIVE_FILE
if 'shortcutDetails' not in fileEntry or 'targetMimeType' not in fileEntry['shortcutDetails']:
return Ent.DRIVE_SHORTCUT
return Ent.DRIVE_FOLDER_SHORTCUT if fileEntry['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_SHORTCUT
def _getTargetEntityMimeType(fileEntry):
return Ent.DRIVE_FOLDER if fileEntry['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE
CORPORA_ALL_DRIVES = 'allDrives'
CORPORA_CHOICE_MAP = {
'alldrives': CORPORA_ALL_DRIVES,
'allshareddrives': CORPORA_ALL_DRIVES,
'allteamdrives': CORPORA_ALL_DRIVES,
'domain': 'domain',
'onlyshareddrives': CORPORA_ALL_DRIVES,
'onlyteamdrives': CORPORA_ALL_DRIVES,
'user': 'user',
}
QUERY_SHORTCUTS_MAP = {
'allfiles': f"mimeType != '{MIMETYPE_GA_FOLDER}'",
'allfolders': f"mimeType = '{MIMETYPE_GA_FOLDER}'",
'allforms': f"mimeType = '{MIMETYPE_GA_FORM}'",
'allgooglefiles': f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
'allnongooglefiles': "not mimeType contains 'vnd.google'",
'allshortcuts': f"mimeType = '{MIMETYPE_GA_SHORTCUT}'",
'all3pshortcuts': f"mimeType = '{MIMETYPE_GA_3P_SHORTCUT}'",
'allitems': 'allitems',
'mycommentableitems': ME_IN_OWNERS_AND+f"(mimeType = '{MIMETYPE_GA_DOCUMENT}' or mimeType = '{MIMETYPE_GA_SPREADSHEET}' or mimeType = '{MIMETYPE_GA_PRESENTATION}')",
'mydocs': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_DOCUMENT}'",
'myfiles': ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}'",
'myfolders': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FOLDER}'",
'myforms': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FORM}'",
'mygooglefiles': ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
'mynongooglefiles': ME_IN_OWNERS_AND+"not mimeType contains 'vnd.google'",
'mypresentations': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_PRESENTATION}'",
'mypublishableitems': ME_IN_OWNERS_AND+f"(mimeType = '{MIMETYPE_GA_DOCUMENT}' or mimeType = '{MIMETYPE_GA_SPREADSHEET}' or mimeType = '{MIMETYPE_GA_FORM}' or mimeType = '{MIMETYPE_GA_PRESENTATION}')",
'mysheets': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_SPREADSHEET}'",
'myshortcuts': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_SHORTCUT}'",
'myslides': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_PRESENTATION}'",
'my3pshortcuts': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_3P_SHORTCUT}'",
'myitems': ME_IN_OWNERS,
'mytopfiles': ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}' and 'root' in parents",
'mytopfolders': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FOLDER}' and 'root' in parents",
'mytopitems': ME_IN_OWNERS_AND+"'root' in parents",
'othersfiles': NOT_ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}'",
'othersfolders': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FOLDER}'",
'othersforms': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FORM}'",
'othersgooglefiles': NOT_ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
'othersnongooglefiles': NOT_ME_IN_OWNERS_AND+"not mimeType contains 'vnd.google'",
'othersshortcuts': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_SHORTCUT}'",
'others3pshortcuts': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_3P_SHORTCUT}'",
'othersitems': NOT_ME_IN_OWNERS,
'writablefiles': f"'me' in writers and mimeType != '{MIMETYPE_GA_FOLDER}'",
}
SHAREDDRIVE_QUERY_SHORTCUTS_MAP = {
'allfiles': f"mimeType != '{MIMETYPE_GA_FOLDER}'",
'allfolders': f"mimeType = '{MIMETYPE_GA_FOLDER}'",
'allgooglefiles': f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
'allnongooglefiles': "not mimeType contains 'vnd.google'",
'allitems': 'allitems',
}
def doDriveSearch(drive, user, i, count, query=None, parentQuery=False, emptyQueryOK=False, orderBy=None, sharedDriveOnly=False, **kwargs):
if query == 'allitems':
query = None
if GC.Values[GC.SHOW_GETTINGS]:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count, query=query)
try:
files = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query, orderBy=orderBy, fields='nextPageToken,files(id,driveId)', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **kwargs)
if files or not parentQuery:
return [f_file['id'] for f_file in files if not sharedDriveOnly or f_file.get('driveId')]
if emptyQueryOK:
return []
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None],
emptyQuery(query, Ent.DRIVE_FILE_OR_FOLDER if not parentQuery else Ent.DRIVE_PARENT_FOLDER), i, count)
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], invalidQuery(query), i, count)
except GAPI.fileNotFound:
printGotEntityItemsForWhom(0)
if emptyQueryOK:
return []
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, kwargs['driveId']], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return None
def doSharedDriveSearch(drive, user, i, count, query, useDomainAdminAccess):
if GC.Values[GC.SHOW_GETTINGS]:
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, user, i, count, query=query)
try:
files = callGAPIpages(drive.drives(), 'list', 'drives',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.QUERY_REQUIRES_ADMIN_CREDENTIALS,
GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE],
q=query, useDomainAdminAccess=useDomainAdminAccess,
fields='nextPageToken,drives(id)', pageSize=100)
if files:
return [f_file['id'] for f_file in files]
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], emptyQuery(query, Ent.SHAREDDRIVE), i, count)
except (GAPI.invalidQuery, GAPI.invalid):
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], invalidQuery(query), i, count)
except (GAPI.queryRequiresAdminCredentials, GAPI.noListTeamDrivesAdministratorPrivilege) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return None
def _getFileIdFromURL(fileId):
loc = fileId.find('/d/')
if loc > 0:
fileId = fileId[loc+3:]
loc = fileId.find('/')
return fileId[:loc] if loc != -1 else fileId
loc = fileId.find('?id=')
if loc > 0:
fileId = fileId[loc+4:]
loc = fileId.find('&')
return fileId[:loc] if loc != -1 else fileId
loc = fileId.find('/files/')
if loc > 0:
fileId = fileId[loc+7:]
loc = fileId.find('&')
if loc > 0:
return fileId[:loc]
loc = fileId.find('?')
return fileId[:loc] if loc != -1 else fileId
loc = fileId.find('/folders/')
if loc > 0:
fileId = fileId[loc+9:]
loc = fileId.find('&')
if loc > 0:
return fileId[:loc]
loc = fileId.find('?')
return fileId[:loc] if loc != -1 else fileId
return None
def cleanFileIDsList(fileIdEntity, fileIds):
fileIdEntity['list'] = []
fileIdEntity[ROOT] = []
i = 0
for fileId in fileIds:
if fileId[:8].lower() == 'https://' or fileId[:7].lower() == 'http://':
fileId = _getFileIdFromURL(fileId)
if fileId is None:
continue
elif fileId.lower() in {ROOT, ROOTID}:
fileIdEntity[ROOT].append(i)
fileId = fileId.lower()
fileIdEntity['list'].append(fileId)
i += 1
TITLE_QUERY_PATTERN = re.compile(r'title((?: *!?=)|(?: +contains))', flags=re.IGNORECASE)
def _mapDrive2QueryToDrive3(query):
if query:
query = TITLE_QUERY_PATTERN.sub(r'name\1', query).replace('modifiedDate', 'modifiedTime').replace('lastViewedByMeDate', 'viewedByMeTime')
query = mapQueryRelativeTimes(query, ['modifiedTime', 'viewedByMeTime'])
return query
def escapeDriveFileName(filename):
if filename.find("'") == -1 and filename.find('\\') == -1:
return filename
encfilename = ''
for c in filename:
if c == "'":
encfilename += "\\'"
elif c == '\\':
encfilename += '\\\\'
else:
encfilename += c
return encfilename
def getEscapedDriveFileName():
return escapeDriveFileName(getString(Cmd.OB_DRIVE_FILE_NAME))
def getEscapedDriveFolderName():
return escapeDriveFileName(getString(Cmd.OB_DRIVE_FOLDER_NAME))
def initDriveFileEntity():
return {'list': [], 'shareddrivename': None, 'shareddriveadminquery': None, 'shareddrivefilequery': None,
'query': None, 'dict': None, ROOT: [], 'shareddrive': {},
'location': None, 'nonDomainAdminAccess': False}
DRIVE_BY_NAME_CHOICE_MAP = {
'anyname': WITH_ANY_FILE_NAME,
'anydrivefilename': WITH_ANY_FILE_NAME,
'anyownername': WITH_ANY_FILE_NAME,
'anyownerdrivefilename': WITH_ANY_FILE_NAME,
'sharedname': WITH_ANY_FILE_NAME,
'shareddrivefilename': WITH_ANY_FILE_NAME,
'name': WITH_MY_FILE_NAME,
'drivefilename': WITH_MY_FILE_NAME,
'othername': WITH_OTHER_FILE_NAME,
'otherdrivefilename': WITH_ANY_FILE_NAME,
}
LOCATION_ALL_DRIVES = 0
LOCATION_MYDRIVE = 1
LOCATION_ORPHANS = 2
LOCATION_OWNEDBY_ANY = 3
LOCATION_OWNEDBY_OTHERS = 4
LOCATION_SHARED_WITHME = 5
LOCATION_ONLY_SHARED_DRIVES = 6
LOCATION_CHOICE_MAP = {
'alldrives': {'fileids': [ROOT, ORPHANS, SHARED_WITHME, SHARED_DRIVES], 'owner': None, 'location': LOCATION_ALL_DRIVES, 'setShowOwnedBy': True},
'root': {'fileids': [ROOT], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
'rootwithorphans': {'fileids': [ROOT, ORPHANS], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
'mydrive': {'fileids': [ROOT], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
'mydrivewithorphans': {'fileids': [ROOT, ORPHANS], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
'mydriveany': {'fileids': [ROOT, ORPHANS], 'owner': None, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
'mydriveme': {'fileids': [ROOT, ORPHANS], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
'mydriveothers': {'fileids': [ROOT], 'owner': False, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
'onlyshareddrives': {'fileids': [SHARED_DRIVES], 'owner': False, 'location': LOCATION_ONLY_SHARED_DRIVES, 'setShowOwnedBy': True},
'onlyteamdrives': {'fileids': [SHARED_DRIVES], 'owner': False, 'location': LOCATION_ONLY_SHARED_DRIVES, 'setShowOwnedBy': True},
'orphans': {'fileids': [ORPHANS], 'owner': True, 'location': LOCATION_ORPHANS, 'setShowOwnedBy': True},
'ownedbyany': {'fileids': [ROOT, ORPHANS, SHARED_WITHME], 'owner': None, 'location': LOCATION_OWNEDBY_ANY, 'setShowOwnedBy': True},
'ownedbyme': {'fileids': [ROOT, ORPHANS, SHARED_WITHME], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
'ownedbyothers': {'fileids': [ROOT, ORPHANS, SHARED_WITHME], 'owner': False, 'location': LOCATION_OWNEDBY_OTHERS, 'setShowOwnedBy': True},
'sharedwithme': {'fileids': [ROOT, SHARED_WITHME], 'owner': False, 'location': LOCATION_OWNEDBY_OTHERS, 'setShowOwnedBy': True},
'sharedwithmemydrive': {'fileids': [ROOT], 'owner': False, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
'sharedwithmenotmydrive': {'fileids': [SHARED_WITHME], 'owner': False, 'location': LOCATION_SHARED_WITHME, 'setShowOwnedBy': True},
}
def getDriveFileEntity(queryShortcutsOK=True, DLP=None):
def _getKeywordColonValue(kwColonValue):
kw, value = kwColonValue.split(':', 1)
kw = kw.lower().replace('_', '').replace('-', '')
if kw == 'id':
cleanFileIDsList(fileIdEntity, [value])
elif kw == 'ids':
cleanFileIDsList(fileIdEntity, value.replace(',', ' ').split())
elif kw == 'query':
fileIdEntity['query'] = _mapDrive2QueryToDrive3(value)
fileIdEntity['nonDomainAdminAccess'] = True
elif kw in DRIVE_BY_NAME_CHOICE_MAP:
fileIdEntity['query'] = DRIVE_BY_NAME_CHOICE_MAP[kw].format(escapeDriveFileName(value))
fileIdEntity['nonDomainAdminAccess'] = True
else:
return False
return True
def _getTDKeywordColonValue(kwColonValue):
kw, value = kwColonValue.split(':', 1)
kw = kw.lower().replace('_', '').replace('-', '')
if kw in {'teamdriveid', 'shareddriveid'}:
fileIdEntity['shareddrive']['driveId'] = value
elif kw in {'teamdrive', 'shareddrive'}:
fileIdEntity['shareddrivename'] = value
elif kw in {'teamdriveadminquery', 'shareddriveadminquery'}:
fileIdEntity['shareddriveadminquery'] = value
elif kw in {'teamdrivefilename', 'shareddrivefilename'}:
fileIdEntity['shareddrivefilequery'] = WITH_ANY_FILE_NAME.format(escapeDriveFileName(value))
elif kw in {'teamdrivequery', 'shareddrivequery'}:
fileIdEntity['shareddrivefilequery'] = _mapDrive2QueryToDrive3(value)
else:
return False
return True
fileIdEntity = initDriveFileEntity()
entitySelector = getEntitySelector()
if entitySelector:
entityList = getEntitySelection(entitySelector, False)
if isinstance(entityList, dict):
fileIdEntity['dict'] = entityList
else:
cleanFileIDsList(fileIdEntity, entityList)
return fileIdEntity
fileIdEntity['location'] = Cmd.Location()
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
mycmd = myarg.lower().replace('_', '').replace('-', '')
if mycmd == 'id':
cleanFileIDsList(fileIdEntity, getStringReturnInList(Cmd.OB_DRIVE_FILE_ID))
elif mycmd == 'ids':
cleanFileIDsList(fileIdEntity, getString(Cmd.OB_DRIVE_FILE_ID).replace(',', ' ').split())
elif mycmd == 'query':
fileIdEntity['query'] = _mapDrive2QueryToDrive3(getString(Cmd.OB_QUERY))
fileIdEntity['nonDomainAdminAccess'] = True
elif queryShortcutsOK and mycmd in QUERY_SHORTCUTS_MAP:
fileIdEntity['query'] = QUERY_SHORTCUTS_MAP[mycmd]
fileIdEntity['nonDomainAdminAccess'] = True
elif mycmd in DRIVE_BY_NAME_CHOICE_MAP:
fileIdEntity['query'] = DRIVE_BY_NAME_CHOICE_MAP[mycmd].format(getEscapedDriveFileName())
fileIdEntity['nonDomainAdminAccess'] = True
elif mycmd in {'rootid', 'mydriveid'}:
cleanFileIDsList(fileIdEntity, [ROOTID])
fileIdEntity['nonDomainAdminAccess'] = True
elif not DLP and mycmd in {'root', 'mydrive'}:
cleanFileIDsList(fileIdEntity, [ROOT])
fileIdEntity['nonDomainAdminAccess'] = True
elif DLP and mycmd in LOCATION_CHOICE_MAP:
DLP.SetLocation(LOCATION_CHOICE_MAP[mycmd])
cleanFileIDsList(fileIdEntity, DLP.locationFileIds)
fileIdEntity['nonDomainAdminAccess'] = True
elif mycmd.startswith('teamdrive') or mycmd.startswith('shareddrive'):
fileIdEntity['shareddrive'] = {'driveId': None,
'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
while True:
if mycmd in {'teamdriveid', 'shareddriveid'}:
fileIdEntity['shareddrive']['driveId'] = getString(Cmd.OB_SHAREDDRIVE_ID).strip()
elif mycmd in {'teamdrive', 'shareddrive'}:
fileIdEntity['shareddrivename'] = getString(Cmd.OB_SHAREDDRIVE_NAME)
elif mycmd in {'teamdriveadminquery', 'shareddriveadminquery'}:
fileIdEntity['shareddriveadminquery'] = getString(Cmd.OB_QUERY)
elif mycmd in {'teamdrivefilename', 'shareddrivefilename'}:
fileIdEntity['shareddrivefilequery'] = WITH_ANY_FILE_NAME.format(getEscapedDriveFileName())
elif mycmd in {'teamdrivequery', 'shareddrivequery'}:
fileIdEntity['shareddrivefilequery'] = _mapDrive2QueryToDrive3(getString(Cmd.OB_QUERY))
elif queryShortcutsOK and mycmd in SHAREDDRIVE_QUERY_SHORTCUTS_MAP:
fileIdEntity['shareddrivefilequery'] = SHAREDDRIVE_QUERY_SHORTCUTS_MAP[mycmd]
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
pass
else:
unknownArgumentExit()
if Cmd.ArgumentsRemaining():
myarg = getString(Cmd.OB_STRING)
mycmd = myarg.lower().replace('_', '').replace('-', '')
if (mycmd.startswith('teamdriveparent') or mycmd.startswith('shareddriveparent') or
((not (mycmd.startswith('teamdrive') or mycmd.startswith('shareddrive'))) and
(not (queryShortcutsOK and mycmd in SHAREDDRIVE_QUERY_SHORTCUTS_MAP)))):
Cmd.Backup()
break
else:
break
if not fileIdEntity['shareddrive'].get('driveId'):
fileIdEntity['shareddrive']['corpora'] = CORPORA_ALL_DRIVES
elif (mycmd.find(':') > 0) and _getKeywordColonValue(myarg):
pass
else:
cleanFileIDsList(fileIdEntity, [myarg])
return fileIdEntity
def getDriveFileEntitySharedDriveOnly():
def _getTDKeywordColonValue(kwColonValue):
kw, value = kwColonValue.split(':', 1)
kw = kw.lower().replace('_', '').replace('-', '')
if kw in {'teamdriveid', 'shareddriveid'}:
fileIdEntity['shareddrive']['driveId'] = value
elif kw in {'teamdrive', 'shareddrive'}:
fileIdEntity['shareddrivename'] = value
elif kw in {'teamdriveadminquery', 'shareddriveadminquery'}:
fileIdEntity['shareddriveadminquery'] = value
else:
return False
return True
fileIdEntity = initDriveFileEntity()
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
mycmd = myarg.lower().replace('_', '').replace('-', '')
if mycmd.startswith('teamdrive') or mycmd.startswith('shareddrive'):
fileIdEntity['shareddrive'] = {'driveId': None,
'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
if mycmd in {'teamdriveid', 'shareddriveid'}:
fileIdEntity['shareddrive']['driveId'] = getString(Cmd.OB_SHAREDDRIVE_ID)
elif mycmd in {'teamdrive', 'shareddrive'}:
fileIdEntity['shareddrivename'] = getString(Cmd.OB_SHAREDDRIVE_NAME)
elif mycmd in {'teamdriveadminquery', 'shareddriveadminquery'}:
fileIdEntity['shareddriveadminquery'] = getString(Cmd.OB_QUERY)
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
pass
else:
unknownArgumentExit()
if not fileIdEntity['shareddrive'].get('driveId'):
fileIdEntity['shareddrive']['corpora'] = CORPORA_ALL_DRIVES
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
pass
else:
unknownArgumentExit()
return fileIdEntity
def getSharedDriveEntity():
def _getTDKeywordColonValue(kwColonValue):
kw, value = kwColonValue.split(':', 1)
kw = kw.lower().replace('_', '').replace('-', '')
if kw in {'teamdriveid', 'shareddriveid'}:
fileIdEntity['shareddrive']['driveId'] = value
elif kw in {'teamdrive', 'shareddrive', 'name'}:
fileIdEntity['shareddrivename'] = value
else:
return False
return True
fileIdEntity = initDriveFileEntity()
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
mycmd = myarg.lower().replace('_', '').replace('-', '')
fileIdEntity['shareddrive'] = {'driveId': None,
'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
if mycmd in {'teamdriveid', 'shareddriveid'}:
fileIdEntity['shareddrive']['driveId'] = getString(Cmd.OB_SHAREDDRIVE_ID)
elif mycmd in {'teamdrive', 'shareddrive', 'name'}:
fileIdEntity['shareddrivename'] = getString(Cmd.OB_SHAREDDRIVE_NAME)
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
pass
else:
fileIdEntity['shareddrive']['driveId'] = myarg
return fileIdEntity
def _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity, useDomainAdminAccess=False):
try:
if "\\'" in fileIdEntity['shareddrivename']:
name = fileIdEntity['shareddrivename']
else:
name = fileIdEntity['shareddrivename'].replace("'", "\\'")
tdlist = callGAPIpages(drive.drives(), 'list', 'drives',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
useDomainAdminAccess=useDomainAdminAccess,
q=f"name='{name}'",
fields='nextPageToken,drives(id,name)', pageSize=100)
if "\\'" in fileIdEntity['shareddrivename']:
fileIdEntity['shareddrivename'] = fileIdEntity['shareddrivename'].replace("\\'", "'")
if not tdlist:
name = fileIdEntity['shareddrivename'].lower()
feed = callGAPIpages(drive.drives(), 'list', 'drives',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
useDomainAdminAccess=useDomainAdminAccess,
fields='nextPageToken,drives(id,name)', pageSize=100)
tdlist = [td for td in feed if td['name'].lower() == name]
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_NAME, fileIdEntity['shareddrivename']], Msg.DOES_NOT_EXIST, i, count)
return False
jcount = len(tdlist)
if jcount == 1:
fileIdEntity['shareddrive']['driveId'] = tdlist[0]['id']
return True
if jcount == 0:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_NAME, fileIdEntity['shareddrivename']], Msg.DOES_NOT_EXIST, i, count)
else:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_NAME, fileIdEntity['shareddrivename']],
Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.SHAREDDRIVE_ID), jcount,
','.join([td['id'] for td in tdlist])), i, count)
return False
def _getSharedDriveNameFromId(sharedDriveId):
sharedDriveName = GM.Globals[GM.MAP_SHAREDDRIVE_ID_TO_NAME].get(sharedDriveId)
if not sharedDriveName:
if not GM.Globals[GM.ADMIN_DRIVE]:
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
if GM.Globals[GM.ADMIN_DRIVE]:
try:
sharedDriveName = callGAPI(GM.Globals[GM.ADMIN_DRIVE].drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
useDomainAdminAccess=True,
driveId=sharedDriveId, fields='name')['name']
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
sharedDriveName = TEAM_DRIVE
else:
sharedDriveName = TEAM_DRIVE
GM.Globals[GM.MAP_SHAREDDRIVE_ID_TO_NAME][sharedDriveId] = sharedDriveName
return sharedDriveName
def _getDriveFileNameFromId(drive, fileId, combineTitleId=True, useDomainAdminAccess=False):
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
fileId=fileId, fields='name,mimeType,driveId', supportsAllDrives=True)
if result:
fileName = result['name']
if (result['mimeType'] == MIMETYPE_GA_FOLDER) and result.get('driveId') and (result['name'] == TEAM_DRIVE):
fileName = _getSharedDriveNameFromId(result['driveId'])
if combineTitleId:
fileName += '('+fileId+')'
return (fileName, _getEntityMimeType(result), result['mimeType'])
except GAPI.fileNotFound:
if useDomainAdminAccess:
try:
result = callGAPI(drive.drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
useDomainAdminAccess=useDomainAdminAccess,
driveId=fileId, fields='name')
if result:
fileName = result['name']
if combineTitleId:
fileName += '('+fileId+')'
return (fileName, Ent.DRIVE_FOLDER, MIMETYPE_GA_FOLDER)
except GAPI.notFound:
pass
except (GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.internalError,
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
pass
return (fileId, Ent.DRIVE_FILE_OR_FOLDER_ID, None)
def _simpleFileIdEntityList(fileIdEntityList):
for fileId in fileIdEntityList:
if fileId not in {ROOT, ORPHANS, SHARED_WITHME, SHARED_DRIVES}:
return False
return True
def _validateUserGetFileIDs(user, i, count, fileIdEntity, drive=None, entityType=None, orderBy=None, useDomainAdminAccess=False):
def _identifyRoot():
try:
rootFolderId = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
for j in fileIdEntity[ROOT]:
fileIdEntity['list'][j] = rootFolderId
return True
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return False
if fileIdEntity['dict']:
cleanFileIDsList(fileIdEntity, fileIdEntity['dict'][user])
if not drive:
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
return (user, None, 0)
else:
user = convertUIDtoEmailAddress(user)
if fileIdEntity['list'] and _simpleFileIdEntityList(fileIdEntity['list']):
l = len(fileIdEntity['list'])
if ROOT in fileIdEntity['list'] and fileIdEntity[ROOT]:
if not _identifyRoot():
return (user, None, 0)
if entityType:
entityPerformActionNumItems([Ent.USER, user], l, entityType, i, count)
return (user, drive, l)
if fileIdEntity['shareddrivename'] and 'driveId' not in fileIdEntity:
if not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity, useDomainAdminAccess):
return (user, None, 0)
if not fileIdEntity['shareddrivefilequery']:
fileIdEntity['list'] = [fileIdEntity['shareddrive']['driveId']]
fileIdEntity['shareddrive']['corpora'] = 'drive'
elif fileIdEntity['shareddrive'].get('driveId'):
if not fileIdEntity['shareddrivefilequery']:
fileIdEntity['list'] = [fileIdEntity['shareddrive']['driveId']]
fileIdEntity['shareddrive']['corpora'] = 'drive'
if fileIdEntity['query']:
fileIdEntity['list'] = doDriveSearch(drive, user, i, count, query=fileIdEntity['query'], orderBy=orderBy)
if fileIdEntity['list'] is None:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (user, None, 0)
elif fileIdEntity['shareddriveadminquery']:
fileIdEntity['list'] = doSharedDriveSearch(drive, user, i, count, fileIdEntity['shareddriveadminquery'], useDomainAdminAccess)
if fileIdEntity['list'] is None:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (user, None, 0)
elif fileIdEntity['shareddrivefilequery']:
if not fileIdEntity['shareddrive'].get('driveId'):
fileIdEntity['shareddrive']['corpora'] = CORPORA_ALL_DRIVES
fileIdEntity['list'] = doDriveSearch(drive, user, i, count, query=fileIdEntity['shareddrivefilequery'], orderBy=orderBy, sharedDriveOnly=True, **fileIdEntity['shareddrive'])
if fileIdEntity['list'] is None or not fileIdEntity['list']:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (user, None, 0)
fileIdEntity['shareddrive'].pop('driveId', None)
fileIdEntity['shareddrive'].pop('corpora', None)
elif fileIdEntity[ROOT]:
if not _identifyRoot():
return (user, None, 0)
l = len(fileIdEntity['list'])
if l == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if entityType:
entityPerformActionNumItems([Ent.USER, user], l, entityType, i, count)
return (user, drive, l)
# Disc File Access paramaters
DFA_CREATED_TIME = 'createdTime'
DFA_MODIFIED_TIME = 'modifiedTime'
DFA_PRESERVE_FILE_TIMES = 'preserveFileTimes'
DFA_IGNORE_DEFAULT_VISIBILITY = 'ignoreDefaultVisibility'
DFA_KEEP_REVISION_FOREVER = 'keepRevisionForever'
DFA_URL = 'url'
DFA_LOCALFILEPATH = 'localFilepath'
DFA_LOCALFILENAME = 'localFilename'
DFA_LOCALMIMETYPE = 'localMimeType'
DFA_STRIPNAMEPREFIX = 'stripNamePrefix'
DFA_REPLACEFILENAME = 'replaceFileName'
DFA_OCRLANGUAGE = 'ocrLanguage'
DFA_PARENTID = 'parentId'
DFA_PARENTQUERY = 'parentQuery'
DFA_ADD_PARENT_IDS = 'addParentIds'
DFA_ADD_PARENT_NAMES = 'addParentNames'
DFA_REMOVE_PARENT_IDS = 'removeParentIds'
DFA_REMOVE_PARENT_NAMES = 'removeParentNames'
DFA_SHAREDDRIVE_PARENT = 'sharedDriveParent'
DFA_SHAREDDRIVE_PARENTID = 'sharedDriveParentId'
DFA_SHAREDDRIVE_PARENTQUERY = 'sharedDriveParentQuery'
DFA_KWARGS = 'kwargs'
DFA_SEARCHARGS = 'searchargs'
DFA_USE_CONTENT_AS_INDEXABLE_TEXT = 'useContentAsIndexableText'
DFA_TIMESTAMP = 'timestamp'
DFA_TIMEFORMAT = 'timeformat'
def _driveFileParentSpecified(parameters):
return (parameters[DFA_PARENTID] or parameters[DFA_PARENTQUERY] or
parameters[DFA_SHAREDDRIVE_PARENT] or parameters[DFA_SHAREDDRIVE_PARENTID] or
parameters[DFA_SHAREDDRIVE_PARENTQUERY])
def _getDriveFileParentInfo(drive, user, i, count, body, parameters, emptyQueryOK=False, defaultToRoot=True, entityType=Ent.DRIVE_FILE):
def _setSearchArgs(driveId):
parameters[DFA_SEARCHARGS] = {'driveId': driveId, 'corpora': 'drive',
'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
body.pop('parents', None)
if parameters[DFA_PARENTID]:
body.setdefault('parents', [])
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
fileId=parameters[DFA_PARENTID], fields='id,name,mimeType,driveId', supportsAllDrives=True)
if result['mimeType'] != MIMETYPE_GA_FOLDER:
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
f'parentid: {parameters[DFA_PARENTID]}, {Msg.NOT_AN_ENTITY.format((Ent.Singular(Ent.DRIVE_FOLDER)))}', i, count)
return False
body['parents'].append(result['id'])
if result.get('driveId'):
_setSearchArgs(result['driveId'])
except GAPI.fileNotFound as e:
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
f'parentid: {parameters[DFA_PARENTID]}, {str(e)}', i, count)
return False
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return False
if parameters[DFA_PARENTQUERY]:
parents = doDriveSearch(drive, user, i, count, query=parameters[DFA_PARENTQUERY], parentQuery=True, emptyQueryOK=emptyQueryOK)
if parents is None:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return False
body.setdefault('parents', [])
for parent in parents:
body['parents'].append(parent)
if parameters[DFA_SHAREDDRIVE_PARENTID]:
try:
if not parameters[DFA_SHAREDDRIVE_PARENTQUERY]:
body.setdefault('parents', [])
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
fileId=parameters[DFA_SHAREDDRIVE_PARENTID], fields='id,mimeType,driveId', supportsAllDrives=True)
if result['mimeType'] != MIMETYPE_GA_FOLDER:
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
f'shareddriveparentid: {parameters[DFA_SHAREDDRIVE_PARENTID]}, {Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER))}', i, count)
return False
if not result.get('driveId'):
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
f'shareddriveparentid: {parameters[DFA_SHAREDDRIVE_PARENTID]}, {Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.SHAREDDRIVE_FOLDER))}', i, count)
return False
body['parents'].append(result['id'])
_setSearchArgs(result['driveId'])
else:
result = callGAPI(drive.drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
driveId=parameters[DFA_SHAREDDRIVE_PARENTID], fields='id')
parameters[DFA_KWARGS]['corpora'] = 'drive'
parameters[DFA_KWARGS]['driveId'] = result['id']
_setSearchArgs(result['id'])
except (GAPI.fileNotFound, GAPI.notFound) as e:
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
f'shareddriveparentid: {parameters[DFA_SHAREDDRIVE_PARENTID]}, {str(e)}', i, count)
return False
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return False
if parameters[DFA_SHAREDDRIVE_PARENT]:
tempIdEntity = {'shareddrivename': parameters[DFA_SHAREDDRIVE_PARENT], 'shareddrive': {}}
if not _convertSharedDriveNameToId(drive, user, i, count, tempIdEntity):
return False
if not parameters[DFA_SHAREDDRIVE_PARENTQUERY]:
body.setdefault('parents', [])
body['parents'].append(tempIdEntity['shareddrive']['driveId'])
else:
parameters[DFA_KWARGS]['corpora'] = 'drive'
parameters[DFA_KWARGS]['driveId'] = tempIdEntity['shareddrive']['driveId']
_setSearchArgs(tempIdEntity['shareddrive']['driveId'])
if parameters[DFA_SHAREDDRIVE_PARENTQUERY]:
parents = doDriveSearch(drive, user, i, count, query=parameters[DFA_SHAREDDRIVE_PARENTQUERY], parentQuery=True, emptyQueryOK=emptyQueryOK,
sharedDriveOnly=True, **parameters[DFA_KWARGS])
if parents is None:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return False
body.setdefault('parents', [])
for parent in parents:
body['parents'].append(parent)
if defaultToRoot and ('parents' not in body or not body['parents']):
body['parents'] = [ROOT]
numParents = len(body.get('parents', []))
if numParents > 1:
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
Msg.MULTIPLE_PARENTS_SPECIFIED.format(numParents), i, count)
return False
return True
def _getDriveFileAddRemoveParentInfo(user, i, count, parameters, drive):
addParents = parameters[DFA_ADD_PARENT_IDS][:]
for query in parameters[DFA_ADD_PARENT_NAMES]:
parents = doDriveSearch(drive, user, i, count, query=query, parentQuery=True)
if parents is None:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (False, None, None)
addParents.extend(parents)
removeParents = parameters[DFA_REMOVE_PARENT_IDS][:]
for query in parameters[DFA_REMOVE_PARENT_NAMES]:
parents = doDriveSearch(drive, user, i, count, query=query, parentQuery=True)
if parents is None:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (False, None, None)
removeParents.extend(parents)
return (True, addParents, removeParents)
def _validateUserGetSharedDriveFileIDs(user, i, count, fileIdEntity, drive=None, entityType=None):
if fileIdEntity['dict']:
cleanFileIDsList(fileIdEntity, fileIdEntity['dict'][user])
if not drive:
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
return (user, None, 0)
else:
user = convertUIDtoEmailAddress(user)
if fileIdEntity.get('shareddrivename') and not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity):
return (user, None, 0)
if fileIdEntity['shareddrivefilequery']:
fileIdEntity['list'] = doDriveSearch(drive, user, i, count, query=fileIdEntity['shareddrivefilequery'], sharedDriveOnly=True, **fileIdEntity['shareddrive'])
if fileIdEntity['list'] is None or not fileIdEntity['list']:
setSysExitRC(NO_ENTITIES_FOUND_RC)
return (user, None, 0)
fileIdEntity['shareddrive'].pop('driveId', None)
fileIdEntity['shareddrive'].pop('corpora', None)
l = len(fileIdEntity['list'])
if l == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if entityType:
entityPerformActionNumItems([Ent.USER, user], l, entityType, i, count)
return (user, drive, l)
def _validateUserSharedDrive(user, i, count, fileIdEntity, useDomainAdminAccess=False):
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
return (user, None)
if fileIdEntity.get('shareddrivename'):
if not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity, useDomainAdminAccess):
return (user, None)
fileIdEntity['shareddrive']['corpora'] = 'drive'
return (user, drive)
DRIVE_LABEL_CHOICE_MAP = {
'modified': 'modifiedByMe',
'modifiedbyme': 'modifiedByMe',
'restrict': 'copyRequiresWriterPermission',
'restricted': 'copyRequiresWriterPermission',
'star': 'starred',
'starred': 'starred',
'trash': 'trashed',
'trashed': 'trashed',
'view': 'viewedByMe',
'viewed': 'viewedByMe',
'viewedbyme': 'viewedByMe',
}
MIMETYPE_CHOICE_MAP = {
'gdoc': MIMETYPE_GA_DOCUMENT,
'gdocument': MIMETYPE_GA_DOCUMENT,
'gdrawing': MIMETYPE_GA_DRAWING,
'gfile': MIMETYPE_GA_FILE,
'gfolder': MIMETYPE_GA_FOLDER,
'gdirectory': MIMETYPE_GA_FOLDER,
'gform': MIMETYPE_GA_FORM,
'gfusion': MIMETYPE_GA_FUSIONTABLE,
'gfusiontable': MIMETYPE_GA_FUSIONTABLE,
'gjam': MIMETYPE_GA_JAM,
'gmap': MIMETYPE_GA_MAP,
'gpresentation': MIMETYPE_GA_PRESENTATION,
'gscript': MIMETYPE_GA_SCRIPT,
'gshortcut': MIMETYPE_GA_SHORTCUT,
'g3pshortcut': MIMETYPE_GA_3P_SHORTCUT,
'gsite': MIMETYPE_GA_SITE,
'gsheet': MIMETYPE_GA_SPREADSHEET,
'gspreadsheet': MIMETYPE_GA_SPREADSHEET,
'shortcut': MIMETYPE_GA_SHORTCUT,
}
MIMETYPE_TYPES = ['application', 'audio', 'font', 'image', 'message', 'model', 'multipart', 'text', 'video']
def validateMimeType(mimeType):
if mimeType in MIMETYPE_CHOICE_MAP:
return MIMETYPE_CHOICE_MAP[mimeType]
if mimeType.startswith(APPLICATION_VND_GOOGLE_APPS):
return mimeType
if mimeType.find('/') > 0:
mediaType, subType = mimeType.split('/', 1)
if mediaType in MIMETYPE_TYPES and subType:
return mimeType
invalidChoiceExit(mimeType, list(MIMETYPE_CHOICE_MAP)+[f'({formatChoiceList(MIMETYPE_TYPES)})/mediatype'], True)
def getMimeType():
return validateMimeType(getString(Cmd.OB_MIMETYPE).lower())
class MimeTypeCheck():
def __init__(self):
self.mimeTypes = set()
self.reverse = False
self.category = set()
def Get(self):
if checkArgumentPresent('category'):
for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split():
if mimeType in MIMETYPE_TYPES:
self.category.add(mimeType)
else:
invalidChoiceExit(mimeType, MIMETYPE_TYPES, True)
return
self.reverse = checkArgumentPresent('not')
for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split():
self.mimeTypes.add(validateMimeType(mimeType))
def AddMimeTypeToQuery(self, query):
if query:
query += ' and ('
else:
query = '('
if not self.reverse:
for mimeType in self.mimeTypes:
query += f"mimeType = '{mimeType}' or "
for mimeType in self.category:
query += f"mimeType contains '{mimeType}' or "
query = query[:-4]
else:
for mimeType in self.mimeTypes:
query += f"mimeType != '{mimeType}' and "
query = query[:-5]
query += ')'
return query
def Check(self, fileEntry):
if not self.mimeTypes and not self.category:
return True
for mimeType in self.category:
if fileEntry['mimeType'].startswith(mimeType):
return True
if not self.reverse:
return fileEntry['mimeType'] in self.mimeTypes
return fileEntry['mimeType'] not in self.mimeTypes
def initDriveFileAttributes():
return {DFA_CREATED_TIME: None,
DFA_MODIFIED_TIME: None,
DFA_PRESERVE_FILE_TIMES: False,
DFA_IGNORE_DEFAULT_VISIBILITY: False,
DFA_KEEP_REVISION_FOREVER: False,
DFA_URL: None,
DFA_LOCALFILEPATH: None,
DFA_LOCALFILENAME: None,
DFA_LOCALMIMETYPE: None,
DFA_STRIPNAMEPREFIX: None,
DFA_REPLACEFILENAME: [],
DFA_OCRLANGUAGE: None,
DFA_PARENTID: None,
DFA_PARENTQUERY: None,
DFA_ADD_PARENT_IDS: [],
DFA_ADD_PARENT_NAMES: [],
DFA_REMOVE_PARENT_IDS: [],
DFA_REMOVE_PARENT_NAMES: [],
DFA_SHAREDDRIVE_PARENT: None,
DFA_SHAREDDRIVE_PARENTID: None,
DFA_SHAREDDRIVE_PARENTQUERY: None,
DFA_KWARGS: {},
DFA_SEARCHARGS: {},
DFA_USE_CONTENT_AS_INDEXABLE_TEXT: False,
DFA_TIMESTAMP: False,
DFA_TIMEFORMAT: None}
DRIVEFILE_PROPERTY_VISIBILITY_CHOICE_MAP = {
'private': 'appProperties',
'public': 'properties'
}
DRIVE_FILE_CONTENT_RESTRICTIONS_CHOICE_MAP = {
'readonly': 'readOnly',
'ownerrestricted': 'ownerRestricted',
}
def getDriveFileProperty(visibility=None):
key = getString(Cmd.OB_PROPERTY_KEY)
value = getString(Cmd.OB_PROPERTY_VALUE, minLen=0) or None
if visibility is None:
if Cmd.PeekArgumentPresent(list(DRIVEFILE_PROPERTY_VISIBILITY_CHOICE_MAP.keys())):
visibility = getChoice(DRIVEFILE_PROPERTY_VISIBILITY_CHOICE_MAP, mapChoice=True)
else:
visibility = 'properties'
return {'key': key, 'value': value, 'visibility': visibility}
def getDriveFileParentAttribute(myarg, parameters):
if myarg == 'parentid':
parameters[DFA_PARENTID] = getString(Cmd.OB_DRIVE_FOLDER_ID)
elif myarg == 'parentname':
parameters[DFA_PARENTQUERY] = MY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName())
elif myarg in {'anyownerparentname', 'sharedparentname'}:
parameters[DFA_PARENTQUERY] = ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName())
elif myarg in {'teamdriveparent', 'shareddriveparent'}:
parameters[DFA_SHAREDDRIVE_PARENT] = getString(Cmd.OB_SHAREDDRIVE_NAME)
elif myarg in {'teamdriveparentid', 'shareddriveparentid'}:
parameters[DFA_SHAREDDRIVE_PARENTID] = getString(Cmd.OB_DRIVE_FOLDER_ID)
elif myarg in {'teamdriveparentname', 'shareddriveparentname'}:
parameters[DFA_SHAREDDRIVE_PARENTQUERY] = ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName())
parameters[DFA_KWARGS]['corpora'] = CORPORA_ALL_DRIVES
parameters[DFA_KWARGS]['includeItemsFromAllDrives'] = True
parameters[DFA_KWARGS]['supportsAllDrives'] = True
elif myarg == 'enforcesingleparent':
deprecatedArgument(myarg)
else:
return False
return True
def getDriveFileAddRemoveParentAttribute(myarg, parameters):
if myarg in {'addparent', 'addparents'}:
parameters[DFA_ADD_PARENT_IDS].extend(getString(Cmd.OB_DRIVE_FOLDER_ID_LIST).replace(',', ' ').split())
elif myarg in {'removeparent', 'removeparents'}:
parameters[DFA_REMOVE_PARENT_IDS].extend(getString(Cmd.OB_DRIVE_FOLDER_ID_LIST).replace(',', ' ').split())
elif myarg == 'addparentname':
parameters[DFA_ADD_PARENT_NAMES].append(MY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
elif myarg == 'removeparentname':
parameters[DFA_REMOVE_PARENT_NAMES].append(MY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
elif myarg in {'addanyownerparentname', 'addsharedparentname'}:
parameters[DFA_ADD_PARENT_NAMES].append(ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
elif myarg in {'removeanyownerparentname', 'removesharedparentname'}:
parameters[DFA_REMOVE_PARENT_NAMES].append(ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
elif myarg == 'enforcesingleparent':
deprecatedArgument(myarg)
else:
return False
return True
def getDriveFileCopyAttribute(myarg, body, parameters):
if myarg == 'ignoredefaultvisibility':
parameters[DFA_IGNORE_DEFAULT_VISIBILITY] = getBoolean()
elif myarg in {'keeprevisionforever', 'pinned'}:
parameters[DFA_KEEP_REVISION_FOREVER] = getBoolean()
elif myarg == 'ocrlanguage':
parameters[DFA_OCRLANGUAGE] = getLanguageCode(LANGUAGE_CODES_MAP)
elif myarg == 'description':
body['description'] = getStringWithCRsNLs()
elif myarg == 'mimetype':
body['mimeType'] = getMimeType()
elif myarg in {'lastviewedbyme', 'lastviewedbyuser', 'lastviewedbymedate', 'lastviewedbymetime'}:
body['viewedByMeTime'] = getTimeOrDeltaFromNow()
elif myarg in {'modifieddate', 'modifiedtime'}:
body['modifiedTime'] = getTimeOrDeltaFromNow()
elif myarg == 'viewerscancopycontent':
body['copyRequiresWriterPermission'] = not getBoolean()
elif myarg in {'copyrequireswriterpermission', 'restrict', 'restricted'}:
body['copyRequiresWriterPermission'] = getBoolean()
elif myarg == 'writerscanshare':
body['writersCanShare'] = getBoolean()
elif myarg == 'writerscantshare':
body['writersCanShare'] = not getBoolean()
elif myarg == 'contentrestrictions':
while Cmd.PeekArgumentPresent(list(DRIVE_FILE_CONTENT_RESTRICTIONS_CHOICE_MAP.keys())):
body.setdefault('contentRestrictions', [{}])
restriction = getChoice(DRIVE_FILE_CONTENT_RESTRICTIONS_CHOICE_MAP, mapChoice=True)
body['contentRestrictions'][0][restriction] = getBoolean()
if restriction == 'readOnly':
if checkArgumentPresent(['reason']):
if body['contentRestrictions'][0][restriction]:
body['contentRestrictions'][0]['reason'] = getString(Cmd.OB_STRING, minLen=0)
else:
Cmd.Backup()
usageErrorExit(Msg.REASON_ONLY_VALID_WITH_CONTENTRESTRICTIONS_READONLY_TRUE)
elif myarg == 'inheritedpermissionsdisabled':
body['inheritedPermissionsDisabled'] = getBoolean()
elif myarg == 'property':
driveprop = getDriveFileProperty()
body.setdefault(driveprop['visibility'], {})
body[driveprop['visibility']][driveprop['key']] = driveprop['value']
elif myarg == 'privateproperty':
driveprop = getDriveFileProperty('appProperties')
body.setdefault(driveprop['visibility'], {})
body[driveprop['visibility']][driveprop['key']] = driveprop['value']
elif myarg == 'publicproperty':
driveprop = getDriveFileProperty('properties')
body.setdefault(driveprop['visibility'], {})
body[driveprop['visibility']][driveprop['key']] = driveprop['value']
else:
return False
return True
def getDriveFileAttribute(myarg, body, parameters, updateCmd):
if myarg == 'localfile':
parameters[DFA_URL] = None
parameters[DFA_LOCALFILEPATH] = os.path.expanduser(getString(Cmd.OB_FILE_NAME))
if parameters[DFA_LOCALFILEPATH] != '-':
try:
f = open(parameters[DFA_LOCALFILEPATH], 'rb')
f.close()
# See http://stackoverflow.com/a/39501288/1709587 for explanation.
mtime = os.path.getmtime(parameters[DFA_LOCALFILEPATH])
parameters[DFA_MODIFIED_TIME] = formatLocalSecondsTimestamp(mtime)
if not updateCmd:
if platform.system() == 'Windows':
ctime = os.path.getctime(parameters[DFA_LOCALFILEPATH])
else:
stat = os.stat(parameters[DFA_LOCALFILEPATH])
if hasattr(stat, 'st_birthtime'):
ctime = stat.st_birthtime
else:
# We're probably on Linux. No easy way to get creation dates here,
# so we'll settle for when its content was last modified.
ctime = stat.st_mtime
parameters[DFA_CREATED_TIME] = formatLocalSecondsTimestamp(ctime)
except IOError as e:
Cmd.Backup()
usageErrorExit(f'{parameters[DFA_LOCALFILEPATH]}: {str(e)}')
parameters[DFA_LOCALFILENAME] = os.path.basename(parameters[DFA_LOCALFILEPATH])
if not updateCmd:
body.setdefault('name', parameters[DFA_LOCALFILENAME])
body['mimeType'] = mimetypes.guess_type(parameters[DFA_LOCALFILEPATH])[0]
if body['mimeType'] is None:
body['mimeType'] = 'application/octet-stream'
else:
parameters[DFA_LOCALFILENAME] = '-'
if body.get('mimeType') is None:
body['mimeType'] = 'application/octet-stream'
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
elif myarg == 'url':
parameters[DFA_LOCALFILEPATH] = None
parameters[DFA_URL] = getString(Cmd.OB_URL)
elif myarg =='stripnameprefix':
parameters[DFA_STRIPNAMEPREFIX] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'replacefilename':
parameters[DFA_REPLACEFILENAME].append(getREPatternSubstitution(re.IGNORECASE))
elif myarg in {'convert', 'ocr'}:
deprecatedArgument(myarg)
stderrWarningMsg(Msg.USE_MIMETYPE_TO_SPECIFY_GOOGLE_FORMAT)
elif myarg in DRIVE_LABEL_CHOICE_MAP:
myarg = DRIVE_LABEL_CHOICE_MAP[myarg]
body[myarg] = getBoolean()
elif not updateCmd and myarg in {'createddate', 'createdtime'}:
body['createdTime'] = getTimeOrDeltaFromNow()
elif myarg == 'preservefiletimes':
parameters[DFA_PRESERVE_FILE_TIMES] = getBoolean()
elif myarg == 'shortcut':
body['mimeType'] = MIMETYPE_GA_SHORTCUT
body['shortcutDetails'] = {'targetId': getString(Cmd.OB_DRIVE_FOLDER_ID)}
elif getDriveFileParentAttribute(myarg, parameters):
pass
elif myarg == 'foldercolorrgb':
body['folderColorRgb'] = getColor()
elif myarg == 'usecontentasindexabletext':
parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT] = getBoolean()
elif myarg == 'indexabletext':
body.setdefault('contentHints', {})
body['contentHints']['indexableText'] = getString(Cmd.OB_STRING)
elif myarg == 'securityupdate':
body['linkShareMetadata'] = {'securityUpdateEnabled': getBoolean(), 'securityUpdateEligible': True}
elif myarg == 'timestamp':
parameters[DFA_TIMESTAMP] = getBoolean()
elif myarg == 'timeformat':
parameters[DFA_TIMEFORMAT] = getString(Cmd.OB_STRING, minLen=0)
elif getDriveFileCopyAttribute(myarg, body, parameters):
pass
else:
unknownArgumentExit()
def setPreservedFileTimes(body, parameters, updateCmd):
body['modifiedTime'] = parameters[DFA_MODIFIED_TIME]
if not updateCmd:
body['createdTime'] = parameters[DFA_CREATED_TIME]
def getMediaBody(parameters):
if parameters[DFA_URL]:
try:
status, c = getHttpObj(timeout=10).request(parameters[DFA_URL])
if status['status'] != '200':
entityActionFailedExit([Ent.URL, parameters[DFA_URL]], Msg.URL_ERROR.format(status['status']))
parameters[DFA_LOCALMIMETYPE] = status['content-type']
return googleapiclient.http.MediaIoBaseUpload(io.BytesIO(c), mimetype=status['content-type'], resumable=True)
except (IOError, httplib2.error.ServerNotFoundError) as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(parameters[DFA_URL], str(e), entityType=Ent.URL))
else:
try:
if parameters[DFA_LOCALFILEPATH] != '-':
media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True)
else:
media_body = googleapiclient.http.MediaIoBaseUpload(io.BytesIO(sys.stdin.buffer.read()), mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True)
if media_body.size() == 0:
media_body = None
return media_body
except IOError as e:
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(parameters[DFA_LOCALFILEPATH], str(e)))
DRIVE_ACTIVITY_ACTION_MAP = {
'comment': 'COMMENT',
'create': 'CREATE',
'delete': 'DELETE',
'dlpchange': 'DLP_CHANGE',
'edit': 'EDIT',
'emptytrash': 'DELETE',
'move': 'MOVE',
'permissionchange': 'PERMISSION_CHANGE',
'reference': 'REFERENCE',
'rename': 'RENAME',
'restore': 'RESTORE',
'settingschange': 'SETTINGS_CHANGE',
'trash': 'DELETE',
'untrash': 'RESTORE',
'upload': 'CREATE',
}
CONSOLIDATION_GROUPING_STRATEGY_CHOICE_MAP = {'driveui': 'legacy', 'legacy': 'legacy', 'none': 'none'}
# gam <UserTypeEntity> print driveactivity [todrive <ToDriveAttribute>*]
# [(fileid <DriveFileID>) | (folderid <DriveFolderID>) |
# (drivefilename <DriveFileName>) | (drivefoldername <DriveFolderName>) | (query <QueryDriveFile>)]
# [([start <Date>|<Time>] [end <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)|
# yesterday|today|thismonth|(previousmonths <Integer>)]
# [action|actions [not] <DriveActivityActionList>]
# [consolidationstrategy legacy|none]
# [idmapfile <CSVFileInput> endcsv]
# [stripcrsfromname] [formatjson [quotechar <Character>]]
def printDriveActivity(users):
def _getUserInfo(userId):
if userId.startswith('people/'):
userId = userId[7:]
entry = userInfo.get(userId)
if entry is None:
try:
result = callGAPI(cd.users(), 'get',
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT],
userKey=userId, fields='primaryEmail,name.fullName')
entry = (result['primaryEmail'], result['name']['fullName'])
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.invalidInput):
entry = (f'uid:{userId}', UNKNOWN)
userInfo[userId] = entry
return entry
def _updateKnownUsers(structure):
if isinstance(structure, list):
for v in structure:
if isinstance(v, (dict, list)):
_updateKnownUsers(v)
elif isinstance(structure, dict):
for k, v in sorted(iter(structure.items())):
if k != 'knownUser':
if isinstance(v, (dict, list)):
_updateKnownUsers(v)
else:
entry = _getUserInfo(v['personName'])
v['emailAddress'] = entry[0]
v['personName'] = entry[1]
break
cd = buildGAPIObject(API.DIRECTORY)
startEndTime = StartEndTime()
baseFileList = []
query = ''
activityFilter = ''
actions = set()
strategy = 'none'
negativeAction = stripCRsFromName = False
checkArgumentPresent(['v2'])
csvPF = CSVPrintFile([f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name',
f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}emailAddress',
f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id',
f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name',
f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType',
'eventTime'],
'sortall')
FJQC = FormatJSONQuoteChar(csvPF)
userInfo = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'fileid':
baseFileList.append({'id': getString(Cmd.OB_DRIVE_FILE_ID), 'mimeType': MIMETYPE_GA_DOCUMENT})
elif myarg == 'folderid':
baseFileList.append({'id': getString(Cmd.OB_DRIVE_FOLDER_ID), 'mimeType': MIMETYPE_GA_FOLDER})
elif myarg == 'drivefilename':
query = f"mimeType != '{MIMETYPE_GA_FOLDER}' and name = '{getEscapedDriveFileName()}'"
elif myarg == 'drivefoldername':
query = f"mimeType = '{MIMETYPE_GA_FOLDER}' and name = '{getEscapedDriveFileName()}'"
elif myarg == 'query':
query = _mapDrive2QueryToDrive3(getString(Cmd.OB_QUERY))
elif myarg in {'start', 'starttime', 'end', 'endtime', 'yesterday', 'today', 'range', 'thismonth', 'previousmonths'}:
startEndTime.Get(myarg)
elif myarg in {'action', 'actions'}:
negativeAction = checkArgumentPresent('not')
for action in _getFieldsList():
if action in DRIVE_ACTIVITY_ACTION_MAP:
mappedAction = DRIVE_ACTIVITY_ACTION_MAP[action]
if mappedAction:
actions.add(mappedAction)
else:
invalidChoiceExit(action, DRIVE_ACTIVITY_ACTION_MAP, True)
elif myarg in {'allevents', 'combinedevents', 'singleevents'}:
pass
elif myarg in {'consolidationstrategy', 'groupingstrategy'}:
strategy = getChoice(CONSOLIDATION_GROUPING_STRATEGY_CHOICE_MAP, mapChoice=True)
elif myarg == 'idmapfile':
f, csvFile, _ = openCSVFileReader(getString(Cmd.OB_FILE_NAME))
for row in csvFile:
userInfo[row['id']] = (row['primaryEmail'], row.get('name.fullName', UNKNOWN))
closeFile(f)
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not baseFileList and not query:
baseFileList = [{'id': ROOT, 'mimeType': MIMETYPE_GA_FOLDER}]
if startEndTime.startTime:
if activityFilter:
activityFilter += ' AND '
activityFilter += f'time >= "{startEndTime.startTime}"'
if startEndTime.endTime:
if activityFilter:
activityFilter += ' AND '
activityFilter += f'time <= "{startEndTime.endTime}"'
if actions:
if activityFilter:
activityFilter += ' AND '
activityFilter += f'{"-" if negativeAction else ""}detail.action_detail_case:({" ".join(actions)})'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, activity = buildGAPIServiceObject(API.DRIVEACTIVITY, user, i, count)
if not activity:
continue
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
fileList = baseFileList[:]
if query:
if GC.Values[GC.SHOW_GETTINGS]:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count, query=query)
try:
fileList.extend(callGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query, fields='nextPageToken,files(id,mimeType)', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS]))
if not fileList:
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], emptyQuery(query, Ent.DRIVE_FILE_OR_FOLDER), i, count)
continue
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(query), i, count)
break
except GAPI.fileNotFound:
printGotEntityItemsForWhom(0)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
for f_file in fileList:
fileId = f_file['id']
entityType = Ent.DRIVE_FOLDER_ID if f_file['mimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_ID
if entityType == Ent.DRIVE_FILE_ID:
drive_key = 'itemName'
else:
drive_key = 'ancestorName'
qualifier = f' for {Ent.Singular(entityType)}: {fileId}'
printGettingAllEntityItemsForWhom(Ent.ACTIVITY, user, i, count, qualifier=qualifier)
pageMessage = getPageMessageForWhom()
kwargs = {
'consolidationStrategy': {strategy: {}},
'pageSize': GC.Values[GC.ACTIVITY_MAX_RESULTS],
'pageToken': None,
drive_key: f'items/{fileId}',
'filter': activityFilter}
try:
feed = yieldGAPIpages(activity.activity(), 'query', 'activities',
pageMessage=pageMessage,
throwReasons=GAPI.ACTIVITY_THROW_REASONS,
pageArgsInBody=True,
fields='nextPageToken,activities', body=kwargs)
for activities in feed:
for activityEvent in activities:
eventRow = {}
actors = activityEvent.get('actors', [])
if actors:
userId = actors[0].get('user', {}).get('knownUser', {}).get('personName', '')
if not userId:
userId = actors[0].get('impersonation', {}).get('impersonatedUser', {}).get('knownUser', {}).get('personName', '')
if userId:
entry = _getUserInfo(userId)
eventRow[f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = entry[1]
eventRow[f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}emailAddress'] = entry[0]
targets = activityEvent.get('targets', [])
if targets:
driveItem = targets[0].get('driveItem')
if driveItem:
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id'] = driveItem['name'][6:]
if stripCRsFromName:
driveItem['title'] = _stripControlCharsFromName(driveItem['title'])
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = driveItem['title']
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType'] = driveItem['mimeType']
else:
sharedDrive = targets[0].get('teamDrive')
if sharedDrive:
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id'] = sharedDrive['name'][11:]
if stripCRsFromName:
sharedDrive['title'] = _stripControlCharsFromName(sharedDrive['title'])
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = sharedDrive['title']
if 'timestamp' in activityEvent:
eventRow['eventTime'] = formatLocalTime(activityEvent['timestamp'])
elif 'timeRange' in activityEvent:
timeRange = activityEvent['timeRange']
eventRow['eventTime'] = f'{formatLocalTime(timeRange["startTime"])}-{formatLocalTime(timeRange["endTime"])}'
_updateKnownUsers(activityEvent)
if not FJQC.formatJSON:
activityEvent.pop('timestamp', None)
activityEvent.pop('timeRange', None)
flattenJSON(activityEvent, flattened=eventRow)
csvPF.WriteRowTitles(eventRow)
else:
checkRow = eventRow.copy()
flattenJSON(activityEvent, flattened=checkRow)
if csvPF.CheckRowTitles(checkRow):
eventRow['JSON'] = json.dumps(cleanJSON(activityEvent), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(eventRow)
except GAPI.badRequest as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileId], str(e), i, count)
continue
except GAPI.serviceNotAvailable as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
csvPF.writeCSVfile('Drive Activity')
DRIVESETTINGS_FIELDS_CHOICE_MAP = {
'appinstalled': 'appInstalled',
'exportformats': 'exportFormats',
'foldercolorpalette': 'folderColorPalette',
'importformats': 'importFormats',
'largestchangeid': 'largestChangeId',
'limit': 'limit',
'maximportsizes': 'maxImportSizes',
'maxuploadsize': 'maxUploadSize',
'name': 'name',
'permissionid': 'permissionId',
'rootfolderid': 'rootFolderId',
'drivethemes': 'driveThemes',
'shareddrivethemes': 'driveThemes',
'teamdrivethemes': 'driveThemes',
'usage': 'usage',
'usageindrive': 'usageInDrive',
'usageindrivetrash': 'usageInDriveTrash',
}
DRIVESETTINGS_SCALAR_FIELDS = [
'name',
'appInstalled',
'largestChangeId',
'limit',
'maxUploadSize',
'permissionId',
'rootFolderId',
'usage',
'usageInDrive',
'usageInDriveTrash',
]
DRIVESETTINGS_USAGE_BYTES_FIELDS = {
'usage': 'usageBytes',
'usageInDrive': 'usageInDriveBytes',
'usageInDriveTrash': 'usageInDriveTrashBytes',
}
def _showSharedDriveThemeSettings(themes):
Ind.Increment()
for theme in themes:
printKeyValueList(['id', theme['id']])
Ind.Increment()
printKeyValueList(['backgroundImageLink', theme['backgroundImageLink']])
printKeyValueList(['colorRgb', theme['colorRgb']])
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> print drivesettings [todrive <ToDriveAttribute>*]
# [allfields|<DriveSettingsFieldName>*|(fields <DriveSettingsFieldNameList>)]
# [delimiter <Character>] [showusagebytes]
# gam <UserTypeEntity> show drivesettings
# [allfields|<DriveSettingsFieldName>*|(fields <DriveSettingsFieldNameList>)]
# [delimiter <Character>] [showusagebytes]
def printShowDriveSettings(users):
def _showFormats(title):
if title in fieldsList and title in feed:
printKeyValueList([title, None])
Ind.Increment()
for item, value in sorted(iter(feed[title].items())):
printKeyValueList([item, delimiter.join(value)])
Ind.Decrement()
def _showSetting(title):
if title in fieldsList and title in feed:
if not isinstance(feed[title], list):
printKeyValueList([title, feed[title]])
else:
printKeyValueList([title, delimiter.join(feed[title])])
def _addFormats(row, title):
if title in fieldsList and title in feed:
jcount = len(feed[title])
row[title] = jcount
j = 0
for item, value in sorted(iter(feed[title].items())):
row[f'{title}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{item}'] = delimiter.join(value)
j += 1
def _addSetting(row, title):
if title in fieldsList and title in feed:
if not isinstance(feed[title], list):
row[title] = feed[title]
else:
row[title] = delimiter.join(feed[title])
csvPF = CSVPrintFile(['email'], ['email']+DRIVESETTINGS_SCALAR_FIELDS) if Act.csvFormat() else None
fieldsList = []
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showUsageBytes = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'allfields':
fieldsList.extend(DRIVESETTINGS_FIELDS_CHOICE_MAP.values())
elif getFieldsList(myarg, DRIVESETTINGS_FIELDS_CHOICE_MAP, fieldsList):
pass
elif myarg == 'showusagebytes':
showUsageBytes = True
else:
unknownArgumentExit()
if not fieldsList:
fieldsList = DRIVESETTINGS_SCALAR_FIELDS[:]
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.DRIVE_SETTINGS, user, i, count)
try:
feed = callGAPI(drive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='*')
feed['name'] = feed['user']['displayName']
feed['maxUploadSize'] = formatFileSize(int(feed['maxUploadSize']))
feed['permissionId'] = feed['user']['permissionId']
if 'limit' in feed['storageQuota']:
feed['limit'] = formatFileSize(int(feed['storageQuota']['limit']))
else:
feed['limit'] = 'UNLIMITED'
for setting in ['usage', 'usageInDrive', 'usageInDriveTrash']:
uval = int(feed['storageQuota'].get(setting, '0'))
feed[setting] = formatFileSize(uval)
if showUsageBytes:
feed[DRIVESETTINGS_USAGE_BYTES_FIELDS[setting]] = uval
if 'rootFolderId' in fieldsList:
feed['rootFolderId'] = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
if 'largestChangeId' in fieldsList:
feed['largestChangeId'] = callGAPI(drive.changes(), 'getStartPageToken',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='startPageToken')['startPageToken']
if not csvPF:
entityPerformActionNumItems([Ent.USER, user], 1, Ent.DRIVE_SETTINGS, i, count)
Ind.Increment()
for setting in DRIVESETTINGS_SCALAR_FIELDS:
_showSetting(setting)
if showUsageBytes:
for title, setting in DRIVESETTINGS_USAGE_BYTES_FIELDS.items():
if title in fieldsList and setting in feed:
printKeyValueList([setting, feed[setting]])
_showSetting('folderColorPalette')
_showFormats('exportFormats')
_showFormats('importFormats')
if 'maxImportSizes' in fieldsList and 'maxImportSizes' in fieldsList:
printKeyValueList(['maxImportSizes', None])
Ind.Increment()
for setting, value in iter(feed['maxImportSizes'].items()):
printKeyValueList([setting, formatFileSize(int(value))])
Ind.Decrement()
if 'driveThemes' in fieldsList and 'driveThemes' in feed:
printKeyValueList(['driveThemes', None])
_showSharedDriveThemeSettings(feed['driveThemes'])
Ind.Decrement()
else:
row = {'email': user}
for setting in DRIVESETTINGS_SCALAR_FIELDS:
_addSetting(row, setting)
if showUsageBytes:
for title, setting in DRIVESETTINGS_USAGE_BYTES_FIELDS.items():
if title in fieldsList and setting in feed:
row[setting] = feed[setting]
_addSetting(row, 'folderColorPalette')
_addFormats(row, 'exportFormats')
_addFormats(row, 'importFormats')
if 'maxImportSizes' in fieldsList and 'maxImportSizes' in fieldsList:
jcount = len(feed['maxImportSizes'])
row['maxImportSizes'] = jcount
j = 0
for setting, value in iter(feed['maxImportSizes'].items()):
row[f'maxImportSizes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{setting}'] = formatFileSize(int(value))
j += 1
if 'driveThemes' in fieldsList and 'driveThemes' in feed:
jcount = len(feed['driveThemes'])
row['driveThemes'] = jcount
j = 0
for setting in feed['driveThemes']:
row[f'driveThemes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id'] = setting['id']
row[f'driveThemes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}backgroundImageLink'] = setting['backgroundImageLink']
row[f'driveThemes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}colorRgb'] = setting['colorRgb']
j += 1
csvPF.WriteRowTitles(row)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('User Drive Settings')
# gam <UserTypeEntity> show shareddrivethemes
def showSharedDriveThemes(users):
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
try:
themes = callGAPIitems(drive.about(), 'get', 'driveThemes',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='driveThemes')
jcount = len(themes)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE_THEME, i, count)
_showSharedDriveThemeSettings(themes)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
def doShowSharedDriveThemes():
showSharedDriveThemes([_getAdminEmail()])
def initFilePathInfo(delimiter):
return {'ids': {}, 'allPaths': {}, 'localPaths': None, 'delimiter': delimiter}
def getFilePaths(drive, fileTree, initialResult, filePathInfo, addParentsToTree=False,
fullpath=False, showDepth=False, folderPathOnly=False):
def _getParentName(result):
if (result['mimeType'] == MIMETYPE_GA_FOLDER) and result.get('driveId') and (result['name'] == TEAM_DRIVE):
parentName = _getSharedDriveNameFromId(result['driveId'])
if parentName != TEAM_DRIVE:
return f'{SHARED_DRIVES}{filePathInfo["delimiter"]}{parentName}'
return result['name']
def _followParent(paths, parentId):
result = None
paths.setdefault(parentId, {})
if fileTree:
parentEntry = fileTree.get(parentId)
if not parentEntry:
if not addParentsToTree:
return
parentEntry = fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
if parentEntry['info']['name'] == parentEntry['info']['id'] and parentEntry['info']['id'] not in {ORPHANS, SHARED_WITHME, SHARED_DRIVES}:
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=parentId, fields='name,parents,mimeType,driveId', supportsAllDrives=True)
parentEntry['info']['name'] = _getParentName(result)
parentEntry['info']['parents'] = result.get('parents', [])
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
pass
filePathInfo['ids'][parentId] = parentEntry['info']['name']
parents = parentEntry['info'].get('parents', [])
else:
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=parentId, fields='name,parents,mimeType,driveId', supportsAllDrives=True)
filePathInfo['ids'][parentId] = _getParentName(result)
parents = result.get('parents', [])
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
return
for lparentId in parents:
if lparentId not in filePathInfo['allPaths']:
_followParent(paths[parentId], lparentId)
filePathInfo['allPaths'][lparentId] = paths[parentId][lparentId]
else:
paths[parentId][lparentId] = filePathInfo['allPaths'][lparentId]
def _makeFilePaths(localPaths, fplist, filePaths, name, maxDepth):
for k, v in iter(localPaths.items()):
fplist.append(filePathInfo['ids'].get(k, ''))
if not v:
fp = fplist[:]
if showDepth:
depth = len(fp)
if depth > maxDepth:
maxDepth = depth-1
fp.reverse()
if initialMimeType == MIMETYPE_GA_FOLDER or not folderPathOnly:
fp.append(name)
filePaths.append(filePathInfo['delimiter'].join(fp))
else:
maxDepth = _makeFilePaths(v, fplist, filePaths, name, maxDepth)
fplist.pop()
return maxDepth
filePaths = []
parents = initialResult.get('parents', [])
initialMimeType = initialResult['mimeType']
if parents:
filePathInfo['localPaths'] = {}
for parentId in parents:
if parentId not in filePathInfo['allPaths']:
_followParent(filePathInfo['allPaths'], parentId)
filePathInfo['localPaths'][parentId] = filePathInfo['allPaths'][parentId]
fplist = []
maxDepth = _makeFilePaths(filePathInfo['localPaths'], fplist, filePaths, initialResult['name'], -1)
else:
if (fullpath and initialMimeType == MIMETYPE_GA_FOLDER and
((initialResult['name'] == MY_DRIVE) or
(initialResult.get('driveId') and initialResult['name'].startswith(SHARED_DRIVES)))):
filePaths.append(initialResult['name'])
maxDepth = 0
return (_getEntityMimeType(initialResult), filePaths, maxDepth)
DRIVEFILE_ORDERBY_CHOICE_MAP = {
'createddate': 'createdTime',
'createdtime': 'createdTime',
'folder': 'folder',
'lastviewedbyme': 'viewedByMeTime',
'lastviewedbymedate': 'viewedByMeTime',
'lastviewedbymetime': 'viewedByMeTime',
'lastviewedbyuser': 'viewedByMeTime',
'modifiedbyme': 'modifiedByMeTime',
'modifiedbymedate': 'modifiedByMeTime',
'modifiedbymetime': 'modifiedByMeTime',
'modifiedbyuser': 'modifiedByMeTime',
'modifieddate': 'modifiedTime',
'modifiedtime': 'modifiedTime',
'name': 'name',
'namenatural': 'name_natural',
'quotabytesused': 'quotaBytesUsed',
'quotaused': 'quotaBytesUsed',
'recency': 'recency',
'sharedwithmedate': 'sharedWithMeTime',
'sharedwithmetime': 'sharedWithMeTime',
'starred': 'starred',
'title': 'name',
'titlenatural': 'name_natural',
'viewedbymedate': 'viewedByMeTime',
'viewedbymetime': 'viewedByMeTime',
}
def _mapDrive3TitlesToDrive2(titles, drive3TitlesMap):
for i, title in enumerate(titles):
if title in drive3TitlesMap:
titles[i] = drive3TitlesMap[title]
def _mapDriveUser(field):
if 'me' in field:
field['isAuthenticatedUser'] = field.pop('me')
if 'photoLink' in field:
field['picture'] = {'url': field.pop('photoLink')}
def _mapDrivePermissionNames(permission):
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
if 'displayName' in permission:
permission['name'] = permission.pop('displayName')
if 'expirationTime' in permission:
permission['expirationDate'] = formatLocalTime(permission.pop('expirationTime'))
if 'allowFileDiscovery' in permission:
permission['withLink'] = not permission.pop('allowFileDiscovery')
emailAddress = permission.get('emailAddress')
if emailAddress:
_, permission['domain'] = splitEmailAddress(emailAddress)
def _mapDriveParents(f_file, parentsSubFields):
if 'parents' in f_file:
parents = f_file.pop('parents')
if len(parents) == 1 and parents[0] == ORPHANS:
return
f_file['parents'] = []
for parentId in parents:
parent = {}
if parentsSubFields['id']:
parent['id'] = parentId
if parentsSubFields['isRoot']:
parent['isRoot'] = parentId == parentsSubFields['rootFolderId']
f_file['parents'].append(parent)
def _mapDriveProperties(f_file):
appProperties = f_file.pop('appProperties', [])
properties = f_file.pop('properties', [])
if appProperties:
f_file.setdefault('properties', [])
for key, value in sorted(iter(appProperties.items())):
f_file['properties'].append({'key': key, 'value': value, 'visibility': 'PRIVATE'})
if properties:
f_file.setdefault('properties', [])
for key, value in sorted(iter(properties.items())):
f_file['properties'].append({'key': key, 'value': value, 'visibility': 'PUBLIC'})
def _mapDriveFieldNames(f_file, user, parentsSubFields, mapToLabels):
if mapToLabels:
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_LABELS_MAP.items()):
if attrib in f_file:
f_file.setdefault('labels', {})
f_file['labels'][v2attrib] = f_file.pop(attrib)
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP.items()):
if attrib in f_file:
f_file[v2attrib] = f_file.pop(attrib)
capabilities = f_file.get('capabilities')
if capabilities:
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_CAPABILITIES_FIELDS_MAP.items()):
if attrib in capabilities:
f_file[v2attrib] = capabilities[attrib]
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_CAPABILITIES_NAMES_MAP.items()):
if attrib in capabilities:
capabilities[v2attrib] = capabilities.pop(attrib)
if 'spaces' in f_file:
f_file['appDataContents'] = 'appDataFolder' in f_file['spaces']
if 'lastModifyingUser' in f_file:
if 'displayName' in f_file['lastModifyingUser']:
f_file['lastModifyingUserName'] = f_file['lastModifyingUser']['displayName']
_mapDriveUser(f_file['lastModifyingUser'])
if 'owners' in f_file:
for owner in f_file['owners']:
_mapDriveUser(owner)
if 'displayName' in owner:
f_file.setdefault('ownerNames', [])
f_file['ownerNames'].append(owner['displayName'])
_mapDriveUser(f_file.get('sharingUser', {}))
_mapDriveParents(f_file, parentsSubFields)
_mapDriveProperties(f_file)
for permission in f_file.get('permissions', []):
if (permission.get('type') == 'user') and (permission.get('emailAddress', '').lower() == user) and ('role' in permission):
f_file['userPermission'] = {'id': 'me', 'role': permission['role'], 'type': permission['type']}
_mapDrivePermissionNames(permission)
def _mapDriveRevisionNames(revision):
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_REVISIONS_FIELDS_MAP.items()):
if attrib in revision:
revision[v2attrib] = revision.pop(attrib)
if 'lastModifyingUser' in revision:
if 'displayName' in revision['lastModifyingUser']:
revision['lastModifyingUserName'] = revision['lastModifyingUser']['displayName']
_mapDriveUser(revision['lastModifyingUser'])
DRIVEFILE_BASIC_PERMISSION_FIELDS = [
'displayName', 'id', 'emailAddress', 'domain', 'role', 'type',
'allowFileDiscovery', 'expirationTime', 'deleted', 'permissionDetails' #permissionDetails must be last
]
DRIVE_FIELDS_CHOICE_MAP = {
'alternatelink': 'webViewLink',
'appdatacontents': 'spaces',
'appproperties': 'appProperties',
'basicpermissions': ['permissions.displayName', 'permissions.id', 'permissions.emailAddress', 'permissions.domain',
'permissions.role', 'permissions.type', 'permissions.allowFileDiscovery',
'permissions.expirationTime', 'permissions.deleted'],
'cancomment': 'capabilities.canComment',
'canreadrevisions': 'capabilities.canReadRevisions',
'capabilities': 'capabilities',
'contenthints': 'contentHints',
'contentrestrictions': 'contentRestrictions',
'copyable': 'capabilities.canCopy',
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
'createddate': 'createdTime',
'createdtime': 'createdTime',
'description': 'description',
'driveid': 'driveId',
'drivename': 'driveId',
'editable': 'capabilities.canEdit',
'explicitlytrashed': 'explicitlyTrashed',
'exportlinks': 'exportLinks',
'fileextension': 'fileExtension',
'filesize': 'size',
'foldercolorrgb': 'folderColorRgb',
'fullfileextension': 'fullFileExtension',
'hasaugmentedpermissions': 'hasAugmentedPermissions',
'hasthumbnail': 'hasThumbnail',
'headrevisionid': 'headRevisionId',
'iconlink': 'iconLink',
'id': 'id',
'imagemediametadata': 'imageMediaMetadata',
'inheritedpermissionsdisabled': 'inheritedPermissionsDisabled',
'isappauthorized': 'isAppAuthorized',
'labelinfo': 'labelInfo',
'labels': ['modifiedByMe', 'copyRequiresWriterPermission', 'starred', 'trashed', 'viewedByMe'],
'lastmodifyinguser': 'lastModifyingUser',
'lastmodifyingusername': 'lastModifyingUser.displayName',
'lastviewedbyme': 'viewedByMeTime',
'lastviewedbymedate': 'viewedByMeTime',
'lastviewedbymetime': 'viewedByMeTime',
'lastviewedbyuser': 'viewedByMeTime',
'linksharemetadata': 'linkShareMetadata',
'md5': 'md5Checksum',
'md5checksum': 'md5Checksum',
'md5sum': 'md5Checksum',
'mime': 'mimeType',
'mimetype': 'mimeType',
'modifiedbymedate': 'modifiedByMeTime',
'modifiedbymetime': 'modifiedByMeTime',
'modifiedbyuser': 'modifiedByMeTime',
'modifieddate': 'modifiedTime',
'modifiedtime': 'modifiedTime',
'name': 'name',
'originalfilename': 'originalFilename',
'ownedbyme': 'ownedByMe',
'ownernames': 'owners.displayName',
'owners': 'owners',
'parents': 'parents',
'permissiondetails': 'permissions.permissionDetails',
'permissionids': 'permissionIds',
'permissions': 'permissions',
'properties': 'properties',
'quotabytesused': 'quotaBytesUsed',
'quotaused': 'quotaBytesUsed',
'resourcekey': 'resourceKey',
'shareable': 'capabilities.canShare',
'shared': 'shared',
'shareddriveid': 'driveId',
'shareddrivename': 'driveId',
'sharedwithmedate': 'sharedWithMeTime',
'sharedwithmetime': 'sharedWithMeTime',
'sharinguser': 'sharingUser',
'shortcutdetails': 'shortcutDetails',
'sha1checksum': 'sha1Checksum',
'sha256checksum': 'sha256Checksum',
'size': 'size',
'spaces': 'spaces',
'teamdriveid': 'driveId',
'teamdrivename': 'driveId',
'thumbnaillink': 'thumbnailLink',
'thumbnailversion': 'thumbnailVersion',
'title': 'name',
'trasheddate': 'trashedTime',
'trashedtime': 'trashedTime',
'trashinguser': 'trashingUser',
'userpermission': 'ownedByMe,capabilities.canEdit,capabilities.canComment',
'version': 'version',
'videomediametadata': 'videoMediaMetadata',
'viewedbymedate': 'viewedByMeTime',
'viewedbymetime': 'viewedByMeTime',
'viewerscancopycontent': 'copyRequiresWriterPermission',
'webcontentlink': 'webContentLink',
'webviewlink': 'webViewLink',
'writerscanshare': 'writersCanShare',
}
DRIVE_CAPABILITIES_SUBFIELDS_CHOICE_MAP = {
'canacceptownership': 'canAcceptOwnership',
'canaddchildren': 'canAddChildren',
'canaddfolderfromanotherdrive': 'canAddFolderFromAnotherDrive',
'canaddmydriveparent': 'canAddMyDriveParent',
'canchangecopyrequireswriterpermission': 'canChangeCopyRequiresWriterPermission',
'canchangecopyrequireswriterpermissionrestriction': 'canChangeCopyRequiresWriterPermissionRestriction',
'canchangedomainusersonlyrestriction': 'canChangeDomainUsersOnlyRestriction',
'canchangedrivebackground': 'canChangeDriveBackground',
'canchangedrivemembersonlyrestriction': 'canChangeDriveMembersOnlyRestriction',
'canchangesecurityupdateenabled': 'canChangeSecurityUpdateEnabled',
'canchangesharingfoldersrequiresorganizerpermissionrestriction': 'canChangeSharingFoldersRequiresOrganizerPermissionRestriction',
'canchangeviewerscancopycontent': 'canChangeViewersCanCopyContent',
'cancomment': 'canComment',
'cancopy': 'canCopy',
'candelete': 'canDelete',
'candeletechildren': 'canDeleteChildren',
'candeletedrive': 'canDeleteDrive',
'candisableinheritedpermissions': 'canDisableInheritedPermissions',
'candownload': 'canDownload',
'canedit': 'canEdit',
'canenableinheritedpermissions': 'canEnableInheritedPermissions',
'canlistchildren': 'canListChildren',
'canmanagemembers': 'canManageMembers',
'canmodifycontent': 'canModifyContent',
'canmodifycontentrestriction': 'canModifyContentRestriction',
'canmodifyeditorcontentrestriction': 'canModifyEditorContentRestriction',
'canmodifylabels': 'canModifyLabels',
'canmodifyownercontentrestriction': 'canModifyOwnerContentRestriction',
'canmovechildrenoutofdrive': 'canMoveChildrenOutOfDrive',
'canmovechildrenoutofteamdrive': 'canMoveChildrenOutOfDrive',
'canmovechildrenwithindrive': 'canMoveChildrenWithinDrive',
'canmovechildrenwithinteamdrive': 'canMoveChildrenWithinDrive',
'canmoveitemintodrive': 'canMoveItemIntoDrive',
'canmoveitemintoteamdrive': 'canMoveItemIntoDrive',
'canmoveitemoutofdrive': 'canMoveItemOutOfDrive',
'canmoveitemoutofteamdrive': 'canMoveItemOutOfDrive',
'canmoveitemwithindrive': 'canMoveItemWithinDrive',
'canmoveitemwithinteamdrive': 'canMoveItemWithinDrive',
'canmoveteamdriveitem': 'canMoveTeamDriveItem',
'canreaddrive': 'canReadDrive',
'canreadlabels': 'canReadLabels',
'canreadrevisions': 'canReadRevisions',
'canreadteamdrive': 'canReadDrive',
'canremovechildren': 'canRemoveChildren',
'canremovecontentrestriction': 'canRemoveContentRestriction',
'canremovemydriveparent': 'canRemoveMyDriveParent',
'canrename': 'canRename',
'canrenamedrive': 'canRenameDrive',
'canresetdriverestrictions': 'canResetDriveRestrictions',
'canshare': 'canShare',
'cantrash': 'canTrash',
'cantrashchildren': 'canTrashChildren',
'canuntrash': 'canUntrash',
}
DRIVE_CONTENT_RESTRICTIONS_SUBFIELDS_CHOICE_MAP = {
'ownerrestricted': 'ownerRestricted',
'readonly': 'readOnly',
'reason': 'reason',
'restrictinguser': 'restrictingUser',
'restrictiontime': 'restrictionTime',
'type': 'type',
}
DRIVE_LABELINFO_SUBFIELDS_CHOICE_MAP = {
'id': 'labels(id)',
'fields': 'labels(fields)',
'revisionid': 'labels(revisionId)',
}
DRIVE_OWNERS_SUBFIELDS_CHOICE_MAP = {
'displayname': 'displayName',
'emailaddress': 'emailAddress',
'isauthenticateduser': 'me',
'me': 'me',
'permissionid': 'permissionId',
'photolink': 'photoLink',
'picture': 'photoLink',
}
DRIVE_PARENTS_SUBFIELDS_CHOICE_MAP = {
'id': 'id',
'isroot': 'isRoot',
}
DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP = {
'additionalroles': 'role',
'allowfilediscovery': 'allowFileDiscovery',
'deleted': 'deleted',
'displayname': 'displayName',
'domain': 'domain',
'emailaddress': 'emailAddress',
'expirationdate': 'expirationTime',
'expirationtime': 'expirationTime',
'id': 'id',
'inheritedpermissionsdisabled': 'inheritedPermissionsDisabled',
'name': 'displayName',
'pendingowner': 'pendingOwner',
'permissiondetails': 'permissionDetails',
'photolink': 'photoLink',
'role': 'role',
'shareddrivepermissiondetails': 'permissionDetails',
'teamdrivepermissiondetails': 'permissionDetails',
'type': 'type',
'view': 'view',
'withlink': 'allowFileDiscovery',
}
DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP = {
'displayname': 'displayName',
'emailaddress': 'emailAddress',
'isauthenticateduser': 'me',
'me': 'me',
'name': 'displayName',
'permissionid': 'permissionId',
'photolink': 'photoLink',
'picture': 'photoLink',
}
DRIVE_SHORTCUTDETAILS_SUBFIELDS_CHOICE_MAP = {
'targetid': 'targetId',
'targetmimetype': 'targetMimeType',
'targetresourcekey': 'targetResourceKey',
}
DRIVE_SUBFIELDS_CHOICE_MAP = {
'capabilities': DRIVE_CAPABILITIES_SUBFIELDS_CHOICE_MAP,
'contentrestrictions': DRIVE_CONTENT_RESTRICTIONS_SUBFIELDS_CHOICE_MAP,
'labelinfo': DRIVE_LABELINFO_SUBFIELDS_CHOICE_MAP,
'labels': DRIVE_LABEL_CHOICE_MAP,
'lastmodifyinguser': DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP,
'owners': DRIVE_OWNERS_SUBFIELDS_CHOICE_MAP,
'parents': DRIVE_PARENTS_SUBFIELDS_CHOICE_MAP,
'permissions': DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP,
'sharinguser': DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP,
'trashinguser': DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP,
'shortcutdetails': DRIVE_SHORTCUTDETAILS_SUBFIELDS_CHOICE_MAP,
}
DRIVE_LIST_FIELDS = {'owners', 'parents', 'permissions', 'permissionIds', 'spaces'}
FILEINFO_FIELDS_TITLES = ['name', 'mimeType']
FILEPATH_FIELDS_TITLES = ['name', 'id', 'mimeType', 'ownedByMe', 'parents', 'sharedWithMeTime', 'driveId']
FILEPATH_FIELDS = ','.join(FILEPATH_FIELDS_TITLES)
def _getDriveTimeObjects():
timeObjects = ['createdTime', 'viewedByMeTime', 'modifiedByMeTime', 'modifiedTime', 'restrictionTime', 'sharedWithMeTime', 'trashedTime']
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDrive3TitlesToDrive2(timeObjects, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
return set(timeObjects)
def _getDriveFieldSubField(field, fieldsList, parentsSubFields):
field, subField = field.split('.', 1)
if field in DRIVE_SUBFIELDS_CHOICE_MAP:
if field == 'parents':
fieldsList.append(DRIVE_FIELDS_CHOICE_MAP[field])
parentsSubFields[DRIVE_SUBFIELDS_CHOICE_MAP[field][subField]] = True
elif subField in DRIVE_SUBFIELDS_CHOICE_MAP[field]:
if field != 'labels':
if not isinstance(DRIVE_SUBFIELDS_CHOICE_MAP[field][subField], list):
fieldsList.append(f'{DRIVE_FIELDS_CHOICE_MAP[field]}.{DRIVE_SUBFIELDS_CHOICE_MAP[field][subField]}')
else:
for subSubField in DRIVE_SUBFIELDS_CHOICE_MAP[field][subField]:
fieldsList.append(f'{DRIVE_FIELDS_CHOICE_MAP[field]}.{subSubField}')
else:
fieldsList.append(DRIVE_SUBFIELDS_CHOICE_MAP[field][subField])
else:
invalidChoiceExit(subField, list(DRIVE_SUBFIELDS_CHOICE_MAP[field]), True)
else:
invalidChoiceExit(field, list(DRIVE_SUBFIELDS_CHOICE_MAP), True)
class DriveFileFields():
def __init__(self):
self.showSharedDriveNames = False
self.allFields = False
self.OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
self.fieldsList = []
self.includeLabels = []
self.parentsSubFields = {'id': False, 'isRoot': False, 'rootFolderId': None}
def SetAllParentsSubFields(self):
self.parentsSubFields['id'] = self.parentsSubFields['isRoot'] = True
def ProcessArgument(self, myarg):
if myarg == 'allfields':
self.fieldsList = []
self.allFields = True
elif myarg in DRIVE_LABEL_CHOICE_MAP:
addFieldToFieldsList(myarg, DRIVE_LABEL_CHOICE_MAP, self.fieldsList)
elif myarg in DRIVE_FIELDS_CHOICE_MAP:
addFieldToFieldsList(myarg, DRIVE_FIELDS_CHOICE_MAP, self.fieldsList)
if myarg == 'parents':
self.SetAllParentsSubFields()
elif myarg in {'drivename', 'shareddrivename', 'teamdrivename'}:
self.showSharedDriveNames = True
elif myarg == 'fields':
for field in _getFieldsList():
if field in DRIVE_LABEL_CHOICE_MAP:
addFieldToFieldsList(field, DRIVE_LABEL_CHOICE_MAP, self.fieldsList)
elif field.find('.') == -1:
if field in DRIVE_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, DRIVE_FIELDS_CHOICE_MAP, self.fieldsList)
if field == 'parents':
self.SetAllParentsSubFields()
elif field in {'drivename', 'shareddrivename', 'teamdrivename'}:
self.showSharedDriveNames = True
else:
invalidChoiceExit(field, list(DRIVE_FIELDS_CHOICE_MAP)+list(DRIVE_LABEL_CHOICE_MAP), True)
else:
_getDriveFieldSubField(field, self.fieldsList, self.parentsSubFields)
elif myarg == 'includelabels':
labelIds = getEntityList(Cmd.OB_CLASSIFICATION_LABEL_ID, shlexSplit=True)
for labelId in labelIds:
self.includeLabels.append(normalizeDriveLabelID(labelId))
elif myarg.find('.') != -1:
_getDriveFieldSubField(myarg, self.fieldsList, self.parentsSubFields)
elif myarg == 'orderby':
self.OBY.GetChoice()
elif myarg in {'showdrivename', 'showshareddrivename', 'showteamdrivename'}:
self.showSharedDriveNames = True
else:
return False
return True
@property
def orderBy(self):
return self.OBY.orderBy
def _setSkipObjects(skipObjects, skipTitles, fieldsList):
for field in skipTitles:
if field != 'parents':
if field not in fieldsList:
skipObjects.add(field)
fieldsList.append(field)
else:
for xfield in fieldsList:
if xfield.startswith('parents'):
break
else:
skipObjects.add(field)
fieldsList.append('parents')
def _setGetPermissionsForSharedDrives(fieldsList):
getPermissionsForSharedDrives = False
permissionsFieldsList = []
permissionsFields = None
for field in fieldsList:
if field.startswith('permissions'):
getPermissionsForSharedDrives = True
if field.find('.') != -1:
field, subField = field.split('.', 1)
permissionsFieldsList.append(subField)
if getPermissionsForSharedDrives:
permissionsFields = getItemFieldsFromFieldsList('permissions', permissionsFieldsList, True)
return (getPermissionsForSharedDrives, permissionsFields)
SHOWLABELS_CHOICES = {'details', 'ids'}
def _formatFileDriveLabels(showLabels, labels, result, printMode, delimiter):
if showLabels == 'details':
result['labels'] = labels
else:
if printMode:
result['labels'] = len(labels)
result['labelsIds'] = delimiter.join([label['id'] for label in labels])
# gam <UserTypeEntity> info drivefile <DriveFileEntity>
# [returnidonly]
# [filepath|fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)] [formatjson]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [showdrivename] [showshareddrivepermissions]
# [(showlabels details|ids)|(includelabels <DriveLabelIDList>)]
# [showparentsidsaslist] [followshortcuts [<Boolean>]]
# [stripcrsfromname] [formatjson]
# gam <UserTypeEntity> show fileinfo <DriveFileEntity>
# [returnidonly]
# [filepath|fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)] [formatjson]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [showdrivename] [showshareddrivepermissions]
# [(showlabels details|ids)|(includelabels <DriveLabelIDList>)]
# [showparentsidsaslist] [followshortcuts [<Boolean>]]
# [stripcrsfromname] [formatjson]
def showFileInfo(users):
def _setSelectionFields():
_setSkipObjects(skipObjects, FILEINFO_FIELDS_TITLES, DFF.fieldsList)
if filepath:
_setSkipObjects(skipObjects, FILEPATH_FIELDS_TITLES, DFF.fieldsList)
if getPermissionsForSharedDrives or DFF.showSharedDriveNames:
_setSkipObjects(skipObjects, ['driveId'], DFF.fieldsList)
if followShortcuts:
_setSkipObjects(skipObjects, ['mimeType', 'shortcutDetails'], DFF.fieldsList)
getPermissionsForSharedDrives = filepath = fullpath = folderPathOnly = followShortcuts = \
returnIdOnly = showParentsIdsAsList = showNoParents = stripCRsFromName = False
pathDelimiter = '/'
showLabels = None
simpleLists = []
skipObjects = set()
fileIdEntity = getDriveFileEntity()
DFF = DriveFileFields()
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'filepath':
filepath = True
elif myarg == 'fullpath':
filepath = fullpath = True
elif myarg == 'folderpathonly':
folderPathOnly = getBoolean()
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'showparentsidsaslist':
showParentsIdsAsList = True
simpleLists.append('parentsIds')
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
elif myarg == 'showlabels':
showLabels = getChoice(SHOWLABELS_CHOICES)
elif myarg == 'showshareddrivepermissions':
getPermissionsForSharedDrives = True
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
elif myarg == 'returnidonly':
returnIdOnly = True
elif myarg == 'followshortcuts':
followShortcuts = getBoolean()
elif DFF.ProcessArgument(myarg):
pass
else:
FJQC.GetFormatJSON(myarg)
if DFF.fieldsList:
if not getPermissionsForSharedDrives:
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
_setSelectionFields()
if followShortcuts:
DFF.fieldsList.extend(['mimeType', 'shortcutDetails'])
fields = getFieldsFromFieldsList(DFF.fieldsList)
showNoParents = 'parents' in DFF.fieldsList
else:
fields = '*'
DFF.SetAllParentsSubFields()
skipObjects = skipObjects.union(DEFAULT_SKIP_OBJECTS)
showNoParents = True
includeLabels = ','.join(DFF.includeLabels)
pathFields = FILEPATH_FIELDS
timeObjects = _getDriveTimeObjects()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if returnIdOnly:
GC.Values[GC.SHOW_GETTINGS] = False
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][FJQC.formatJSON or returnIdOnly],
orderBy=DFF.orderBy)
if jcount == 0:
continue
if returnIdOnly:
for fileId in fileIdEntity['list']:
writeStdout(f'{fileId}\n')
continue
if not showParentsIdsAsList and DFF.parentsSubFields['isRoot']:
try:
DFF.parentsSubFields['rootFolderId'] = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
if filepath:
filePathInfo = initFilePathInfo(pathDelimiter)
if fullpath:
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), None, [], True, user, i, count)
if not status:
continue
else:
fileTree = None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS+[GAPI.INVALID],
fileId=fileId, includeLabels=includeLabels, fields=fields, supportsAllDrives=True)
if followShortcuts and result['mimeType'] == MIMETYPE_GA_SHORTCUT:
fileId = result['shortcutDetails']['targetId']
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS+[GAPI.INVALID],
fileId=fileId, includeLabels=includeLabels, fields=fields, supportsAllDrives=True)
if stripCRsFromName:
result['name'] = _stripControlCharsFromName(result['name'])
driveId = result.get('driveId')
if driveId:
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
result['name'] = _getSharedDriveNameFromId(driveId)
if DFF.showSharedDriveNames:
result['driveName'] = _getSharedDriveNameFromId(driveId)
if showNoParents:
result.setdefault('parents', [])
if getPermissionsForSharedDrives and driveId and 'permissions' not in result:
try:
result['permissions'] = callGAPIpages(drive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fileId=fileId, fields=permissionsFields, supportsAllDrives=True)
for permission in result['permissions']:
permission.pop('teamDrivePermissionDetails', None)
except (GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions) as e:
if fields != '*':
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
continue
if showLabels is not None:
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
fileId=fileId)
_formatFileDriveLabels(showLabels, labels, result, False, ' ')
if not FJQC.formatJSON:
printEntity([_getEntityMimeType(result), f'{result["name"]} ({fileId})'], j, jcount)
Ind.Increment()
if filepath:
if fullpath:
extendFileTree(fileTree, [result], None, False)
extendFileTreeParents(drive, fileTree, pathFields)
if not FJQC.formatJSON:
_, paths, _ = getFilePaths(drive, fileTree, result, filePathInfo, addParentsToTree=True,
fullpath=fullpath, folderPathOnly=folderPathOnly)
kcount = len(paths)
printKeyValueList(['paths', kcount])
Ind.Increment()
for path in sorted(paths):
printKeyValueList(['path', path])
Ind.Decrement()
else:
addFilePathsToInfo(drive, fileTree, result, filePathInfo,
addParentsToTree=True, folderPathOnly=folderPathOnly)
if fullpath:
# Save simple parents list as mappings turn it into a list of dicts
fpparents = result['parents'][:]
if showParentsIdsAsList and 'parents' in result:
result['parentsIds'] = result.pop('parents')
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDriveFieldNames(result, user, DFF.parentsSubFields, True)
else:
_mapDriveParents(result, DFF.parentsSubFields)
_mapDriveProperties(result)
for permission in result.get('permissions', []):
_mapDrivePermissionNames(permission)
if not FJQC.formatJSON:
showJSON(None, result, skipObjects=skipObjects, timeObjects=timeObjects, simpleLists=simpleLists,
dictObjectsKey={'owners': 'displayName', 'fields': 'id', 'labels': 'id', 'user': 'emailAddress', 'parents': 'id',
'permissions': ['name', 'displayName'][GC.Values[GC.DRIVE_V3_NATIVE_NAMES]]})
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(result, skipObjects=skipObjects, timeObjects=timeObjects), ensure_ascii=False, sort_keys=True))
if fullpath:
# Restore simple parents list
fileTree[fileId]['info']['parents'] = fpparents[:]
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.invalid) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
def getRevisionsEntity():
revisionsEntity = {'list': [], 'dict': None, 'count': None, 'time': None, 'range': None}
startEndTime = StartEndTime()
entitySelector = getEntitySelector()
if entitySelector:
entityList = getEntitySelection(entitySelector, False)
if isinstance(entityList, dict):
revisionsEntity['dict'] = entityList
else:
revisionsEntity['list'] = entityList
else:
myarg = getString(Cmd.OB_DRIVE_FILE_REVISION_ID, checkBlank=True)
mycmd = myarg.lower()
if mycmd == 'id':
revisionsEntity['list'] = getStringReturnInList(Cmd.OB_DRIVE_FILE_REVISION_ID)
elif mycmd[:3] == 'id:':
revisionsEntity['list'] = [myarg[3:]]
elif mycmd == 'ids':
revisionsEntity['list'] = getString(Cmd.OB_DRIVE_FILE_REVISION_ID).replace(',', ' ').split()
elif mycmd[:4] == 'ids:':
revisionsEntity['list'] = myarg[4:].replace(',', ' ').split()
elif mycmd in {'first', 'last', 'allexceptfirst', 'allexceptlast'}:
revisionsEntity['count'] = (mycmd, getInteger(minVal=1))
elif mycmd in {'before', 'after'}:
dateTime, _, _ = getTimeOrDeltaFromNow(True)
revisionsEntity['time'] = (mycmd, dateTime)
elif mycmd == 'range':
startEndTime.Get(mycmd)
revisionsEntity['range'] = (mycmd, startEndTime.startDateTime, startEndTime.endDateTime)
else:
revisionsEntity['list'] = [myarg]
return revisionsEntity
def _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revisionsEntity):
if revisionsEntity['list']:
return revisionsEntity['list']
if revisionsEntity['dict']:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
return revisionsEntity['dict'][fileId]
return revisionsEntity['dict'][origUser][fileId]
try:
results = callGAPIpages(drive.revisions(), 'list', 'revisions',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISIONS_NOT_SUPPORTED],
fileId=fileId, fields='nextPageToken,revisions(id,modifiedTime)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.revisionsNotSupported) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
return []
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return []
numRevisions = len(results)
if numRevisions == 0:
return []
if revisionsEntity['count']:
countType = revisionsEntity['count'][0]
count = revisionsEntity['count'][1]
revisionIds = [revision['id'] for revision in results]
if countType == 'first':
if count >= numRevisions:
return revisionIds
return revisionIds[:count]
if countType == 'last':
if count >= numRevisions:
return revisionIds
return revisionIds[-count:]
if countType == 'allexceptfirst':
if count >= numRevisions:
return []
return revisionIds[count:]
# count: allexceptlast
if count >= numRevisions:
return []
return revisionIds[:-count]
revisionIds = []
if revisionsEntity['time']:
dateTime = revisionsEntity['time'][1]
count = 0
if revisionsEntity['time'][0] == 'before':
for revision in results:
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
if modifiedDateTime >= dateTime:
break
revisionIds.append(revision['id'])
count += 1
if count >= numRevisions:
return revisionIds[:-1]
return revisionIds
# time: after
for revision in results:
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
if modifiedDateTime >= dateTime:
revisionIds.append(revision['id'])
count += 1
if count >= numRevisions:
return revisionIds[1:]
return revisionIds
# range
startDateTime = revisionsEntity['range'][1]
endDateTime = revisionsEntity['range'][2]
count = 0
for revision in results:
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
if modifiedDateTime >= startDateTime:
if modifiedDateTime >= endDateTime:
break
revisionIds.append(revision['id'])
count += 1
if count >= numRevisions:
return revisionIds[1:]
return revisionIds
# gam <UserTypeEntity> delete filerevisions <DriveFileEntity> select <DriveFileRevisionIdEntity> [previewdelete]
# [showtitles] [doit] [max_to_delete <Number>]
def deleteFileRevisions(users):
fileIdEntity = getDriveFileEntity()
revisionsEntity = None
previewDelete = showTitles = doIt = False
maxToProcess = 1
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'select':
revisionsEntity = getRevisionsEntity()
elif myarg == 'previewdelete':
previewDelete = True
elif myarg == 'showtitles':
showTitles = True
elif myarg == 'doit':
doIt = True
elif myarg in {'maxtodelete', 'maxtoprocess'}:
maxToProcess = getInteger(minVal=0)
else:
unknownArgumentExit()
if not revisionsEntity:
missingArgumentExit('select <DriveFileRevisionIdEntity>')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
if jcount == 0:
continue
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles:
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId)
revisionIds = _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revisionsEntity)
kcount = len(revisionIds)
if kcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.DRIVE_FILE_REVISION)), j, jcount)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if not previewDelete:
if maxToProcess and kcount > maxToProcess:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(kcount, Act.ToPerform(), maxToProcess), j, jcount)
continue
if not doIt:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
continue
entityPerformActionNumItems([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, j, jcount)
else:
entityPerformActionNumItemsModifier([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, Msg.PREVIEW_ONLY, j, jcount)
Ind.Increment()
k = 0
for revisionId in revisionIds:
k += 1
if not previewDelete:
try:
callGAPI(drive.revisions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISION_NOT_FOUND, GAPI.REVISION_DELETION_NOT_SUPPORTED,
GAPI.CANNOT_DELETE_ONLY_REVISION, GAPI.REVISIONS_NOT_SUPPORTED],
fileId=fileId, revisionId=revisionId)
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.revisionDeletionNotSupported, GAPI.cannotDeleteOnlyRevision, GAPI.revisionsNotSupported) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
except GAPI.revisionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
else:
entityActionNotPerformedWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], Msg.PREVIEW_ONLY, k, kcount)
Ind.Decrement()
REVISIONS_FIELDS_CHOICE_MAP = {
'keepforever': 'keepForever',
'published': 'published',
'publishauto': 'publishAuto',
'publishedoutsidedomain': 'publishedOutsideDomain'
}
# gam <UserTypeEntity> update filerevisions <DriveFileEntity> select <DriveFileRevisionIdEntity> [previewupdate]
# [published [<Boolean>]] [publishauto [<Boolean>]] [publishedoutsidedomain [<Boolean>]]
# [keepforever [<Boolean>]}
# [showtitles] [doit] [max_to_update <Number>]
def updateFileRevisions(users):
fileIdEntity = getDriveFileEntity()
revisionsEntity = None
previewUpdate = showTitles = doIt = False
maxToProcess = 1
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'select':
revisionsEntity = getRevisionsEntity()
elif myarg in REVISIONS_FIELDS_CHOICE_MAP:
body[REVISIONS_FIELDS_CHOICE_MAP[myarg]] = getBoolean()
elif myarg == 'previewupdate':
previewUpdate = True
elif myarg == 'showtitles':
showTitles = True
elif myarg == 'doit':
doIt = True
elif myarg in {'maxtoupdate', 'maxtoprocess'}:
maxToProcess = getInteger(minVal=0)
else:
unknownArgumentExit()
if not revisionsEntity:
missingArgumentExit('select <DriveFileRevisionIdEntity>')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
if jcount == 0:
continue
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles:
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId)
revisionIds = _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revisionsEntity)
kcount = len(revisionIds)
if kcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.DRIVE_FILE_REVISION)), j, jcount)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if not previewUpdate:
if maxToProcess and kcount > maxToProcess:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(kcount, Act.ToPerform(), maxToProcess), j, jcount)
continue
if not doIt:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
continue
entityPerformActionNumItems([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, j, jcount)
else:
entityPerformActionNumItemsModifier([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, Msg.PREVIEW_ONLY, j, jcount)
Ind.Increment()
k = 0
for revisionId in revisionIds:
k += 1
if not previewUpdate:
try:
callGAPI(drive.revisions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISION_NOT_FOUND, GAPI.REVISIONS_NOT_SUPPORTED],
fileId=fileId, revisionId=revisionId, body=body)
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.revisionsNotSupported) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
except GAPI.revisionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
else:
entityActionNotPerformedWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], Msg.PREVIEW_ONLY, k, kcount)
Ind.Decrement()
def _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDelete):
numRevisions = len(results)
if numRevisions == 0:
return results
if revisionsEntity['count']:
countType = revisionsEntity['count'][0]
count = revisionsEntity['count'][1]
if countType == 'first':
if count >= numRevisions:
if previewDelete:
return results[:-1]
return results
return results[:count]
if countType == 'last':
if count >= numRevisions:
if previewDelete:
return results[1:]
return results
return results[-count:]
if countType == 'allexceptfirst':
if count >= numRevisions:
return []
return results[count:]
# count: allexceptlast
if count >= numRevisions:
return []
return results[:-count]
if revisionsEntity['time']:
dateTime = revisionsEntity['time'][1]
count = 0
if revisionsEntity['time'][0] == 'before':
for revision in results:
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
if modifiedDateTime >= dateTime:
break
count += 1
if count >= numRevisions:
if previewDelete:
return results[:-1]
return results
return results[:count]
# time: after
for revision in results:
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
if modifiedDateTime >= dateTime:
break
count += 1
if count == 0:
if previewDelete:
return results[1:]
return results
if count >= numRevisions:
return []
return results[count:]
if revisionsEntity['range']:
startDateTime = revisionsEntity['range'][1]
endDateTime = revisionsEntity['range'][2]
count = 0
selectedResults = []
for revision in results:
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
if modifiedDateTime >= startDateTime:
if modifiedDateTime >= endDateTime:
break
selectedResults.append(revision)
count += 1
if count >= numRevisions:
if previewDelete:
return selectedResults[1:]
return selectedResults
# revisionsIds
selectedResults = []
if revisionsEntity['dict']:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
revisionIds = revisionsEntity['dict'][fileId]
else:
revisionIds = revisionsEntity['dict'][origUser][fileId]
else:
revisionIds = revisionsEntity['list']
return [revision for revision in results if revision['id'] in revisionIds]
FILEREVISIONS_FIELDS_CHOICE_MAP = {
'filesize': 'size',
'id': 'id',
'keepforever': 'keepForever',
'lastmodifyinguser': 'lastModifyingUser',
'lastmodifyingusername': 'lastModifyingUser.displayName',
'md5checksum': 'md5Checksum',
'mimetype': 'mimeType',
'modifieddate': 'modifiedTime',
'modifiedtime': 'modifiedTime',
'originalfilename': 'originalFilename',
'pinned': 'keepForever',
'published': 'published',
'publishauto': 'publishAuto',
'publishedoutsidedomain': 'publishedOutsideDomain',
'size': 'size',
}
def _getFileRevisionsTimeObjects():
timeObjects = ['modifiedTime']
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDrive3TitlesToDrive2(timeObjects, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
return set(timeObjects)
def _showRevision(revision, timeObjects, i=0, count=0):
printEntity([Ent.DRIVE_FILE_REVISION, revision['id']], i, count)
Ind.Increment()
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDriveRevisionNames(revision)
showJSON(None, revision, ['id'], timeObjects)
Ind.Decrement()
DRIVE_REVISIONS_INDEXED_TITLES = ['revisions']
# gam <UserTypeEntity> show filerevisions <DriveFileEntity>
# [select <DriveFileRevisionIDEntity>]
# [previewdelete] [showtitles]
# [<RevisionsFieldName>*|(fields <RevisionsFieldNameList>)]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [stripcrsfromname]
# gam <UserTypeEntity> print filerevisions <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [select <DriveFileRevisionIDEntity>]
# [previewdelete] [showtitles] [oneitemperrow]
# [<RevisionsFieldName>*|(fields <RevisionsFieldNameList>)]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [stripcrsfromname]
def printShowFileRevisions(users):
csvPF = CSVPrintFile(['Owner', 'id']) if Act.csvFormat() else None
fieldsList = []
fileIdEntity = getDriveFileEntity()
revisionsEntity = None
oneItemPerRow = previewDelete = showTitles = stripCRsFromName = False
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'select':
revisionsEntity = getRevisionsEntity()
elif csvPF and myarg == 'oneitemperrow':
oneItemPerRow = True
if csvPF:
csvPF.AddTitles('revision.id')
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'previewdelete':
previewDelete = True
elif myarg == 'showtitles':
showTitles = True
if csvPF:
csvPF.AddTitles(fileNameTitle)
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
elif getFieldsList(myarg, FILEREVISIONS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass
else:
unknownArgumentExit()
if fieldsList:
fields = getItemFieldsFromFieldsList('revisions', fieldsList)
else:
fields = '*'
timeObjects = _getFileRevisionsTimeObjects()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][csvPF is not None],
orderBy=OBY.orderBy)
if jcount == 0:
continue
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles:
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId, not csvPF)
if stripCRsFromName:
fileName = _stripControlCharsFromName(fileName)
try:
results = callGAPIpages(drive.revisions(), 'list', 'revisions',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISIONS_NOT_SUPPORTED],
fileId=fileId, fields=fields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.revisionsNotSupported) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if revisionsEntity:
results = _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDelete)
if not csvPF:
kcount = len(results)
entityPerformActionNumItems([entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, j, jcount)
Ind.Increment()
k = 0
for revision in results:
k += 1
_showRevision(revision, timeObjects, k, kcount)
Ind.Decrement()
elif results:
if oneItemPerRow:
for revision in results:
row = {'Owner': user, 'id': fileId}
if showTitles:
row[fileNameTitle] = fileName
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDriveRevisionNames(revision)
csvPF.WriteRowTitles(flattenJSON({'revision': revision}, flattened=row, timeObjects=timeObjects))
else:
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
for revision in results:
_mapDriveRevisionNames(revision)
if showTitles:
csvPF.WriteRowTitles(flattenJSON({'revisions': results}, flattened={'Owner': user, 'id': fileId, fileNameTitle: fileName}, timeObjects=timeObjects))
else:
csvPF.WriteRowTitles(flattenJSON({'revisions': results}, flattened={'Owner': user, 'id': fileId}, timeObjects=timeObjects))
Ind.Decrement()
if csvPF:
if oneItemPerRow:
csvPF.SetSortTitles(['Owner', 'id', fileNameTitle, 'revision.id'])
else:
csvPF.SetSortTitles(['Owner', 'id', 'revisions'])
csvPF.SetIndexedTitles(DRIVE_REVISIONS_INDEXED_TITLES)
csvPF.writeCSVfile('Drive File Revisions')
def _stripMeInOwners(query):
if not query:
return query
query = query.replace(ME_IN_OWNERS_AND, '')
query = query.replace(AND_ME_IN_OWNERS, '')
return query.replace(ME_IN_OWNERS, '').strip()
def _stripNotMeInOwners(query):
if not query:
return query
query = query.replace(NOT_ME_IN_OWNERS_AND, '')
query = query.replace(AND_NOT_ME_IN_OWNERS, '')
return query.replace(NOT_ME_IN_OWNERS, '').strip()
def _updateAnyOwnerQuery(query):
query = _stripNotMeInOwners(query)
return _stripMeInOwners(query)
def initFileTree(drive, shareddrive, DLP, shareddriveFields, showParent, user, i, count):
fileTree = {
ORPHANS: {'info': {'id': ORPHANS, 'name': ORPHANS, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': True, 'parents': []},
'noParents': True, 'children': []},
SHARED_WITHME: {'info': {'id': SHARED_WITHME, 'name': SHARED_WITHME, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': False, 'parents': []},
'noParents': True, 'children': []},
SHARED_DRIVES: {'info': {'id': SHARED_DRIVES, 'name': SHARED_DRIVES, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': False, 'parents': [], 'driveId': SHARED_DRIVES},
'noParents': True, 'children': []},
}
try:
if not shareddrive:
fileId = ROOT
f_file = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=fileId, fields=FILEPATH_FIELDS)
f_file['parents'] = []
fileTree[f_file['id']] = {'info': f_file, 'noParents': True, 'children': []}
elif 'driveId' in shareddrive:
fileId = shareddrive['driveId']
f_file = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FILE_NOT_FOUND],
fileId=fileId, supportsAllDrives=True, fields=FILEPATH_FIELDS)
f_file['parents'] = []
fileTree[f_file['id']] = {'info': f_file, 'noParents': True, 'children': []}
name = callGAPI(drive.drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
driveId=fileId, fields='name')['name']
fileTree[f_file['id']]['info']['name'] = f'{SHARED_DRIVES}/{name}'
else:
fileId = None
if DLP and (DLP.getSharedDriveNames or DLP.checkLocation in {LOCATION_ALL_DRIVES, LOCATION_ONLY_SHARED_DRIVES}):
tdrives = callGAPIpages(drive.drives(), 'list', 'drives',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID, GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE],
fields='nextPageToken,drives(id,name)', pageSize=100)
for tdrive in tdrives:
fileId = tdrive['id']
if fileId not in fileTree:
fileTree[fileId] = {'info': {'id': fileId, 'name': f'{SHARED_DRIVES}/{tdrive["name"]}', 'mimeType': MIMETYPE_GA_FOLDER},
'noParents': True, 'children': []}
for field in shareddriveFields:
if field in tdrive:
fileTree[fileId]['info'][field] = tdrive[field]
if showParent:
fileTree[fileId]['info']['driveId'] = fileId
fileTree[SHARED_DRIVES]['children'].append(fileId)
except (GAPI.notFound, GAPI.fileNotFound, GAPI.invalid, GAPI.noListTeamDrivesAdministratorPrivilege) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileId], str(e), i, count)
return (None, False)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return (None, False)
return (fileTree, True)
def extendFileTree(fileTree, feed, DLP, stripCRsFromName):
for f_file in feed:
if DLP and (not DLP.CheckOnlySharedDrives(f_file) or not DLP.CheckExcludeTrashed(f_file)):
continue
if stripCRsFromName:
f_file['name'] = _stripControlCharsFromName(f_file['name'])
fileId = f_file['id']
if not f_file.get('parents', []):
if not f_file.get('driveId'):
if f_file['mimeType'] == MIMETYPE_GA_FOLDER and f_file['name'] == MY_DRIVE:
f_file['parents'] = []
else:
f_file['parents'] = [ORPHANS] if f_file.get('ownedByMe', False) and 'sharedWithMeTime' not in f_file else [SHARED_WITHME]
else:
f_file['parents'] = [SHARED_DRIVES] if 'sharedWithMeTime' not in f_file else [SHARED_WITHME]
if fileId not in fileTree:
fileTree[fileId] = {'info': f_file, 'children': []}
else:
fileTree[fileId]['info'] = f_file
for parentId in f_file['parents']:
if parentId not in fileTree:
fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
fileTree[parentId]['children'].append(fileId)
def extendFileTreeParents(drive, fileTree, fields):
def _followParent(fileId):
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=fields, supportsAllDrives=True)
if not result.get('parents', []):
if not result.get('driveId'):
result['parents'] = [ORPHANS] if result.get('ownedByMe', False) and 'sharedWithMeTime' not in result else [SHARED_WITHME]
else:
if result['name'] == TEAM_DRIVE:
result['name'] = _getSharedDriveNameFromId(result['driveId'])
result['parents'] = [SHARED_DRIVES] if 'sharedWithMeTime' not in result else [SHARED_WITHME]
fileTree[fileId]['info'] = result
fileTree[fileId]['info']['noDisplay'] = True
for parentId in result['parents']:
if parentId not in fileTree:
fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
_followParent(parentId)
fileTree[parentId]['children'].append(fileId)
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
fileTree[fileId] = {'info': {'id': fileId, 'name': fileId, 'mimeType': MIMETYPE_GA_FOLDER, 'parents': [], 'noDisplay': True},
'children': []}
for fileId in list(fileTree):
f_file = fileTree[fileId]
if 'parents' not in f_file['info'] and not f_file.get('noParents', False):
f_file['info']['noDisplay'] = True
_followParent(fileId)
def buildFileTree(feed, drive):
fileTree = {
ORPHANS: {'info': {'id': ORPHANS, 'name': ORPHANS, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': True}, 'children': []},
}
try:
f_file = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields=FILEPATH_FIELDS)
fileTree[f_file['id']] = {'info': f_file, 'children': []}
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
GAPI.notFound, GAPI.invalid, GAPI.noListTeamDrivesAdministratorPrivilege):
pass
for f_file in feed:
if 'parents' not in f_file:
f_file['parents'] = []
fileId = f_file['id']
if fileId not in fileTree:
fileTree[fileId] = {'info': f_file, 'children': []}
else:
fileTree[fileId]['info'] = f_file
parents = f_file['parents']
if not parents:
parents = [ORPHANS]
for parentId in parents:
if parentId not in fileTree:
fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
fileTree[parentId]['children'].append(fileId)
return fileTree
def addFilePathsToRow(drive, fileTree, fileEntryInfo, filePathInfo, csvPF, row,
fullpath=False, showDepth=False, folderPathOnly=False):
_, paths, maxDepth = getFilePaths(drive, fileTree, fileEntryInfo, filePathInfo,
fullpath=fullpath, showDepth=showDepth, folderPathOnly=folderPathOnly)
kcount = len(paths)
if showDepth:
row['depth'] = maxDepth
row['paths'] = kcount
k = 0
for path in sorted(paths):
key = f'path{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'
csvPF.AddTitles(key)
if GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] and (path.find('\n') >= 0 or path.find('\r') >= 0):
row[key] = escapeCRsNLs(path)
else:
row[key] = path
k += 1
def addFilePathsToInfo(drive, fileTree, fileEntryInfo, filePathInfo, addParentsToTree=False, folderPathOnly=False):
_, paths, _ = getFilePaths(drive, fileTree, fileEntryInfo, filePathInfo, addParentsToTree=addParentsToTree,
showDepth=False, folderPathOnly=folderPathOnly)
fileEntryInfo['paths'] = []
for path in sorted(paths):
if GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] and (path.find('\n') >= 0 or path.find('\r') >= 0):
fileEntryInfo['paths'].append(escapeCRsNLs(path))
else:
fileEntryInfo['paths'].append(path)
def _validateACLOwnerType(location, body):
if body.get('role', '') == 'owner' and body['type'] != 'user':
Cmd.SetLocation(location)
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format(f'role {body["role"]}', body['type']))
def _validateACLAttributes(myarg, location, body, field, validTypes):
if field in body and body['type'] not in validTypes:
Cmd.SetLocation(location-1)
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format(myarg, body['type']))
def _validatePermissionOwnerType(location, body):
if 'role' in body:
badTypes = body['type']-{'user'}
if 'owner' in body['role'] and badTypes:
Cmd.SetLocation(location)
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format('role owner', ','.join(badTypes)))
def _validatePermissionAttributes(myarg, location, body, field, validTypes):
if field in body:
badTypes = body['type']-validTypes
if badTypes:
Cmd.SetLocation(location-1)
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format(myarg, ','.join(badTypes)))
DRIVEFILE_ACL_ROLES_MAP = {
'commenter': 'commenter',
'contentmanager': 'fileOrganizer',
'contributor': 'writer',
'editor': 'writer',
'fileorganizer': 'fileOrganizer',
'fileorganiser': 'fileOrganizer',
'manager': 'organizer',
'organizer': 'organizer',
'organiser': 'organizer',
'owner': 'owner',
'read': 'reader',
'reader': 'reader',
'viewer': 'reader',
'writer': 'writer',
}
DRIVEFILE_ACL_PERMISSION_TYPES = ['anyone', 'domain', 'group', 'user'] # anyone must be first element
DRIVEFILE_ACL_PERMISSION_DETAILS_TYPES = ['file', 'member']
class PermissionMatch():
_PERMISSION_MATCH_ACTION_MAP = {'process': True, 'skip': False}
_PERMISSION_MATCH_MODE_MAP = {'or': True, 'and': False}
def __init__(self):
self.permissionMatches = []
self.permissionMatchKeep = self.permissionMatchOr = True
self.permissionFields = set()
self.clearDefaultMatch = False
def GetMatch(self):
startEndTime = StartEndTime('expirationstart', 'expirationend')
roleLocation = withLinkLocation = expirationStartLocation = expirationEndLocation = deletedLocation = None
requiredMatch = not checkArgumentPresent('not')
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'type', 'nottype'}:
body[myarg] = set()
body[myarg].add(getChoice(DRIVEFILE_ACL_PERMISSION_TYPES))
self.permissionFields.add('type')
elif myarg in {'typelist', 'nottypelist'}:
arg = 'type' if myarg == 'typelist' else 'nottype'
body[arg] = set()
for ptype in getString(Cmd.OB_PERMISSION_TYPE_LIST).lower().replace(',', ' ').split():
if ptype in DRIVEFILE_ACL_PERMISSION_TYPES:
body[arg].add(ptype)
else:
invalidChoiceExit(ptype, DRIVEFILE_ACL_PERMISSION_TYPES, True)
self.permissionFields.add('type')
elif myarg in {'role', 'notrole'}:
roleLocation = Cmd.Location()
body[myarg] = set()
body[myarg].add(getChoice(DRIVEFILE_ACL_ROLES_MAP, mapChoice=True))
self.permissionFields.add('role')
elif myarg in {'rolelist', 'notrolelist'}:
arg = 'role' if myarg == 'rolelist' else 'notrole'
body[arg] = set()
roleLocation = Cmd.Location()
for prole in getString(Cmd.OB_PERMISSION_ROLE_LIST).lower().replace(',', ' ').split():
if prole in DRIVEFILE_ACL_ROLES_MAP:
body[arg].add(DRIVEFILE_ACL_ROLES_MAP[prole])
else:
invalidChoiceExit(prole, DRIVEFILE_ACL_ROLES_MAP, True)
self.permissionFields.add('role')
elif myarg == 'emailaddress':
body['emailAddress'] = getREPattern(re.IGNORECASE)
self.permissionFields.add('emailAddress')
elif myarg == 'emailaddresslist':
body[myarg] = set(getString(Cmd.OB_EMAIL_ADDRESS_LIST).replace(',', ' ').lower().split())
self.permissionFields.add('emailAddress')
elif myarg == 'permissionidlist':
body[myarg] = set(getString(Cmd.OB_PERMISSION_ID_LIST).replace(',', ' ').split())
self.permissionFields.add('id')
elif myarg in {'domain', 'notdomain'}:
body[myarg] = getREPattern(re.IGNORECASE)
self.permissionFields.add('domain')
self.permissionFields.add('emailAddress')
elif myarg in {'domainlist', 'notdomainlist'}:
body[myarg] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').lower().split())
self.permissionFields.add('domain')
self.permissionFields.add('emailAddress')
elif myarg == 'withlink':
withLinkLocation = Cmd.Location()
body['allowFileDiscovery'] = not getBoolean()
self.permissionFields.add('allowFileDiscovery')
elif myarg in {'allowfilediscovery', 'discoverable'}:
withLinkLocation = Cmd.Location()
body['allowFileDiscovery'] = getBoolean()
self.permissionFields.add('allowFileDiscovery')
elif myarg in {'name', 'displayname'}:
body['displayName'] = getREPattern(re.IGNORECASE)
self.permissionFields.add('displayName')
elif myarg == 'expirationstart':
expirationStartLocation = Cmd.Location()
startEndTime.Get(myarg)
body[myarg] = startEndTime.startDateTime
self.permissionFields.add('expirationTime')
elif myarg == 'expirationend':
expirationEndLocation = Cmd.Location()
startEndTime.Get(myarg)
body[myarg] = startEndTime.endDateTime
self.permissionFields.add('expirationTime')
elif myarg == 'deleted':
deletedLocation = Cmd.Location()
body[myarg] = getBoolean()
self.permissionFields.add('deleted')
elif myarg == 'inherited':
body[myarg] = getBoolean()
self.permissionFields.add('permissionDetails')
elif myarg == 'permtype':
body['permissionType'] = getChoice(DRIVEFILE_ACL_PERMISSION_DETAILS_TYPES)
self.permissionFields.add('permissionDetails')
elif myarg in {'em', 'endmatch'}:
break
else:
unknownArgumentExit()
if self.clearDefaultMatch:
self.permissionMatches = []
self.clearDefaultMatch = False
if body:
if 'type' in body:
_validatePermissionOwnerType(roleLocation, body)
_validatePermissionAttributes('allowfilediscovery/withlink', withLinkLocation, body, 'allowFileDiscovery', {'anyone', 'domain'})
_validatePermissionAttributes('expirationstart', expirationStartLocation, body, 'expirationstart', {'user', 'group'})
_validatePermissionAttributes('expirationend', expirationEndLocation, body, 'expirationend', {'user', 'group'})
_validatePermissionAttributes('deleted', deletedLocation, body, 'deleted', {'user', 'group'})
self.permissionMatches.append((requiredMatch, body))
def SetDefaultMatch(self, requiredMatch, body):
self.clearDefaultMatch = True
self.permissionMatches.append((requiredMatch, body))
def ProcessArgument(self, myarg):
if myarg in {'pm', 'permissionmatch'}:
self.GetMatch()
elif myarg in {'pma', 'permissionmatchaction'}:
self.permissionMatchKeep = getChoice(PermissionMatch._PERMISSION_MATCH_ACTION_MAP, mapChoice=True)
elif myarg in {'pmm', 'permissionmatchmode'}:
self.permissionMatchOr = getChoice(PermissionMatch._PERMISSION_MATCH_MODE_MAP, mapChoice=True)
else:
return False
return True
@staticmethod
def CheckPermissionMatch(permission, permissionMatch):
match = False
for field, value in iter(permissionMatch[1].items()):
if field in {'type', 'role'}:
if permission.get(field, '') not in value:
break
elif field == 'nottype':
if permission.get('type', '') in value:
break
elif field == 'notrole':
if permission.get('role', '') in value:
break
elif field in {'allowFileDiscovery', 'deleted'}:
if value != permission.get(field, False):
break
elif field == 'inherited':
if 'permissionDetails' in permission:
if value != permission['permissionDetails'][0].get(field, False):
break
else:
break
elif field == 'permissionType':
if 'permissionDetails' in permission:
if value != permission['permissionDetails'][0].get(field, ''):
break
else:
break
elif field in {'expirationstart', 'expirationend'}:
if 'expirationTime' in permission:
expirationDateTime, _ = iso8601.parse_date(permission['expirationTime'])
if field == 'expirationstart':
if expirationDateTime < value:
break
else:
if expirationDateTime > value:
break
else:
break
elif field == 'emailaddresslist':
emailAddress = permission.get('emailAddress')
if emailAddress:
if emailAddress.lower() not in value:
break
else:
break
elif field == 'permissionidlist':
permissionId = permission.get('id')
if permissionId:
if permissionId not in value:
break
else:
break
elif field not in {'domain', 'notdomain', 'domainlist', 'notdomainlist'}:
if not value.match(permission.get(field, '')):
break
else:
if 'domain' in permission:
domain = permission['domain'].lower()
elif 'emailAddress' in permission and permission['emailAddress']:
_, domain = splitEmailAddress(permission['emailAddress'].lower())
else:
break
if ((field == 'domain' and not value.match(domain)) or
(field == 'notdomain' and value.match(domain)) or
(field == 'domainlist' and domain not in value) or
(field == 'notdomainlist' and domain in value)):
break
else:
match = True
return match == permissionMatch[0]
def GetMatchingPermissions(self, permissions):
if not self.permissionMatches:
return permissions
matchingPermissions = []
for permission in permissions:
requiredMatches = 1 if self.permissionMatchOr else len(self.permissionMatches)
for permissionMatch in self.permissionMatches:
if self.CheckPermissionMatch(permission, permissionMatch):
requiredMatches -= 1
if requiredMatches == 0:
if self.permissionMatchKeep:
matchingPermissions.append(permission)
break
else:
if not self.permissionMatchKeep:
matchingPermissions.append(permission)
return matchingPermissions
def CheckPermissionMatches(self, permissions):
if not self.permissionMatches:
return True
requiredMatches = 1 if self.permissionMatchOr else len(self.permissionMatches)
for permission in permissions:
for permissionMatch in self.permissionMatches:
if self.CheckPermissionMatch(permission, permissionMatch):
requiredMatches -= 1
if requiredMatches == 0:
return self.permissionMatchKeep
return not self.permissionMatchKeep
def noFileSelectFileIdEntity(fileIdEntity):
return (not fileIdEntity
or (not fileIdEntity['dict']
and not fileIdEntity['query']
and not fileIdEntity['shareddrivefilequery']
and not fileIdEntity['list']))
SHOW_OWNED_BY_CHOICE_MAP = {'any': None, 'me': True, 'others': False}
class DriveListParameters():
def __init__(self, myargOptions):
self.PM = PermissionMatch()
self.myargOptions = myargOptions
self.checkLocation = None
self.excludeTrashed = False
self.filenameMatchPattern = None
self.getSharedDriveNames = False
self.kwargs = {}
self.fileIdEntity = {}
self.locationFileIds = []
self.locationSet = False
self.maxItems = 0
self.mimeTypeCheck = MimeTypeCheck()
self.maximumFileSize = None
self.minimumFileSize = None
self.onlySharedDrives = False
self.queryTimes = {}
self.showOwnedBy = True
self.showSharedByMe = None
SHOW_SHARED_BY_ME_CHOICE_MAP = {'any': None, 'true': True, 'false': False}
def ProcessArgument(self, myarg, fileIdEntity):
if myarg == 'maxfiles':
self.maxItems = getInteger(minVal=0)
elif myarg == 'maximumfilesize':
self.maximumFileSize = getInteger(minVal=0)
elif myarg == 'minimumfilesize':
self.minimumFileSize = getInteger(minVal=0)
elif myarg == 'showsharedbyme':
self.showSharedByMe = getChoice(self.SHOW_SHARED_BY_ME_CHOICE_MAP, mapChoice=True)
elif myarg == 'filenamematchpattern':
self.filenameMatchPattern = getREPattern(re.IGNORECASE)
elif self.PM.ProcessArgument(myarg):
pass
elif myarg == 'anyowner':
self.showOwnedBy = None
self.UpdateAnyOwnerQuery()
elif myarg == 'showownedby':
self.showOwnedBy = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True)
self.UpdateQueryWithShowOwnedBy()
elif myarg == 'showmimetype':
self.mimeTypeCheck.Get()
if self.myargOptions['mimeTypeInQuery']:
self.AppendToQuery(self.mimeTypeCheck.AddMimeTypeToQuery(self.fileIdEntity.get('query', '')))
elif myarg == 'excludetrashed':
self.excludeTrashed = True
elif myarg.startswith('querytime'):
self.queryTimes[myarg] = getTimeOrDeltaFromNow()
elif noFileSelectFileIdEntity(fileIdEntity):
if self.myargOptions['allowQuery'] and myarg == 'query':
self.AppendToQuery(getString(Cmd.OB_QUERY))
elif self.myargOptions['allowQuery'] and myarg.startswith('query:'):
self.AppendToQuery(Cmd.Previous().strip()[6:])
elif self.myargOptions['allowQuery'] and myarg == 'fullquery':
self.SetQuery(getString(Cmd.OB_QUERY, minLen=0))
elif self.myargOptions['allowQuery'] and myarg in QUERY_SHORTCUTS_MAP:
self.UpdateAnyOwnerQuery()
self.AppendToQuery(QUERY_SHORTCUTS_MAP[myarg])
elif self.myargOptions['allowChoose'] and myarg == 'choose':
myarg = checkGetArgument()
if myarg in DRIVE_BY_NAME_CHOICE_MAP:
self.SetQuery(DRIVE_BY_NAME_CHOICE_MAP[myarg].format(getEscapedDriveFileName()))
elif myarg in LOCATION_CHOICE_MAP:
self.locationSet = True
self.SetLocation(LOCATION_CHOICE_MAP[myarg])
elif myarg.find(':') > 0:
kw, value = myarg.split(':', 1)
if kw in DRIVE_BY_NAME_CHOICE_MAP:
self.SetQuery(DRIVE_BY_NAME_CHOICE_MAP[kw].format(escapeDriveFileName(value)))
else:
invalidChoiceExit(myarg, list(DRIVE_BY_NAME_CHOICE_MAP), True)
else:
invalidChoiceExit(myarg, list(DRIVE_BY_NAME_CHOICE_MAP)+list(LOCATION_CHOICE_MAP), True)
elif self.myargOptions['allowCorpora'] and myarg == 'corpora':
corpora = getChoice(CORPORA_CHOICE_MAP)
self.kwargs['corpora'] = CORPORA_CHOICE_MAP[corpora]
self.kwargs['includeItemsFromAllDrives'] = self.kwargs['supportsAllDrives'] = True
self.onlySharedDrives = corpora in {'onlyshareddrives', 'onlyteamdrives'}
self.getSharedDriveNames = True
self.UpdateAnyOwnerQuery()
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['alldrives' if not self.onlySharedDrives else 'onlyshareddrives'])
else:
return False
else:
if (myarg == 'query' or
myarg.startswith('query:') or
myarg == 'fullquery' or
myarg in QUERY_SHORTCUTS_MAP or
myarg in DRIVE_BY_NAME_CHOICE_MAP):
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('select', myarg))
return False
return True
def InitDriveIdEntity(self):
if not self.fileIdEntity:
self.fileIdEntity = initDriveFileEntity()
self.fileIdEntity['query'] = ME_IN_OWNERS
def AppendToQuery(self, query):
self.InitDriveIdEntity()
if self.fileIdEntity['query']:
self.fileIdEntity['query'] += ' and ('+query+')'
else:
self.fileIdEntity['query'] = query
def SetQuery(self, query):
self.InitDriveIdEntity()
self.fileIdEntity['query'] = query
def UpdateAnyOwnerQuery(self):
self.InitDriveIdEntity()
self.fileIdEntity['query'] = _updateAnyOwnerQuery(self.fileIdEntity['query'])
def UpdateQueryWithShowOwnedBy(self):
self.InitDriveIdEntity()
if self.showOwnedBy is None:
self.fileIdEntity['query'] = _updateAnyOwnerQuery(self.fileIdEntity['query'])
elif not self.showOwnedBy:
if self.fileIdEntity['query'].find(NOT_ME_IN_OWNERS) >= 0:
pass
else:
self.fileIdEntity['query'] = _stripMeInOwners(self.fileIdEntity['query'])
if self.fileIdEntity['query']:
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS_AND+self.fileIdEntity['query']
else:
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS
else:
if self.fileIdEntity['query'].find(ME_IN_OWNERS) >= 0:
pass
else:
self.fileIdEntity['query'] = _stripNotMeInOwners(self.fileIdEntity['query'])
if self.fileIdEntity['query']:
self.fileIdEntity['query'] = ME_IN_OWNERS_AND+self.fileIdEntity['query']
else:
self.fileIdEntity['query'] = ME_IN_OWNERS
def CheckShowOwnedBy(self, fileInfo):
return self.showOwnedBy is None or fileInfo.get('ownedByMe', self.showOwnedBy) == self.showOwnedBy
def CheckShowSharedByMe(self, fileInfo):
return self.showSharedByMe is None or (fileInfo.get('shared', self.showSharedByMe) == self.showSharedByMe and fileInfo.get('ownedByMe', False))
def SetLocationFileIDsList(self, location):
self.locationFileIds = location['fileids']
self.checkLocation = location['location']
self.InitDriveIdEntity()
cleanFileIDsList(self.fileIdEntity, self.locationFileIds)
def SetLocation(self, location):
self.SetLocationFileIDsList(location)
if self.checkLocation in {LOCATION_ALL_DRIVES, LOCATION_ONLY_SHARED_DRIVES}:
self.kwargs = {'corpora': 'allDrives', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
self.showOwnedBy = None
self.fileIdEntity['query'] = ''
self.onlySharedDrives = self.checkLocation == LOCATION_ONLY_SHARED_DRIVES
elif location['setShowOwnedBy']:
self.showOwnedBy = location['owner']
if self.showOwnedBy is None:
self.fileIdEntity['query'] = ''
elif self.showOwnedBy:
self.fileIdEntity['query'] = ME_IN_OWNERS
else:
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS
else:
if self.fileIdEntity['query'].find(NOT_ME_IN_OWNERS) >= 0:
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS
elif self.fileIdEntity['query'].find(ME_IN_OWNERS) >= 0:
self.fileIdEntity['query'] = ME_IN_OWNERS
def SetShowOwnedBy(self, showOwnedBy):
self.showOwnedBy = showOwnedBy
def GetFileIdEntity(self):
if not self.fileIdEntity:
self.fileIdEntity = initDriveFileEntity()
self.fileIdEntity['query'] = ME_IN_OWNERS
return self.fileIdEntity
def AddMimeTypeToQuery(self):
if not self.fileIdEntity:
self.fileIdEntity = initDriveFileEntity()
if self.mimeTypeCheck.mimeTypes or self.mimeTypeCheck.category:
self.fileIdEntity['query'] = self.mimeTypeCheck.AddMimeTypeToQuery(self.fileIdEntity['query'])
def Finalize(self, fileIdEntity):
self.fileIdEntity.setdefault('query', '')
if self.excludeTrashed:
self.AppendToQuery('trashed=false')
if self.fileIdEntity['query']:
for queryTimeName, queryTimeValue in iter(self.queryTimes.items()):
self.fileIdEntity['query'] = self.fileIdEntity['query'].replace(f'#{queryTimeName}#', queryTimeValue)
self.fileIdEntity['query'] = _mapDrive2QueryToDrive3(self.fileIdEntity['query'])
if not fileIdEntity.get('shareddrive'):
if self.fileIdEntity['query']:
if self.fileIdEntity['query'].find(NOT_ME_IN_OWNERS) >= 0 or (not self.showOwnedBy and self.showOwnedBy is not None):
if not self.locationFileIds:
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyothers'])
self.SetShowOwnedBy(False)
elif self.fileIdEntity['query'].find(ME_IN_OWNERS) >= 0 or self.showOwnedBy:
if not self.locationFileIds:
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyme'])
self.SetShowOwnedBy(True)
else:
if not self.locationFileIds:
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyany'])
self.SetShowOwnedBy(None)
else:
if not self.locationFileIds:
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyany'])
self.SetShowOwnedBy(None)
else:
self.UpdateAnyOwnerQuery()
# if not self.locationFileIds:
# self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['onlyshareddrives'])
self.SetShowOwnedBy(None)
def GetLocationFileIdsFromTree(self, fileTree, fileIdEntity):
cleanList = []
for fileId in self.locationFileIds:
if fileId == ROOT or (fileId in fileTree and fileTree[fileId]['children']):
cleanList.append(fileId)
cleanFileIDsList(fileIdEntity, cleanList)
def CheckExcludeTrashed(self, fileInfo):
return not self.excludeTrashed or not fileInfo.get('trashed', False)
def CheckFilenameMatch(self, fileInfo):
return not self.filenameMatchPattern or self.filenameMatchPattern.match(fileInfo['name'])
def CheckMimeType(self, fileInfo):
return self.mimeTypeCheck.Check(fileInfo)
def CheckFileSize(self, fileInfo, sizeField):
size = int(fileInfo.get(sizeField, '0'))
if self.minimumFileSize is not None and size < self.minimumFileSize:
return False
if self.maximumFileSize is not None and size > self.maximumFileSize:
return False
return True
def CheckOnlySharedDrives(self, fileInfo):
return not self.onlySharedDrives or fileInfo.get('driveId') is not None
def CheckFilePermissionMatches(self, fileInfo):
return self.PM.CheckPermissionMatches(fileInfo.get('permissions', []))
def GetFileMatchingPermission(self, fileInfo):
return self.PM.GetMatchingPermissions(fileInfo.get('permissions', []))
def _getGettingEntity(user, fileIdEntity):
driveId = fileIdEntity.get('shareddrive', {}).get('driveId', None)
if not driveId:
return user
return f"{user} on {Ent.Singular(Ent.SHAREDDRIVE_ID)}: {driveId}"
OWNED_BY_ME_FIELDS_TITLES = ['ownedByMe']
FILELIST_FIELDS_TITLES = ['id', 'name', 'mimeType', 'parents']
DRIVE_INDEXED_TITLES = ['parents', 'path', 'permissions']
CHECK_LOCATION_FIELDS_TITLES = ['driveId', 'id', 'mimeType', 'ownedByMe', 'parents', 'sharedWithMeTime', 'shared']
FILECOUNT_SUMMARY_NONE = 0
FILECOUNT_SUMMARY_ONLY = -1
FILECOUNT_SUMMARY_PLUS = 1
FILECOUNT_SUMMARY_CHOICE_MAP = {
'none': FILECOUNT_SUMMARY_NONE,
'only': FILECOUNT_SUMMARY_ONLY,
'plus': FILECOUNT_SUMMARY_PLUS
}
FILECOUNT_SUMMARY_USER = 'Summary'
SIZE_FIELD_CHOICE_MAP = {
'size': 'size',
'quotabytesused': 'quotaBytesUsed'
}
# gam <UserTypeEntity> print filelist [todrive <ToDriveAttribute>*]
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [continueoninvalidquery [<Boolean>]]
# [choose <DriveFileNameEntity>|<DriveFileEntityShortcut>]
# [corpora <CorporaAttribute>]
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>]
# [(norecursion [<Boolean>])|(depth <Number>)] [showparent]]
# [anyowner|(showownedby any|me|others)]
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>] [mimetypeinquery [<Boolean>]]
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
# [filenamematchpattern <REMatchPattern>]
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>] [pmfilter] [oneitemperrow]
# [excludetrashed]
# [maxfiles <Integer>] [nodataheaders <String>]
# [countsonly [summary none|only|plus] [summaryuser <String>]
# [showsource] [showsize] [showmimetypesize]]
# [countsrowfilter]
# [filepath|fullpath [folderpathonly [<Boolean>]] [pathdelimiter <Character>] [addpathstojson] [showdepth]] [buildtree]
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)]
# [showdrivename] [showshareddrivepermissions]
# (showlabels details|ids)|(includelabels <DriveLabelIDList>)]
# [showparentsidsaslist] [showpermissionslast]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
# [stripcrsfromname]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
def printFileList(users):
def _setSelectionFields():
if fileIdEntity or filepath:
_setSkipObjects(skipObjects, FILELIST_FIELDS_TITLES, DFF.fieldsList)
if DLP.showOwnedBy is not None:
_setSkipObjects(skipObjects, OWNED_BY_ME_FIELDS_TITLES, DFF.fieldsList)
if DLP.showSharedByMe is not None or DLP.checkLocation is not None:
_setSkipObjects(skipObjects, CHECK_LOCATION_FIELDS_TITLES, DFF.fieldsList)
if DLP.mimeTypeCheck.mimeTypes:
_setSkipObjects(skipObjects, ['mimeType'], DFF.fieldsList)
if countsOnly:
skipObjects.discard('mimeType')
if 'mimeType' not in DFF.fieldsList:
DFF.fieldsList.append('mimeType')
skipObjects.discard(sizeField)
if showSize and sizeField not in DFF.fieldsList:
DFF.fieldsList.append(sizeField)
if (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
_setSkipObjects(skipObjects, [sizeField], DFF.fieldsList)
if DLP.filenameMatchPattern or showParent:
_setSkipObjects(skipObjects, ['name'], DFF.fieldsList)
if DLP.excludeTrashed:
_setSkipObjects(skipObjects, ['trashed'], DFF.fieldsList)
if DLP.PM.permissionMatches:
for field in DFF.fieldsList:
if field.startswith('permissions'):
break
else:
skipObjects.add('permissions')
if 'permissions' not in DFF.fieldsList:
for field in DLP.PM.permissionFields:
permfield = 'permissions.'+field
if permfield not in DFF.fieldsList:
DFF.fieldsList.append(permfield)
if DLP.onlySharedDrives or getPermissionsForSharedDrives or DFF.showSharedDriveNames:
_setSkipObjects(skipObjects, ['driveId'], DFF.fieldsList)
def _printFileInfoRow(baserow, fileInfo):
row = baserow.copy()
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
simpleLists=simpleLists, delimiter=delimiter))
else:
row['JSON'] = json.dumps(cleanJSON(fileInfo, skipObjects=skipObjects, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowTitlesJSONNoFilter(row)
def _printFileInfo(drive, user, f_file, cleanFileName):
nonlocal getSharedDriveACLsCount, getSharedDriveACLsCountMsg
driveId = f_file.get('driveId')
checkSharedDrivePermissions = getPermissionsForSharedDrives and driveId and 'permissions' not in f_file
if (f_file.get('noDisplay', False) or
not DLP.CheckShowOwnedBy(f_file) or
not DLP.CheckShowSharedByMe(f_file) or
not DLP.CheckExcludeTrashed(f_file) or
not DLP.CheckMimeType(f_file) or
not DLP.CheckFileSize(f_file, sizeField) or
not DLP.CheckFilenameMatch(f_file) or
(not checkSharedDrivePermissions and not DLP.CheckFilePermissionMatches(f_file)) or
(DLP.onlySharedDrives and not driveId)):
return
if checkSharedDrivePermissions:
if not incrementalPrint:
getSharedDriveACLsCount += 1
if getSharedDriveACLsCount % 100 == 0:
writeStderr(f'{Msg.GOT} {getSharedDriveACLsCount} {getSharedDriveACLsCountMsg}')
try:
f_file['permissions'] = callGAPIpages(drive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fileId=f_file['id'], fields=permissionsFields, supportsAllDrives=True)
if not DLP.CheckFilePermissionMatches(f_file):
return
for permission in f_file['permissions']:
permission.pop('teamDrivePermissionDetails', None)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid,
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
pass
row = {'Owner': user}
fileInfo = f_file.copy()
if cleanFileName:
fileInfo['name'] = _stripControlCharsFromName(fileInfo['name'])
if not pmselect and 'permissions' in fileInfo:
fileInfo['permissions'] = DLP.GetFileMatchingPermission(fileInfo)
if DFF.showSharedDriveNames and driveId:
fileInfo['driveName'] = _getSharedDriveNameFromId(driveId)
if filepath:
if not FJQC.formatJSON or not addPathsToJSON:
addFilePathsToRow(drive, fileTree, fileInfo, filePathInfo, csvPF, row,
fullpath=fullpath, showDepth=showDepth, folderPathOnly=folderPathOnly)
else:
addFilePathsToInfo(drive, fileTree, fileInfo, filePathInfo, folderPathOnly=folderPathOnly)
if showParentsIdsAsList and 'parents' in fileInfo:
fileInfo['parentsIds'] = fileInfo.pop('parents')
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDriveFieldNames(fileInfo, user, DFF.parentsSubFields, False)
else:
_mapDriveParents(fileInfo, DFF.parentsSubFields)
_mapDriveProperties(fileInfo)
for permission in fileInfo.get('permissions', []):
_mapDrivePermissionNames(permission)
if showParentsIdsAsList and 'parentsIds' in fileInfo:
fileInfo['parents'] = len(fileInfo['parentsIds'])
if addCSVData:
fileInfo.update(addCSVData)
if not countsOnly:
if not oneItemPerRow or 'permissions' not in fileInfo:
if not FJQC.formatJSON:
csvPF.WriteRowTitles(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
simpleLists=simpleLists, delimiter=delimiter))
else:
if 'id' in fileInfo:
row['id'] = fileInfo['id']
if fileNameTitle in fileInfo:
row[fileNameTitle] = fileInfo[fileNameTitle]
if 'owners' in fileInfo:
flattenJSON({'owners': fileInfo['owners']}, flattened=row, skipObjects=skipObjects)
row['JSON'] = json.dumps(cleanJSON(fileInfo, skipObjects=skipObjects, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowTitlesJSONNoFilter(row)
else:
baserow = row.copy()
if 'id' in fileInfo:
baserow['id'] = fileInfo['id']
if fileNameTitle in fileInfo:
baserow[fileNameTitle] = fileInfo[fileNameTitle]
if 'owners' in fileInfo:
flattenJSON({'owners': fileInfo['owners']}, flattened=baserow, skipObjects=skipObjects)
for permission in fileInfo.pop('permissions'):
fileInfo['permission'] = permission
pdetails = fileInfo['permission'].pop('permissionDetails', [])
if not pdetails:
_printFileInfoRow(baserow, fileInfo)
else:
for pdetail in pdetails:
fileInfo['permission']['permissionDetails'] = pdetail
_printFileInfoRow(baserow, fileInfo)
else:
if not countsRowFilter:
csvPFco.UpdateMimeTypeCounts(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
simpleLists=simpleLists, delimiter=delimiter), mimeTypeInfo, sizeField)
else:
mimeTypeInfo.setdefault(fileInfo['mimeType'], {'count': 0, 'size': 0})
mimeTypeInfo[fileInfo['mimeType']]['count'] += 1
mimeTypeInfo[fileInfo['mimeType']]['size'] += int(fileInfo.get(sizeField, '0'))
def _printChildDriveFolderContents(drive, fileEntry, user, i, count, depth):
parentFileEntry = fileTree.get(fileEntry['id'])
if parentFileEntry and 'children' in parentFileEntry:
for childFileId in parentFileEntry['children']:
childEntry = fileTree.get(childFileId)
if childEntry:
if not DLP.CheckExcludeTrashed(childEntry['info']):
continue
if childFileId not in filesPrinted:
filesPrinted.add(childFileId)
# Don't show My Drive/Shared Drive unless asked when parent is 'SharedDrives'
if showParent or parentFileEntry['info']['id'] != SHARED_DRIVES:
_printFileInfo(drive, user, childEntry['info'].copy(), False)
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
_printChildDriveFolderContents(drive, childEntry['info'], user, i, count, depth+1)
return
q = WITH_PARENTS.format(fileEntry['id'])
if selectSubQuery:
q += ' and ('+selectSubQuery+')'
if depth == 0:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count, query=q)
pageMessage = getPageMessageForWhom()
else:
setGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, query=q)
pageMessage = getPageMessageForWhom(clearLastGotMsgLen=False)
try:
children = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=pageMessage, noFinalize=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
q=q, orderBy=DFF.orderBy, includeLabels=includeLabels, fields=pagesFields,
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], includeItemsFromAllDrives=True, supportsAllDrives=True)
for childEntryInfo in children:
childFileId = childEntryInfo['id']
if showLabels is not None:
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
fileId=childFileId)
_formatFileDriveLabels(showLabels, labels, childEntryInfo, True, delimiter)
if filepath:
fileTree.setdefault(childFileId, {'info': childEntryInfo})
if childFileId not in filesPrinted:
filesPrinted.add(childFileId)
_printFileInfo(drive, user, childEntryInfo.copy(), stripCRsFromName)
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
_printChildDriveFolderContents(drive, childEntryInfo, user, i, count, depth+1)
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(selectSubQuery), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
def writeMimeTypeCountsRow(user, sourceId, sourceName, mimeTypeInfo):
countTotal = sizeTotal = 0
for mtinfo in iter(mimeTypeInfo.values()):
countTotal += mtinfo['count']
sizeTotal += mtinfo['size']
row = {'Owner': user, 'Total': countTotal}
if showSource:
row['Source'] = sourceId
row['Name'] = sourceName
if showSize:
row['Size'] = sizeTotal
if addCSVData:
row.update(addCSVData)
for mimeType, mtinfo in sorted(iter(mimeTypeInfo.items())):
row[f'{mimeType}'] = mtinfo['count']
if showMimeTypeSize:
row[f'{mimeType}:Size'] = mtinfo['size']
if not countsRowFilter:
csvPFco.WriteRowTitlesNoFilter(row)
else:
csvPFco.WriteRowTitles(row)
csvPF = CSVPrintFile('Owner', indexedTitles=DRIVE_INDEXED_TITLES)
csvPFco = None
FJQC = FormatJSONQuoteChar(csvPF)
addPathsToJSON = continueOnInvalidQuery = countsRowFilter = buildTree = countsOnly = filepath = fullpath = folderPathOnly = \
getPermissionsForSharedDrives = mimeTypeInQuery = noRecursion = oneItemPerRow = stripCRsFromName = \
showParentsIdsAsList = showDepth = showParent = showSize = showMimeTypeSize = showSource = False
sizeField = 'quotaBytesUsed'
pathDelimiter = '/'
pmselect = True
showLabels = None
rootFolderId = ROOT
rootFolderName = MY_DRIVE
maxdepth = -1
nodataFields = []
simpleLists = ['permissionIds', 'spaces']
skipObjects = set()
fileIdEntity = {}
getSharedDriveACLsCountMsg = selectSubQuery = ''
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
DLP = DriveListParameters({'allowChoose': True, 'allowCorpora': True, 'allowQuery': True, 'mimeTypeInQuery': False})
DFF = DriveFileFields()
summary = FILECOUNT_SUMMARY_NONE
summaryUser = FILECOUNT_SUMMARY_USER
summaryMimeTypeInfo = {}
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif DLP.ProcessArgument(myarg, fileIdEntity):
pass
elif DFF.ProcessArgument(myarg):
pass
elif myarg == 'select':
if fileIdEntity:
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
if not DLP.fileIdEntity:
fileIdEntity = getDriveFileEntity(DLP=DLP)
else:
fileIdEntity = getDriveFileEntitySharedDriveOnly()
elif myarg == 'selectsubquery':
selectSubQuery = getString(Cmd.OB_QUERY, minLen=0)
elif myarg == 'norecursion':
noRecursion = getBoolean()
elif myarg == 'mimetypeinquery':
mimeTypeInQuery = getBoolean()
elif myarg == 'depth':
maxdepth = getInteger(minVal=-1)
elif myarg == 'showdepth':
showDepth = True
elif myarg == 'showparent':
showParent = getBoolean()
elif myarg == 'nodataheaders':
nodataFields = getString(Cmd.OB_FIELD_NAME_LIST).replace('_', '').replace(',', ' ').split()
elif myarg in {'filepath', 'fullpath'}:
filepath = True
fullpath = myarg == 'fullpath'
elif myarg == 'folderpathonly':
folderPathOnly = getBoolean()
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'addpathstojson':
addPathsToJSON = True
elif myarg == 'buildtree':
buildTree = True
elif myarg == 'countsrowfilter':
countsRowFilter = True
elif myarg == 'countsonly':
countsOnly = True
csvPFco = CSVPrintFile(['Owner', 'Total'], 'sortall')
csvPFco.SetZeroBlankMimeTypeCounts(True)
elif myarg == 'summary':
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
elif myarg == 'summaryuser':
summaryUser = getString(Cmd.OB_STRING)
elif myarg == 'showsource':
showSource = True
if countsOnly:
if not showSize:
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total'])
else:
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total', 'Size'])
csvPFco.SetSortAllTitles()
elif myarg in {'showsize', 'showmimetypesize'}:
showSize = True
if countsOnly:
if not showSource:
csvPFco.SetTitles(['Owner', 'Total', 'Size'])
else:
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total', 'Size'])
csvPFco.SetSortAllTitles()
if myarg == 'showmimetypesize':
showMimeTypeSize = True
elif myarg == 'sizefield':
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'showparentsidsaslist':
showParentsIdsAsList = True
simpleLists.append('parentsIds')
elif myarg == 'showpermissionslast':
csvPF.SetShowPermissionsLast(True)
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
elif myarg == 'showlabels':
showLabels = getChoice(SHOWLABELS_CHOICES)
elif myarg == 'showshareddrivepermissions':
getPermissionsForSharedDrives = True
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
getSharedDriveACLsCountMsg = f'{Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER_ACL)} {Msg.FOR} {Ent.Plural(Ent.SHAREDDRIVE)}\n'
elif myarg == 'pmfilter':
pmselect = False
elif myarg == 'oneitemperrow':
oneItemPerRow = True
csvPF.RemoveIndexedTitles('permissions')
elif myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'continueoninvalidquery':
continueOnInvalidQuery = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg)
if not filepath and not fullpath:
showDepth = False
noSelect = noFileSelectFileIdEntity(fileIdEntity)
if noSelect:
buildTree = True
if maxdepth != -1 or filepath:
if not fileIdEntity:
fileIdEntity = initDriveFileEntity()
DLP.GetFileIdEntity()
if not fileIdEntity['shareddrive']:
cleanFileIDsList(fileIdEntity, [ROOT, ORPHANS])
if maxdepth != -1:
noSelect = False
elif not fileIdEntity:
fileIdEntity = DLP.GetFileIdEntity()
elif not buildTree:
buildTree = (not noRecursion
and not fileIdEntity['dict']
and not fileIdEntity['query']
and not fileIdEntity['shareddrivefilequery']
and _simpleFileIdEntityList(fileIdEntity['list']))
incrementalPrint = buildTree and (not filepath) and noSelect and not DLP.locationSet and not showParent
if buildTree and ((not filepath) or mimeTypeInQuery) and noSelect and not DLP.locationSet and not showParent:
DLP.AddMimeTypeToQuery()
if buildTree:
if not fileIdEntity.get('shareddrive'):
btkwargs = DLP.kwargs
else:
btkwargs = fileIdEntity['shareddrive']
DLP.Finalize(fileIdEntity)
if DLP.PM.permissionMatches:
getPermissionsForSharedDrives = True
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
elif DFF.fieldsList:
if not getPermissionsForSharedDrives:
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
if DFF.fieldsList:
_setSelectionFields()
fields = getFieldsFromFieldsList(DFF.fieldsList)
pagesFields = getItemFieldsFromFieldsList('files', DFF.fieldsList)
elif not DFF.allFields:
_setSelectionFields()
if not countsOnly and not set(DFF.fieldsList)-skipObjects:
for field in ['name', 'webviewlink']:
skipObjects.discard(DRIVE_FIELDS_CHOICE_MAP[field])
csvPF.AddField(field, DRIVE_FIELDS_CHOICE_MAP, DFF.fieldsList)
fields = getFieldsFromFieldsList(DFF.fieldsList)
pagesFields = getItemFieldsFromFieldsList('files', DFF.fieldsList)
else:
fields = pagesFields = '*'
DFF.SetAllParentsSubFields()
skipObjects = skipObjects.union(DEFAULT_SKIP_OBJECTS)
if stripCRsFromName:
if (countsOnly and not showSource) or (fields != '*' and 'name' not in DFF.fieldsList):
stripCRsFromName = False
shareddriveFields = []
for field in ['capabilities', 'createdTime']:
if fields == '*' or field in DFF.fieldsList:
shareddriveFields.append(field)
if filepath and not countsOnly:
csvPF.AddTitles('paths')
csvPF.SetFixPaths(True)
includeLabels = ','.join(DFF.includeLabels)
timeObjects = _getDriveTimeObjects()
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
fileNameTitle = 'title'
csvPF.SetMapDrive3Titles(True)
else:
fileNameTitle = 'name'
csvPF.RemoveTitles(['capabilities'])
if DLP.queryTimes and selectSubQuery:
for queryTimeName, queryTimeValue in iter(DLP.queryTimes.items()):
selectSubQuery = selectSubQuery.replace(f'#{queryTimeName}#', queryTimeValue)
selectSubQuery = _mapDrive2QueryToDrive3(selectSubQuery)
if addCSVData:
if not countsOnly:
csvPF.AddTitles(sorted(addCSVData.keys()))
else:
csvPFco.AddTitles(sorted(addCSVData.keys()))
if showSize:
csvPFco.MoveTitlesToEnd(['Size'])
csvPFco.MoveTitlesToEnd(['Total'])
csvPFco.SetSortAllTitles()
if not nodataFields:
if DFF.fieldsList:
if not FJQC.formatJSON:
nodataFields = ['Owner']+list(set(DFF.fieldsList)-skipObjects)
else:
nodataFields = ['Owner', 'id', fileNameTitle, 'owners.emailAddress']
else:
nodataFields = ['Owner', 'id', fileNameTitle, 'owners.emailAddress']
if not FJQC.formatJSON:
nodataFields.append('permissions')
if filepath:
nodataFields.append('paths')
if FJQC.formatJSON:
nodataFields.append('JSON')
csvPF.SetNodataFields(True, nodataFields, DRIVE_LIST_FIELDS, DRIVE_SUBFIELDS_CHOICE_MAP, oneItemPerRow)
else:
csvPF.SetNodataFields(False, nodataFields, None, None, False)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
if ((incrementalPrint and countsOnly) or
(not showParentsIdsAsList and DFF.parentsSubFields['isRoot'])):
try:
if not fileIdEntity['shareddrive']:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id,name')
rootFolderId = result['id']
rootFolderName = result['name']
else:
rootFolderId = fileIdEntity['shareddrive']['driveId']
if not fileIdEntity['shareddrivename']:
fileIdEntity['shareddrivename'] = callGAPI(drive.drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FILE_NOT_FOUND],
driveId=rootFolderId, fields='name')['name']
rootFolderName = fileIdEntity['shareddrivename']
if not showParentsIdsAsList and DFF.parentsSubFields['isRoot']:
DFF.parentsSubFields['rootFolderId'] = rootFolderId
except (GAPI.notFound, GAPI.fileNotFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
if filepath:
filePathInfo = initFilePathInfo(pathDelimiter)
filesPrinted = set()
mimeTypeInfo = {}
getSharedDriveACLsCount = 0
if buildTree:
gettingEntity = _getGettingEntity(user, fileIdEntity)
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=DLP.fileIdEntity['query'])
if not incrementalPrint:
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), DLP, shareddriveFields, showParent, user, i, count)
if not status:
continue
try:
feed = yieldGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(), maxItems=DLP.maxItems,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
q=DLP.fileIdEntity['query'], orderBy=DFF.orderBy, includeLabels=includeLabels,
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
for files in feed:
if showLabels is not None:
for f_file in files:
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
fileId=f_file['id'])
_formatFileDriveLabels(showLabels, labels, f_file, True, delimiter)
if not incrementalPrint:
extendFileTree(fileTree, files, DLP, stripCRsFromName)
else:
for f_file in files:
if stripCRsFromName:
f_file['name'] = _stripControlCharsFromName(f_file['name'])
_printFileInfo(drive, user, f_file, False)
if incrementalPrint:
if countsOnly:
if summary != FILECOUNT_SUMMARY_NONE:
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
if summary != FILECOUNT_SUMMARY_ONLY:
writeMimeTypeCountsRow(user, rootFolderId, rootFolderName, mimeTypeInfo)
continue
extendFileTreeParents(drive, fileTree, fields)
DLP.GetLocationFileIdsFromTree(fileTree, fileIdEntity)
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest) as e:
errMsg = str(e)
if 'Invalid field selection' in errMsg:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], errMsg, i, count)
break
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], invalidQuery(DLP.fileIdEntity['query']), i, count)
if not continueOnInvalidQuery:
break
continue
except GAPI.fileNotFound:
printGotEntityItemsForWhom(0)
continue
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
else:
fileTree = {}
user, drive, jcount = _validateUserGetFileIDs(origUser, i, count, fileIdEntity, drive=drive)
if jcount == 0:
continue
j = 0
for fileId in fileIdEntity['list']:
j += 1
if showSource:
mimeTypeInfo = {}
fileEntry = fileTree.get(fileId)
if fileEntry:
fileEntryInfo = fileEntry['info']
else:
try:
fileEntryInfo = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, includeLabels=includeLabels, fields=fields, supportsAllDrives=True)
if stripCRsFromName:
fileEntryInfo['name'] = _stripControlCharsFromName(fileEntryInfo['name'])
if showLabels is not None:
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
fileId=fileId)
_formatFileDriveLabels(showLabels, labels, fileEntryInfo, True, delimiter)
if filepath:
fileTree[fileId] = {'info': fileEntryInfo}
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
continue
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if fullpath:
getFilePaths(drive, fileTree, fileEntryInfo, filePathInfo, addParentsToTree=True,
fullpath=fullpath, showDepth=showDepth, folderPathOnly=folderPathOnly)
if ((showParent and (fileEntryInfo['id'] not in {ORPHANS, SHARED_WITHME, SHARED_DRIVES})) or
fileEntryInfo['mimeType'] != MIMETYPE_GA_FOLDER or noRecursion):
if fileId not in filesPrinted:
filesPrinted.add(fileId)
_printFileInfo(drive, user, fileEntryInfo.copy(), False)
if fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and not noRecursion:
_printChildDriveFolderContents(drive, fileEntryInfo, user, i, count, 0)
if GC.Values[GC.SHOW_GETTINGS] and not GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
writeStderr('\r\n')
flushStderr()
if countsOnly:
if showSource:
if summary != FILECOUNT_SUMMARY_NONE:
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
if summary != FILECOUNT_SUMMARY_ONLY:
writeMimeTypeCountsRow(user, fileId, fileEntryInfo['name'], mimeTypeInfo)
if countsOnly:
if not showSource:
if summary != FILECOUNT_SUMMARY_NONE:
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
if summary != FILECOUNT_SUMMARY_ONLY:
writeMimeTypeCountsRow(user, 'Various', 'Various', mimeTypeInfo)
if not countsOnly:
if not csvPF.rows:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not FJQC.formatJSON:
csvPF.SetSortTitles(['Owner', 'id', fileNameTitle])
else:
if 'JSON' in csvPF.JSONtitlesList:
csvPF.MoveJSONTitlesToEnd(['JSON'])
if GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] is None:
csvPF.writeCSVfile(f'{Cmd.Argument(GM.Globals[GM.ENTITY_CL_START])} {Cmd.Argument(GM.Globals[GM.ENTITY_CL_START]+1)} Drive Files')
else:
csvPF.writeCSVfile('Drive Files')
else:
if not csvPFco.rows:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if summary != FILECOUNT_SUMMARY_NONE:
writeMimeTypeCountsRow(summaryUser, 'Various', 'Various', summaryMimeTypeInfo)
csvPFco.todrive = csvPF.todrive
if not countsRowFilter:
csvPFco.SetRowFilter([], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
if GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] is None:
csvPFco.writeCSVfile(f'{Cmd.Argument(GM.Globals[GM.ENTITY_CL_START])} {Cmd.Argument(GM.Globals[GM.ENTITY_CL_START]+1)} Drive File Counts')
else:
csvPFco.writeCSVfile('Drive File Counts')
FILECOMMENTS_FIELDS_CHOICE_MAP = {
'action': 'action',
'author': 'author',
'content': 'content',
'createddate': 'createdTime',
'createdtime': 'createdTime',
'deleted': 'deleted',
'htmlcontent': 'htmlContent',
'id': 'id',
'modifieddate': 'modifiedTime',
'modifiedtime': 'modifiedTime',
'quotedfilecontent': 'quotedFileContent',
'reply': 'replies',
'replies': 'replies',
'resolved': 'resolved',
}
FILECOMMENTS_AUTHOR_SUBFIELDS_CHOICE_MAP = {
'displayname': 'displayName',
'emailaddress': 'emailAddress',
'me': 'me',
'permissionid': 'permissionId',
'photolink': 'photoLink',
}
FILECOMMENTS_REPLIES_SUBFIELDS_CHOICE_MAP = {
'action': 'action',
'author': 'author',
'content': 'content',
'createddate': 'createdTime',
'createdtime': 'createdTime',
'deleted': 'deleted',
'htmlcontent': 'htmlContent',
'id': 'id',
'modifieddate': 'modifiedTime',
'modifiedtime': 'modifiedTime',
}
FILECOMMENTS_SUBFIELDS_CHOICE_MAP = {
'author': FILECOMMENTS_AUTHOR_SUBFIELDS_CHOICE_MAP,
'reply': FILECOMMENTS_REPLIES_SUBFIELDS_CHOICE_MAP,
'replies': FILECOMMENTS_REPLIES_SUBFIELDS_CHOICE_MAP,
}
def _getCommentFields(fieldsList):
for field in _getFieldsList():
if field.find('.') == -1:
if field in FILECOMMENTS_FIELDS_CHOICE_MAP:
addFieldToFieldsList(field, FILECOMMENTS_FIELDS_CHOICE_MAP, fieldsList)
else:
invalidChoiceExit(field, FILECOMMENTS_FIELDS_CHOICE_MAP, True)
else:
field, subField = field.split('.', 1)
if field in FILECOMMENTS_SUBFIELDS_CHOICE_MAP:
if subField.find('.') == -1:
if subField in FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field]:
fieldsList.append(f'{FILECOMMENTS_FIELDS_CHOICE_MAP[field]}.{FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field][subField]}')
else:
invalidChoiceExit(subField, FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field], True)
else:
subField, subSubField = subField.split('.', 1)
if subField in FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field]:
if subSubField in FILECOMMENTS_SUBFIELDS_CHOICE_MAP[subField]:
fieldsList.append(f'{FILECOMMENTS_FIELDS_CHOICE_MAP[field]}.{FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field][subField]}.{FILECOMMENTS_SUBFIELDS_CHOICE_MAP[subField][subSubField]}')
else:
invalidChoiceExit(subSubField, FILECOMMENTS_SUBFIELDS_CHOICE_MAP[subField], True)
else:
invalidChoiceExit(subField, FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field], True)
else:
invalidChoiceExit(field, FILECOMMENTS_SUBFIELDS_CHOICE_MAP, True)
FILECOMMENTS_INDEXED_TITLES = ['replies']
FILECOMMENTS_TIME_OBJECTS = {'createdTime', 'modifiedTime'}
def _stripCommentPhotoLinks(comment):
if 'author' in comment:
comment['author'].pop('photoLink', None)
for reply in comment.get('replies', []):
if 'author' in reply:
reply['author'].pop('photoLink', None)
def _showComment(comment, stripPhotoLinks, timeObjects, i=0, count=0, FJQC=None):
if stripPhotoLinks:
_stripCommentPhotoLinks(comment)
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(comment, timeObjects=FILECOMMENTS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.DRIVE_FILE_COMMENT, comment['id']], i, count)
Ind.Increment()
showJSON(None, comment, ['id'], timeObjects)
Ind.Decrement()
# gam <UserTypeEntity> show filecomments <DriveFileEntity>
# [showdeleted] [start <Date>|<Time>] [countsonly|positivecountsonly]
# [fields <CommentsFieldNameList>] [showphotolinks]
# [countsonly]
# [formatjson]
# gam <UserTypeEntity> print filecomments <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [showdeleted] [start <Date>|<Time>]
# [fields <CommentsFieldNameList>] [showphotolinks]
# [countsonly|positivecountsonly]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
def printShowFileComments(users):
def _printComment(comment, commentId, replyId, baserow):
row = flattenJSON(comment, flattened=baserow.copy(), timeObjects=timeObjects)
row['commentId'] = commentId
row['replyId'] = replyId
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['commentId'] = commentId
comment['id'] = commentId
row['replyId'] = replyId
if replyId:
comment['reply']['id'] = replyId
row['JSON'] = json.dumps(cleanJSON(comment, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
csvPF = CSVPrintFile(['User', 'fileId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fieldsList = []
fileIdEntity = getDriveFileEntity()
countsOnly = positiveCountsOnly = False
stripPhotoLinks = True
kwargs = {}
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'showdeleted':
kwargs['includeDeleted'] = True
elif myarg == 'start':
kwargs['startModifiedTime'] = getTimeOrDeltaFromNow()
elif myarg == 'showphotolinks':
stripPhotoLinks = False
elif myarg == 'fields':
_getCommentFields(fieldsList)
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'countsonly':
countsOnly = True
elif myarg == 'positivecountsonly':
countsOnly = positiveCountsOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg)
if csvPF:
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if not countsOnly:
csvPF.AddTitles(['commentId', 'replyId'])
else:
csvPF.AddTitles(['comments', 'replies'])
if FJQC.formatJSON:
csvPF.AddTitles(['JSON'])
csvPF.SetJSONTitles(csvPF.titlesList)
if fieldsList:
if 'id' not in fieldsList:
fieldsList.append('id')
if 'replies' not in fieldsList:
for field in fieldsList.copy():
if field.startswith('replies.'):
fieldsList.append('replies.id')
break
fields = getItemFieldsFromFieldsList('comments', fieldsList)
else:
fields = '*'
timeObjects = FILECOMMENTS_TIME_OBJECTS
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.DRIVE_FILE_COMMENT, None][csvPF is not None])
if jcount == 0:
continue
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
comments = callGAPIpages(drive.comments(), 'list', 'comments',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
fileId=fileId, fields=fields, **kwargs)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_ID, fileId], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
kcount = len(comments)
if countsOnly:
numReplies = 0
for comment in comments:
numReplies += len(comment['replies'])
if not csvPF:
if countsOnly:
if not positiveCountsOnly or kcount > 0:
printKeyValueList([Ent.Singular(Ent.DRIVE_FILE_ID), fileId, 'comments', kcount, 'replies', numReplies])
else:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.DRIVE_FILE_ID, fileId], kcount, Ent.DRIVE_FILE_COMMENT, j, jcount)
Ind.Increment()
if not countsOnly:
k = 0
for comment in comments:
k += 1
_showComment(comment, stripPhotoLinks, timeObjects, k, kcount, FJQC)
Ind.Decrement()
elif countsOnly:
if not positiveCountsOnly or kcount > 0:
row = {'User': user, 'fileId': fileId}
if addCSVData:
row.update(addCSVData)
row['comments'] = kcount
row['replies'] = numReplies
csvPF.WriteRowTitles(row)
elif comments:
baserow = {'User': user, 'fileId': fileId}
if addCSVData:
baserow.update(addCSVData)
for comment in comments:
if stripPhotoLinks:
_stripCommentPhotoLinks(comment)
commentId = comment.pop('id')
replies = comment.pop('replies')
if not replies:
_printComment(comment, commentId, '', baserow)
else:
for reply in replies:
replyId = reply.pop('id')
baserow['replyId'] = replyId
comment['reply'] = reply
_printComment(comment, commentId, replyId, baserow)
Ind.Decrement()
if csvPF:
csvPF.SetIndexedTitles(FILECOMMENTS_INDEXED_TITLES)
csvPF.writeCSVfile('Drive File Comments')
# gam <UserTypeEntity> print filepaths <DriveFileEntity> [todrive <ToDriveAttribute>*]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [stripcrsfromname] [oneitemperrow]
# [fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
# [followshortcuts [<Boolean>]]
# gam <UserTypeEntity> show filepaths <DriveFileEntity>
# [returnpathonly]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [stripcrsfromname]
# [fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
# [followshortcuts [<Boolean>]]
def printShowFilePaths(users):
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
csvPF = CSVPrintFile(['Owner', 'id', fileNameTitle, 'paths'], 'sortall', ['paths']) if Act.csvFormat() else None
fileIdEntity = getDriveFileEntity()
fullpath = folderPathOnly = followShortcuts = oneItemPerRow = returnPathOnly = stripCRsFromName = False
pathDelimiter = '/'
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'fullpath':
fullpath = True
elif myarg == 'folderpathonly':
folderPathOnly = getBoolean()
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
elif csvPF is None and myarg == 'returnpathonly':
returnPathOnly = True
elif myarg == 'followshortcuts':
followShortcuts = getBoolean()
elif csvPF and myarg == 'oneitemperrow':
oneItemPerRow = True
csvPF.RemoveTitles('paths')
csvPF.AddTitles('path')
csvPF.SetSortAllTitles()
csvPF.SetIndexedTitles([])
elif myarg == 'orderby':
OBY.GetChoice()
else:
unknownArgumentExit()
fieldsList = FILEPATH_FIELDS_TITLES
if followShortcuts:
fieldsList.append('shortcutDetails')
pathFields = ','.join(fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if returnPathOnly:
GC.Values[GC.SHOW_GETTINGS] = False
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][(csvPF is not None) or returnPathOnly],
orderBy=OBY.orderBy)
if jcount == 0:
continue
filePathInfo = initFilePathInfo(pathDelimiter)
if fullpath:
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), None, [], True, user, i, count)
if not status:
continue
else:
fileTree = None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=pathFields, supportsAllDrives=True)
if followShortcuts and result['mimeType'] == MIMETYPE_GA_SHORTCUT:
fileId = result['shortcutDetails']['targetId']
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=pathFields, supportsAllDrives=True)
if returnPathOnly and result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == MY_DRIVE and not result.get('parents'):
writeStdout(f'{MY_DRIVE}\n')
continue
if stripCRsFromName:
result['name'] = _stripControlCharsFromName(result['name'])
driveId = result.get('driveId')
if driveId:
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
result['name'] = _getSharedDriveNameFromId(driveId)
if returnPathOnly:
if fullpath:
writeStdout(f'{SHARED_DRIVES}/{result["name"]}\n')
else:
writeStdout(f'{result["name"]}\n')
continue
if fullpath:
extendFileTree(fileTree, [result], None, False)
extendFileTreeParents(drive, fileTree, pathFields)
entityType, paths, _ = getFilePaths(drive, fileTree, result, filePathInfo, addParentsToTree=True,
fullpath=fullpath, folderPathOnly=folderPathOnly)
if returnPathOnly:
for path in paths:
writeStdout(f'{path}\n')
elif not csvPF:
kcount = len(paths)
entityPerformActionNumItems([entityType, f'{result["name"]} ({fileId})'], kcount, Ent.DRIVE_PATH, j, jcount)
Ind.Increment()
k = 0
for path in paths:
k += 1
printEntity([Ent.DRIVE_PATH, path], k, kcount)
Ind.Decrement()
else:
if oneItemPerRow:
if paths:
for path in paths:
csvPF.WriteRow({'Owner': user, 'id': fileId, fileNameTitle: result['name'], 'path': path})
else:
csvPF.WriteRow({'Owner': user, 'id': fileId, fileNameTitle: result['name']})
else:
csvPF.WriteRowTitles(flattenJSON({'paths': paths}, flattened={'Owner': user, 'id': fileId, fileNameTitle: result['name']}))
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Drive File Paths')
# gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [stripcrsfromname]
def printFileParentTree(users):
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
csvPF = CSVPrintFile(['Owner', 'isBase', 'baseId', 'id', fileNameTitle, 'parentId', 'depth', 'isRoot'], 'sortall')
fileIdEntity = getDriveFileEntity()
stripCRsFromName = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.FILE_PARENT_TREE)
if jcount == 0:
continue
try:
rootId = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileList = []
baseId = fileId
while True:
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='id,name,mimeType,parents,driveId', supportsAllDrives=True)
if stripCRsFromName:
result['name'] = _stripControlCharsFromName(result['name'])
result['isRoot'] = False
if not result.get('parents', []):
if fileId == rootId:
result['isRoot'] = True
else:
driveId = result.get('driveId')
if driveId:
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
result['name'] = _getSharedDriveNameFromId(driveId)
result['isRoot'] = True
result['parents'] = ['']
fileList.append(result)
break
fileList.append(result)
fileId = result['parents'][0]
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
kcount = len(fileList)
isBase = True
for result in fileList:
csvPF.WriteRow({'Owner': user, 'isBase': isBase, 'baseId': baseId, 'id': result['id'], fileNameTitle: result['name'],
'parentId': result['parents'][0], 'depth': kcount, 'isRoot': result['isRoot']})
isBase = False
kcount -= 1
csvPF.writeCSVfile('Drive File Parent Tree')
# Last file modification utilities
def _initLastModification():
return {'lastModifiedFileId': '', 'lastModifiedFileName': '',
'lastModifiedFileMimeType': '', 'lastModifiedFilePath': '',
'lastModifyingUser': '', 'lastModifiedTime': NEVER_TIME,
'fileEntryInfo': {}}
def _checkUpdateLastModifiction(f_file, userLastModification):
if f_file.get('modifiedTime', NEVER_TIME) > userLastModification['lastModifiedTime'] and 'lastModifyingUser' in f_file:
userLastModification['lastModifiedFileId'] = f_file['id']
userLastModification['lastModifiedFileName'] = _stripControlCharsFromName(f_file['name'])
userLastModification['lastModifiedFileMimeType'] = f_file['mimeType']
userLastModification['lastModifiedTime'] = f_file['modifiedTime']
userLastModification['lastModifyingUser'] = f_file['lastModifyingUser'].get('emailAddress',
f_file['lastModifyingUser'].get('displayName', UNKNOWN))
userLastModification['fileEntryInfo'] = f_file.copy()
def _getLastModificationPath(drive, userLastModification, pathDelimiter):
if userLastModification['fileEntryInfo']:
filePathInfo = initFilePathInfo(pathDelimiter)
_, paths, _ = getFilePaths(drive, {}, userLastModification['fileEntryInfo'], filePathInfo)
userLastModification['lastModifiedFilePath'] = paths[0] if paths else UNKNOWN
def _showLastModification(lastModification):
printKeyValueList(['lastModifiedFileId', lastModification['lastModifiedFileId']])
printKeyValueList(['lastModifiedFileName', lastModification['lastModifiedFileName']])
printKeyValueList(['lastModifiedFileMimeType', lastModification['lastModifiedFileMimeType']])
printKeyValueList(['lastModifiedFilePath', lastModification['lastModifiedFilePath']])
printKeyValueList(['lastModifyingUser', lastModification['lastModifyingUser']])
printKeyValueList(['lastModifiedTime', formatLocalTime(lastModification['lastModifiedTime'])])
def _updateLastModificationRow(row, lastModification):
row.update({'lastModifiedFileId': lastModification['lastModifiedFileId'],
'lastModifiedFileName': lastModification['lastModifiedFileName'],
'lastModifiedFileMimeType': lastModification['lastModifiedFileMimeType'],
'lastModifiedFilePath': lastModification['lastModifiedFilePath'],
'lastModifyingUser': lastModification['lastModifyingUser'],
'lastModifiedTime': formatLocalTime(lastModification['lastModifiedTime'])})
# gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*]
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [continueoninvalidquery [<Boolean>]]
# [corpora <CorporaAttribute>]
# [select <SharedDriveEntity>]
# [anyowner|(showownedby any|me|others)]
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
# [filenamematchpattern <REMatchPattern>]
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
# [excludetrashed] (addcsvdata <FieldName> <String>)*
# [showsize] [showmimetypesize]
# [showlastmodification] [pathdelimiter <Character>]
# (addcsvdata <FieldName> <String>)*
# [summary none|only|plus] [summaryuser <String>]
# gam <UserTypeEntity> show filecounts
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [continueoninvalidquery [<Boolean>]]
# [corpora <CorporaAttribute>]
# [select <SharedDriveEntity>]
# [anyowner|(showownedby any|me|others)]
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
# [filenamematchpattern <REMatchPattern>]
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
# [excludetrashed]
# [showsize] [showmimetypesize]
# [showlastmodification] [pathdelimiter <Character>]
# [summary none|only|plus] [summaryuser <String>]
def printShowFileCounts(users):
def _setSelectionFields():
if DLP.showOwnedBy is not None:
fieldsList.extend(OWNED_BY_ME_FIELDS_TITLES)
if showSize or (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
fieldsList.append(sizeField)
if showLastModification:
fieldsList.extend(['id,name,modifiedTime,lastModifyingUser(me, displayName, emailAddress),parents'])
if DLP.filenameMatchPattern:
fieldsList.append('name')
if DLP.excludeTrashed:
fieldsList.append('trashed')
if DLP.PM.permissionMatches:
fieldsList.extend(['id', 'permissions'])
if DLP.onlySharedDrives or getPermissionsForSharedDrives:
fieldsList.append('driveId')
def showMimeTypeInfo(user, mimeTypeInfo, sharedDriveId, sharedDriveName, lastModification, i, count):
if summary != FILECOUNT_SUMMARY_NONE:
if count != 0:
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
if summary == FILECOUNT_SUMMARY_ONLY:
return
countTotal = sizeTotal = 0
for mtinfo in iter(mimeTypeInfo.values()):
countTotal += mtinfo['count']
sizeTotal += mtinfo['size']
if not csvPF:
if sharedDriveId:
kvList = [Ent.USER, user, Ent.SHAREDDRIVE, f'{sharedDriveName} ({sharedDriveId})']
else:
kvList = [Ent.USER, user]
dataList = [Ent.Choose(Ent.DRIVE_FILE_OR_FOLDER, countTotal), countTotal]
if showSize:
dataList.extend([Ent.Singular(Ent.SIZE), sizeTotal])
if sharedDriveId:
dataList.extend(['Item cap', f"{countTotal/SHARED_DRIVE_MAX_FILES_FOLDERS:.2%}"])
printEntityKVList(kvList, dataList, i, count)
Ind.Increment()
if showLastModification:
_showLastModification(lastModification)
for mimeType, mtinfo in sorted(iter(mimeTypeInfo.items())):
if not showMimeTypeSize:
printKeyValueList([mimeType, mtinfo['count']])
else:
printKeyValueList([mimeType, f"{mtinfo['count']}, {mtinfo['size']}"])
Ind.Decrement()
else:
if sharedDriveId:
row = {'User': user, 'id': sharedDriveId, 'name': sharedDriveName, 'Total': countTotal, 'Item cap': f"{countTotal/SHARED_DRIVE_MAX_FILES_FOLDERS:.2%}"}
else:
row = {'User': user, 'Total': countTotal}
if showSize:
row['Size'] = sizeTotal
if showLastModification:
_updateLastModificationRow(row, lastModification)
if addCSVData:
row.update(addCSVData)
for mimeType, mtinfo in sorted(iter(mimeTypeInfo.items())):
row[f'{mimeType}'] = mtinfo['count']
if showMimeTypeSize:
row[f'{mimeType}:Size'] = mtinfo['size']
csvPF.WriteRowTitles(row)
csvPF = CSVPrintFile() if Act.csvFormat() else None
if csvPF:
csvPF.SetZeroBlankMimeTypeCounts(True)
fieldsList = ['mimeType']
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': True, 'allowQuery': True, 'mimeTypeInQuery': True})
pathDelimiter = '/'
sharedDriveId = sharedDriveName = ''
continueOnInvalidQuery = showSize = showLastModification = showMimeTypeSize = False
sizeField = 'quotaBytesUsed'
summary = FILECOUNT_SUMMARY_NONE
summaryUser = FILECOUNT_SUMMARY_USER
summaryMimeTypeInfo = {}
fileIdEntity = {}
addCSVData = {}
summaryLastModification = _initLastModification()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif DLP.ProcessArgument(myarg, fileIdEntity):
pass
elif myarg == 'select':
if fileIdEntity:
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
fileIdEntity = getSharedDriveEntity()
elif myarg == 'showsize':
showSize = True
elif myarg == 'sizefield':
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
elif myarg == 'showlastmodification':
showLastModification = True
elif myarg == 'showmimetypesize':
showMimeTypeSize = showSize = True
elif myarg == 'summary':
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
elif myarg == 'summaryuser':
summaryUser = getString(Cmd.OB_STRING)
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'continueoninvalidquery':
continueOnInvalidQuery = getBoolean()
else:
unknownArgumentExit()
if not fileIdEntity:
fileIdEntity = DLP.GetFileIdEntity()
if not fileIdEntity.get('shareddrive'):
btkwargs = DLP.kwargs
else:
btkwargs = fileIdEntity['shareddrive']
fieldsList.append('driveId')
DLP.Finalize(fileIdEntity)
if DLP.PM.permissionMatches:
getPermissionsForSharedDrives = True
permissionsFields = 'nextPageToken,permissions'
else:
getPermissionsForSharedDrives = False
_setSelectionFields()
if csvPF:
sortTitles = ['User', 'id', 'name', 'Total', 'Item cap'] if fileIdEntity.get('shareddrive') else ['User', 'Total']
if showSize:
sortTitles.insert(sortTitles.index('Total')+1, 'Size')
if showLastModification:
sortTitles.extend(['lastModifiedFileId', 'lastModifiedFileName',
'lastModifiedFileMimeType', 'lastModifiedFilePath',
'lastModifyingUser', 'lastModifiedTime'])
if addCSVData:
sortTitles.extend(sorted(addCSVData.keys()))
csvPF.SetTitles(sortTitles)
csvPF.SetSortAllTitles()
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
sharedDriveName = _getSharedDriveNameFromId(sharedDriveId) if sharedDriveId else ''
mimeTypeInfo = {}
userLastModification = _initLastModification()
gettingEntity = _getGettingEntity(user, fileIdEntity)
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=DLP.fileIdEntity['query'])
try:
feed = yieldGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=DLP.fileIdEntity['query'],
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
for files in feed:
for f_file in files:
driveId = f_file.get('driveId')
checkSharedDrivePermissions = getPermissionsForSharedDrives and driveId and 'permissions' not in f_file
if (not DLP.CheckShowOwnedBy(f_file) or
not DLP.CheckShowSharedByMe(f_file) or
not DLP.CheckExcludeTrashed(f_file) or
not DLP.CheckFileSize(f_file, sizeField) or
not DLP.CheckFilenameMatch(f_file) or
(not checkSharedDrivePermissions and not DLP.CheckFilePermissionMatches(f_file)) or
(DLP.onlySharedDrives and not driveId)):
continue
if checkSharedDrivePermissions:
try:
f_file['permissions'] = callGAPIpages(drive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS+[GAPI.BAD_REQUEST],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fileId=f_file['id'], fields=permissionsFields, supportsAllDrives=True)
if not DLP.CheckFilePermissionMatches(f_file):
continue
for permission in f_file['permissions']:
permission.pop('teamDrivePermissionDetails', None)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid, GAPI.badRequest,
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
continue
mimeTypeInfo.setdefault(f_file['mimeType'], {'count': 0, 'size': 0})
mimeTypeInfo[f_file['mimeType']]['count'] += 1
mimeTypeInfo[f_file['mimeType']]['size'] += int(f_file.get(sizeField, '0'))
if showLastModification:
_checkUpdateLastModifiction(f_file, userLastModification)
_getLastModificationPath(drive, userLastModification, pathDelimiter)
showMimeTypeInfo(user, mimeTypeInfo, sharedDriveId, sharedDriveName, userLastModification, i, count)
if showLastModification and userLastModification['lastModifiedTime'] > summaryLastModification['lastModifiedTime']:
summaryLastModification = userLastModification.copy()
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], invalidQuery(DLP.fileIdEntity['query']), i, count)
if not continueOnInvalidQuery:
break
continue
except GAPI.fileNotFound:
printGotEntityItemsForWhom(0)
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, sharedDriveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
if summary != FILECOUNT_SUMMARY_NONE:
showMimeTypeInfo(summaryUser, summaryMimeTypeInfo,
'' if count > 1 else sharedDriveId,
'' if count > 1 else sharedDriveName,
summaryLastModification, 0, 0)
if csvPF:
csvPF.writeCSVfile('Drive File Counts')
# gam <UserTypeEntity> print drivelastmodification [todrive <ToDriveAttribute>*]
# [select <SharedDriveEntity>]
# [pathdelimiter <Character>]
# (addcsvdata <FieldName> <String>)*
# gam <UserTypeEntity> show drivelastmodification
# [select <SharedDriveEntity>]
# [pathdelimiter <Character>]
def printShowDrivelastModifications(users):
def showLastModificationInfo(user, sharedDriveId, sharedDriveName, lastModification, i, count):
if not csvPF:
if sharedDriveId:
kvList = [Ent.USER, user, Ent.SHAREDDRIVE, f'{sharedDriveName} ({sharedDriveId})']
else:
kvList = [Ent.USER, user]
printEntity(kvList, i, count)
Ind.Increment()
_showLastModification(lastModification)
Ind.Decrement()
else:
if sharedDriveId:
row = {'User': user, 'id': sharedDriveId, 'name': sharedDriveName}
else:
row = {'User': user}
_updateLastModificationRow(row, lastModification)
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(row)
csvPF = CSVPrintFile() if Act.csvFormat() else None
fieldsList = ['id', 'driveId', 'name', 'mimeType', 'lastModifyingUser', 'modifiedTime', 'parents']
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': True})
pathDelimiter = '/'
sharedDriveId = sharedDriveName = ''
fileIdEntity = {}
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'select':
if fileIdEntity:
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
fileIdEntity = getSharedDriveEntity()
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
unknownArgumentExit()
if not fileIdEntity:
fileIdEntity = DLP.GetFileIdEntity()
if not fileIdEntity.get('shareddrive'):
btkwargs = DLP.kwargs
else:
btkwargs = fileIdEntity['shareddrive']
fieldsList.append('driveId')
DLP.Finalize(fileIdEntity)
if csvPF:
sortTitles = ['User', 'id', 'name'] if fileIdEntity.get('shareddrive') else ['User']
sortTitles.extend(['lastModifiedFileId', 'lastModifiedFileName',
'lastModifiedFileMimeType', 'lastModifiedFilePath',
'lastModifyingUser', 'lastModifiedTime'])
if addCSVData:
sortTitles.extend(sorted(addCSVData.keys()))
csvPF.SetTitles(sortTitles)
csvPF.SetSortAllTitles()
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
sharedDriveName = _getSharedDriveNameFromId(sharedDriveId) if sharedDriveId else ''
userLastModification = _initLastModification()
gettingEntity = _getGettingEntity(user, fileIdEntity)
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count)
try:
feed = yieldGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=[GAPI.UNKNOWN_ERROR],
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
for files in feed:
for f_file in files:
_checkUpdateLastModifiction(f_file, userLastModification)
_getLastModificationPath(drive, userLastModification, pathDelimiter)
showLastModificationInfo(user, sharedDriveId, sharedDriveName, userLastModification, i, count)
except (GAPI.invalid, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], str(e), i, count)
continue
except GAPI.fileNotFound:
printGotEntityItemsForWhom(0)
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, sharedDriveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
if csvPF:
csvPF.writeCSVfile('Drive File Last Modification')
DISKUSAGE_SHOW_CHOICES = {'all', 'summary', 'summaryandtrash'}
# gam <UserTypeEntity> print diskusage <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [anyowner|(showownedby any|me|others)]
# [sizefield quotabytesused|size]
# [pathdelimiter <Character>] [excludetrashed] [stripcrsfromname]
# (addcsvdata <FieldName> <String>)*
# [noprogress] [show all|summary|summaryandtrash]
def printDiskUsage(users):
def _getChildDriveFolderInfo(drive, fileEntry, user, i, count, depth):
fileEntry['depth'] = depth
q = WITH_PARENTS.format(fileEntry['id'])
try:
children = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=q, orderBy=orderBy, fields=pagesFields,
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], supportsAllDrives=True, includeItemsFromAllDrives=True)
except (GAPI.invalidQuery, GAPI.invalid):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, None], invalidQuery(q), i, count)
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return
Ind.Increment()
if showProgress:
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER, fileEntry['path']])
for childEntryInfo in children:
trashed = childEntryInfo['trashed']
if trashed and excludeTrashed:
continue
mimeType = childEntryInfo.pop('mimeType')
if mimeType == MIMETYPE_GA_FOLDER:
fileEntry['directFolderCount'] += 1
fileEntry['totalFolderCount'] += 1
if trashed:
trashFolder['totalFolderCount'] += 1
if childEntryInfo['explicitlyTrashed']:
trashFolder['directFolderCount'] += 1
childEntryInfo['User'] = user
if includeOwner:
owners = childEntryInfo.pop('owners', [])
if owners:
childEntryInfo['Owner'] = owners[0].get('emailAddress', 'Unknown')
childEntryInfo.update(zeroFolderInfo)
if stripCRsFromName:
childEntryInfo['name'] = _stripControlCharsFromName(childEntryInfo['name'])
childEntryInfo['path'] = fileEntry['path']+pathDelimiter+childEntryInfo['name']
childEntryInfo.pop(sizeField, None)
foldersList.append(childEntryInfo)
_getChildDriveFolderInfo(drive, childEntryInfo, user, i, count, depth+1)
fileEntry['totalFileCount'] += childEntryInfo['totalFileCount']
fileEntry['totalFileSize'] += childEntryInfo['totalFileSize']
fileEntry['totalFolderCount'] += childEntryInfo['totalFolderCount']
elif mimeType != MIMETYPE_GA_SHORTCUT:
if includeOwner and showOwnedBy is not None and childEntryInfo['ownedByMe'] != showOwnedBy:
continue
fsize = int(childEntryInfo.get(sizeField, '0'))
fileEntry['directFileCount'] += 1
fileEntry['directFileSize'] += fsize
fileEntry['totalFileCount'] += 1
fileEntry['totalFileSize'] += fsize
if trashed:
trashFolder['totalFileCount'] += 1
trashFolder['totalFileSize'] += fsize
if childEntryInfo['explicitlyTrashed']:
trashFolder['directFileCount'] += 1
trashFolder['directFileSize'] += fsize
Ind.Decrement()
csvPF = CSVPrintFile(['User', 'Owner', 'id', 'name', 'ownedByMe', 'trashed', 'explicitlyTrashed',
'directFileCount', 'directFileSize', 'directFolderCount',
'totalFileCount', 'totalFileSize', 'totalFolderCount', 'depth', 'path'])
excludeTrashed = stripCRsFromName = False
includeOwner = True
orderBy = 'folder,name'
zeroFolderInfo = {'directFileCount': 0, 'directFileSize': 0, 'directFolderCount': 0,
'totalFileCount': 0, 'totalFileSize': 0, 'totalFolderCount': 0}
sizeField = 'quotaBytesUsed'
showOwnedBy = showProgress = True
pathDelimiter = '/'
fileIdEntity = getDriveFileEntity()
addCSVData = {}
showResults = 'all'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'anyowner':
showOwnedBy = None
elif myarg == 'showownedby':
showOwnedBy = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True)
elif myarg == 'sizefield':
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'excludetrashed':
excludeTrashed = True
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
elif myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'show':
showResults = getChoice(DISKUSAGE_SHOW_CHOICES)
elif myarg == 'noprogress':
showProgress = False
else:
unknownArgumentExit()
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
fieldsList = ['id', 'name', 'mimeType', sizeField, 'trashed', 'explicitlyTrashed', 'owners(emailAddress)', 'ownedByMe']
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
topFieldsList = fieldsList[:]
topFieldsList.extend(['driveId', 'parents'])
topFields = getFieldsFromFieldsList(topFieldsList)
i, count, users = getEntityArgument(users)
i = 0
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(origUser, i, count, fileIdEntity, entityType=Ent.DRIVE_DISK_USAGE)
if jcount == 0:
continue
j = 0
for fileId in fileIdEntity['list']:
j += 1
foldersList = []
trashFolder = {'User': user, 'id': 'Trash', 'name': 'Trash', 'path': 'Trash'}
trashFolder.update(zeroFolderInfo)
try:
topFolder = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=topFields, supportsAllDrives=True)
if stripCRsFromName:
topFolder['name'] = _stripControlCharsFromName(topFolder['name'])
mimeType = topFolder.pop('mimeType')
if mimeType != MIMETYPE_GA_FOLDER:
entityValueList = [Ent.USER, user, _getEntityMimeType(topFolder), topFolder['name']]
entityActionNotPerformedWarning(entityValueList, Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_FOLDER), i, count)
continue
if topFolder['trashed']:
if excludeTrashed:
entityValueList = [Ent.USER, user, Ent.DRIVE_FOLDER, topFolder['name']]
entityActionNotPerformedWarning(entityValueList, Msg.IN_TRASH_AND_EXCLUDE_TRASHED, i, count)
continue
trashFolder['totalFolderCount'] += 1
if topFolder['explicitlyTrashed']:
trashFolder['directFolderCount'] += 1
driveId = topFolder.pop('driveId', None)
if driveId:
includeOwner = False
csvPF.RemoveTitles(['Owner', 'ownedByMe'])
if topFolder['name'] == TEAM_DRIVE and not topFolder.get('parents'):
topFolder['name'] = _getSharedDriveNameFromId(driveId)
topFolder['path'] = f'{SHARED_DRIVES}{pathDelimiter}{topFolder["name"]}'
else:
topFolder['path'] = topFolder['name']
elif topFolder['name'] == MY_DRIVE and not topFolder.get('parents'):
topFolder['path'] = MY_DRIVE
else:
topFolder['path'] = topFolder['name']
topFolder['User'] = user
if includeOwner:
owners = topFolder.pop('owners', [])
if owners:
topFolder['Owner'] = owners[0].get('emailAddress', 'Unknown')
trashFolder['Owner'] = topFolder['Owner']
topFolder.pop('ownedByMe', None)
topFolder.pop('parents', None)
topFolder.update(zeroFolderInfo)
topFolder.pop(sizeField, None)
foldersList.append(topFolder)
_getChildDriveFolderInfo(drive, topFolder, user, i, count, -1)
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
continue
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if showResults == 'all':
for folder in foldersList:
if addCSVData:
folder.update(addCSVData)
csvPF.WriteRow(folder)
else:
folder = foldersList[0]
if addCSVData:
folder.update(addCSVData)
csvPF.WriteRow(folder)
if showResults != 'summary' and not excludeTrashed:
trashFolder['trashed'] = trashFolder['totalFileCount']+trashFolder['totalFolderCount'] > 0
trashFolder['explicitlyTrashed'] = trashFolder['directFileCount']+trashFolder['directFolderCount'] > 0
if addCSVData:
trashFolder.update(addCSVData)
trashFolder['depth'] = -1
csvPF.WriteRow(trashFolder)
csvPF.writeCSVfile('Drive Disk Usage')
FILESHARECOUNTS_OWNER = 'Owner'
FILESHARECOUNTS_TOTAL = 'Total'
FILESHARECOUNTS_SHARED = 'Shared'
FILESHARECOUNTS_SHARED_EXTERNAL = 'Shared External'
FILESHARECOUNTS_SHARED_INTERNAL = 'Shared Internal'
FILESHARECOUNTS_ZEROCOUNTS = {
FILESHARECOUNTS_TOTAL: 0,
FILESHARECOUNTS_SHARED: 0,
FILESHARECOUNTS_SHARED_EXTERNAL: 0,
FILESHARECOUNTS_SHARED_INTERNAL: 0,
'anyone': 0, 'anyoneWithLink': 0,
'externalDomain': 0, 'externalDomainWithLink': 0,
'internalDomain': 0, 'internalDomainWithLink': 0,
'externalGroup': 0, 'internalGroup': 0,
'externalUser': 0, 'internalUser': 0,
'deletedGroup': 0, 'deletedUser': 0,
}
FILESHARECOUNTS_CATEGORIES = {
'anyone': {False: 'anyone', True: 'anyoneWithLink'},
'domain': {False: {False: 'externalDomain', True: 'externalDomainWithLink'}, True: {False: 'internalDomain', True: 'internalDomainWithLink'}},
'group': {False: 'externalGroup', True: 'internalGroup'},
'user': {False: 'externalUser', True: 'internalUser'},
'deleted': {'group': 'deletedGroup', 'user': 'deletedUser'},
}
# gam <UserTypeEntity> print filesharecounts [todrive <ToDriveAttribute>*]
# [excludetrashed]
# [internaldomains <DomainNameList>]
# [summary none|only|plus] [summaryuser <String>]
# gam <UserTypeEntity> show filesharecounts
# [excludetrashed]
# [internaldomains <DomainNameList>]
# [summary none|only|plus] [summaryuser <String>]
def printShowFileShareCounts(users):
def incrementCounter(counter):
if not counterSet[counter]:
userShareCounts[counter] += 1
counterSet[counter] = True
def showShareCounts(user, shareCounts, i, count):
if summary != FILECOUNT_SUMMARY_NONE:
if count != 0:
for field, shareCount in iter(shareCounts.items()):
summaryShareCounts[field] += shareCount
if summary == FILECOUNT_SUMMARY_ONLY:
return
if not csvPF:
printEntity([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, shareCounts[FILESHARECOUNTS_TOTAL]], i, count)
Ind.Increment()
for field, shareCount in iter(shareCounts.items()):
printKeyValueList([field, shareCount])
Ind.Decrement()
else:
row = {FILESHARECOUNTS_OWNER: user}
row.update(shareCounts)
csvPF.WriteRow(row)
csvPF = CSVPrintFile([FILESHARECOUNTS_OWNER]+list(FILESHARECOUNTS_ZEROCOUNTS.keys())) if Act.csvFormat() else None
query = ME_IN_OWNERS
summary = FILECOUNT_SUMMARY_NONE
summaryUser = FILECOUNT_SUMMARY_USER
fileIdEntity = {}
internalDomains = [GC.Values[GC.DOMAIN]]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'excludetrashed':
query += ' and trashed=false'
elif myarg == 'internaldomains':
internalDomains = getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').split()
elif myarg == 'summary':
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
elif myarg == 'summaryuser':
summaryUser = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
summaryShareCounts = FILESHARECOUNTS_ZEROCOUNTS.copy()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
userShareCounts = FILESHARECOUNTS_ZEROCOUNTS.copy()
gettingEntity = _getGettingEntity(user, fileIdEntity)
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=query)
try:
feed = yieldGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query, fields='nextPageToken,files(permissions(type,role,emailAddress,domain,allowFileDiscovery,deleted))',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
for files in feed:
for f_file in files:
counterSet = {FILESHARECOUNTS_TOTAL: False, FILESHARECOUNTS_SHARED: False,
FILESHARECOUNTS_SHARED_EXTERNAL: False, FILESHARECOUNTS_SHARED_INTERNAL: False}
for permission in f_file['permissions']:
if permission['role'] == 'owner':
incrementCounter(FILESHARECOUNTS_TOTAL)
else:
incrementCounter(FILESHARECOUNTS_SHARED)
ptype = permission['type']
if ptype == 'anyone':
incrementCounter(FILESHARECOUNTS_SHARED_EXTERNAL)
userShareCounts[FILESHARECOUNTS_CATEGORIES[ptype][not permission['allowFileDiscovery']]] += 1
else:
domain = permission.get('domain', '')
if not domain and ptype in ['user', 'group']:
if permission.get('deleted', False):
userShareCounts[FILESHARECOUNTS_CATEGORIES['deleted'][ptype]] += 1
continue
emailAddress = permission['emailAddress']
domain = emailAddress[emailAddress.find('@')+1:]
internal = domain in internalDomains
incrementCounter([FILESHARECOUNTS_SHARED_EXTERNAL, FILESHARECOUNTS_SHARED_INTERNAL][internal])
if ptype == 'domain':
userShareCounts[FILESHARECOUNTS_CATEGORIES[ptype][internal][not permission['allowFileDiscovery']]] +=1
else: # group, user
userShareCounts[FILESHARECOUNTS_CATEGORIES[ptype][internal]] += 1
showShareCounts(user, userShareCounts, i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
if summary != FILECOUNT_SUMMARY_NONE:
showShareCounts(summaryUser, summaryShareCounts, 0, 0)
if csvPF:
csvPF.writeCSVfile('Drive File Share Counts')
FILETREE_FIELDS_CHOICE_MAP = {
'explicitlytrashed': 'explicitlyTrashed',
'filesize': 'size',
'id': 'id',
'mime': 'mimeType',
'mimetype': 'mimeType',
'owners': 'owners',
'parents': 'parents',
'size': 'size',
'trashed': 'trashed',
}
FILETREE_FIELDS_PRINT_ORDER = ['id', 'parents', 'owners', 'mimeType', 'size', 'explicitlyTrashed', 'trashed']
# gam <UserTypeEntity> print filetree [todrive <ToDriveAttribute>*]
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>] [depth <Number>]]
# [anyowner|(showownedby any|me|others)]
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
# [filenamematchpattern <REMatchPattern>]
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
# [excludetrashed]
# [fields <FileTreeFieldNameList>]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
# [noindent] [stripcrsfromname]
# gam <UserTypeEntity> show filetree
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>] [depth <Number>]]
# [anyowner|(showownedby any|me|others)]
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
# [filenamematchpattern <REMatchPattern>]
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
# [excludetrashed]
# [fields <FileTreeFieldNameList>]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
# [stripcrsfromname]
def printShowFileTree(users):
def _showFileInfo(fileEntry, depth, j=0, jcount=0):
if not DLP.CheckExcludeTrashed(fileEntry):
return
if stripCRsFromName:
fileEntry['name'] = _stripControlCharsFromName(fileEntry['name'])
if not csvPF:
fileInfoList = []
for field in FILETREE_FIELDS_PRINT_ORDER:
if showFields[field]:
if field == 'parents':
parents = fileEntry.get(field, [])
fileInfoList.extend([field, f'{len(parents)} [{delimiter.join(parents)}]'])
elif field == 'owners':
owners = [owner['emailAddress'] for owner in fileEntry.get(field, [])]
if owners:
fileInfoList.extend([field, delimiter.join(owners)])
elif field in {'explicitlyTrashed', 'trashed'}:
trashed = fileEntry.get(field, False)
if trashed:
fileInfoList.extend([field, trashed])
elif field == 'size':
fileInfoList.extend([field, fileEntry.get(sizeField, 0)])
else:
fileInfoList.extend([field, fileEntry.get(field, '')])
if fileInfoList:
printKeyValueListWithCount([fileEntry['name'], formatKeyValueList('(', fileInfoList, ')')], j, jcount)
else:
printKeyValueList([fileEntry['name']])
else:
userInfo['index'] += 1
row = userInfo.copy()
row['depth'] = depth
row[fileNameTitle] = ('' if noindent else Ind.SpacesSub1())+fileEntry['name']
for field in FILETREE_FIELDS_PRINT_ORDER:
if showFields[field]:
if field == 'parents':
row[field] = delimiter.join(fileEntry.get(field, []))
elif field == 'owners':
row[field] = delimiter.join([owner['emailAddress'] for owner in fileEntry.get(field, [])])
elif field == 'size':
row[fileSize] = fileEntry.get(sizeField, 0)
else:
row[field] = fileEntry.get(field, '')
csvPF.WriteRow(row)
def _showDriveFolderContents(fileEntry, depth):
for childId in fileEntry['children']:
childEntry = fileTree.get(childId)
if childEntry:
if not DLP.CheckExcludeTrashed(childEntry['info']):
continue
if (DLP.CheckMimeType(childEntry['info']) and
DLP.CheckFileSize(childEntry['info'], sizeField) and
DLP.CheckFilenameMatch(childEntry['info']) and
DLP.CheckFilePermissionMatches(childEntry['info'])):
_showFileInfo(childEntry['info'], depth)
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
Ind.Increment()
_showDriveFolderContents(childEntry, depth+1)
Ind.Decrement()
def _showChildDriveFolderContents(drive, fileEntry, user, i, count, depth):
if not DLP.CheckExcludeTrashed(fileEntry):
return
q = WITH_PARENTS.format(fileEntry['id'])
if selectSubQuery:
q += ' and ('+selectSubQuery+')'
try:
children = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=q, orderBy=OBY.orderBy, fields=pagesFields,
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], supportsAllDrives=True, includeItemsFromAllDrives=True)
except (GAPI.invalidQuery, GAPI.invalid):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(selectSubQuery), i, count)
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return
for childEntryInfo in children:
if not DLP.CheckExcludeTrashed(childEntryInfo):
continue
if (DLP.CheckShowOwnedBy(childEntryInfo) and
DLP.CheckMimeType(childEntryInfo) and
DLP.CheckFileSize(childEntryInfo, sizeField) and
DLP.CheckFilenameMatch(childEntryInfo) and
DLP.CheckFilePermissionMatches(childEntryInfo)):
_showFileInfo(childEntryInfo, depth)
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
Ind.Increment()
_showChildDriveFolderContents(drive, childEntryInfo, user, i, count, depth+1)
Ind.Decrement()
csvPF = CSVPrintFile(['User', 'index', 'depth', 'name']) if Act.csvFormat() else None
maxdepth = -1
fileIdEntity = {}
selectSubQuery = ''
sizeField = 'quotaBytesUsed'
showFields = {}
for mappedField in FILETREE_FIELDS_CHOICE_MAP.values():
showFields[mappedField] = False
buildTree = noindent = stripCRsFromName = False
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': False})
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif DLP.ProcessArgument(myarg, fileIdEntity):
pass
elif myarg == 'select':
if fileIdEntity:
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
fileIdEntity = getDriveFileEntity(DLP=DLP)
elif myarg == 'selectsubquery':
selectSubQuery = getString(Cmd.OB_QUERY, minLen=0)
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'depth':
maxdepth = getInteger(minVal=-1)
elif myarg == 'sizefield':
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
elif myarg == 'fields':
for field in _getFieldsList():
if field in FILETREE_FIELDS_CHOICE_MAP:
showFields[FILETREE_FIELDS_CHOICE_MAP[field]] = True
if csvPF:
csvPF.AddTitle(FILETREE_FIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, FILETREE_FIELDS_CHOICE_MAP, True)
elif myarg == 'delimiter':
delimiter = getCharacter()
elif csvPF and myarg == 'noindent':
noindent = True
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
else:
unknownArgumentExit()
fieldsList = ['driveId', 'id', 'name', 'parents', 'mimeType', 'ownedByMe', 'owners(emailAddress)',
'shared', sizeField, 'explicitlyTrashed', 'trashed']
if csvPF:
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
fileNameTitle = 'title'
fileSize = 'fileSize'
csvPF.SetMapDrive3Titles(True)
else:
fileNameTitle = 'name'
fileSize = 'size'
buildTree = (not fileIdEntity
or (not fileIdEntity['dict']
and not fileIdEntity['query']
and not fileIdEntity['shareddrivefilequery']
and _simpleFileIdEntityList(fileIdEntity['list'])))
if buildTree:
if not fileIdEntity:
fileIdEntity = initDriveFileEntity()
DLP.GetFileIdEntity()
if not fileIdEntity.get('shareddrive'):
btkwargs = DLP.kwargs
btkwargs['q'] = DLP.fileIdEntity['query']
cleanFileIDsList(fileIdEntity, [ROOT, ORPHANS])
else:
btkwargs = fileIdEntity['shareddrive']
DLP.Finalize(fileIdEntity)
elif not fileIdEntity:
fileIdEntity = initDriveFileEntity()
if DLP.PM.permissionMatches:
fieldsList.append('permissions')
fields = getFieldsFromFieldsList(fieldsList)
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
shareddriveFields = []
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
if buildTree:
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), DLP, shareddriveFields, True, user, i, count)
if not status:
continue
gettingEntity = _getGettingEntity(user, fileIdEntity)
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=DLP.fileIdEntity['query'])
try:
feed = yieldGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy,
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
for files in feed:
extendFileTree(fileTree, files, DLP, stripCRsFromName)
extendFileTreeParents(drive, fileTree, fields)
DLP.GetLocationFileIdsFromTree(fileTree, fileIdEntity)
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
else:
fileTree = {}
user, drive, jcount = _validateUserGetFileIDs(origUser, i, count, fileIdEntity, drive=drive, entityType=Ent.DRIVE_FILE_OR_FOLDER)
if jcount == 0:
continue
if csvPF:
userInfo = {'User': user, 'index': 0, 'depth': 0, fileNameTitle: ''}
j = 0
Ind.Increment()
for fileId in fileIdEntity['list']:
j += 1
fileEntry = fileTree.get(fileId)
if fileEntry:
fileEntryInfo = fileEntry['info']
else:
try:
fileEntryInfo = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=fields, supportsAllDrives=True)
if (fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and fileEntryInfo.get('driveId') and
fileEntryInfo['name'] == TEAM_DRIVE and not fileEntryInfo.get('parents', [])):
fileEntryInfo['name'] = f"{SHARED_DRIVES}/{_getSharedDriveNameFromId(fileId)}"
if stripCRsFromName:
fileEntryInfo['name'] = _stripControlCharsFromName(fileEntryInfo['name'])
if buildTree:
fileTree[fileId] = {'info': fileEntryInfo}
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
continue
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
_showFileInfo(fileEntryInfo, -1, j, jcount)
Ind.Increment()
if buildTree:
_showDriveFolderContents(fileEntry, 0)
else:
_showChildDriveFolderContents(drive, fileEntryInfo, user, i, count, 0)
Ind.Decrement()
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Drive File Tree')
def getCreationModificationTimes(path_to_file):
"""
Try to get the date that a file was created, falling back to when it was
last modified if that isn't possible.
See http://stackoverflow.com/a/39501288/1709587 for explanation.
"""
mtime = os.path.getmtime(path_to_file)
if platform.system() == 'Windows':
ctime = os.path.getctime(path_to_file)
else:
stat = os.stat(path_to_file)
try:
ctime = stat.st_birthtime
except AttributeError:
# We're probably on Linux. No easy way to get creation dates here,
# so we'll settle for when its content was last modified.
ctime = stat.st_mtime
return (formatLocalSecondsTimestamp(ctime), formatLocalSecondsTimestamp(mtime))
def writeReturnIdLink(returnIdLink, mimeType, result):
if returnIdLink != 'editLink':
writeStdout(f'{result[returnIdLink]}\n')
return
if mimeType is None:
writeStdout(f'{result["webViewLink"]}\n')
return
for mt in MICROSOFT_FORMATS_LIST:
if mimeType == mt['mime']:
if mt['ext'][1] == 'd':
writeStdout(f'https://docs.google.com/document/d/{result["id"]}/edit\n')
return
if mt['ext'][1] == 'x':
writeStdout(f'https://docs.google.com/spreadsheets/d/{result["id"]}/edit\n')
return
if mt['ext'][1] == 'p':
writeStdout(f'https://docs.google.com/presentation/d/{result["id"]}/edit\n')
return
writeStdout(f'https://drive.google.com/file/d/{result["id"]}/edit\n')
def processFilenameReplacements(name, replacements):
for replacement in replacements:
name = re.sub(replacement[0], replacement[1], name)
return name
def addTimestampToFilename(parameters, body):
tdtime = datetime.datetime.now(GC.Values[GC.TIMEZONE])
body['name'] += ' - '
if not parameters[DFA_TIMEFORMAT]:
body['name'] += ISOformatTimeStamp(tdtime)
else:
body['name'] += tdtime.strftime(parameters[DFA_TIMEFORMAT])
createReturnItemMap = {
'returnidonly': 'id',
'returnlinkonly': 'webViewLink',
'returneditlinkonly': 'editLink'
}
# gam <UserTypeEntity> create drivefile
# [(localfile <FileName>|-)|(url <URL>)]
# [(drivefilename|newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
# [stripnameprefix <String>]
# [timestamp <Boolean>]] [timeformat <String>]
# <DriveFileCreateAttribute>* [noduplicate]
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*)) |
# (returnidonly|returnlinkonly|returneditlinkonly|showdetails)]
def createDriveFile(users):
csvPF = media_body = None
addCSVData = {}
returnIdLink = None
noDuplicate = showDetails = False
body = {}
newName = None
assignLocalName = True
parameters = initDriveFileAttributes()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'drivefilename', 'newfilename'}:
newName = getString(Cmd.OB_DRIVE_FILE_NAME)
assignLocalName = False
elif myarg in createReturnItemMap:
returnIdLink = createReturnItemMap[myarg]
showDetails = False
elif myarg == 'showdetails':
returnIdLink = None
showDetails = True
elif myarg == 'noduplicate':
noDuplicate = True
elif myarg == 'csv':
csvPF = CSVPrintFile()
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
getDriveFileAttribute(myarg, body, parameters, False)
if assignLocalName and parameters[DFA_LOCALFILENAME] and parameters[DFA_LOCALFILENAME] != '-':
newName = parameters[DFA_LOCALFILENAME]
if newName:
if parameters[DFA_STRIPNAMEPREFIX] and newName.startswith(parameters[DFA_STRIPNAMEPREFIX]):
newName = newName[len(parameters[DFA_STRIPNAMEPREFIX]):]
if parameters[DFA_REPLACEFILENAME]:
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
else:
body['name'] = newName
else:
body['name'] = 'Untitled'
if parameters[DFA_TIMESTAMP]:
addTimestampToFilename(parameters, body)
if parameters[DFA_LOCALFILEPATH]:
if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
setPreservedFileTimes(body, parameters, False)
if body.get('mimeType') == MIMETYPE_GA_SCRIPT_JSON:
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
media_body = getMediaBody(parameters)
elif parameters[DFA_URL]:
media_body = getMediaBody(parameters)
body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
if csvPF:
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
csvPF.SetTitles(['User', fileNameTitle, 'id'])
if showDetails:
csvPF.AddTitles(['parentId', 'mimeType'])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
Act.Set(Act.CREATE)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if not _getDriveFileParentInfo(drive, user, i, count, body, parameters):
continue
entityType = _getEntityMimeType(body) if 'mimeType' in body else Ent.DRIVE_FILE
try:
if noDuplicate:
# Check for existing file/folder, do not duplicate
files = callGAPIitems(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
q=f"name = '{escapeDriveFileName(body['name'])}'and '{body['parents'][0]}' in parents and trashed = false",
fields='files(id)', **parameters['searchargs'])
if files:
entityActionNotPerformedWarning([Ent.USER, user, entityType, body['name'], Ent.DRIVE_PARENT_FOLDER_ID, body['parents'][0]],
f"{Msg.DUPLICATE} IDs {','.join([file['id'] for file in files])}", i, count)
continue
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.PERMISSION_DENIED, GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
GAPI.UPLOAD_TOO_LARGE, GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
ocrLanguage=parameters[DFA_OCRLANGUAGE],
ignoreDefaultVisibility=parameters[DFA_IGNORE_DEFAULT_VISIBILITY],
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
media_body=media_body, body=body, fields='id,name,mimeType,parents,webViewLink', supportsAllDrives=True)
parentId = result['parents'][0] if 'parents' in result and result['parents'] else UNKNOWN
if returnIdLink:
writeReturnIdLink(returnIdLink, parameters[DFA_LOCALMIMETYPE], result)
elif not csvPF:
kvList = [Ent.USER, user]
if not showDetails:
titleInfo = f'{result["name"]}({result["id"]})'
if parameters[DFA_LOCALFILENAME]:
kvList.extend([Ent.DRIVE_FILE, titleInfo])
else:
kvList.extend([_getEntityMimeType(result), titleInfo])
else:
if result['mimeType'] != MIMETYPE_GA_FOLDER:
kvList.extend([Ent.DRIVE_FILE, result['name'], Ent.DRIVE_FILE_ID, result['id']])
else:
kvList.extend([Ent.DRIVE_FOLDER, result['name'], Ent.DRIVE_FOLDER_ID, result['id']])
kvList.extend([Ent.DRIVE_PARENT_FOLDER_ID, parentId, Ent.MIMETYPE, result['mimeType']])
if media_body:
entityModifierNewValueActionPerformed(kvList, Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME] or parameters[DFA_URL], i, count)
else:
entityActionPerformed(kvList, i, count)
else:
row = {'User': user, fileNameTitle: result['name'], 'id': result['id']}
if showDetails:
row.update({'parentId': parentId, 'mimeType': result['mimeType']})
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.invalidQuery, GAPI.permissionDenied, GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
GAPI.uploadTooLarge, GAPI.teamDrivesShortcutFileNotSupported) as e:
entityActionFailedWarning([Ent.USER, user, entityType, body['name']], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Files')
# gam <UserTypeEntity> create drivefolderpath
# [pathdelimiter <Character>]
# ((fullpath <DriveFolderPath) |
# (path <DriveFolderPath [<DriveFileParentAttribute>]) |
# (list <DriveFolderNameList>) [<DriveFileParentAttribute>]))
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) |
# returnidonly]
def createDriveFolderPath(users):
csvPF = None
addCSVData = {}
fullPath = parentSpecified = returnIdOnly = False
parentBody = {}
parentParms = initDriveFileAttributes()
driveFolderNameList = []
pathDelimiter = '/'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'fullpath':
folderPathLocation = Cmd.Location()
driveFolderNameList = getString(Cmd.OB_DRIVE_FOLDER_PATH).lstrip(pathDelimiter).strip(' ').split(pathDelimiter)
if len(driveFolderNameList) > 0:
if driveFolderNameList[0].lower() == MY_DRIVE.lower():
parentParms[DFA_PARENTID] = ROOT
driveFolderNameList = driveFolderNameList[1:]
elif driveFolderNameList[0].lower() == SHARED_DRIVES.lower() and len(driveFolderNameList) > 1:
parentParms[DFA_SHAREDDRIVE_PARENT] = driveFolderNameList[1]
driveFolderNameList = driveFolderNameList[2:]
else:
usageErrorExit(Msg.FULL_PATH_MUST_START_WITH_DRIVE.format(MY_DRIVE, f'{SHARED_DRIVES}{pathDelimiter}<SharedDriveName>'))
fullPath = True
elif myarg == 'path':
folderPathLocation = Cmd.Location()
driveFolderNameList = getString(Cmd.OB_DRIVE_FOLDER_PATH).strip(' ').split(pathDelimiter)
elif myarg == 'list':
folderPathLocation = Cmd.Location()
driveFolderNameList = shlexSplitList(getString(Cmd.OB_DRIVE_FOLDER_NAME_LIST), dataDelimiter=',')
elif getDriveFileParentAttribute(myarg, parentParms):
parentSpecified = True
elif myarg == 'returnidonly':
returnIdOnly = True
elif myarg == 'csv':
csvPF = CSVPrintFile(['User', 'name', 'id', 'status', 'pathIndex', 'pathLength'], 'sortall')
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
unknownArgumentExit()
if not driveFolderNameList:
Cmd.SetLocation(folderPathLocation)
emptyArgumentExit('fullpath|path|list')
if fullPath and parentSpecified:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('fullpath', '<DriveFileParentAttribute>'))
for folderName in driveFolderNameList:
if not folderName.strip():
Cmd.SetLocation(folderPathLocation)
usageErrorExit(Msg.ALL_FOLDER_NAMES_MUST_BE_NON_BLANK.format(driveFolderNameList))
if csvPF:
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms,
defaultToRoot=True, entityType=Ent.DRIVE_FOLDER):
continue
parentId = parentBody['parents'][0]
if parentParms.get('searchargs', {}).get('corpora', ''):
query = ANY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS
else:
query = MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS
errors = False
createOnly = False
if not returnIdOnly and not csvPF:
entityPerformAction([Ent.USER, user, Ent.DRIVE_FOLDER_PATH, ''], i, count)
jcount = len(driveFolderNameList)
Ind.Increment()
j = 0
for folderName in driveFolderNameList:
j += 1
try:
folderFound = False
if not createOnly:
op = 'Find Folder'
result = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INSUFFICIENT_PERMISSIONS],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query.format(escapeDriveFileName(folderName), parentId),
supportsAllDrives=True, includeItemsFromAllDrives=True,
fields='nextPageToken,files(id,name)')
if result:
if len(result) > 1:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], f'{op}: {len(result)} Folders with same name')
errors = True
break
parentId = result[0]['id']
parentName = result[0]['name']
folderFound = True
Act.Set(Act.EXISTS)
if not folderFound:
op = 'Create Folder'
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.UNKNOWN_ERROR, GAPI.BAD_REQUEST,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
body={'parents': [parentId], 'name': folderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id,name', supportsAllDrives=True)
parentId = result['id']
parentName = result['name']
createOnly = True
Act.Set(Act.CREATE)
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
GAPI.unknownError, GAPI.badRequest, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], f'{op}: {str(e)}', j, jcount)
errors = True
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
errors = True
break
if not returnIdOnly:
if not csvPF:
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER_NAME, f'{parentName}({parentId})'],
j, jcount)
else:
row = {'User': user, 'name': parentName, 'id': parentId, 'status': Act.Performed(), 'pathIndex': j, 'pathLength': jcount}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
if returnIdOnly and not errors:
writeStdout(f'{parentId}\n')
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Folders')
# gam <UserTypeEntity> create drivefileshortcut <DriveFileEntity> [shortcutname <String>]
# [<DriveFileParentAttribute>|convertparents]
# [(csv [todrive <ToDriveAttribute>*]) | returnidonly]
def createDriveFileShortcut(users):
csvPF = baseShortcutName = None
convertParents = newParentsSpecified = returnIdOnly = False
fileIdEntity = getDriveFileEntity()
parentBody = {}
parentParms = initDriveFileAttributes()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'shortcutname':
baseShortcutName = getString(Cmd.OB_DRIVE_FILE_NAME)
elif myarg == 'returnidonly':
returnIdOnly = True
elif myarg == 'csv':
csvPF = CSVPrintFile(['User', 'name', 'id', 'targetName', 'targetId'], 'sortall')
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getDriveFileParentAttribute(myarg, parentParms):
if convertParents:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'convertparents'))
newParentsSpecified = True
elif myarg == 'convertparents':
if newParentsSpecified:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, '<DriveFileParentAttribute>'))
convertParents = True
else:
unknownArgumentExit()
if fileIdEntity['query']:
fileIdEntity['query'] = fileIdEntity['query']+AND_NOT_SHORTCUT
elif fileIdEntity['shareddrivefilequery']:
fileIdEntity['shareddrivefilequery'] = fileIdEntity['shareddrivefilequery']+AND_NOT_SHORTCUT
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
if not returnIdOnly and not csvPF:
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_SHORTCUT,
Act.MODIFIER_FOR, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
if jcount == 0:
continue
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
continue
if newParentsSpecified:
newParents = parentBody['parents']
numNewParents = len(newParents)
elif not convertParents:
try:
rootFolderId = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
newParents = [rootFolderId]
numNewParents = 1
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
Act.Set(Act.CREATE)
try:
target = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='mimeType,name,parents', supportsAllDrives=True)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.teamDrivesSharingRestrictionNotAllowed) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
targetName = target['name']
if baseShortcutName:
shortcutName = baseShortcutName.replace('#filename#', targetName)
else:
shortcutName = targetName
targetEntityType = _getEntityMimeType(target)
if convertParents:
newParents = target.get('parents', [])[:-1]
numNewParents = len(newParents)
if numNewParents <= 1:
entityActionNotPerformedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, None],
Msg.NO_PARENTS_TO_CONVERT_TO_SHORTCUTS, j, jcount)
continue
removeParents = []
body = {'name': shortcutName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': None, 'shortcutDetails': {'targetId': fileId}}
if not returnIdOnly and not csvPF:
entityPerformActionNumItems([Ent.USER, user, targetEntityType, targetName], numNewParents, Ent.DRIVE_FILE_SHORTCUT, j, jcount)
try:
existingShortcuts = callGAPI(drive.files(), 'list',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
retryReasons=[GAPI.UNKNOWN_ERROR],
supportsAllDrives=True, includeItemsFromAllDrives=True,
q=f"shortcutDetails.targetId = '{fileId}' and trashed = False", fields='files(id,name,parents)')['files']
except (GAPI.invalidQuery, GAPI.invalid, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
existingShortcuts = []
Ind.Increment()
k = 0
for parentId in newParents:
k += 1
duplicateShortcut = False
for shortcut in existingShortcuts:
if parentId in shortcut['parents'] and shortcutName == shortcut['name']:
entityActionNotPerformedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, f'{shortcut["name"]}({shortcut["id"]})'],
Msg.DUPLICATE, k, numNewParents)
duplicateShortcut = True
break
if duplicateShortcut:
continue
body['parents'] = [parentId]
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
GAPI.SHORTCUT_TARGET_INVALID],
body=body, fields='id,name', supportsAllDrives=True)
removeParents.append(parentId)
if returnIdOnly:
writeStdout(f'{result["id"]}\n')
elif not csvPF:
entityActionPerformed([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, f'{result["name"]}({result["id"]})'],
k, numNewParents)
else:
csvPF.WriteRow({'User': user, 'name': result['name'], 'id': result['id'], 'targetName': targetName, 'targetId': fileId})
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
entityActionFailedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), k, numNewParents)
Ind.Decrement()
if convertParents and removeParents:
if not returnIdOnly and not csvPF:
lcount = len(removeParents)
Act.Set(Act.DELETE)
entityPerformActionNumItems([Ent.USER, user, targetEntityType, targetName], lcount, Ent.DRIVE_PARENT_FOLDER_REFERENCE, j, jcount)
try:
callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
fileId=fileId,
removeParents=','.join(removeParents), body={}, fields='id', supportsAllDrives=True)
if not returnIdOnly and not csvPF:
Ind.Increment()
for l, parent in enumerate(removeParents):
entityActionPerformed([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_PARENT_FOLDER_REFERENCE, parent], l+1, lcount)
Ind.Decrement()
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.teamDrivesSharingRestrictionNotAllowed) as e:
entityActionFailedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_PARENT_FOLDER_REFERENCE, str(l)], str(e), j, jcount)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Shortcuts')
SHORTCUT_CODE_VALID = 0
SHORTCUT_CODE_SHORTCUT_NOT_FOUND = 1
SHORTCUT_CODE_NOT_A_SHORTCUT = 2
SHORTCUT_CODE_TARGET_NOT_FOUND = 3
SHORTCUT_CODE_MIMETYPE_MISMATCH = 4
# gam <UserTypeEntity> check drivefileshortcut <DriveFileEntity>
# [csv [todrive <ToDriveAttribute>*]]
def checkDriveFileShortcut(users):
csvPF = None
fileIdEntity = getDriveFileEntity()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'csv':
csvPF = CSVPrintFile(['User', 'name', 'id', 'owner', 'parentId', 'shortcutDetails.targetId', 'shortcutDetails.targetMimeType',
'targetName', 'targetId', 'targetMimeType', 'code'], 'sortall')
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
scfields = 'id,name,mimeType,owners(emailAddress),parents,shortcutDetails'
trfields = 'id,name,mimeType'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
if not csvPF:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DRIVE_FILE_SHORTCUT, i, count)
if jcount == 0:
continue
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
row = {'User': user, 'id': fileId}
try:
scresult = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=scfields, supportsAllDrives=True)
row['name'] = scresult['name']
if scresult['mimeType'] != MIMETYPE_GA_SHORTCUT:
if not csvPF:
entityActionFailedWarning([Ent.USER, user, _getEntityMimeType(scresult), fileId],
Msg.INVALID_MIMETYPE.format(scresult['mimeType'], MIMETYPE_GA_SHORTCUT), j, jcount)
else:
row['code'] = SHORTCUT_CODE_NOT_A_SHORTCUT
csvPF.WriteRow(row)
continue
if 'owners' in scresult:
row['owner'] = scresult['owners'][0]['emailAddress']
row['parentId'] = scresult['parents'][0]
row[f'shortcutDetails{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}targetId'] = scresult['shortcutDetails']['targetId']
row[f'shortcutDetails{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}targetMimeType'] = scresult['shortcutDetails']['targetMimeType']
trfileId = scresult['shortcutDetails']['targetId']
try:
trresult = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=trfileId, fields=trfields, supportsAllDrives=True)
row['targetName'] = trresult['name']
row['targetId'] = trresult['id']
row['targetMimeType'] = trresult['mimeType']
entityList = [Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, f"{scresult['name']}({fileId})",
_getEntityMimeType(trresult), f"{trresult['name']}({trfileId})"]
if scresult['shortcutDetails']['targetMimeType'] == trresult['mimeType']:
if not csvPF:
entityActionPerformed(entityList, j, jcount)
else:
row['code'] = SHORTCUT_CODE_VALID
else:
if not csvPF:
entityActionFailedWarning(entityList, Msg.MIMETYPE_MISMATCH.format(scresult['shortcutDetails']['targetMimeType'], trresult['mimeType']), j, jcount)
else:
row['code'] = SHORTCUT_CODE_MIMETYPE_MISMATCH
except GAPI.fileNotFound:
if not csvPF:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, f"{scresult['name']}({fileId})",
_getTargetEntityMimeType(scresult), trfileId], Msg.NOT_FOUND, j, jcount)
else:
row['code'] = SHORTCUT_CODE_TARGET_NOT_FOUND
except GAPI.fileNotFound:
if not csvPF:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, fileId], Msg.NOT_FOUND, j, jcount)
else:
row['code'] = SHORTCUT_CODE_SHORTCUT_NOT_FOUND
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if csvPF:
csvPF.WriteRow(row)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Check Shortcuts')
# gam <UserTypeEntity> update drivefile <DriveFileEntity> [copy] [returnidonly|returnlinkonly]
# [(localfile <FileName>|-)|(url <URL>)]
# [retainname | (newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
# [stripnameprefix <String>]
# [timestamp <Boolean>]] [timeformat <String>]
# <DriveFileUpdateAttribute>*
# [(gsheet|csvsheet <SheetEntity> [clearfilter])|(addsheet <String>)]
# [charset <String>] [columndelimiter <Character>]
def updateDriveFile(users):
fileIdEntity = getDriveFileEntity()
body = {}
newName = None
assignLocalName = True
parameters = initDriveFileAttributes()
media_body = None
addSheetBody = addSheetEntity = None
updateSheetEntity = None
clearFilter = False
preserveModifiedTime = False
encoding = GC.Values[GC.CHARSET]
columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
returnIdLink = None
operation = 'update'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'copy':
operation = 'copy'
Act.Set(Act.COPY)
elif myarg == 'returnidonly':
returnIdLink = 'id'
elif myarg == 'returnlinkonly':
returnIdLink = 'webViewLink'
elif myarg == 'retainname':
assignLocalName = False
elif myarg == 'newfilename':
newName = getString(Cmd.OB_DRIVE_FILE_NAME)
assignLocalName = False
elif getDriveFileAddRemoveParentAttribute(myarg, parameters):
pass
elif myarg == 'addsheet':
if updateSheetEntity:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'csvsheet'))
sheetTitle = getString(Cmd.OB_STRING)
addSheetEntity = {'sheetType': Ent.SHEET, 'sheetValue': sheetTitle, 'sheetId': None, 'sheetTitle': sheetTitle}
addSheetBody = {'requests': [{'addSheet': {'properties': {'title': sheetTitle, 'sheetType': 'GRID'}}}]}
elif myarg in {'gsheet', 'csvsheet'}:
if addSheetEntity:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'addsheet'))
updateSheetEntity = getSheetEntity(False)
elif myarg == 'clearfilter':
clearFilter = getBoolean()
elif myarg == 'charset':
encoding = getString(Cmd.OB_CHAR_SET)
elif myarg == 'columndelimiter':
columnDelimiter = getCharacter()
else:
getDriveFileAttribute(myarg, body, parameters, True)
if assignLocalName and parameters[DFA_LOCALFILENAME] and parameters[DFA_LOCALFILENAME] != '-':
newName = parameters[DFA_LOCALFILENAME]
if newName:
if parameters[DFA_STRIPNAMEPREFIX] and newName.startswith(parameters[DFA_STRIPNAMEPREFIX]):
newName = newName[len(parameters[DFA_STRIPNAMEPREFIX]):]
if operation == 'update' and parameters[DFA_LOCALFILEPATH]:
if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
setPreservedFileTimes(body, parameters, True)
if not addSheetEntity and not updateSheetEntity:
media_body = getMediaBody(parameters)
elif operation == 'update' and parameters[DFA_URL]:
addSheetEntity = updateSheetEntity = None
media_body = getMediaBody(parameters)
body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
elif operation == 'update' and parameters[DFA_PRESERVE_FILE_TIMES]:
preserveModifiedTime = True
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=Ent.DRIVE_FILE_OR_FOLDER if returnIdLink is None else None)
if jcount == 0:
continue
if not _getDriveFileParentInfo(drive, user, i, count, body, parameters, defaultToRoot=False):
continue
newParents = body.pop('parents', [])
if operation == 'update':
status, addParentsBase, removeParentsBase = _getDriveFileAddRemoveParentInfo(user, i, count, parameters, drive)
if not status:
continue
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
addParents = addParentsBase[:]
removeParents = removeParentsBase[:]
if newParents or (not newName and parameters[DFA_REPLACEFILENAME]) or preserveModifiedTime:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,parents,modifiedTime', supportsAllDrives=True)
if newParents:
addParents.extend(newParents)
removeParents.extend(result.get('parents', []))
if not newName and parameters[DFA_REPLACEFILENAME]:
body['name'] = processFilenameReplacements(result['name'], parameters[DFA_REPLACEFILENAME])
if preserveModifiedTime:
body['modifiedTime'] = result['modifiedTime']
if newName:
if parameters[DFA_REPLACEFILENAME]:
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
else:
body['name'] = newName
if parameters[DFA_TIMESTAMP]:
addTimestampToFilename(parameters, body)
if addSheetEntity or updateSheetEntity:
entityValueList = [Ent.USER, user, Ent.DRIVE_FILE_ID, fileId]
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
fileId=fileId, fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
entityActionNotPerformedWarning(entityValueList, f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}', j, jcount)
continue
if not result['capabilities']['canEdit']:
entityActionNotPerformedWarning(entityValueList, Msg.NOT_WRITABLE, j, jcount)
continue
_, sheet = buildGAPIServiceObject(API.SHEETS, user)
if sheet is None:
continue
if addSheetEntity:
addresult = callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, body=addSheetBody)
sheetEntity = addSheetEntity.copy()
sheetEntity['sheetId'] = addresult['replies'][0]['addSheet']['properties']['sheetId']
entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetTitle']])
sheetEntity['sheetType'] = Ent.SHEET_ID # Temporarily set addsheet type to ID
else:
sheetEntity = updateSheetEntity.copy()
entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetValue']])
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId,
fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
if sheetId is None:
entityActionNotPerformedWarning(entityValueList, Msg.NOT_FOUND, j, jcount)
continue
if protectedSheetId(spreadsheet, sheetId):
entityActionNotPerformedWarning(entityValueList, Msg.NOT_WRITABLE, j, jcount)
continue
if addSheetEntity: # Restore addsheet type
sheetEntity['sheetType'] = Ent.SHEET
result = callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT,
GAPI.TEAMDRIVES_PARENT_LIMIT,
GAPI.TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED,
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED],
fileId=fileId,
ocrLanguage=parameters[DFA_OCRLANGUAGE],
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
addParents=','.join(addParents), removeParents=','.join(removeParents),
body=body, fields='id,name,mimeType,webViewLink', supportsAllDrives=True)
### File size check??
sbody = {'requests': []}
if clearFilter and updateSheetEntity:
sbody['requests'].append({'clearBasicFilter': {'sheetId': sheetId}})
sbody['requests'].append({'updateCells': {'range': {'sheetId': sheetId}, 'fields': '*'}})
sbody['requests'].append({'pasteData': {'coordinate': {'sheetId': sheetId, 'rowIndex': '0', 'columnIndex': '0'},
'data': readFile(parameters[DFA_LOCALFILEPATH], encoding=encoding),
'type': 'PASTE_NORMAL', 'delimiter': columnDelimiter}})
callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, body=sbody)
if returnIdLink:
writeStdout(f'{result[returnIdLink]}\n')
else:
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']],
Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME], j, jcount)
except GAPI.fileNotFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_ID, fileId], str(e), j, jcount)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning(entityValueList, str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
else:
result = callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.FILE_NEVER_WRITABLE, GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT,
GAPI.SHARE_OUT_NOT_PERMITTED, GAPI.SHARE_OUT_NOT_PERMITTED_TO_USER,
GAPI.TEAMDRIVES_PARENT_LIMIT, GAPI.TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED,
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.CROSS_DOMAIN_MOVE_RESTRICTION, GAPI.UPLOAD_TOO_LARGE,
GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED,
GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED],
fileId=fileId,
ocrLanguage=parameters[DFA_OCRLANGUAGE],
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
addParents=','.join(addParents), removeParents=','.join(removeParents),
media_body=media_body, body=body, fields='id,name,mimeType,webViewLink',
supportsAllDrives=True)
if result:
if returnIdLink:
writeStdout(f'{result[returnIdLink]}\n')
elif media_body:
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME] or parameters[DFA_URL], j, jcount)
else:
entityActionPerformed([Ent.USER, user, _getEntityMimeType(result), result['name']], j, jcount)
else:
if returnIdLink:
writeStdout(f'{fileId}\n')
else:
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.unknownError, GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
GAPI.fileNeverWritable, GAPI.cannotModifyViewersCanCopyContent,
GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.teamDrivesParentLimit, GAPI.teamDrivesFolderMoveInNotSupported,
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.crossDomainMoveRestriction,
GAPI.uploadTooLarge, GAPI.teamDrivesShortcutFileNotSupported,
GAPI.fileOwnerNotMemberOfWriterDomain, GAPI.fileWriterTeamDriveMoveInDisabled) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
else:
if newName:
if parameters[DFA_REPLACEFILENAME]:
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
else:
body['name'] = newName
if parameters[DFA_TIMESTAMP]:
addTimestampToFilename(parameters, body)
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
result = callGAPI(drive.files(), 'copy',
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT],
fileId=fileId,
ignoreDefaultVisibility=parameters[DFA_IGNORE_DEFAULT_VISIBILITY],
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
body=body, fields='id,name,webViewLink', supportsAllDrives=True)
if returnIdLink:
writeReturnIdLink(returnIdLink, parameters[DFA_LOCALMIMETYPE], result)
else:
entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, fileId],
Act.MODIFIER_TO, result['name'], [Ent.DRIVE_FILE_ID, result['id']], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
GAPI.cannotModifyViewersCanCopyContent) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, fileId], str(e), j, jcount)
except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, fileId], str(e), j, jcount)
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
STAT_FOLDER_TOTAL = 0
STAT_FOLDER_COPIED_MOVED = 1
STAT_FOLDER_SHORTCUT_CREATED = 2
STAT_FOLDER_SHORTCUT_EXISTS = 3
STAT_FOLDER_MERGED = 4
STAT_FOLDER_DUPLICATE = 5
STAT_FOLDER_FAILED = 6
STAT_FOLDER_NOT_WRITABLE = 7
STAT_FOLDER_PERMISSIONS_FAILED = 8
STAT_FILE_TOTAL = 9
STAT_FILE_COPIED_MOVED = 10
STAT_FILE_SHORTCUT_CREATED = 11
STAT_FILE_SHORTCUT_EXISTS = 12
STAT_FILE_DUPLICATE = 13
STAT_FILE_FAILED = 14
STAT_FILE_NOT_COPYABLE_MOVABLE = 15
STAT_FILE_IN_SKIPIDS = 16
STAT_FILE_PERMISSIONS_FAILED = 17
STAT_FILE_PROTECTEDRANGES_FAILED = 18
STAT_USER_NOT_ORGANIZER = 19
STAT_LENGTH = 20
FOLDER_SUBTOTAL_STATS = [STAT_FOLDER_COPIED_MOVED, STAT_FOLDER_SHORTCUT_CREATED, STAT_FOLDER_SHORTCUT_EXISTS,
STAT_FOLDER_DUPLICATE, STAT_FOLDER_MERGED, STAT_FOLDER_FAILED, STAT_FOLDER_NOT_WRITABLE]
FILE_SUBTOTAL_STATS = [STAT_FILE_COPIED_MOVED, STAT_FILE_SHORTCUT_CREATED, STAT_FILE_SHORTCUT_EXISTS,
STAT_FILE_DUPLICATE, STAT_FILE_FAILED, STAT_FILE_NOT_COPYABLE_MOVABLE, STAT_FILE_IN_SKIPIDS]
def _initStatistics():
return [0] * STAT_LENGTH
def _incrStatistic(statistics, stat):
statistics[stat] += 1
if stat in FOLDER_SUBTOTAL_STATS:
statistics[STAT_FOLDER_TOTAL] += 1
elif stat in FILE_SUBTOTAL_STATS:
statistics[STAT_FILE_TOTAL] += 1
def _printStatistics(user, statistics, i, count, copy):
if statistics[STAT_FOLDER_TOTAL]:
if copy:
printEntityKVList([Ent.USER, user],
[Ent.Plural(Ent.DRIVE_FOLDER),
Msg.STATISTICS_COPY_FOLDER.format(statistics[STAT_FOLDER_TOTAL],
statistics[STAT_FOLDER_COPIED_MOVED],
statistics[STAT_FOLDER_SHORTCUT_CREATED],
statistics[STAT_FOLDER_SHORTCUT_EXISTS],
statistics[STAT_FOLDER_DUPLICATE],
statistics[STAT_FOLDER_MERGED],
statistics[STAT_FOLDER_FAILED],
statistics[STAT_FOLDER_NOT_WRITABLE],
statistics[STAT_FOLDER_PERMISSIONS_FAILED])],
i, count)
else:
printEntityKVList([Ent.USER, user],
[Ent.Plural(Ent.DRIVE_FOLDER),
Msg.STATISTICS_MOVE_FOLDER.format(statistics[STAT_FOLDER_TOTAL],
statistics[STAT_FOLDER_COPIED_MOVED],
statistics[STAT_FOLDER_SHORTCUT_CREATED],
statistics[STAT_FOLDER_SHORTCUT_EXISTS],
statistics[STAT_FOLDER_DUPLICATE],
statistics[STAT_FOLDER_MERGED],
statistics[STAT_FOLDER_FAILED],
statistics[STAT_FOLDER_NOT_WRITABLE])],
i, count)
if statistics[STAT_FILE_TOTAL]:
if copy:
printEntityKVList([Ent.USER, user],
[Ent.Plural(Ent.DRIVE_FILE),
Msg.STATISTICS_COPY_FILE.format(statistics[STAT_FILE_TOTAL],
statistics[STAT_FILE_COPIED_MOVED],
statistics[STAT_FILE_SHORTCUT_CREATED],
statistics[STAT_FILE_SHORTCUT_EXISTS],
statistics[STAT_FILE_DUPLICATE],
statistics[STAT_FILE_FAILED],
statistics[STAT_FILE_NOT_COPYABLE_MOVABLE],
statistics[STAT_FILE_IN_SKIPIDS],
statistics[STAT_FILE_PERMISSIONS_FAILED],
statistics[STAT_FILE_PROTECTEDRANGES_FAILED])],
i, count)
else:
printEntityKVList([Ent.USER, user],
[Ent.Plural(Ent.DRIVE_FILE),
Msg.STATISTICS_MOVE_FILE.format(statistics[STAT_FILE_TOTAL],
statistics[STAT_FILE_COPIED_MOVED],
statistics[STAT_FILE_SHORTCUT_CREATED],
statistics[STAT_FILE_SHORTCUT_EXISTS],
statistics[STAT_FILE_DUPLICATE],
statistics[STAT_FILE_FAILED],
statistics[STAT_FILE_NOT_COPYABLE_MOVABLE])],
i, count)
if statistics[STAT_USER_NOT_ORGANIZER]:
printEntityKVList([Ent.USER, user],
[Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER),
Msg.STATISTICS_USER_NOT_ORGANIZER.format(statistics[STAT_USER_NOT_ORGANIZER])],
i, count)
DUPLICATE_FILE_OVERWRITE_OLDER = 0
DUPLICATE_FILE_OVERWRITE_ALL = 1
DUPLICATE_FILE_DUPLICATE_NAME = 2
DUPLICATE_FILE_UNIQUE_NAME = 3
DUPLICATE_FILE_SKIP = 4
DUPLICATE_FOLDER_MERGE = 0
DUPLICATE_FOLDER_DUPLICATE_NAME = 1
DUPLICATE_FOLDER_UNIQUE_NAME = 2
DUPLICATE_FOLDER_SKIP = 3
DEST_PARENT_MYDRIVE_ROOT = 0
DEST_PARENT_MYDRIVE_FOLDER = 1
DEST_PARENT_SHAREDDRIVE_ROOT = 2
DEST_PARENT_SHAREDDRIVE_FOLDER = 3
COPY_NONINHERITED_PERMISSIONS_NEVER = 0
COPY_NONINHERITED_PERMISSIONS_ALWAYS = 1
COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS = 2
COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS = 3
COPY_NONINHERITED_PERMISSIONS_CHOICES_MAP = {
'never': COPY_NONINHERITED_PERMISSIONS_NEVER,
'always': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
'syncallfolders': COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS,
'syncupdatedfolders': COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS,
}
def initCopyMoveOptions(copyCmd):
return {
'copyCmd': copyCmd,
'sourceDriveId': None,
'destDriveId': None,
'destParentType': False,
'newFilename': None,
'stripNamePrefix': None,
'replaceFilename': [],
'summary': False,
'mergeWithParent': False,
'mergeWithParentRetain': False,
'retainSourceFolders': False,
'sourceIsMyDriveSharedDrive': False,
'showPermissionMessages': False,
'sendEmailIfRequired': False,
'useDomainAdminAccess': False,
'enforceExpansiveAccess': False,
'copiedShortcutsPointToCopiedFiles': True,
'createShortcutsForNonmovableFiles': False,
'duplicateFiles': DUPLICATE_FILE_OVERWRITE_OLDER,
'duplicateFolders': DUPLICATE_FOLDER_MERGE,
'copyFilePermissions': False,
'copyFileInheritedPermissions': True,
'copyFileNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
'copyMergeWithParentFolderPermissions': False,
'copyMergedTopFolderPermissions': copyCmd,
'copyMergedSubFolderPermissions': copyCmd,
'copyTopFolderPermissions': True,
'copySubFolderPermissions': True,
'copyTopFolderInheritedPermissions': True,
'copySubFolderInheritedPermissions': True,
'copyTopFolderNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
'copySubFolderNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
'noCopyNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_NEVER,
'excludePermissionsFromDomains': set(),
'includePermissionsFromDomains': set(),
'mapPermissionsDomains': {},
'copySheetProtectedRangesInheritedPermissions': False,
'copySheetProtectedRangesNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_NEVER,
'copySubFiles': True,
'copySubFolders': True,
'copySubShortcuts': True,
'fileNameMatchPattern': None,
'folderNameMatchPattern': None,
'shortcutNameMatchPattern': None,
'fileMimeTypes': set(),
'notMimeTypes': False,
'copySubFilesOwnedBy': None,
'copyPermissionRoles': set(DRIVEFILE_ACL_ROLES_MAP.values()),
'copyPermissionTypes': set(DRIVEFILE_ACL_PERMISSION_TYPES),
}
DUPLICATE_FILE_CHOICES = {
'overwriteall': DUPLICATE_FILE_OVERWRITE_ALL,
'overwriteolder': DUPLICATE_FILE_OVERWRITE_OLDER,
'duplicatename': DUPLICATE_FILE_DUPLICATE_NAME,
'uniquename': DUPLICATE_FILE_UNIQUE_NAME,
'skip': DUPLICATE_FILE_SKIP,
}
DUPLICATE_FOLDER_CHOICES = {
'merge': DUPLICATE_FOLDER_MERGE,
'duplicatename': DUPLICATE_FOLDER_DUPLICATE_NAME,
'uniquename': DUPLICATE_FOLDER_UNIQUE_NAME,
'skip': DUPLICATE_FOLDER_SKIP,
}
def getCopyMoveOptions(myarg, copyMoveOptions):
# Copy/Move arguments
if myarg == 'newfilename':
copyMoveOptions['newFilename'] = getString(Cmd.OB_DRIVE_FILE_NAME)
elif myarg =='stripnameprefix':
copyMoveOptions['stripNamePrefix'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'replacefilename':
copyMoveOptions['replaceFilename'].append(getREPatternSubstitution(re.IGNORECASE))
elif myarg == 'showpermissionmessages':
copyMoveOptions['showPermissionMessages'] = getBoolean()
elif myarg == 'sendemailifrequired':
copyMoveOptions['sendEmailIfRequired'] = getBoolean()
elif myarg == 'summary':
copyMoveOptions['summary'] = getBoolean()
elif myarg == 'mergewithparent':
copyMoveOptions['mergeWithParent'] = getBoolean()
if copyMoveOptions['mergeWithParent']:
copyMoveOptions['mergeWithParentRetain'] = False
elif myarg == 'duplicatefiles':
copyMoveOptions['duplicateFiles'] = getChoice(DUPLICATE_FILE_CHOICES, mapChoice=True)
elif myarg == 'duplicatefolders':
copyMoveOptions['duplicateFolders'] = getChoice(DUPLICATE_FOLDER_CHOICES, mapChoice=True)
elif myarg == 'copymergewithparentfolderpermissions':
copyMoveOptions['copyMergeWithParentFolderPermissions'] = getBoolean()
elif myarg == 'copymergedtopfolderpermissions':
copyMoveOptions['copyMergedTopFolderPermissions'] = getBoolean()
elif myarg == 'copytopfolderpermissions':
copyMoveOptions['copyTopFolderPermissions'] = getBoolean()
elif myarg == 'copytopfolderinheritedpermissions':
copyMoveOptions['copyTopFolderInheritedPermissions'] = getBoolean()
elif myarg == 'copytopfoldernoninheritedpermissions':
copyMoveOptions['copyTopFolderNonInheritedPermissions'] = getChoice(COPY_NONINHERITED_PERMISSIONS_CHOICES_MAP, mapChoice=True)
elif myarg == 'copymergedsubfolderpermissions':
copyMoveOptions['copyMergedSubFolderPermissions'] = getBoolean()
elif myarg == 'copysubfolderpermissions':
copyMoveOptions['copySubFolderPermissions'] = getBoolean()
elif myarg == 'copysubfolderinheritedpermissions':
copyMoveOptions['copySubFolderInheritedPermissions'] = getBoolean()
elif myarg == 'copysubfoldernoninheritedpermissions':
copyMoveOptions['copySubFolderNonInheritedPermissions'] = getChoice(COPY_NONINHERITED_PERMISSIONS_CHOICES_MAP, mapChoice=True)
elif myarg == 'excludepermissionsfromdomains':
copyMoveOptions['excludePermissionsFromDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).lower().replace(',', ' ').split())
copyMoveOptions['includePermissionsFromDomains'] = set()
elif myarg == 'includepermissionsfromdomains':
copyMoveOptions['includePermissionsFromDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).lower().replace(',', ' ').split())
copyMoveOptions['excludePermissionsFromDomains'] = set()
elif myarg == 'mappermissionsdomain':
oldDomain = getString(Cmd.OB_DOMAIN_NAME).lower()
copyMoveOptions['mapPermissionsDomains'][oldDomain] = getString(Cmd.OB_DOMAIN_NAME).lower()
elif myarg == 'enforceexpansiveaccess':
copyMoveOptions['enforceExpansiveAccess'] = getBoolean()
else:
# Move arguments
if not copyMoveOptions['copyCmd']:
if myarg == 'retainsourcefolders':
copyMoveOptions['retainSourceFolders'] = getBoolean()
elif myarg == 'mergewithparentretain':
copyMoveOptions['mergeWithParentRetain'] = getBoolean()
if copyMoveOptions['mergeWithParentRetain']:
copyMoveOptions['mergeWithParent'] = False
elif myarg == 'createshortcutsfornonmovablefiles':
copyMoveOptions['createShortcutsForNonmovableFiles'] = getBoolean()
else:
return False
# Copy arguments
else:
if myarg == 'copiedshortcutspointtocopiedfiles':
copyMoveOptions['copiedShortcutsPointToCopiedFiles'] = getBoolean()
elif myarg == 'copyfilepermissions':
copyMoveOptions['copyFilePermissions'] = getBoolean()
elif myarg == 'copyfileinheritedpermissions':
copyMoveOptions['copyFileInheritedPermissions'] = getBoolean()
elif myarg == 'copyfilenoninheritedpermissions':
copyMoveOptions['copyFileNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_ALWAYS if getBoolean() else COPY_NONINHERITED_PERMISSIONS_NEVER
elif myarg == 'copypermissionroles':
copyMoveOptions['copyPermissionRoles'] = set()
for prole in getString(Cmd.OB_PERMISSION_ROLE_LIST).lower().replace(',', ' ').split():
if prole in DRIVEFILE_ACL_ROLES_MAP:
copyMoveOptions['copyPermissionRoles'].add(DRIVEFILE_ACL_ROLES_MAP[prole])
else:
invalidChoiceExit(prole, DRIVEFILE_ACL_ROLES_MAP, True)
elif myarg == 'copypermissiontypes':
copyMoveOptions['copyPermissionTypes'] = set()
for ptype in getString(Cmd.OB_PERMISSION_TYPE_LIST).lower().replace(',', ' ').split():
if ptype in DRIVEFILE_ACL_PERMISSION_TYPES:
copyMoveOptions['copyPermissionTypes'].add(ptype)
else:
invalidChoiceExit(ptype, DRIVEFILE_ACL_PERMISSION_TYPES, True)
elif myarg == 'copysheetprotectedranges':
if getBoolean():
copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] = True
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_ALWAYS
else:
copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] = False
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_NEVER
elif myarg == 'copysheetprotectedrangesinheritedpermissions':
copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] = getBoolean()
elif myarg == 'copysheetprotectedrangesnoninheritedpermissions':
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_ALWAYS if getBoolean() else COPY_NONINHERITED_PERMISSIONS_NEVER
elif myarg == 'copysubfiles':
copyMoveOptions['copySubFiles'] = getBoolean()
elif myarg == 'copysubfolders':
copyMoveOptions['copySubFolders'] = getBoolean()
elif myarg == 'copysubshortcuts':
copyMoveOptions['copySubShortcuts'] = getBoolean()
elif myarg == 'filenamematchpattern':
copyMoveOptions['fileNameMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'foldernamematchpattern':
copyMoveOptions['folderNameMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'shortcutnamematchpattern':
copyMoveOptions['shortcutNameMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'filemimetype':
copyMoveOptions['notMimeTypes'] = checkArgumentPresent('not')
for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split():
copyMoveOptions['fileMimeTypes'].add(validateMimeType(mimeType))
elif myarg == 'copysubfilesownedby':
copyMoveOptions['copySubFilesOwnedBy'] = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True)
else:
return False
return True
def _targetFilenameExists(destFilename, mimeType, targetChildren):
destFilenameLower = destFilename.lower()
for target in targetChildren:
if destFilenameLower == target['name'].lower() and mimeType == target['mimeType']:
return target
return None
UNIQUE_PREFIX_PATTERN = re.compile(r'^(.+)\((\d+)\)$')
def _getFilenameParts(destFilename):
base, ext = os.path.splitext(destFilename)
if ext and base.endswith('.tar'):
ext = '.tar'+ext
base = base[:-4]
elif len(ext) > 5:
base = destFilename
ext = ''
mg = UNIQUE_PREFIX_PATTERN.match(base)
if mg:
return (mg.group(1), int(mg.group(2)), ext)
return (base, 0, ext)
def _getFilenamePrefix(destFilename):
base, _, _ = _getFilenameParts(destFilename)
return base
def _getUniqueFilename(destFilename, mimeType, targetChildren):
if _targetFilenameExists(destFilename, mimeType, targetChildren) is None:
return destFilename
base, _, ext = _getFilenameParts(destFilename)
n = 0
for target in targetChildren:
tbase, tcnt, text = _getFilenameParts(target['name'])
if base != tbase or ext != text:
continue
if tcnt > n:
n = tcnt
if ext:
return f'{base}({n+1}){ext}'
return f'{base}({n+1})'
def _copyPermissions(drive, user, i, count, j, jcount,
entityType, fileId, fileTitle, newFileId, newFileTitle,
statistics, stat, copyMoveOptions, atTop, copyInherited, copyNonInherited,
updateOwner):
def getPermissions(fid):
permissions = {}
try:
result = callGAPIpages(drive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fileId=fid,
fields='nextPageToken,permissions(allowFileDiscovery,domain,emailAddress,expirationTime,id,role,type,deleted,view,pendingOwner,permissionDetails)',
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], supportsAllDrives=True)
for permission in result:
permission.pop('teamDrivePermissionDetails', None)
permission['inherited'] = permission.pop('permissionDetails', [{'inherited': False}])[0]['inherited']
permissions[permission['id']] = permission
return permissions
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileTitle], str(e), j, jcount)
_incrStatistic(statistics, stat)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
return None
def permissionKVList(user, entityType, title, permission):
permstr = f"{['noninherited', 'inherited'][permission['inherited']]}/{permission['role']}/{permission['type']}"
if permission['type'] in {'group', 'user'}:
permstr += f"/{permission['emailAddress']}"
elif permission['type'] == 'domain':
permstr += f"/{permission['domain']}"
return [Ent.USER, user, entityType, title, Ent.PERMISSION, permstr]
def isPermissionCopyable(kvList, permission):
role = permission['role']
emailAddress = permission.get('emailAddress', '')
permissionType = permission['type']
domain = ''
if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']:
if permissionType in {'group', 'user'}:
atLoc = emailAddress.find('@')
if atLoc > 0:
domain = emailAddress[atLoc+1:]
elif permissionType == 'domain':
domain = permission.get('domain', '')
if role not in copyMoveOptions['copyPermissionRoles']:
notCopiedMessage = f'role {role} not selected'
elif permissionType not in copyMoveOptions['copyPermissionTypes']:
notCopiedMessage = f'type {permissionType} not selected'
elif permission['inherited'] and not copyMoveOptions[copyInherited]:
notCopiedMessage = 'inherited not selected'
elif not permission['inherited'] and copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_NEVER:
notCopiedMessage = 'noninherited not selected'
elif role == 'owner':
if emailAddress == user or copyMoveOptions['destDriveId'] or not updateOwner:
notCopiedMessage = f'role {role} copy not required/appropriate'
else:
permission['role'] = 'writer'
return True
elif updateOwner and emailAddress == user:
notCopiedMessage = 'user is now owner'
elif domain and domain in copyMoveOptions['excludePermissionsFromDomains']:
notCopiedMessage = f'domain {domain} excluded'
elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']:
notCopiedMessage = f'domain {domain} not included'
elif permission.pop('deleted', False) or (permissionType in {'group', 'user'} and not emailAddress):
notCopiedMessage = f"{permissionType} deleted or has blank email address"
elif ((copyInherited == 'copySheetProtectedRangesInheritedPermissions' and copyMoveOptions[copyInherited]) or
(copyNonInherited == 'copySheetProtectedRangesNonInheritedPermissions' and
copyMoveOptions[copyNonInherited] != COPY_NONINHERITED_PERMISSIONS_NEVER)):
if role in {'organizer', 'fileOrganizer'}:
permission['role'] = 'writer'
return True
elif role == 'organizer' and (not atTop or copyMoveOptions['destParentType'] != DEST_PARENT_SHAREDDRIVE_ROOT):
notCopiedMessage = f'role {role} not copyable to {Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER)}'
elif role == 'fileOrganizer' and entityType == Ent.DRIVE_FILE:
notCopiedMessage = f'role {role} not copyable to {Ent.Plural(entityType)}'
elif role == 'fileOrganizer' and not copyMoveOptions['destDriveId']:
notCopiedMessage = f'role {role} only copyable to {Ent.Singular(Ent.SHAREDDRIVE)} {Ent.Plural(Ent.DRIVE_FOLDER)}'
else:
return True
if copyMoveOptions['showPermissionMessages']:
entityActionNotPerformedWarning(kvList, notCopiedMessage, 0, 0)
return False
def getNonInheritedPermissions(permissions):
nonInheritedPermIds = set()
for permissionId, permission in iter(permissions.items()):
if not permission['inherited']:
nonInheritedPermIds.add(permissionId)
return nonInheritedPermIds
def mapPermissionsDomains(srcPerm):
if 'emailAddress' in srcPerm:
email, domain = srcPerm['emailAddress'].lower().split('@', 1)
if domain in copyMoveOptions['mapPermissionsDomains']:
srcPerm['emailAddress'] = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
return True
elif 'domain' in srcPerm:
domain = srcPerm['domain'].lower()
if domain in copyMoveOptions['mapPermissionsDomains']:
srcPerm['domain'] = copyMoveOptions['mapPermissionsDomains'][domain]
return True
return False
sourcePerms = getPermissions(fileId)
if sourcePerms is None:
return
copySourcePerms = {}
deleteTargetPermIds = set()
updateTargetPerms = {}
for permissionId, permission in iter(sourcePerms.items()):
kvList = permissionKVList(user, entityType, newFileTitle, permission)
if isPermissionCopyable(kvList, permission):
copySourcePerms[permissionId] = permission
if copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_ALWAYS:
if copyMoveOptions['mapPermissionsDomains']:
for permissionId in getNonInheritedPermissions(copySourcePerms):
mapPermissionsDomains(copySourcePerms[permissionId])
elif copyMoveOptions[copyNonInherited] in {COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS,
COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS}:
targetPerms = getPermissions(newFileId)
if targetPerms is None:
return
sourceNonInheritedPermIDs = getNonInheritedPermissions(copySourcePerms)
targetNonInheritedPermIDs = getNonInheritedPermissions(targetPerms)
# Permissions in Source only
if copyMoveOptions['mapPermissionsDomains']:
for permissionId in sourceNonInheritedPermIDs-targetNonInheritedPermIDs:
mapPermissionsDomains(copySourcePerms[permissionId])
# Permissions in Target only
deleteTargetPermIds = targetNonInheritedPermIDs-sourceNonInheritedPermIDs
# Permissions in Source and Target
for permissionId in targetNonInheritedPermIDs&sourceNonInheritedPermIDs:
srcPerm = copySourcePerms[permissionId]
if copyMoveOptions['mapPermissionsDomains'] and mapPermissionsDomains(srcPerm):
deleteTargetPermIds.add(permissionId)
continue
tgtPerm = targetPerms[permissionId]
updatePerm = {}
for field in ['expirationTime', 'role', 'view', 'pendingOwner']:
if field in srcPerm:
if field not in tgtPerm:
if field != 'pendingOwner' or srcPerm[field]:
updatePerm[field] = srcPerm[field]
elif srcPerm[field] != tgtPerm[field]:
updatePerm[field] = srcPerm[field]
elif field in tgtPerm:
if field == 'expirationTime':
updatePerm['removeExpiration'] = True
elif field == 'pendingOwner':
updatePerm[field] = False
if updatePerm:
updateTargetPerms[permissionId] = targetPerms[permissionId]
updateTargetPerms[permissionId].update(updatePerm)
updateTargetPerms[permissionId]['updates'] = updatePerm
copySourcePerms.pop(permissionId)
deleteUpdateKwargs = {'useDomainAdminAccess': copyMoveOptions['useDomainAdminAccess']}
if entityType != Ent.SHAREDDRIVE:
deleteUpdateKwargs['enforceExpansiveAccess'] = copyMoveOptions['enforceExpansiveAccess']
Ind.Increment()
action = Act.Get()
Act.Set(Act.COPY)
kcount = len(copySourcePerms)
k = 0
for permissionId, permission in iter(copySourcePerms.items()):
k += 1
kvList = permissionKVList(user, entityType, newFileTitle, permission)
permission.pop('id')
permission.pop('inherited')
if copyMoveOptions['destDriveId']:
permission.pop('pendingOwner', None)
sendNotificationEmail = False
while True:
try:
callGAPI(drive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS,
# retryReasons=[GAPI.INVALID_SHARING_REQUEST],
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'],
fileId=newFileId, sendNotificationEmail=sendNotificationEmail, emailMessage=None,
body=permission, fields='', supportsAllDrives=True)
if copyMoveOptions['showPermissionMessages']:
entityActionPerformed(kvList, k, kcount)
break
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
GAPI.ownerOnTeamDriveItemNotSupported,
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility, GAPI.abusiveContentRestriction) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
break
except GAPI.invalidSharingRequest as e:
if not copyMoveOptions['sendEmailIfRequired'] or sendNotificationEmail or 'You are trying to invite' not in str(e):
entityActionFailedWarning(kvList, str(e), k, kcount)
break
sendNotificationEmail = True
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
Ind.Decrement()
Act.Set(action)
return
Act.Set(Act.DELETE)
kcount = len(deleteTargetPermIds)
k = 0
for permissionId in deleteTargetPermIds:
k += 1
kvList = permissionKVList(user, entityType, newFileTitle, targetPerms[permissionId])
try:
callGAPI(drive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
**deleteUpdateKwargs,
fileId=newFileId, permissionId=permissionId, supportsAllDrives=True)
if copyMoveOptions['showPermissionMessages']:
entityActionPerformed(kvList, k, kcount)
except (GAPI.notFound, GAPI.permissionNotFound,
GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
Ind.Decrement()
Act.Set(action)
return
Act.Set(Act.UPDATE)
kcount = len(updateTargetPerms)
k = 0
for permissionId, permission in iter(updateTargetPerms.items()):
k += 1
kvList = permissionKVList(user, entityType, newFileTitle, permission)
removeExpiration = permission['updates'].pop('removeExpiration', False)
try:
callGAPI(drive.permissions(), 'update',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_UPDATE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
removeExpiration=removeExpiration,
**deleteUpdateKwargs,
fileId=newFileId, permissionId=permissionId, body=permission['updates'], supportsAllDrives=True)
if copyMoveOptions['showPermissionMessages']:
entityActionPerformed(kvList, k, kcount)
except (GAPI.notFound, GAPI.permissionNotFound,
GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
Ind.Decrement()
Act.Set(action)
return
Act.Set(action)
Ind.Decrement()
def _updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileTitle,
aclAdd, permission):
ptype = permission['type']
updList = 'users' if ptype == 'user' else 'groups' if ptype == 'group' else ''
if updList:
emailAddress = permission['emailAddress']
else:
updDomain = (ptype == 'domain') and (permission['domain'] == GC.Values[GC.DOMAIN])
if not updDomain:
return
addEditor = aclAdd and (permission['role'] not in {'reader', 'commenter'})
updateProtectedRangeRequests = []
try:
result = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, fields='sheets(protectedRanges)')
for rsheet in result.get('sheets', []):
for protectedRange in rsheet.get('protectedRanges', []):
editors = protectedRange.get('editors', None)
if editors is None:
continue
updReqd = False
if updList:
if addEditor:
if updList not in editors:
editors[updList] = [emailAddress]
updReqd = True
elif emailAddress not in editors[updList]:
editors[updList].append(emailAddress)
updReqd = True
else:
if emailAddress in editors.get(updList, []):
editors[updList].remove(emailAddress)
updReqd = True
elif updDomain:
editors['domainUsersCanEdit'] = addEditor
updReqd = True
if updReqd:
updateProtectedRangeRequests.append({'updateProtectedRange': {'protectedRange': protectedRange, 'fields': 'editors'}})
if updateProtectedRangeRequests:
callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, body={'requests': updateProtectedRangeRequests})
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, fileTitle], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
def _getSheetProtectedRanges(sheet, user, i, count, j, jcount, fileId, fileTitle,
statistics, stat):
sheetProtectedRanges = []
try:
result = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, fields='sheets(protectedRanges)')
for rsheet in result.get('sheets', []):
for protectedRange in rsheet.get('protectedRanges', []):
sheetProtectedRanges.append({'updateProtectedRange': {'protectedRange': protectedRange, 'fields': 'editors'}})
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, fileTitle], str(e), j, jcount)
_incrStatistic(statistics, stat)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
return sheetProtectedRanges
def _updateSheetProtectedRanges(sheet, user, i, count, j, jcount, newFileId, newFileTitle, sheetProtectedRanges,
statistics, stat):
try:
callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=newFileId, body={'requests': sheetProtectedRanges})
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, newFileTitle], str(e), j, jcount)
_incrStatistic(statistics, stat)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
def _identicalSourceTarget(fileId, targetChildren):
for target in targetChildren:
if fileId == target['id']:
return True
return False
def _checkForDuplicateTargetFile(drive, user, k, kcount, child, destFilename, targetChildren, copyMoveOptions, statistics):
destFilenameLower = destFilename.lower()
for target in targetChildren:
if not target.get('processed', False) and destFilenameLower == target['name'].lower() and child['mimeType'] == target['mimeType']:
target['processed'] = True
if copyMoveOptions['duplicateFiles'] in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER]:
if (copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_OVERWRITE_OLDER) and (child['modifiedTime'] <= target['modifiedTime']):
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], Msg.DUPLICATE, k, kcount)
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
return True
if not target['capabilities']['canDelete']:
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], Msg.NOT_DELETABLE, k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
return True
try:
callGAPI(drive.files(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
fileId=target['id'], supportsAllDrives=True)
child['name'] = destFilename
return False
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e:
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], f'{Msg.NOT_DELETABLE}: {str(e)}', k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
return True
if copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_DUPLICATE_NAME:
child['name'] = destFilename
return False
if copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_UNIQUE_NAME:
child['name'] = _getUniqueFilename(destFilename, child['mimeType'], targetChildren)
return False
#copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_SKIP
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], Msg.DUPLICATE, k, kcount)
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
return True
child['name'] = destFilename
return False
def _getCopyMoveParentInfo(drive, user, i, count, j, jcount, newParentId, statistics):
# newParentId is known to be a folder
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=newParentId, fields='name,driveId,parents,modifiedTime', supportsAllDrives=True)
if 'driveId' not in result:
result['driveId'] = None
if result['name'] == MY_DRIVE and not result.get('parents', []):
result['destParentType'] = DEST_PARENT_MYDRIVE_ROOT
else:
result['destParentType'] = DEST_PARENT_MYDRIVE_FOLDER
else:
if result['name'] == TEAM_DRIVE and not result.get('parents', []):
result['name'] = _getSharedDriveNameFromId(result['driveId'])
result['destParentType'] = DEST_PARENT_SHAREDDRIVE_ROOT
else:
result['destParentType'] = DEST_PARENT_SHAREDDRIVE_FOLDER
return result
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.fileNeverWritable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, newParentId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FILE_FAILED)
return None
def _getCopyMoveTargetInfo(drive, user, i, count, j, jcount, source, destFilename, newParentId, statistics, parentParms):
try:
return callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=f"mimeType = '{source['mimeType']}' and name contains '{escapeDriveFileName(_getFilenamePrefix(destFilename))}' and trashed = false and '{newParentId}' in parents",
orderBy='folder desc,name,modifiedTime desc',
fields='nextPageToken,files(id,name,capabilities,mimeType,modifiedTime)',
**parentParms[DFA_SEARCHARGS])
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.fileNeverWritable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, newParentId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FILE_FAILED)
return None
def _verifyUserIsOrganizer(drive, user, i, count, fileId):
role = UNKNOWN
try:
permissionId = getPermissionIdForEmail(user, i, count, user)
role = callGAPI(drive.permissions(), 'get',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.PERMISSION_NOT_FOUND, GAPI.INSUFFICIENT_ADMINISTRATOR_PRIVILEGES],
useDomainAdminAccess=False,
fileId=fileId, permissionId=permissionId, fields='role', supportsAllDrives=True)['role']
if role == 'organizer':
return True
entityActionNotPerformedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId, Ent.ROLE, role], Msg.ROLE_MUST_BE_ORGANIZER, i, count)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.insufficientAdministratorPrivileges) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId], str(e), i, count)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId, Ent.PERMISSION_ID, permissionId], i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
entityActionNotPerformedWarning([Ent.USER, user], Msg.UNABLE_TO_GET_PERMISSION_ID.format(user), i, count)
return False
def _getCopyFolderNonInheritedPermissions(copyMoveOptions, copyNonInherited, sourceModifiedTime, targetModifiedTime):
if ((copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_NEVER) or
((copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS) and
(sourceModifiedTime <= targetModifiedTime))):
return 'noCopyNonInheritedPermissions'
return copyNonInherited
def _checkForExistingShortcut(drive, fileId, fileName, parentId):
try:
existingShortcuts = callGAPI(drive.files(), 'list',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
retryReasons=[GAPI.UNKNOWN_ERROR],
supportsAllDrives=True, includeItemsFromAllDrives=True,
q=f"shortcutDetails.targetId = '{fileId}' and trashed = False", fields='files(id,name,parents)')['files']
for shortcut in existingShortcuts:
if parentId in shortcut['parents'] and fileName == shortcut['name']:
return shortcut['id']
except (GAPI.invalidQuery, GAPI.invalid, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
pass
return None
copyReturnItemMap = {
'returnidonly': 'id',
'returnlinkonly': 'webViewLink',
}
# gam <UserTypeEntity> copy drivefile <DriveFileEntity>
# [newfilename <DriveFileName>] (replacefilename <REMatchPattern> <RESubstitution>)*
# [stripnameprefix <String>]
# [excludetrashed]
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*)) |
# (returnidonly|returnlinkonly)]
# [summary [<Boolean>]] [showpermissionsmessages [<Boolean>]]
# [<DriveFileParentAttribute>]
# [mergewithparent [<Boolean>]] [recursive [depth <Number>]]
# <DriveFileCopyAttribute>*
# [skipids <DriveFileEntity>]
# [copysubfiles [<Boolean>]] [filenamematchpattern <REMatchPattern>] [filemimetype [not] <MimeTypeList>]
# [copysubfolders [<Boolean>]] [foldernamematchpattern <REMatchPattern>]
# [copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <REMatchPattern>]
# [duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip]
# [duplicatefolders merge|duplicatename|uniquename|skip]
# [copiedshortcutspointtocopiedfiles [<Boolean>]]
# [copyfilepermissions [<Boolean>]]
# [copyfileinheritedpermissions [<Boolean>]
# [copyfilenoninheritedpermissions [<Boolean>]
# [copymergewithparentfolderpermissions [<Boolean>]]
# [copymergedtopfolderpermissions [<Boolean>]]
# [copytopfolderpermissions [<Boolean>]]
# [copytopfolderiheritedpermissions [<Boolean>]]
# [copytopfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
# [copymergedsubfolderpermissions [<Boolean>]]
# [copysubfolderpermissions [<Boolean>]]
# [copysubfolderinheritedpermissions [<Boolean>]]
# [copysubfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
# [copypermissionroles <DriveFileACLRoleList>]
# [copypermissiontypes <DriveFileACLTypeList>]
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
# (mappermissionsdomain <DomainName> <DomainName>)*
# [copysheetprotectedranges [<Boolean>]]
# [copysheetprotectedrangesinheritedpermissions [<Boolean>]]
# [copysheetprotectedrangesnoninheritedpermissions [<Boolean>]]
# [sendemailifrequired [<Boolean>]]
# [suppressnotselectedmessages [<Boolean>]]
# [verifyorganizer [<Boolean>]]
# [enforceexpansiveaccess [<Boolean>]]
def copyDriveFile(users):
def _writeCSVData(user, oldName, oldId, newName, newId, mimeType):
row = {'User': user, fileNameTitle: oldName, 'id': oldId,
newFileNameTitle: newName, 'newId': newId, 'mimeType': mimeType}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
def _cloneFolderCopy(drive, user, i, count, j, jcount,
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime,
statistics, copyMoveOptions, atTop):
folderId = source.pop('id')
folderName = source['name']
folderNameId = f'{folderName}({folderId})'
kvList = [Ent.USER, user, Ent.DRIVE_FOLDER, folderNameId]
newParentNameId = f'{newParentName}({newParentId})'
# Merge top parent folder
if atTop and copyMoveOptions['mergeWithParent']:
action = Act.Get()
Act.Set(Act.COPY_MERGE)
if not csvPF:
entityPerformActionModifierItemValueList(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, newParentNameId], j, jcount)
else:
_writeCSVData(user, folderName, folderId, newParentName, newParentId, MIMETYPE_GA_FOLDER)
Act.Set(action)
_incrStatistic(statistics, STAT_FOLDER_MERGED)
if (copyMoveOptions['copyMergeWithParentFolderPermissions'] and
copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT):
copyFolderNonInheritedPermissions =\
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
'copyTopFolderNonInheritedPermissions',
source['modifiedTime'], mergeParentModifiedTime)
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FOLDER, folderId, folderName, newParentId, newFolderName,
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, True,
'copyTopFolderInheritedPermissions',
copyFolderNonInheritedPermissions,
True)
return (newParentId, newParentName, True)
# Merge parent folders
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
newFolderNameLower = newFolderName.lower()
for target in targetChildren:
if not target.get('processed', False) and newFolderNameLower == target['name'].lower() and source['mimeType'] == target['mimeType']:
target['processed'] = True
if target['capabilities']['canAddChildren']:
newFolderId = target['id']
action = Act.Get()
Act.Set(Act.COPY_MERGE)
if not csvPF:
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'], j, jcount)
else:
_writeCSVData(user, folderName, folderId, newFolderName, newFolderId, MIMETYPE_GA_FOLDER)
Act.Set(action)
_incrStatistic(statistics, STAT_FOLDER_MERGED)
if (copyMoveOptions[['copyMergedSubFolderPermissions', 'copyMergedTopFolderPermissions'][atTop]] and
(not atTop or copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT)):
copyFolderNonInheritedPermissions =\
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
source['modifiedTime'], target['modifiedTime'])
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, atTop,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
copyFolderNonInheritedPermissions,
False)
return (newFolderId, newFolderName, True)
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
return (None, None, False)
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
newFolderName = _getUniqueFilename(newFolderName, source['mimeType'], targetChildren)
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
targetChild = _targetFilenameExists(newFolderName, source['mimeType'], targetChildren)
if targetChild is not None:
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{newFolderName}({targetChild['id']})"],
Msg.DUPLICATE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
return (None, None, False)
# Copy parent folders
body = source.copy()
body.pop('capabilities', None)
if copyMoveOptions['sourceDriveId'] or copyMoveOptions['destDriveId']:
body.pop('copyRequiresWriterPermission', None)
body.pop('writersCanShare', None)
body.pop('trashed', None)
body.pop('driveId', None)
body['name'] = newFolderName
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INTERNAL_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.BAD_REQUEST],
body=body, fields='id,webViewLink,modifiedTime', supportsAllDrives=True)
newFolderId = result['id']
if returnIdLink:
writeStdout(f'{result[returnIdLink]}\n')
elif not csvPF:
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'],
j, jcount)
else:
_writeCSVData(user, folderName, folderId, newFolderName, newFolderId, body['mimeType'])
_incrStatistic(statistics, STAT_FOLDER_COPIED_MOVED)
if copyMoveOptions[['copySubFolderPermissions', 'copyTopFolderPermissions'][atTop]]:
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, False,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
True)
return (newFolderId, newFolderName, False)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.internalError,
GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, newFolderName], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FOLDER_FAILED)
copyMoveOptions['retainSourceFolders'] = True
return (None, None, False)
def _makeCopyShortcut(drive, user, k, kcount, entityType, childId, childName, newChildName, newParentId, newParentName, targetId):
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
if entityType == Ent.DRIVE_FILE:
targetEntityType = Ent.DRIVE_FILE_SHORTCUT
statShortcutCreated = STAT_FILE_SHORTCUT_CREATED
statShortcutExists = STAT_FILE_SHORTCUT_EXISTS
else:
targetEntityType = Ent.DRIVE_FOLDER_SHORTCUT
statShortcutCreated = STAT_FOLDER_SHORTCUT_CREATED
statShortcutExists = STAT_FOLDER_SHORTCUT_EXISTS
newParentNameId = f'{newParentName}({newParentId})'
action = Act.Get()
existingShortcut = _checkForExistingShortcut(drive, targetId, newChildName, newParentId)
if existingShortcut:
Act.Set(Act.CREATE_SHORTCUT)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_PREVIOUSLY_IN,
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{childName}({existingShortcut})"],
k, kcount)
Act.Set(action)
_incrStatistic(statistics, statShortcutExists)
return
body = {'name': newChildName, 'mimeType': MIMETYPE_GA_SHORTCUT,
'parents': [newParentId], 'shortcutDetails': {'targetId': targetId}}
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID],
body=body, fields='id', supportsAllDrives=True)
Act.Set(Act.CREATE_SHORTCUT)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{newChildName}({result['id']})"],
k, kcount)
Act.Set(action)
_incrStatistic(statistics, statShortcutCreated)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.invalid, GAPI.badRequest, GAPI.fileNotFound, GAPI.unknownError,
GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
def _checkChildCopyAllowed(childMimeType, childName, child):
if childMimeType == MIMETYPE_GA_FOLDER:
if not copyMoveOptions['copySubFolders']:
return False
nameMatchPattern = copyMoveOptions['folderNameMatchPattern']
elif childMimeType == MIMETYPE_GA_SHORTCUT:
if not copyMoveOptions['copySubShortcuts']:
return False
nameMatchPattern = copyMoveOptions['shortcutNameMatchPattern']
else:
if not copyMoveOptions['copySubFiles']:
return False
if copyMoveOptions['copySubFilesOwnedBy'] is not None:
if child.get('driveId', None) is None and child.get('ownedByMe', False) != copyMoveOptions['copySubFilesOwnedBy']:
return False
if copyMoveOptions['fileMimeTypes']:
if not copyMoveOptions['notMimeTypes']:
if childMimeType not in copyMoveOptions['fileMimeTypes']:
return False
else:
if childMimeType in copyMoveOptions['fileMimeTypes']:
return False
nameMatchPattern = copyMoveOptions['fileNameMatchPattern']
return not nameMatchPattern or nameMatchPattern.match(childName)
def _recursiveFolderCopy(drive, user, i, count, j, jcount,
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime, atTop, depth):
folderId = source['id']
newFolderId, newFolderName, existingTargetFolder = _cloneFolderCopy(drive, user, i, count, j, jcount,
source, targetChildren, newFolderName,
newParentId, newParentName, mergeParentModifiedTime,
statistics, copyMoveOptions, atTop)
if newFolderId is None:
return
newFolderNameId = f'{newFolderName}({newFolderId})'
copiedSourceFiles[folderId] = newFolderId
copiedTargetFiles.add(newFolderId) # Don't recopy folder copied into a sub-folder
if maxdepth != -1 and depth > maxdepth:
return
depth += 1
sourceChildren = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=WITH_PARENTS.format(folderId),
orderBy='folder desc,name,modifiedTime desc',
fields='nextPageToken,files(id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
'description,folderColorRgb,mimeType,modifiedTime,ownedByMe,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare,'\
'shortcutDetails(targetId,targetMimeType))',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs)
kcount = len(sourceChildren)
if kcount > 0:
if existingTargetFolder:
subTargetChildren = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=ANY_NON_TRASHED_WITH_PARENTS.format(newFolderId),
orderBy='folder desc,name,modifiedTime desc',
fields='nextPageToken,files(id,name,capabilities,mimeType,modifiedTime)',
**parentParms[DFA_SEARCHARGS])
else:
subTargetChildren = []
Ind.Increment()
k = 0
for child in sourceChildren:
k += 1
childId = child['id']
childName = child['name']
childNameId = f'{childName}({childId})'
childMimeType = child['mimeType']
if childId in copiedTargetFiles: # Don't recopy file/folder copied into a sub-folder
continue
kvList = [Ent.USER, user, _getEntityMimeType(child), childNameId]
if childId in skipFileIdEntity['list']:
if not suppressNotSelectedMessages:
entityActionNotPerformedWarning(kvList, Msg.IN_SKIPIDS, k, kcount)
_incrStatistic(statistics, STAT_FILE_IN_SKIPIDS)
continue
if not _checkChildCopyAllowed(childMimeType, childName, child):
if not suppressNotSelectedMessages:
entityActionNotPerformedWarning(kvList, Msg.NOT_SELECTED, k, kcount)
continue
child.pop('ownedByMe', None)
trashed = child.pop('trashed', False)
if (childId == newFolderId) or (excludeTrashed and trashed):
entityActionNotPerformedWarning(kvList,
[Msg.NOT_COPYABLE, Msg.IN_TRASH_AND_EXCLUDE_TRASHED][trashed], k, kcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
if copyMoveOptions['replaceFilename']:
newChildName = processFilenameReplacements(childName, copyMoveOptions['replaceFilename'])
else:
newChildName = childName
# If source child has already been copied, make shortcut to its copy
if childId in copiedSourceFiles:
_makeCopyShortcut(drive, user, k, kcount, _getEntityMimeType(child), childId, childName,
newChildName, newFolderId, newFolderName, copiedSourceFiles[childId])
continue
child.pop('parents', [])
child['parents'] = [newFolderId]
if childMimeType == MIMETYPE_GA_FOLDER:
_recursiveFolderCopy(drive, user, i, count, k, kcount,
child, subTargetChildren, newChildName, newFolderId, newFolderName, child['modifiedTime'],
False, depth)
elif childMimeType == MIMETYPE_GA_SHORTCUT:
shortcutsToCreate.append({'childName': childName, 'childId': childId, 'newChildName': newChildName,
'newFolderId': newFolderId, 'newFolderName': newFolderName,
'targetId': child['shortcutDetails']['targetId'], 'mimeType': child['shortcutDetails']['targetMimeType']})
else:
if not child.pop('capabilities')['canCopy']:
entityActionFailedWarning(kvList, Msg.NOT_COPYABLE, k, kcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
if existingTargetFolder:
if _checkForDuplicateTargetFile(drive, user, k, kcount, child, newChildName, subTargetChildren, copyMoveOptions, statistics):
continue
else:
child['name'] = newChildName
child.pop('id')
if copyMoveOptions['destDriveId']:
child.pop('copyRequiresWriterPermission', None)
child.pop('writersCanShare', None)
child.pop('driveId', None)
if childMimeType == MIMETYPE_GA_SHORTCUT:
child.pop('folderColorRgb', None)
child.pop('mimeType')
try:
result = callGAPI(drive.files(), 'copy',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.INTERNAL_ERROR, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
fileId=childId, body=child, fields='id,name', supportsAllDrives=True)
if not csvPF:
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newFolderNameId, Ent.DRIVE_FILE, f"{result['name']}({result['id']})"],
k, kcount)
else:
_writeCSVData(user, childName, childId, result['name'], result['id'], childMimeType)
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
copiedSourceFiles[childId] = result['id']
copiedTargetFiles.add(result['id']) # Don't recopy file copied into a sub-folder
if (childMimeType == MIMETYPE_GA_SPREADSHEET and
(copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] or
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] != COPY_NONINHERITED_PERMISSIONS_NEVER)):
protectedSheetRanges = _getSheetProtectedRanges(sheet, user, i, count, k, kcount, childId, childName,
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
_copyPermissions(drive, user, i, count, k, kcount,
Ent.DRIVE_FILE, childId, childName, result['id'], result['name'],
statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False,
'copySheetProtectedRangesInheritedPermissions',
'copySheetProtectedRangesNonInheritedPermissions',
True)
_updateSheetProtectedRanges(sheet, user, i, count, k, kcount, result['id'], result['name'], protectedSheetRanges,
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
elif copyMoveOptions['copyFilePermissions']:
_copyPermissions(drive, user, i, count, k, kcount,
Ent.DRIVE_FILE, childId, childName, result['id'], result['name'],
statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False,
'copyFileInheritedPermissions',
'copyFileNonInheritedPermissions',
True)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
GAPI.insufficientParentPermissions, GAPI.unknownError,
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
GAPI.internalError, GAPI.teamDrivesShortcutFileNotSupported) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
break
Ind.Decrement()
fileIdEntity = getDriveFileEntity()
csvPF = None
addCSVData = {}
returnIdLink = None
copyBody = {}
parentBody = {}
parentParms = initDriveFileAttributes()
copyParameters = initDriveFileAttributes()
copyMoveOptions = initCopyMoveOptions(True)
excludeTrashed = newParentsSpecified = recursive = suppressNotSelectedMessages = False
maxdepth = -1
verifyOrganizer = True
skipFileIdEntity = initDriveFileEntity()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getCopyMoveOptions(myarg, copyMoveOptions):
pass
elif getDriveFileParentAttribute(myarg, parentParms):
newParentsSpecified = True
elif myarg in copyReturnItemMap:
returnIdLink = copyReturnItemMap[myarg]
elif myarg == 'excludetrashed':
excludeTrashed = True
elif myarg == 'recursive':
recursive = getBoolean()
elif myarg == 'depth':
maxdepth = getInteger(minVal=-1)
elif myarg == 'skipids':
skipFileIdEntity = getDriveFileEntity()
elif myarg == 'convert':
deprecatedArgument(myarg)
elif myarg == 'csv':
csvPF = CSVPrintFile()
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'suppressnotselectedmessages':
suppressNotSelectedMessages = getBoolean()
elif getDriveFileCopyAttribute(myarg, copyBody, copyParameters):
pass
elif myarg == 'verifyorganizer':
verifyOrganizer = getBoolean()
else:
unknownArgumentExit()
if csvPF:
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
newFileNameTitle = f'new{fileNameTitle.capitalize()}'
csvPF.SetTitles(['User', fileNameTitle, 'id', newFileNameTitle, 'newId', 'mimeType'])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=Ent.DRIVE_FILE_OR_FOLDER if returnIdLink is None else None)
if jcount == 0:
continue
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
continue
if (copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] or
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] != COPY_NONINHERITED_PERMISSIONS_NEVER):
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
continue
copiedSourceFiles = {}
copiedTargetFiles = set()
shortcutsToCreate = []
statistics = _initStatistics()
if skipFileIdEntity['query'] or skipFileIdEntity[ROOT]:
_validateUserGetFileIDs(origUser, i, count, skipFileIdEntity, drive=drive)
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
source = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId,
fields='id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
'description,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare',
supportsAllDrives=True)
# Source at root of Shared Drive?
sourceMimeType = source['mimeType']
if sourceMimeType == MIMETYPE_GA_FOLDER and source.get('driveId') and source['name'] == TEAM_DRIVE and not source.get('parents', []):
source['name'] = _getSharedDriveNameFromId(source['driveId'])
sourceName = source['name']
sourceNameId = f"{sourceName}({source['id']})"
copyMoveOptions['sourceDriveId'] = source.get('driveId')
kvList = [Ent.USER, user, _getEntityMimeType(source), sourceNameId]
if fileId in copiedSourceFiles:
entityActionNotPerformedWarning(kvList, Msg.DUPLICATE, j, jcount)
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
continue
if fileId in skipFileIdEntity['list']:
entityActionNotPerformedWarning(kvList, Msg.IN_SKIPIDS, j, jcount)
_incrStatistic(statistics, STAT_FILE_IN_SKIPIDS)
continue
trashed = source.pop('trashed', False)
if excludeTrashed and trashed:
entityActionNotPerformedWarning(kvList, Msg.IN_TRASH_AND_EXCLUDE_TRASHED, j, jcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
if copyMoveOptions['sourceDriveId']:
# If copying from a Shared Drive, user has to be an organizer
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']):
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
continue
sourceSearchArgs = {'driveId': copyMoveOptions['sourceDriveId'], 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
else:
sourceSearchArgs = {}
sourceParents = source.pop('parents', [])
if newParentsSpecified:
newParents = parentBody['parents']
numNewParents = len(newParents)
if numNewParents > 1:
entityActionNotPerformedWarning(kvList, Msg.MULTIPLE_PARENTS_SPECIFIED.format(numNewParents), j, jcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
continue
else:
newParents = sourceParents if sourceParents else [ROOT]
source['parents'] = newParents
newParentId = newParents[0]
dest = _getCopyMoveParentInfo(drive, user, i, count, j, jcount, newParentId, statistics)
if dest is None:
continue
newParentName = dest['name']
newParentNameId = f'{newParentName}({newParentId})'
copyMoveOptions['destDriveId'] = dest['driveId']
copyMoveOptions['destParentType'] = dest['destParentType']
if copyMoveOptions['destDriveId']:
# If copying to a Shared Drive, user has to be an organizer
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']):
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
continue
if not parentParms[DFA_SEARCHARGS]:
parentParms[DFA_SEARCHARGS] = {'driveId': copyMoveOptions['destDriveId'], 'corpora': 'drive',
'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
if copyMoveOptions['newFilename']:
destName = copyMoveOptions['newFilename']
elif (sourceMimeType == MIMETYPE_GA_FOLDER) and copyMoveOptions['mergeWithParent']:
destName = dest['name']
elif ((newParentsSpecified and newParentId not in sourceParents) or
((newParentId in sourceParents and
(sourceMimeType == MIMETYPE_GA_FOLDER and copyMoveOptions['duplicateFolders'] != DUPLICATE_FOLDER_MERGE) or
(sourceMimeType != MIMETYPE_GA_FOLDER and copyMoveOptions['duplicateFiles'] not in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER])))):
if copyMoveOptions['replaceFilename']:
destName = processFilenameReplacements(sourceName, copyMoveOptions['replaceFilename'])
else:
destName = sourceName
elif copyMoveOptions['replaceFilename']:
destName = processFilenameReplacements(sourceName, copyMoveOptions['replaceFilename'])
else:
destName = f'Copy of {sourceName}'
if copyMoveOptions['stripNamePrefix'] and destName.startswith(copyMoveOptions['stripNamePrefix']):
destName = destName[len(copyMoveOptions['stripNamePrefix']):]
targetChildren = _getCopyMoveTargetInfo(drive, user, i, count, j, jcount, source, destName, newParentId, statistics, parentParms)
if targetChildren is None:
continue
# Copy folder
if sourceMimeType == MIMETYPE_GA_FOLDER:
copiedTargetFiles.add(newParentId) # Don't recopy folder copied into a sub-folder
if fileId == newParentId:
entityActionNotPerformedWarning(kvList, Msg.NOT_COPYABLE_INTO_ITSELF, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_FAILED)
continue
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
if _identicalSourceTarget(fileId, targetChildren):
entityActionNotPerformedWarning(kvList, Msg.NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_MERGE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_FAILED)
continue
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
destName = _getUniqueFilename(destName, sourceMimeType, targetChildren)
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
targetChild = _targetFilenameExists(destName, sourceMimeType, targetChildren)
if targetChild is not None:
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{destName}({targetChild['id']})"],
Msg.DUPLICATE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
continue
if recursive:
_recursiveFolderCopy(drive, user, i, count, j, jcount,
source, targetChildren, destName, newParentId, newParentName, dest['modifiedTime'],
True, 0)
kcount = len(shortcutsToCreate)
if kcount > 0:
entityPerformActionNumItems([Ent.USER, user], kcount, Ent.DRIVE_FILE_SHORTCUT, i, count)
Ind.Increment()
k = 0
for shortcut in shortcutsToCreate:
k += 1
targetId = shortcut['targetId']
if targetId in copiedSourceFiles and copyMoveOptions['copiedShortcutsPointToCopiedFiles']:
targetId = copiedSourceFiles[targetId]
_makeCopyShortcut(drive, user, k, kcount, _getEntityMimeType(shortcut), shortcut['childId'], shortcut['childName'],
shortcut['newChildName'], shortcut['newFolderId'], shortcut['newFolderName'], targetId)
Ind.Decrement()
else:
source.update(copyBody)
_cloneFolderCopy(drive, user, i, count, j, jcount,
source, targetChildren, destName, newParentId, newParentName, dest['modifiedTime'],
statistics, copyMoveOptions, True)
# Copy file
else:
if not source.pop('capabilities')['canCopy']:
entityActionFailedWarning(kvList, Msg.NOT_COPYABLE, j, jcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
if copyMoveOptions['duplicateFiles'] in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER] and _identicalSourceTarget(fileId, targetChildren):
entityActionNotPerformedWarning(kvList, Msg.NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE, j, jcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
continue
if _checkForDuplicateTargetFile(drive, user, j, jcount, source, destName, targetChildren, copyMoveOptions, statistics):
continue
sourceId = source.pop('id')
sourceMimeType = source.pop('mimeType')
if copyMoveOptions['destDriveId']:
source.pop('copyRequiresWriterPermission', None)
source.pop('writersCanShare', None)
source.pop('driveId', None)
if sourceMimeType == MIMETYPE_GA_SHORTCUT:
source.pop('folderColorRgb', None)
source.update(copyBody)
result = callGAPI(drive.files(), 'copy',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.INTERNAL_ERROR, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
fileId=fileId,
ignoreDefaultVisibility=copyParameters[DFA_IGNORE_DEFAULT_VISIBILITY],
keepRevisionForever=copyParameters[DFA_KEEP_REVISION_FOREVER],
body=source, fields='id,name,webViewLink', supportsAllDrives=True)
if returnIdLink:
writeStdout(f'{result[returnIdLink]}\n')
elif not csvPF:
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FILE, f"{result['name']}({result['id']})"],
j, jcount)
else:
_writeCSVData(user, sourceName, sourceId, result['name'], result['id'], sourceMimeType)
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
if (sourceMimeType == MIMETYPE_GA_SPREADSHEET and
(copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] or
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] != COPY_NONINHERITED_PERMISSIONS_NEVER)):
protectedSheetRanges = _getSheetProtectedRanges(sheet, user, i, count, j, jcount, sourceId, sourceName,
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FILE, sourceId, sourceName, result['id'], result['name'],
statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False,
'copySheetProtectedRangesInheritedPermissions',
'copySheetProtectedRangesNonInheritedPermissions',
True)
_updateSheetProtectedRanges(sheet, user, i, count, j, jcount, result['id'], result['name'], protectedSheetRanges,
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
elif copyMoveOptions['copyFilePermissions']:
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FILE, sourceId, sourceName, result['id'], result['name'],
statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False,
'copyFileInheritedPermissions',
'copyFileNonInheritedPermissions',
True)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
GAPI.insufficientParentPermissions, GAPI.unknownError,
GAPI.invalid, GAPI.badRequest, GAPI.cannotCopyFile, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FILE_FAILED)
break
Ind.Decrement()
if copyMoveOptions['summary']:
_printStatistics(user, statistics, i, count, True)
if csvPF:
csvPF.writeCSVfile('Copied Files-Folders')
def _updateMoveFilePermissions(drive, user, i, count,
entityType, fileId, fileTitle,
statistics, stat, copyMoveOptions):
def getPermissions(fid):
permissions = {}
try:
result = callGAPIpages(drive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fileId=fid,
fields='nextPageToken,permissions(allowFileDiscovery,domain,emailAddress,expirationTime,id,role,type,deleted,view,pendingOwner,permissionDetails)',
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], supportsAllDrives=True)
for permission in result:
permission.pop('teamDrivePermissionDetails', None)
if not permission.pop('permissionDetails', [{'inherited': False}])[0]['inherited']:
permissions[permission['id']] = permission
return permissions
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid):
pass
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
return None
def permissionKVList(user, entityType, title, permission):
permstr = f"noninherited/{permission['role']}/{permission['type']}"
if permission['type'] in {'group', 'user'}:
permstr += f"/{permission['emailAddress']}"
elif permission['type'] == 'domain':
permstr += f"/{permission['domain']}"
return [Ent.USER, user, entityType, title, Ent.PERMISSION, permstr]
def isPermissionDeletable(kvList, permission):
domain = ''
if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']:
if permission['type'] in {'group', 'user'}:
atLoc = permission.get('emailAddress', '').find('@')
if atLoc > 0:
domain = permission['emailAddress'][atLoc+1:]
elif permission['type'] == 'domain':
domain = permission.get('domain', '')
if domain and domain in copyMoveOptions['excludePermissionsFromDomains']:
notMovedMessage = f'domain {domain} excluded'
elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']:
notMovedMessage = f'domain {domain} not included'
elif permission.pop('deleted', False):
notMovedMessage = f"{permission['type']} {permission['emailAddress']} deleted"
else:
return False
deleteSourcePerms[permission['id']] = permission.copy()
if copyMoveOptions['showPermissionMessages']:
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
return True
def mapPermissionsDomains(kvList, permission):
if 'emailAddress' in permission:
email, domain = permission['emailAddress'].lower().split('@', 1)
if domain in copyMoveOptions['mapPermissionsDomains']:
deleteSourcePerms[permission['id']] = permission.copy()
if copyMoveOptions['showPermissionMessages']:
notMovedMessage = f"domain {domain} mapped to {copyMoveOptions['mapPermissionsDomains'][domain]}"
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
permission['emailAddress'] = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
addSourcePerms[permission['id']] = permission
return True
elif 'domain' in permission:
domain = permission['domain'].lower()
if domain in copyMoveOptions['mapPermissionsDomains']:
deleteSourcePerms[permission['id']] = permission.copy()
if copyMoveOptions['showPermissionMessages']:
notMovedMessage = f"domain {domain} mapped to {copyMoveOptions['mapPermissionsDomains'][domain]}"
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
permission['domain'] = copyMoveOptions['mapPermissionsDomains'][domain]
addSourcePerms[permission['id']] = permission
return True
return False
sourcePerms = getPermissions(fileId)
if sourcePerms is None:
return
Ind.Increment()
deleteSourcePerms = {}
addSourcePerms = {}
for permissionId, permission in iter(sourcePerms.items()):
kvList = permissionKVList(user, entityType, fileTitle, permission)
if isPermissionDeletable(kvList, permission):
pass
elif copyMoveOptions['mapPermissionsDomains']:
mapPermissionsDomains(kvList, permission)
action = Act.Get()
kcount = len(deleteSourcePerms)
if kcount > 0:
Act.Set(Act.DELETE)
k = 0
for permissionId, permission in iter(deleteSourcePerms.items()):
k += 1
kvList = permissionKVList(user, entityType, fileTitle, permission)
try:
callGAPI(drive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'],
enforceExpansiveAccess=copyMoveOptions['enforceExpansiveAccess'],
fileId=fileId, permissionId=permissionId, supportsAllDrives=True)
if copyMoveOptions['showPermissionMessages']:
entityActionPerformed(kvList, k, kcount)
except (GAPI.notFound, GAPI.permissionNotFound,
GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
Ind.Decrement()
Act.Set(action)
return
kcount = len(addSourcePerms)
if kcount > 0:
Act.Set(Act.CREATE)
k = 0
for permissionId, permission in iter(addSourcePerms.items()):
k += 1
kvList = permissionKVList(user, entityType, fileTitle, permission)
permission.pop('id')
if copyMoveOptions['destDriveId']:
permission.pop('pendingOwner', None)
sendNotificationEmail = False
while True:
try:
callGAPI(drive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS,
# retryReasons=[GAPI.INVALID_SHARING_REQUEST],
fileId=fileId, sendNotificationEmail=sendNotificationEmail, emailMessage=None,
body=permission, fields='', useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], supportsAllDrives=True)
if copyMoveOptions['showPermissionMessages']:
entityActionPerformed(kvList, k, kcount)
break
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
GAPI.ownerOnTeamDriveItemNotSupported,
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility, GAPI.abusiveContentRestriction) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
break
except GAPI.invalidSharingRequest as e:
if not copyMoveOptions['sendEmailIfRequired'] or sendNotificationEmail or 'You are trying to invite' not in str(e):
entityActionFailedWarning(kvList, str(e), k, kcount)
break
sendNotificationEmail = True
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, stat)
Ind.Decrement()
Act.Set(action)
return
Ind.Decrement()
Act.Set(action)
# gam <UserTypeEntity> move drivefile <DriveFileEntity> [newfilename <DriveFileName>]
# [summary [<Boolean>]] [showpermissionsmessages [<Boolean>]]
# [<DriveFileParentAttribute>]
# [mergewithparent|mergewithparentretain [<Boolean>]]
# [createshortcutsfornonmovablefiles [<Boolean>]]
# [duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip]
# [duplicatefolders merge|duplicatename|uniquename|skip]
# [copymergewithparentfolderpermissions [<Boolean>]]
# [copymergedtopfolderpermissions [<Boolean>]]
# [copytopfolderpermissions [<Boolean>]]
# [copytopfolderiheritedpermissions [<Boolean>]]
# [copytopfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
# [copymergedsubfolderpermissions [<Boolean>]]
# [copysubfolderpermissions [<Boolean>]]
# [copysubfolderinheritedpermissions [<Boolean>]]
# [copysubfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
# [copypermissionroles <DriveFileACLRoleList>]
# [copypermissiontypes <DriveFileACLTypeList>]
# [synctopfoldernoniheritedpermissions [<Boolean>]] [syncsubfoldernoninheritedpermissions [<Boolean>]]
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
# (mappermissionsdomain <DomainName> <DomainName>)*
# [updatefilepermissions [<Boolean>]]
# [retainsourcefolders [<Boolean>]]
# [sendemailifrequired [<Boolean>]]
# [verifyorganizer [<Boolean>]]
# [enforceexpansiveaccess [<Boolean>]]
def moveDriveFile(users):
def _cloneFolderMove(drive, user, i, count, j, jcount,
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime,
statistics, copyMoveOptions, atTop):
folderId = source.pop('id')
folderName = source['name']
sourceMimeType = source['mimeType']
folderNameId = f'{folderName}({folderId})'
kvList = [Ent.USER, user, Ent.DRIVE_FOLDER, folderNameId]
newParentNameId = f'{newParentName}({newParentId})'
# Merge top parent folder
if atTop and (copyMoveOptions['mergeWithParent'] or copyMoveOptions['mergeWithParentRetain']):
action = Act.Get()
Act.Set(Act.MOVE_MERGE)
entityPerformActionModifierItemValueList(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, newParentNameId], j, jcount)
Act.Set(action)
_incrStatistic(statistics, STAT_FOLDER_MERGED)
if (copyMoveOptions['copyMergeWithParentFolderPermissions'] and
copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT):
copyFolderNonInheritedPermissions =\
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
'copyTopFolderNonInheritedPermissions',
source['modifiedTime'], mergeParentModifiedTime)
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FOLDER, folderId, folderName, newParentId, newFolderName,
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, True,
'copyTopFolderInheritedPermissions',
copyFolderNonInheritedPermissions,
False)
source.pop('oldparents', None)
return (newParentId, newParentName, True)
# Merge parent folders
if atTop and copyMoveOptions['sourceIsMyDriveSharedDrive']:
pass
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
newFolderNameLower = newFolderName.lower()
for target in targetChildren:
if not target.get('processed', False) and newFolderNameLower == target['name'].lower() and sourceMimeType == target['mimeType']:
target['processed'] = True
if target['capabilities']['canAddChildren']:
newFolderId = target['id']
action = Act.Get()
Act.Set(Act.MOVE_MERGE)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'], j, jcount)
Act.Set(action)
_incrStatistic(statistics, STAT_FOLDER_MERGED)
if (copyMoveOptions[['copyMergedSubFolderPermissions', 'copyMergedTopFolderPermissions'][atTop]] and
(not atTop or copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT)):
copyFolderNonInheritedPermissions =\
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
source['modifiedTime'], target['modifiedTime'])
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, atTop,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
copyFolderNonInheritedPermissions,
False)
return (newFolderId, newFolderName, True)
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
copyMoveOptions['retainSourceFolders'] = True
return (None, None, False)
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
newFolderName = _getUniqueFilename(newFolderName, sourceMimeType, targetChildren)
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
targetChild = _targetFilenameExists(newFolderName, sourceMimeType, targetChildren)
if targetChild is not None:
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{newFolderName}({targetChild['id']})"],
Msg.DUPLICATE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
copyMoveOptions['retainSourceFolders'] = True
return (None, None, False)
# Update parents on: not retain and MD->MD, SD->MD, SD->SD
if atTop and copyMoveOptions['sourceIsMyDriveSharedDrive']:
pass
elif (not copyMoveOptions['retainSourceFolders'] and (copyMoveOptions['sourceDriveId'] or not copyMoveOptions['destDriveId'])):
if newFolderName != folderName:
body = {'name': newFolderName}
else:
body = {}
removeParents = ','.join([parentId for parentId in source.pop('oldparents', []) if parentId not in source['parents']])
try:
callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST,
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE,
GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED,
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION,
GAPI.CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE,
GAPI.CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE,
GAPI.CROSS_DOMAIN_MOVE_RESTRICTION,
GAPI.STORAGE_QUOTA_EXCEEDED],
fileId=folderId,
addParents=newParentId, removeParents=removeParents,
body=body, fields=None, supportsAllDrives=True)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({folderId})'],
j, jcount)
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
return (None, None, False)
except (GAPI.badRequest, GAPI.insufficientParentPermissions, GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
GAPI.fileWriterTeamDriveMoveInDisabled, GAPI.targetUserRoleLimitedByLicenseRestriction,
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
GAPI.crossDomainMoveRestriction, GAPI.storageQuotaExceeded) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FILE_FAILED)
copyMoveOptions['retainSourceFolders'] = True
return (None, None, False)
# Create new parent on: retain or MD->SD
source.pop('oldparents', None)
body = source.copy()
body.pop('capabilities', None)
if copyMoveOptions['sourceDriveId'] or copyMoveOptions['destDriveId']:
body.pop('copyRequiresWriterPermission', None)
body.pop('writersCanShare', None)
body.pop('trashed', None)
if not copyMoveOptions['destDriveId']:
body.pop('driveId', None)
body['name'] = newFolderName
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INTERNAL_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
GAPI.BAD_REQUEST, GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
body=body, fields='id', supportsAllDrives=True)
newFolderId = result['id']
action = Act.Get()
Act.Set(Act.RECREATE)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'],
j, jcount)
Act.Set(action)
_incrStatistic(statistics, STAT_FOLDER_COPIED_MOVED)
if copyMoveOptions[['copySubFolderPermissions', 'copyTopFolderPermissions'][atTop]]:
_copyPermissions(drive, user, i, count, j, jcount,
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, False,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
True)
return (newFolderId, newFolderName, False)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
GAPI.badRequest, GAPI.targetUserRoleLimitedByLicenseRestriction) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, newFolderName], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FOLDER_FAILED)
copyMoveOptions['retainSourceFolders'] = True
return (None, None, False)
def _makeMoveShortcut(drive, user, k, kcount, entityType, childId, childName, newParentId, newParentName):
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
if entityType == Ent.DRIVE_FILE:
targetEntityType = Ent.DRIVE_FILE_SHORTCUT
statShortcutCreated = STAT_FILE_SHORTCUT_CREATED
statShortcutExists = STAT_FILE_SHORTCUT_EXISTS
else:
targetEntityType = Ent.DRIVE_FOLDER_SHORTCUT
statShortcutCreated = STAT_FOLDER_SHORTCUT_CREATED
statShortcutExists = STAT_FOLDER_SHORTCUT_EXISTS
newParentNameId = f'{newParentName}({newParentId})'
action = Act.Get()
existingShortcut = _checkForExistingShortcut(drive, childId, childName, newParentId)
if existingShortcut:
Act.Set(Act.CREATE_SHORTCUT)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_PREVIOUSLY_IN,
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{childName}({existingShortcut})"],
k, kcount)
Act.Set(action)
_incrStatistic(statistics, statShortcutExists)
return
body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT,
'parents': [newParentId], 'shortcutDetails': {'targetId': childId}}
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID,
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
body=body, fields='id', supportsAllDrives=True)
Act.Set(Act.CREATE_SHORTCUT)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{childName}({result['id']})"],
k, kcount)
Act.Set(action)
_incrStatistic(statistics, statShortcutCreated)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.invalid, GAPI.badRequest, GAPI.fileNotFound, GAPI.unknownError,
GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid,
GAPI.targetUserRoleLimitedByLicenseRestriction) as e:
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
def _moveFile(drive, user, i, count, k, kcount, entityType, childId, childName, newChildName, newParentId, newParentName, removeParents, body):
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
newParentNameId = f'{newParentName}({newParentId})'
if updateFilePermissions:
_updateMoveFilePermissions(drive, user, i, count,
entityType, childId, childName,
statistics, STAT_FILE_PERMISSIONS_FAILED, copyMoveOptions)
try:
callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST,
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE,
GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED,
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION,
GAPI.CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE,
GAPI.CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE,
GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED,
GAPI.CROSS_DOMAIN_MOVE_RESTRICTION,
GAPI.STORAGE_QUOTA_EXCEEDED],
fileId=childId, addParents=newParentId, removeParents=removeParents,
body=body, fields='', supportsAllDrives=True)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, entityType, f'{newChildName}({childId})'],
k, kcount)
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
return
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.unknownError, GAPI.badRequest,
GAPI.targetUserRoleLimitedByLicenseRestriction,
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
GAPI.teamDrivesShortcutFileNotSupported, GAPI.storageQuotaExceeded) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
except (GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
GAPI.fileWriterTeamDriveMoveInDisabled,
GAPI.crossDomainMoveRestriction) as e:
if not copyMoveOptions['createShortcutsForNonmovableFiles']:
entityActionFailedWarning(kvList, str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
else:
_makeMoveShortcut(drive, user, k, kcount, entityType, childId, childName, newParentId, newParentName)
copyMoveOptions['retainSourceFolders'] = True
def _recursiveFolderMove(drive, user, i, count, j, jcount,
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime, atTop):
folderId = source['id']
newFolderId, newFolderName, existingTargetFolder = _cloneFolderMove(drive, user, i, count, j, jcount,
source, targetChildren, newFolderName,
newParentId, newParentName, mergeParentModifiedTime,
statistics, copyMoveOptions, atTop)
if newFolderId is None:
return
movedFiles[newFolderId] = 1
sourceChildren = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=WITH_PARENTS.format(folderId),
orderBy='folder desc,name,modifiedTime desc',
fields='nextPageToken,files(id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
'description,folderColorRgb,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs)
kcount = len(sourceChildren)
if kcount > 0:
if existingTargetFolder:
subTargetChildren = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=ANY_NON_TRASHED_WITH_PARENTS.format(newFolderId),
orderBy='folder desc,name,modifiedTime desc',
fields='nextPageToken,files(id,name,capabilities,mimeType,modifiedTime)',
**parentParms[DFA_SEARCHARGS])
else:
subTargetChildren = []
Ind.Increment()
k = 0
for child in sourceChildren:
k += 1
childId = child['id']
childName = child['name']
childNameId = f'{childName}({childId})'
if movedFiles.get(childId):
continue
trashed = child.pop('trashed', False)
if (childId == newFolderId) or (copyMoveOptions['destDriveId'] and trashed):
entityActionNotPerformedWarning([Ent.USER, user, _getEntityMimeType(child), childNameId],
[Msg.NOT_MOVABLE, Msg.NOT_MOVABLE_IN_TRASH][trashed], k, kcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
childParents = child.pop('parents', [])
child['parents'] = [newFolderId]
if child['mimeType'] == MIMETYPE_GA_FOLDER:
child['oldparents'] = childParents
_recursiveFolderMove(drive, user, i, count, k, kcount,
child, subTargetChildren, childName, newFolderId, newFolderName, child['modifiedTime'], False)
else:
if existingTargetFolder and _checkForDuplicateTargetFile(drive, user, k, kcount, child, childName, subTargetChildren, copyMoveOptions, statistics):
copyMoveOptions['retainSourceFolders'] = True
continue
if childName != child['name']:
body = {'name': child['name']}
else:
body = {}
removeParents = ','.join(childParents)
_moveFile(drive, user, i, count, k, kcount, Ent.DRIVE_FILE, childId, childName, childName, newFolderId, newFolderName, removeParents, body)
Ind.Decrement()
sourceName = source['name']
kvList = [Ent.USER, user, Ent.DRIVE_FOLDER, f"{sourceName}({folderId})"]
if (atTop and (copyMoveOptions['mergeWithParentRetain'] or copyMoveOptions['sourceIsMyDriveSharedDrive'])) or copyMoveOptions['retainSourceFolders']:
Act.Set(Act.RETAIN)
entityActionPerformed(kvList, i, count)
else:
Act.Set(Act.DELETE)
try:
callGAPI(drive.files(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
fileId=folderId, supportsAllDrives=True)
entityActionPerformed(kvList, i, count)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.fileNeverWritable) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Act.Set(Act.MOVE)
return
def anyFolderPermissionOperations():
for field in ['copyMergeWithParentFolderPermissions',
'copyMergedTopFolderPermissions', 'copyMergedSubFolderPermissions',
'copyTopFolderPermissions', 'copySubFolderPermissions']:
if copyMoveOptions[field]:
return True
return False
fileIdEntity = getDriveFileEntity()
parentBody = {}
parentParms = initDriveFileAttributes()
copyMoveOptions = initCopyMoveOptions(False)
newParentsSpecified = updateFilePermissions = False
movedFiles = {}
verifyOrganizer = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getCopyMoveOptions(myarg, copyMoveOptions):
pass
elif getDriveFileParentAttribute(myarg, parentParms):
newParentsSpecified = True
elif myarg == 'updatefilepermissions':
updateFilePermissions = getBoolean()
elif myarg == 'verifyorganizer':
verifyOrganizer = getBoolean()
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER)
if jcount == 0:
continue
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
continue
statistics = _initStatistics()
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
source = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId,
fields='id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
'description,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare',
supportsAllDrives=True)
# Source at root of My Drive or Shared Drive?
sourceMimeType = source['mimeType']
if sourceMimeType == MIMETYPE_GA_FOLDER and source['name'] in [MY_DRIVE, TEAM_DRIVE] and not source.get('parents', []):
copyMoveOptions['sourceIsMyDriveSharedDrive'] = True
if source.get('driveId'):
source['name'] = _getSharedDriveNameFromId(source['driveId'])
sourceName = source['name']
sourceNameId = f"{sourceName}({source['id']})"
copyMoveOptions['sourceDriveId'] = source.get('driveId')
kvList = [Ent.USER, user, _getEntityMimeType(source), sourceNameId]
if fileId in movedFiles:
entityActionNotPerformedWarning(kvList, Msg.DUPLICATE, j, jcount)
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
continue
if copyMoveOptions['sourceDriveId']:
# If moving from a Shared Drive, user has to be an organizer
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']):
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
continue
if source['trashed']:
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_IN_TRASH, j, jcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
sourceSearchArgs = {'driveId': copyMoveOptions['sourceDriveId'], 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
else:
sourceSearchArgs = {}
sourceParents = source.pop('parents', [])
if newParentsSpecified:
newParents = parentBody['parents']
numNewParents = len(newParents)
if numNewParents > 1:
entityActionNotPerformedWarning(kvList, Msg.MULTIPLE_PARENTS_SPECIFIED.format(numNewParents), j, jcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
continue
else:
newParents = sourceParents if sourceParents else [ROOT]
source['parents'] = newParents
newParentId = newParents[0]
dest = _getCopyMoveParentInfo(drive, user, i, count, j, jcount, newParentId, statistics)
if dest is None:
continue
newParentName = dest['name']
newParentNameId = f'{newParentName}({newParentId})'
copyMoveOptions['destDriveId'] = dest['driveId']
copyMoveOptions['destParentType'] = dest['destParentType']
if copyMoveOptions['destDriveId'] and not parentParms[DFA_SEARCHARGS]:
parentParms[DFA_SEARCHARGS] = {'driveId': copyMoveOptions['destDriveId'], 'corpora': 'drive',
'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
if copyMoveOptions['newFilename']:
destName = copyMoveOptions['newFilename']
elif (sourceMimeType == MIMETYPE_GA_FOLDER) and (copyMoveOptions['mergeWithParent'] or copyMoveOptions['mergeWithParentRetain']):
destName = dest['name']
else:
destName = sourceName
targetChildren = _getCopyMoveTargetInfo(drive, user, i, count, j, jcount, source, destName, newParentId, statistics, parentParms)
if targetChildren is None:
continue
if copyMoveOptions['destDriveId']:
# If moving to a Shared Drive, user has to be an organizer
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']):
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
continue
# 3rd party shortcuts can't be moved to Shared Drives
if sourceMimeType.startswith(MIMETYPE_GA_3P_SHORTCUT):
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_3PSHORTCUT, sourceNameId], Msg.NOT_MOVABLE, j, jcount)
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
continue
# Move folder
if sourceMimeType == MIMETYPE_GA_FOLDER:
if fileId == newParentId:
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_INTO_ITSELF, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_FAILED)
continue
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
if _identicalSourceTarget(fileId, targetChildren):
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_MERGE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_FAILED)
continue
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
destName = _getUniqueFilename(destName, sourceMimeType, targetChildren)
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
targetChild = _targetFilenameExists(destName, sourceMimeType, targetChildren)
if targetChild is not None:
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{destName}({targetChild['id']})"],
Msg.DUPLICATE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
continue
if ((not copyMoveOptions['sourceDriveId'] and copyMoveOptions['destDriveId']) or
(copyMoveOptions['mergeWithParent'] or copyMoveOptions['mergeWithParentRetain'] or copyMoveOptions['retainSourceFolders']) or
(copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE and _targetFilenameExists(destName, sourceMimeType, targetChildren) is not None) or
anyFolderPermissionOperations()):
source['oldparents'] = sourceParents
_recursiveFolderMove(drive, user, i, count, j, jcount,
source, targetChildren, destName, newParentId, newParentName, dest['modifiedTime'], True)
continue
entityType = Ent.DRIVE_FOLDER
# Move file
else:
if copyMoveOptions['duplicateFiles'] in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER] and _identicalSourceTarget(fileId, targetChildren):
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE, j, jcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
continue
if _checkForDuplicateTargetFile(drive, user, j, jcount, source, destName, targetChildren, copyMoveOptions, statistics):
continue
destName = source['name'] # duplicatefiles uniquename may cause rename
entityType = Ent.DRIVE_FILE
# Simple move file/folder
if destName != sourceName:
body = {'name': destName}
else:
body = {}
# All parents removed from top level moved item as non-path parents can't be determined
removeParents = ','.join(sourceParents)
_moveFile(drive, user, i, count, j, jcount, entityType, fileId, sourceName, destName, newParentId, newParentName, removeParents, body)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.invalid, GAPI.badRequest, GAPI.cannotCopyFile,
GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
GAPI.fileWriterTeamDriveMoveInDisabled,
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
GAPI.teamDrivesShortcutFileNotSupported, GAPI.crossDomainMoveRestriction) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
_incrStatistic(statistics, STAT_FILE_FAILED)
break
Ind.Decrement()
if copyMoveOptions['summary']:
_printStatistics(user, statistics, i, count, False)
DELETE_DRIVEFILE_CHOICE_MAP = {'purge': 'delete', 'trash': 'trash', 'untrash': 'untrash'}
DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP = {'delete': Act.PURGE, 'trash': Act.TRASH, 'untrash': Act.UNTRASH}
DELETE_DRIVEFILE_FUNCTION_TO_CAPABILITY_MAP = {'delete': 'canDelete', 'trash': 'canTrash', 'untrash': 'canUntrash'}
# gam <UserTypeEntity> delete drivefile <DriveFileEntity> [purge|trash|untrash] [shortcutandtarget [<Boolean>]]
def deleteDriveFile(users, function=None):
fileIdEntity = getDriveFileEntity()
if not function:
function = getChoice(DELETE_DRIVEFILE_CHOICE_MAP, defaultChoice='trash', mapChoice=True)
if checkArgumentPresent('shortcutandtarget'):
shortcutAndTarget = getBoolean()
else:
shortcutAndTarget = False
checkForExtraneousArguments()
Act.Set(DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP[function])
if function != 'delete':
trash_body = {'trashed': function == 'trash'}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER)
if jcount == 0:
continue
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
fileInfoList = []
if shortcutAndTarget:
capability = DELETE_DRIVEFILE_FUNCTION_TO_CAPABILITY_MAP[function]
fileInfo = (fileId, Ent.DRIVE_FILE_OR_FOLDER, Ent.DRIVE_FILE_OR_FOLDER_ID)
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=f'name,mimeType,shortcutDetails,capabilities({capability})', supportsAllDrives=True)
if result['mimeType'] == MIMETYPE_GA_SHORTCUT:
if not result['capabilities'][capability]:
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_SHORTCUT, result['name']],
Msg.SHORTCUT_TARGET_CAPABILITY_IS_FALSE.format('Shortcut', capability), j, jcount)
continue
fileInfoList.append((fileId, Ent.DRIVE_SHORTCUT, Ent.DRIVE_SHORTCUT_ID))
fileId = result['shortcutDetails']['targetId']
fileInfo = (fileId, Ent.DRIVE_FILE_OR_FOLDER, Ent.DRIVE_FILE_OR_FOLDER_ID)
tresult = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields=f'name,capabilities({capability})', supportsAllDrives=True)
if not tresult['capabilities'][capability]:
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_SHORTCUT, result['name'], Ent.DRIVE_FILE_OR_FOLDER, tresult['name']],
Msg.SHORTCUT_TARGET_CAPABILITY_IS_FALSE.format('Target', capability), j, jcount)
continue
fileInfoList.append((fileId, Ent.DRIVE_FILE_OR_FOLDER, Ent.DRIVE_FILE_OR_FOLDER_ID))
for fileInfo in fileInfoList:
fileId = fileInfo[0]
if function != 'delete':
result = callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
fileId=fileId, body=trash_body, fields='name', supportsAllDrives=True)
if result and 'name' in result:
fileName = result['name']
else:
fileName = fileId
else:
callGAPI(drive.files(), function,
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
fileId=fileId, supportsAllDrives=True)
fileName = fileId
entityActionPerformed([Ent.USER, user, fileInfo[1], fileName], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e:
entityActionFailedWarning([Ent.USER, user, fileInfo[2], fileId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> purge drivefile <DriveFileEntity> [shortcutandtarget [<Boolean>]]
def purgeDriveFile(users):
deleteDriveFile(users, 'delete')
# gam <UserTypeEntity> trash drivefile <DriveFileEntity> [shortcutandtarget [<Boolean>]]
def trashDriveFile(users):
deleteDriveFile(users, 'trash')
# gam <UserTypeEntity> untrash drivefile <DriveFileEntity> [shortcutandtarget [<Boolean>]]
def untrashDriveFile(users):
deleteDriveFile(users, 'untrash')
NON_DOWNLOADABLE_MIMETYPES = [MIMETYPE_GA_FORM, MIMETYPE_GA_FUSIONTABLE, MIMETYPE_GA_MAP, MIMETYPE_GA_FOLDER, MIMETYPE_GA_SHORTCUT]
GOOGLEDOC_VALID_EXTENSIONS_MAP = {
MIMETYPE_GA_DRAWING: ['.jpeg', '.jpg', '.pdf', '.png', '.svg'],
MIMETYPE_GA_DOCUMENT: ['.docx', '.epub', '.html', '.odt', '.pdf', '.rtf', '.txt', '.zip'],
MIMETYPE_GA_JAM: ['.pdf'],
MIMETYPE_GA_PRESENTATION: ['.pdf', '.pptx', '.odp', '.txt'],
MIMETYPE_GA_SCRIPT: ['.json'],
MIMETYPE_GA_SPREADSHEET: ['.csv', '.ods', '.pdf', '.tsv', '.xlsx', '.zip'],
}
MICROSOFT_FORMATS_LIST = [
{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ext': '.docx'},
{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'ext': '.dotx'},
{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ext': '.pptx'},
{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ext': '.potx'},
{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ext': '.xlsx'},
{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'ext': '.xltx'},
{'mime': 'application/msword', 'ext': '.doc'},
{'mime': 'application/msword', 'ext': '.dot'},
{'mime': 'application/vnd.ms-powerpoint', 'ext': '.ppt'},
{'mime': 'application/vnd.ms-powerpoint', 'ext': '.pot'},
{'mime': 'application/vnd.ms-excel', 'ext': '.xls'},
{'mime': 'application/vnd.ms-excel', 'ext': '.xlt'},
]
OPENOFFICE_FORMATS_LIST = [
{'mime': 'application/vnd.oasis.opendocument.presentation', 'ext': '.odp'},
{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'},
]
DOCUMENT_FORMATS_MAP = {
'csv': [{'mime': 'text/csv', 'ext': '.csv'}],
'doc': [{'mime': 'application/msword', 'ext': '.doc'}],
'dot': [{'mime': 'application/msword', 'ext': '.dot'}],
'docx': [{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ext': '.docx'}],
'dotx': [{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'ext': '.dotx'}],
'epub': [{'mime': 'application/epub+zip', 'ext': '.epub'}],
'html': [{'mime': 'text/html', 'ext': '.html'}],
'jpeg': [{'mime': 'image/jpeg', 'ext': '.jpeg'}],
'jpg': [{'mime': 'image/jpeg', 'ext': '.jpg'}],
'json': [{'mime': MIMETYPE_GA_SCRIPT_JSON, 'ext': '.json'}],
'mht': [{'mime': 'message/rfc822', 'ext': 'mht'}],
'odp': [{'mime': 'application/vnd.oasis.opendocument.presentation', 'ext': '.odp'}],
'ods': [{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'}],
'odt': [{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'}],
'pdf': [{'mime': 'application/pdf', 'ext': '.pdf'}],
'png': [{'mime': 'image/png', 'ext': '.png'}],
'ppt': [{'mime': 'application/vnd.ms-powerpoint', 'ext': '.ppt'}],
'pot': [{'mime': 'application/vnd.ms-powerpoint', 'ext': '.pot'}],
'potx': [{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ext': '.potx'}],
'pptx': [{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ext': '.pptx'}],
'rtf': [{'mime': 'application/rtf', 'ext': '.rtf'}],
'svg': [{'mime': 'image/svg+xml', 'ext': '.svg'}],
'tsv': [{'mime': 'text/tab-separated-values', 'ext': '.tsv'},
{'mime': 'text/tsv', 'ext': '.tsv'}],
'txt': [{'mime': 'text/plain', 'ext': '.txt'}],
'xls': [{'mime': 'application/vnd.ms-excel', 'ext': '.xls'}],
'xlt': [{'mime': 'application/vnd.ms-excel', 'ext': '.xlt'}],
'xlsx': [{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ext': '.xlsx'}],
'xltx': [{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'ext': '.xltx'}],
'zip': [{'mime': 'application/zip', 'ext': '.zip'}],
}
MIMETYPE_EXTENSION_MAP = {
'application/epub+zip': '.epub',
'application/msword': '.doc',
'application/octet-stream': '',
'application/pdf': '.pdf',
'application/rtf': '.rtf',
'application/vnd.ms-excel': '.xls',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.oasis.opendocument.presentation': '.odp',
'application/vnd.oasis.opendocument.spreadsheet': '.ods',
'application/vnd.oasis.opendocument.text': '.odt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.openxmlformats-officedocument.presentationml.template': '.potx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': '.xltx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': '.dotx',
'application/x-vnd.oasis.opendocument.spreadsheet': '.ods',
'application/zip': '.zip',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/jpg': '.jpg',
'image/png': '.png',
'image/svg+xml': '.svg',
'image/webp': '.webp',
'message/rfc822': 'mht',
'text/csv': '.csv',
'text/html': '.html',
'text/plain': '.txt',
'text/rtf': '.rtf',
'text/tab-separated-values': '.tsv',
'text/tsv': '.tsv',
}
HTTP_ERROR_PATTERN = re.compile(r'^.*returned "(.*)">$')
# gam <UserTypeEntity> get drivefile <DriveFileEntity> [revision <DriveFileRevisionID>]
# [(format <FileFormatList>)|(gsheet|csvsheet <SheetEntity>)] [exportsheetaspdf <String>]
# [targetfolder <FilePath>] [targetname -|<FileName>]
# [donotfollowshortcuts [<Boolean>]] [overwrite [<Boolean>]] [showprogress [<Boolean>]]
# [acknowledgeabuse [<Boolean>]]
def getDriveFile(users):
def closeRemoveTargetFile(f):
if f and not targetStdout:
closeFile(f)
os.remove(filename)
fileIdEntity = getDriveFileEntity()
sheetEntity = None
exportSheetAsPDF = revisionId = ''
exportFormatName = 'openoffice'
exportFormatChoices = [exportFormatName]
exportFormats = OPENOFFICE_FORMATS_LIST
defaultFormats = True
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
targetNamePattern = None
acknowledgeAbuse = donotFollowShortcuts = overwrite = showProgress = suppressStdoutMsgs = targetStdout = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'format':
exportFormatChoices = getString(Cmd.OB_FORMAT_LIST).replace(',', ' ').lower().split()
exportFormats = []
for exportFormat in exportFormatChoices:
if exportFormat in {'ms', 'microsoft', 'micro$oft'}:
exportFormats.extend(MICROSOFT_FORMATS_LIST)
elif exportFormat == 'openoffice':
exportFormats.extend(OPENOFFICE_FORMATS_LIST)
elif exportFormat in DOCUMENT_FORMATS_MAP:
exportFormats.extend(DOCUMENT_FORMATS_MAP[exportFormat])
else:
invalidChoiceExit(exportFormat, DOCUMENT_FORMATS_MAP, True)
defaultFormats = False
elif myarg == 'targetfolder':
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
elif myarg == 'targetname':
targetNamePattern = getString(Cmd.OB_FILE_NAME)
targetStdout = targetNamePattern == '-'
suppressStdoutMsgs = False if not targetStdout else GM.Globals[GM.STDOUT][GM.REDIRECT_STD]
elif myarg == 'donotfollowshortcuts':
donotFollowShortcuts = getBoolean()
elif myarg == 'overwrite':
overwrite = getBoolean()
elif myarg == 'revision':
revisionId = getString(Cmd.OB_DRIVE_FILE_REVISION_ID)
elif myarg in {'gsheet', 'csvsheet'}:
sheetEntity = getSheetEntity(False)
elif myarg == 'exportsheetaspdf':
exportSheetAsPDF = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'nocache':
pass
elif myarg == 'showprogress':
showProgress = getBoolean()
elif myarg == 'acknowledgeabuse':
acknowledgeAbuse = getBoolean()
else:
unknownArgumentExit()
if exportSheetAsPDF:
exportFormatName = 'pdf'
exportFormatChoices = [exportFormatName]
exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName]
elif sheetEntity:
if defaultFormats:
exportFormatName = 'csv'
else:
exportFormatName = exportFormats[0]['ext'][1:]
exportFormatChoices = [exportFormatName]
exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName]
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE if not suppressStdoutMsgs else None)
if jcount == 0:
continue
_, userName, _ = splitEmailAddressOrUID(user)
if exportSheetAsPDF or sheetEntity:
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
continue
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileExtension = None
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,fullFileExtension,mimeType,quotaBytesUsed,shortcutDetails', supportsAllDrives=True)
mimeType = result['mimeType']
if (mimeType == MIMETYPE_GA_SHORTCUT) and not donotFollowShortcuts:
fileId = result['shortcutDetails']['targetId']
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,fullFileExtension,mimeType,size', supportsAllDrives=True)
mimeType = result['mimeType']
entityValueList = [Ent.USER, user, _getEntityMimeType(result), result['name']]
if mimeType in NON_DOWNLOADABLE_MIMETYPES:
entityActionNotPerformedWarning(entityValueList, Msg.FORMAT_NOT_DOWNLOADABLE, j, jcount)
continue
if revisionId:
callGAPI(drive.revisions(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS+[GAPI.REVISION_NOT_FOUND],
fileId=fileId, revisionId=revisionId, fields='id')
fileExtension = result.get('fullFileExtension')
googleDocExtensions = GOOGLEDOC_VALID_EXTENSIONS_MAP.get(mimeType)
if googleDocExtensions:
my_line = ['Type', 'Google Doc']
googleDoc = True
for exportFormat in exportFormats:
if exportFormat['ext'] in googleDocExtensions:
exportMimeType = exportFormat['mime']
if fileExtension:
extension = '.'+fileExtension
else:
extension = exportFormat['ext']
break
else:
entityActionNotPerformedWarning(entityValueList, Msg.FORMAT_NOT_AVAILABLE.format(','.join(exportFormatChoices)), j, jcount)
continue
else:
if 'quotaBytesUsed' in result:
my_line = ['Size', formatFileSize(int(result['quotaBytesUsed']))]
else:
my_line = ['Size', UNKNOWN]
googleDoc = False
if fileExtension:
extension = '.'+fileExtension
else:
extension = MIMETYPE_EXTENSION_MAP.get(mimeType, '')
while True:
if targetStdout:
filename = 'stdout'
else:
filename, _ = uniqueFilename(targetFolder, targetName or cleanFilename(result['name']), overwrite, extension)
spreadsheetUrl = None
f = None
try:
if googleDoc:
if (not exportSheetAsPDF and not sheetEntity) or mimeType != MIMETYPE_GA_SPREADSHEET:
request = drive.files().export_media(fileId=fileId, mimeType=exportMimeType)
if revisionId:
request.uri = f'{request.uri}&revision={revisionId}'
else:
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))')
# spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?exportFormat={exportFormatName}&format={exportFormatName}&id={fileId}'
spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?format={exportFormatName}&id={fileId}'
if sheetEntity:
entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetValue']])
sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
if sheetId is None:
entityActionNotPerformedWarning(entityValueList, Msg.NOT_FOUND, j, jcount)
break
spreadsheetUrl += f'&gid={sheetId}'
spreadsheetUrl += exportSheetAsPDF
else:
if revisionId:
entityValueList.extend([Ent.DRIVE_FILE_REVISION, revisionId])
request = drive.revisions().get_media(fileId=fileId, revisionId=revisionId)
else:
request = drive.files().get_media(fileId=fileId, acknowledgeAbuse=acknowledgeAbuse)
if not targetStdout:
f = open(filename, 'wb')
else:
f = os.fdopen(os.dup(sys.stdout.fileno()), 'wb')
if not spreadsheetUrl:
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
while not done:
status, done = downloader.next_chunk()
if showProgress and not suppressStdoutMsgs and status.progress() < 1.0:
entityActionPerformedMessage(entityValueList, f'{status.progress():>7.2%}', j, jcount)
else:
if GC.Values[GC.DEBUG_LEVEL] > 0:
sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n')
maxRetries = 10
sleepTime = 5
for retry in range(1, maxRetries+1):
status, content = drive._http.request(uri=spreadsheetUrl, method='GET')
if status['status'] != '429':
break
writeStderr(Msg.RETRYING_GOOGLE_SHEET_EXPORT_SLEEPING.format(retry, maxRetries, sleepTime))
time.sleep(sleepTime)
if status['status'] == '200':
f.write(content)
if targetStdout and content[-1] != '\n':
f.write(bytes('\n', UTF8))
else:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, f'HTTP Error: {status["status"]}', j, jcount)
closeRemoveTargetFile(f)
break
if not targetStdout:
closeFile(f)
if not suppressStdoutMsgs:
entityModifierNewValueKeyValueActionPerformed(entityValueList, Act.MODIFIER_TO, filename, my_line[0], my_line[1], j, jcount)
break
except (IOError, httplib2.HttpLib2Error) as e:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, str(e), j, jcount)
except googleapiclient.http.HttpError as e:
mg = HTTP_ERROR_PATTERN.match(str(e))
if mg:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, mg.group(1), j, jcount)
else:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, str(e), j, jcount)
closeRemoveTargetFile(f)
break
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
except GAPI.revisionNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId, Ent.DRIVE_FILE_REVISION, revisionId], Msg.DOES_NOT_EXIST, j, jcount)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, fileId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
SUGGESTIONS_VIEW_MODE_CHOICE_MAP = {
'default': 'DEFAULT_FOR_CURRENT_ACCESS',
'suggestionsinline': 'SUGGESTIONS_INLINE',
'previewsuggestionsaccepted': 'PREVIEW_SUGGESTIONS_ACCEPTED',
'previewwithoutsuggestions': 'PREVIEW_WITHOUT_SUGGESTIONS'
}
# gam <UserTypeEntity> get document <DriveFileEntity>
# [viewmode default|suggestions_inline|preview_suggestions_accepted|preview_without_suggestions]
# [targetfolder <FilePath>] [targetname <FileName>]
# [donotfollowshortcuts [<Boolean>]] [overwrite [<Boolean>]]
def getGoogleDocument(users):
fileIdEntity = getDriveFileEntity()
suggestionsViewMode = SUGGESTIONS_VIEW_MODE_CHOICE_MAP['default']
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
targetNamePattern = None
donotFollowShortcuts = overwrite = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'viewmode':
suggestionsViewMode = getChoice(SUGGESTIONS_VIEW_MODE_CHOICE_MAP, mapChoice=True)
elif myarg == 'targetfolder':
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
elif myarg == 'targetname':
targetNamePattern = getString(Cmd.OB_FILE_NAME)
elif myarg == 'donotfollowshortcuts':
donotFollowShortcuts = getBoolean()
elif myarg == 'overwrite':
overwrite = getBoolean()
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DOCUMENT)
if jcount == 0:
continue
_, docs = buildGAPIServiceObject(API.DOCS, user, i, count)
if not docs:
continue
_, userName, _ = splitEmailAddressOrUID(user)
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,mimeType,shortcutDetails', supportsAllDrives=True)
mimeType = result['mimeType']
if (mimeType == MIMETYPE_GA_SHORTCUT) and not donotFollowShortcuts:
fileId = result['shortcutDetails']['targetId']
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,mimeType', supportsAllDrives=True)
mimeType = result['mimeType']
docName = result['name']
if mimeType != MIMETYPE_GA_DOCUMENT:
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, docName],
Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_DOCUMENT), j, jcount)
continue
filename, _ = uniqueFilename(targetFolder, targetName or cleanFilename(docName), overwrite)
result = callGAPI(docs.documents(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
documentId=fileId, suggestionsViewMode=suggestionsViewMode)
if writeFile(filename, json.dumps(result, indent=2, sort_keys=True)+'\n', continueOnError=True):
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DOCUMENT, f'{docName}({fileId})'], Act.MODIFIER_TO, filename, j, jcount)
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DOCUMENT, fileId], Msg.DOES_NOT_EXIST, j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> update docuument <DriveFileEntity>
# ((json [charset <Charset>] <SpreadsheetJSONUpdateRequest>) |
# (json file <FileName> [charset <Charset>]))
# [formatjson]
def updateGoogleDocument(users):
fileIdEntity = getDriveFileEntity()
body = {}
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'json':
body = getJSON([])
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DOCUMENT if not FJQC.formatJSON else None)
if jcount == 0:
continue
_, docs = buildGAPIServiceObject(API.DOCS, user, i, count)
if not docs:
continue
Ind.Increment()
j = 0
for documentId in fileIdEntity['list']:
j += 1
try:
result = callGAPI(docs.documents(), 'batchUpdate',
throwReasons=GAPI.DOCS_ACCESS_THROW_REASONS,
documentId=documentId, body=body)
if FJQC.formatJSON:
printLine('{'+f'"User": "{user}", "documentId": "{documentId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
continue
entityActionPerformed([Ent.USER, user, Ent.DOCUMENT, documentId], j, jcount)
Ind.Increment()
for field in ['replies', 'writeControl']:
if field in result:
showJSON(field, result[field])
Ind.Decrement()
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DOCUMENT, documentId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> collect orphans
# [(targetuserfoldername <DriveFolderName>)(targetuserfolderid <DriveFolderID>)]
# [useshortcuts [<Boolean>]]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [preview] [todrive <ToDriveAttribute>*]
def collectOrphans(users):
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
csvPF = None
targetParms = initDriveFileAttributes()
targetUserFolderId = None
targetUserFolderPattern = '#user# orphaned files'
targetParentBody = {}
query = ME_IN_OWNERS_AND+'trashed = false'
useShortcuts = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'targetuserfoldername':
targetUserFolderPattern = getString(Cmd.OB_DRIVE_FOLDER_NAME)
targetUserFolderId = None
elif myarg == 'targetuserfolderid':
targetUserFolderId = getString(Cmd.OB_DRIVE_FOLDER_ID)
targetUserFolderPattern = None
elif myarg == 'useshortcuts':
useShortcuts = getBoolean()
elif myarg == 'preview':
csvPF = CSVPrintFile(['Owner', 'type', 'id', 'name', 'action'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
userName, _ = splitEmailAddress(user)
try:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, Ent.TypeName(Ent.USER, user), i, count, query=query)
feed = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query, orderBy=OBY.orderBy,
fields='nextPageToken,files(id,name,parents,mimeType,sharedWithMeTime,capabilities(canMoveItemWithinDrive))',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
if targetUserFolderPattern:
trgtUserFolderName = _substituteForUser(targetUserFolderPattern, user, userName)
targetParms[DFA_PARENTQUERY] = MY_NON_TRASHED_FOLDER_NAME.format(escapeDriveFileName(trgtUserFolderName))
else:
targetParms[DFA_PARENTID] = targetUserFolderId
trgtUserFolderName = targetUserFolderId
if not _getDriveFileParentInfo(drive, user, i, count, targetParentBody, targetParms, True, False):
continue
orphanDriveFiles = []
for fileEntry in feed:
if not fileEntry.get('parents') and 'sharedWithMeTime' not in fileEntry:
orphanDriveFiles.append(fileEntry)
jcount = len(orphanDriveFiles)
entityPerformActionNumItemsModifier([Ent.USER, user], jcount, Ent.DRIVE_ORPHAN_FILE_OR_FOLDER,
f'{Act.MODIFIER_INTO} {Ent.Singular(Ent.DRIVE_FOLDER)}: {trgtUserFolderName}', i, count)
if jcount == 0:
continue
if not csvPF:
if 'parents' not in targetParentBody or not targetParentBody['parents']:
try:
newParentId = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.UNKNOWN_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI. TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
body={'name': trgtUserFolderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id')['id']
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, trgtUserFolderName], str(e), i, count)
continue
else:
newParentId = targetParentBody['parents'][0]
setSysExitRC(ORPHANS_COLLECTED_RC)
Ind.Increment()
j = 0
for fileEntry in orphanDriveFiles:
j += 1
fileId = fileEntry['id']
fileName = fileEntry['name']
fileType = _getEntityMimeType(fileEntry)
# Deleted 6.26.16
# if fileType == Ent.DRIVE_FOLDER and not fileEntry['capabilities']['canAddMyDriveParent']:
# # Typically Google Backup & Sync images of laptops
# continue
if not useShortcuts and fileEntry['capabilities']['canMoveItemWithinDrive']:
if csvPF:
csvPF.WriteRow({'Owner': user, 'type': Ent.Singular(fileType), 'id': fileId, 'name': fileName, 'action': 'changeParent'})
continue
try:
callGAPI(drive.files(), 'update',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
GAPI.INTERNAL_ERROR, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
retryReasons=[GAPI.FILE_NOT_FOUND],
fileId=fileId, body={}, addParents=newParentId, fields='')
entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, fileType, fileName],
Act.MODIFIER_INTO, None, [Ent.DRIVE_FOLDER, trgtUserFolderName], j, jcount)
except (GAPI.badRequest, GAPI.fileNotFound, GAPI.internalError, GAPI.insufficientParentPermissions,) as e:
entityActionFailedWarning([Ent.USER, user, fileType, fileName], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
else:
if csvPF:
csvPF.WriteRow({'Owner': user, 'type': Ent.Singular(fileType), 'id': fileId, 'name': fileName, 'action': 'createShortcut'})
continue
try:
# Check for existing shortcut, do not duplicate
files = callGAPIitems(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
q=f"'me' in owners and name = '{escapeDriveFileName(fileName)}' and mimeType = '{MIMETYPE_GA_SHORTCUT}' and '{newParentId}' in parents and trashed = false",
fields='files(id,shortcutDetails(targetId))')
existingShortcut = False
for f_file in files:
if f_file['shortcutDetails']['targetId'] == fileId:
entityActionNotPerformedWarning([Ent.USER, user, fileType, fileName, Ent.DRIVE_FILE_SHORTCUT, f"{fileName}({f_file['id']})"],
Msg.ALREADY_EXISTS_IN_TARGET_FOLDER.format(Ent.Singular(Ent.DRIVE_FOLDER), trgtUserFolderName), j, jcount)
existingShortcut = True
break
if existingShortcut:
continue
body = {'name': fileName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': [newParentId], 'shortcutDetails': {'targetId': fileId}}
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
GAPI.SHORTCUT_TARGET_INVALID],
body=body, fields='id,name', supportsAllDrives=True)
entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, fileType, fileName, Ent.DRIVE_FILE_SHORTCUT, f'{result["name"]}({result["id"]})'],
Act.MODIFIER_INTO, None, [Ent.DRIVE_FOLDER, trgtUserFolderName], j, jcount)
except GAPI.invalidQuery:
entityActionFailedWarning([Ent.USER, user, fileType, fileName], invalidQuery(query), j, jcount)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
GAPI.shortcutTargetInvalid) as e:
entityActionFailedWarning([Ent.USER, user, fileType, fileName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Orphans to Collect')
TRANSFER_DRIVEFILE_ACL_ROLES_MAP = {
'commenter': 'commenter',
'contentmanager': 'fileOrganizer',
'contributor': 'writer',
'editor': 'writer',
'fileorganizer': 'fileOrganizer',
'fileorganiser': 'fileOrganizer',
'manager': 'organizer',
'organizer': 'organizer',
'organiser': 'organizer',
'owner': 'organizer',
'read': 'reader',
'reader': 'reader',
'viewer': 'reader',
'writer': 'writer',
'current': 'current',
'none': 'none',
'source': 'source',
}
# gam <UserTypeEntity> transfer drive <UserItem> [select <DriveFileEntity>]
# [(targetfolderid <DriveFolderID>)|(targetfoldername <DriveFolderName>)]
# [targetuserfoldername <DriveFolderName>] [targetuserorphansfoldername <DriveFolderName>]
# [mergewithtarget [<Boolean>]]
# [createshortcutsfornonmovablefiles [<Boolean>]]
# [skipids <DriveFileEntity>]
# [keepuser | (retainrole reader|commenter|writer|editor|fileorganizer|none)] [noretentionmessages]
# [nonowner_retainrole reader|commenter|writer|editor|fileorganizer|current|none]
# [nonowner_targetrole reader|commenter|writer|editor|fileorganizer|current|none|source]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [enforceexpansiveaccess [<Boolean>]]
# [preview] [todrive <ToDriveAttribute>*]
def transferDrive(users):
def _getOwnerUser(childEntryInfo):
if 'owners' not in childEntryInfo or not childEntryInfo['owners']:
return (UNKNOWN, None)
ownerUser = childEntryInfo['owners'][0]['emailAddress']
if ownerUser not in thirdPartyOwners:
_, ownerDrive = buildGAPIServiceObject(API.DRIVE3, ownerUser, displayError=False)
thirdPartyOwners[ownerUser] = ownerDrive
else:
ownerDrive = thirdPartyOwners[ownerUser]
return (ownerUser, ownerDrive)
TARGET_PARENT_ID = 0
TARGET_ORPHANS_PARENT_ID = 1
def _buildTargetFile(folderName, folderParentId):
try:
op = 'Find Target Folder'
result = callGAPIpages(targetDrive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST],
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy,
q=MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS.format(escapeDriveFileName(folderName), folderParentId),
fields='nextPageToken,files(id)')
if result:
return result[0]['id']
op = 'Create Target Folder'
return callGAPI(targetDrive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.UNKNOWN_ERROR, GAPI.BAD_REQUEST, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
body={'parents': [folderParentId], 'name': folderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id')['id']
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions, GAPI.unknownError, GAPI.badRequest,
GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning([Ent.USER, targetUser, Ent.DRIVE_FOLDER, folderName], f'{op}: {str(e)}')
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(targetUser, str(e))
return None
def _buildTargetUserFolder():
folderName = _substituteForUser(targetUserFolderPattern, sourceUser, sourceUserName)
if not folderName:
return targetFolderId
return _buildTargetFile(folderName, targetFolderId)
def _buildTargetUserOrphansFolder():
folderName = _substituteForUser(targetUserOrphansFolderPattern, sourceUser, sourceUserName)
if folderName:
targetIds[TARGET_ORPHANS_PARENT_ID] = _buildTargetFile(folderName, targetIds[TARGET_PARENT_ID])
if targetIds[TARGET_ORPHANS_PARENT_ID] is None:
targetIds[TARGET_ORPHANS_PARENT_ID] = targetIds[TARGET_PARENT_ID]
else:
targetIds[TARGET_ORPHANS_PARENT_ID] = targetIds[TARGET_PARENT_ID]
def _getMappedParentForRootParentOrOrphan(childEntryInfo, atSelectTop):
if 'parents' not in childEntryInfo or not childEntryInfo['parents']:
return (None, targetIds[TARGET_ORPHANS_PARENT_ID])
if atSelectTop:
return (childEntryInfo['parents'], targetIds[TARGET_PARENT_ID])
for parentId in childEntryInfo['parents']:
if parentId == sourceRootId:
return ([sourceRootId], targetIds[TARGET_PARENT_ID])
return (None, None)
def _setUpdateRole(permission):
return {'role': permission['role']}
def _makeXferShortcut(drive, user, j, jcount, entityType, childId, childName, newParentId):
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
targetEntityType = Ent.DRIVE_FILE_SHORTCUT if entityType == Ent.DRIVE_FILE else Ent.DRIVE_FOLDER_SHORTCUT
action = Act.Get()
existingShortcut = _checkForExistingShortcut(drive, childId, childName, newParentId)
if existingShortcut:
Act.Set(Act.CREATE_SHORTCUT)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_PREVIOUSLY_IN,
[Ent.DRIVE_FOLDER, newParentId, targetEntityType, f"{childName}({existingShortcut})"],
j, jcount)
Act.Set(action)
return
body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT,
'parents': [newParentId], 'shortcutDetails': {'targetId': childId}}
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID],
body=body, fields='id', supportsAllDrives=True)
Act.Set(Act.CREATE_SHORTCUT)
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
[Ent.DRIVE_FOLDER, newParentId, targetEntityType, f"{childName}({result['id']})"],
j, jcount)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), j, jcount)
Act.Set(action)
# Recreate source user shortcut in target user
def _transferShortcut(j, jcount, childEntryInfo, childId, childName, newParentId):
entityType = Ent.DRIVE_FOLDER_SHORTCUT if childEntryInfo['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_SHORTCUT
kvList = [Ent.USER, sourceUser, entityType, f'{childName}({childId})']
action = Act.Get()
body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT,
'parents': [newParentId], 'shortcutDetails': {'targetId': childEntryInfo['shortcutDetails']['targetId']}}
Act.Set(Act.RECREATE)
try:
result = callGAPI(targetDrive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID],
body=body, fields='id', supportsAllDrives=True)
shortcutId = result['id']
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.USER, targetUser,
Ent.DRIVE_FOLDER, newParentId, entityType, f"{shortcutId})"],
j, jcount)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), j, jcount)
Act.Set(action)
return
if ownerRetainRoleBody['role'] == 'none':
Act.Set(Act.DELETE_SHORTCUT)
kvList = [Ent.USER, sourceUser, entityType, f'{childName}({childId})']
try:
callGAPI(sourceDrive.files(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
fileId=childId, supportsAllDrives=True)
entityActionPerformed(kvList, j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
Act.Set(action)
def _transferFile(childEntry, i, count, j, jcount, atSelectTop):
childEntryInfo = childEntry['info']
childFileId = childEntryInfo['id']
childFileName = childEntryInfo['name']
childFileType = _getEntityMimeType(childEntryInfo)
# Owned files
if childEntryInfo['ownedByMe']:
childEntryInfo['sourcePermission'] = {'role': 'owner'}
for permission in childEntryInfo.get('permissions', []):
if targetPermissionId == permission['id']:
childEntryInfo['targetPermission'] = _setUpdateRole(permission)
updateTargetPermission = True
break
else:
childEntryInfo['targetPermission'] = {'role': 'none'}
updateTargetPermission = False
if csvPF:
csvPF.WriteRow({'OldOwner': sourceUser, 'NewOwner': targetUser, 'type': Ent.Singular(childFileType), 'id': childFileId, 'name': childFileName, 'role': 'owner'})
return
Act.Set(Act.TRANSFER_OWNERSHIP)
addTargetParent = None
removeSourceParents = set()
removeTargetParents = set()
childParents = childEntryInfo.get('parents', [])
if childParents:
for parentId in childParents:
if parentId in parentIdMap:
addTargetParent = parentIdMap[parentId]
if parentId != sourceRootId:
removeSourceParents.add(parentId)
elif not mergeWithTarget and targetFolderId != targetRootId:
removeTargetParents.add(targetRootId)
else:
if targetIds[TARGET_ORPHANS_PARENT_ID] is None:
_buildTargetUserOrphansFolder()
addTargetParent = targetIds[TARGET_ORPHANS_PARENT_ID]
removeTargetParents.add(targetRootId)
try:
actionUser = sourceUser
if childEntryInfo['mimeType'] != MIMETYPE_GA_SHORTCUT:
if not updateTargetPermission:
op = 'Create Source ACL'
callGAPI(sourceDrive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID_SHARING_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
fileId=childFileId, sendNotificationEmail=False, body=targetWriterPermissionsBody, fields='')
op = 'Update Source ACL'
callGAPI(sourceDrive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID_OWNERSHIP_TRANSFER,
GAPI.PERMISSION_NOT_FOUND, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=childFileId, permissionId=targetPermissionId,
transferOwnership=True, body={'role': 'owner'}, fields='')
if removeSourceParents:
op = 'Remove Source Parents'
callGAPI(sourceDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3,
fileId=childFileId, removeParents=','.join(removeSourceParents), fields='')
actionUser = targetUser
if addTargetParent or removeTargetParents:
op = 'Add/Remove Target Parents'
callGAPI(targetDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3,
fileId=childFileId,
addParents=addTargetParent, removeParents=','.join(removeTargetParents), fields='')
entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
else:
if topSourceId in childParents:
_transferShortcut(j, jcount, childEntryInfo, childFileId, childFileName, addTargetParent)
else:
entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.unknownError,
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotAddParent, GAPI.insufficientParentPermissions) as e:
entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], f'{op}: {str(e)}', j, jcount)
except (GAPI.insufficientFilePermissions, GAPI.fileOwnerNotMemberOfWriterDomain, GAPI.crossDomainMoveRestriction) as e:
if not createShortcutsForNonmovableFiles:
entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], f'{op}: {str(e)}', j, jcount)
else:
_makeXferShortcut(targetDrive, targetUser, j, jcount, childFileType, childFileId, childFileName, addTargetParent)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, actionUser, childFileType, childFileName, Ent.PERMISSION_ID, targetPermissionId], j, jcount)
except GAPI.invalidSharingRequest as e:
entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], Ent.TypeNameMessage(Ent.PERMISSION_ID, targetPermissionId, str(e)), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(actionUser, str(e), i, count)
# Non-owned files
else:
Act.Set(Act.PROCESS)
for permission in childEntryInfo.get('permissions', []):
if sourcePermissionId == permission['id']:
childEntryInfo['sourcePermission'] = _setUpdateRole(permission)
getSourcePermissionFromOwner = False
break
else:
childEntryInfo['sourcePermission'] = nonOwnerRetainRoleBody
getSourcePermissionFromOwner = True
for permission in childEntryInfo.get('permissions', []):
if targetPermissionId == permission['id']:
childEntryInfo['targetPermission'] = _setUpdateRole(permission)
getTargetPermissionFromOwner = False
break
else:
childEntryInfo['targetPermission'] = {'role': 'none'}
getTargetPermissionFromOwner = True
ownerUser, ownerDrive = _getOwnerUser(childEntryInfo)
if not ownerDrive:
entityActionNotPerformedWarning([Ent.USER, sourceUser, childFileType, childFileName],
Msg.SERVICE_NOT_APPLICABLE_THIS_ADDRESS.format(ownerUser), j, jcount)
return
if getSourcePermissionFromOwner or getTargetPermissionFromOwner:
try:
permissions = callGAPIpages(ownerDrive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS+[GAPI.BAD_REQUEST],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
fileId=childFileId, fields='nextPageToken,permissions')
if getSourcePermissionFromOwner:
for permission in permissions:
if sourcePermissionId == permission['id']:
childEntryInfo['sourcePermission'] = _setUpdateRole(permission)
break
else:
childEntryInfo['sourcePermission'] = nonOwnerRetainRoleBody
if getTargetPermissionFromOwner:
for permission in permissions:
if targetPermissionId == permission['id']:
childEntryInfo['targetPermission'] = _setUpdateRole(permission)
break
else:
childEntryInfo['targetPermission'] = {'role': 'none'}
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid, GAPI.badRequest) as e:
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(ownerUser, str(e), i, count)
return
if csvPF:
csvPF.WriteRow({'OldOwner': sourceUser, 'NewOwner': targetUser, 'type': Ent.Singular(childFileType),
'id': childFileId, 'name': childFileName, 'role': childEntryInfo['sourcePermission']['role']})
return
if (childFileType == Ent.DRIVE_FOLDER) and (childEntryInfo['targetPermission']['role'] == 'none') and (ownerRetainRoleBody['role'] == 'none'):
if targetIds[TARGET_ORPHANS_PARENT_ID] is None:
_buildTargetUserOrphansFolder()
parentIdMap[childFileId] = _buildTargetFile(childFileName, targetIds[TARGET_ORPHANS_PARENT_ID])
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], j, jcount)
return
existingParentIds, mappedParentId = _getMappedParentForRootParentOrOrphan(childEntryInfo, atSelectTop)
if mappedParentId is not None:
# Give temporary writer access to target user so other actions can be performed
if childEntryInfo['targetPermission']['role'] in {'none', 'reader'}:
try:
callGAPI(ownerDrive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
fileId=childFileId, sendNotificationEmail=False, body=targetWriterPermissionsBody, fields='')
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.sharingRateLimitExceeded) as e:
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
return
except GAPI.invalidSharingRequest as e:
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName],
Ent.TypeNameMessage(Ent.PERMISSION_ID, sourcePermissionId, str(e)), j, jcount)
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(ownerUser, str(e), i, count)
return
if existingParentIds is not None:
# We have to make a shortcut to a non-owned non-orphan as we can't change the parents
try:
body = {'name': childFileName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': [mappedParentId], 'shortcutDetails': {'targetId': childFileId}}
callGAPI(targetDrive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
body=body, fields='', supportsAllDrives=True)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning([Ent.USER, targetUser, childFileType, childFileName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), j, jcount)
return
# Delete existing parents
try:
callGAPI(sourceDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
retryReasons=[GAPI.FILE_NOT_FOUND], triesLimit=3,
fileId=childFileId,
removeParents=','.join(existingParentIds), body={}, fields='')
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest) as e:
entityActionFailedWarning([Ent.USER, sourceUser, childFileType, childFileName], str(e), j, jcount)
return
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
return
# 5.31.03 - Non-owned files without parents are SharedWithMe, parents can not be changed
# else:
## We can add a parent when transferring an orphan
# try:
# callGAPI(targetDrive.files(), 'update',
# throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
# retryReasons=[GAPI.FILE_NOT_FOUND], triesLimit=3,
# fileId=childFileId,
# addParents=mappedParentId, body={}, fields='')
# except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.unknownError,
# GAPI.badRequest, GAPI.cannotAddParent) as e:
# entityActionFailedWarning([Ent.USER, targetUser, childFileType, childFileName], str(e), j, jcount)
# return
# except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
# userDriveServiceNotEnabledWarning(targetUser, str(e), i, count)
# return
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], j, jcount)
def _manageRoleRetention(childEntry, i, count, j, jcount, atSelectTop):
def _setTargetInsertBody(permission):
return {'role': permission['role'], 'type': 'user', 'emailAddress': targetUser}
def _checkForDiminishedTargetRole(currentPermission, newPermission):
if currentPermission['role'] in {'owner', 'organizer', 'fileOrganizer', 'writer'}:
return False
if (currentPermission['role'] == 'commenter') and (newPermission['role'] == 'reader'):
return False
return True
childEntryInfo = childEntry['info']
childFileId = childEntryInfo['id']
childFileName = childEntryInfo['name']
childFileType = _getEntityMimeType(childEntryInfo)
if childEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT:
if showRetentionMessages:
entityActionNotPerformedWarning([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, ownerRetainRoleBody['role']], Msg.NOT_APPROPRIATE, j, jcount)
return
if childEntryInfo['ownedByMe']:
try:
if ownerRetainRoleBody['role'] != 'none':
if ownerRetainRoleBody['role'] != 'writer':
callGAPI(targetDrive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=childFileId, permissionId=sourcePermissionId, body=ownerRetainRoleBody, fields='')
else:
callGAPI(targetDrive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED, GAPI.CANNOT_REMOVE_OWNER],
fileId=childFileId, permissionId=sourcePermissionId)
if showRetentionMessages:
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, ownerRetainRoleBody['role']], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotRemoveOwner, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning([Ent.USER, sourceUser, childFileType, childFileName], str(e), j, jcount)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, sourceUser, childFileType, childFileName, Ent.PERMISSION_ID, sourcePermissionId], j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
else:
ownerUser, ownerDrive = _getOwnerUser(childEntryInfo)
if not ownerDrive:
return
if nonOwnerRetainRoleBody['role'] == 'current':
sourceUpdateRole = childEntryInfo['sourcePermission']
else:
sourceUpdateRole = nonOwnerRetainRoleBody
if 'targetPermission' not in childEntryInfo:
childEntryInfo['targetPermission'] = {'role': 'current'}
errorTargetRole = True
else:
errorTargetRole = False
if nonOwnerTargetRoleBody['role'] == 'current':
targetInsertBody = _setTargetInsertBody(childEntryInfo['targetPermission'])
resetTargetRole = False
elif nonOwnerTargetRoleBody['role'] == 'source':
targetInsertBody = _setTargetInsertBody(childEntryInfo['sourcePermission'])
resetTargetRole = True
else:
targetInsertBody = _setTargetInsertBody(nonOwnerTargetRoleBody)
resetTargetRole = True
if not errorTargetRole:
if resetTargetRole:
resetTargetRole = _checkForDiminishedTargetRole(childEntryInfo['targetPermission'], targetInsertBody)
else:
_, mappedParentId = _getMappedParentForRootParentOrOrphan(childEntryInfo, atSelectTop)
if mappedParentId is not None and childEntryInfo['targetPermission']['role'] in {'none', 'reader'}:
resetTargetRole = True
# Update owner permissions
try:
if nonOwnerRetainRoleBody['role'] != 'none':
if nonOwnerRetainRoleBody['role'] != 'current':
callGAPI(ownerDrive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=childFileId, permissionId=sourcePermissionId, body=sourceUpdateRole, fields='')
else:
try:
callGAPI(ownerDrive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED, GAPI.CANNOT_REMOVE_OWNER],
fileId=childFileId, permissionId=sourcePermissionId)
except GAPI.permissionNotFound:
pass
if showRetentionMessages:
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, sourceUpdateRole['role']], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotRemoveOwner, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, ownerUser, childFileType, childFileName, Ent.PERMISSION_ID, sourcePermissionId], j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(ownerUser, str(e), i, count)
# Update target permissions
if resetTargetRole and targetUser != ownerUser:
try:
if nonOwnerTargetRoleBody['role'] != 'none':
if nonOwnerTargetRoleBody['role'] != 'current' and targetInsertBody['role'] not in {'current', 'none'}:
callGAPI(ownerDrive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
fileId=childFileId, sendNotificationEmail=False, body=targetInsertBody, fields='')
else:
try:
callGAPI(ownerDrive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
fileId=childFileId, permissionId=targetPermissionId)
except GAPI.permissionNotFound:
pass
if showRetentionMessages:
entityActionPerformed([Ent.USER, targetUser, childFileType, childFileName, Ent.ROLE, targetInsertBody['role']], j, jcount)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
except GAPI.invalidSharingRequest as e:
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], Ent.TypeNameMessage(Ent.PERMISSION_ID, targetPermissionId, str(e)), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
elif showRetentionMessages:
entityActionPerformed([Ent.USER, targetUser, childFileType, childFileName, Ent.ROLE, childEntryInfo['targetPermission']['role']], j, jcount)
def _transferDriveFilesFromTree(fileEntry, i, count):
jcount = len(fileEntry['children'])
if jcount == 0:
return
j = 0
for childFileId in fileEntry['children']:
j += 1
childEntry = fileTree.get(childFileId)
if not childEntry or childFileId in filesTransferred:
continue
if childFileId in skipFileIdEntity['list']:
entityActionNotPerformedWarning([Ent.USER, sourceUser, _getEntityMimeType(childEntry['info']), f'{childEntry["info"]["name"]} ({childFileId})'],
Msg.IN_SKIPIDS, j, jcount)
continue
filesTransferred.add(childFileId)
_transferFile(childEntry, i, count, j, jcount, False)
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER:
Ind.Increment()
_transferDriveFilesFromTree(childEntry, i, count)
Ind.Decrement()
def _manageRoleRetentionDriveFilesFromTree(fileEntry, i, count):
jcount = len(fileEntry['children'])
if jcount == 0:
return
j = 0
for childFileId in fileEntry['children']:
j += 1
childEntry = fileTree.get(childFileId)
if not childEntry or childFileId in filesTransferred or childFileId in skipFileIdEntity['list']:
continue
filesTransferred.add(childFileId)
_manageRoleRetention(childEntry, i, count, j, jcount, False)
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER:
Ind.Increment()
_manageRoleRetentionDriveFilesFromTree(childEntry, i, count)
Ind.Decrement()
def _identifyDriveFileAndChildren(fileEntry, i, count):
fileId = fileEntry['id']
if fileId not in fileTree:
fileTree[fileId] = {'info': fileEntry, 'children': []}
if fileEntry['mimeType'] != MIMETYPE_GA_FOLDER:
return
try:
children = callGAPIpages(sourceDrive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy, q=WITH_PARENTS.format(fileId),
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
return
for childEntry in children:
if not childEntry['trashed']:
childId = childEntry['id']
if childId in skipFileIdEntity['list']:
entityActionNotPerformedWarning([Ent.USER, sourceUser, _getEntityMimeType(childEntry), f'{childEntry["name"]} ({childId})'],
Msg.IN_SKIPIDS)
continue
fileTree[fileId]['children'].append(childId)
_identifyDriveFileAndChildren(childEntry, i, count)
def _transferDriveFileAndChildren(fileEntry, i, count, j, jcount, atSelectTop):
fileId = fileEntry['info']['id']
if fileId in filesTransferred:
return
if fileEntry['info']['name'] != MY_DRIVE:
filesTransferred.add(fileId)
if not atSelectTop or not mergeWithTarget:
_transferFile(fileEntry, i, count, j, jcount, atSelectTop)
kcount = len(fileEntry['children'])
if kcount == 0:
return
k = 0
for childFileId in fileEntry['children']:
k += 1
childEntry = fileTree.get(childFileId)
if childEntry:
Ind.Increment()
_transferDriveFileAndChildren(childEntry, i, count, k, kcount, False)
Ind.Decrement()
def _manageRoleRetentionDriveFileAndChildren(fileEntry, i, count, j, jcount, atSelectTop):
fileId = fileEntry['info']['id']
if fileId in filesTransferred:
return
if fileEntry['info']['name'] != MY_DRIVE:
filesTransferred.add(fileId)
if not atSelectTop or not mergeWithTarget:
_manageRoleRetention(fileEntry, i, count, j, jcount, atSelectTop)
kcount = len(fileEntry['children'])
if kcount == 0:
return
k = 0
for childFileId in fileEntry['children']:
k += 1
childEntry = fileTree.get(childFileId)
if childEntry:
Ind.Increment()
_manageRoleRetentionDriveFileAndChildren(childEntry, i, count, k, kcount, False)
Ind.Decrement()
targetUser = getEmailAddress()
buildTree = True
csvPF = None
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
ownerRetainRoleBody = {'role': 'none'}
nonOwnerRetainRoleBody = {}
nonOwnerTargetRoleBody = {'role': 'source'}
showRetentionMessages = True
targetFolderId = targetFolderName = None
targetUserFolderPattern = '#user# old files'
targetUserOrphansFolderPattern = '#user# orphaned files'
targetIds = [None, None]
createShortcutsForNonmovableFiles = enforceExpansiveAccess = False
mergeWithTarget = False
thirdPartyOwners = {}
skipFileIdEntity = initDriveFileEntity()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'keepuser':
ownerRetainRoleBody['role'] = 'writer'
elif myarg == 'retainrole':
ownerRetainRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
if ownerRetainRoleBody['role'] in {'source', 'current'}:
ownerRetainRoleBody['role'] = 'writer'
elif myarg == 'nonownerretainrole':
nonOwnerRetainRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
if nonOwnerRetainRoleBody['role'] == 'source':
nonOwnerRetainRoleBody['role'] = 'current'
elif myarg == 'nonownertargetrole':
nonOwnerTargetRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
elif myarg == 'enforceexpansiveaccess':
enforceExpansiveAccess = getBoolean()
elif myarg == 'noretentionmessages':
showRetentionMessages = False
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'targetfolderid':
targetFolderIdLocation = Cmd.Location()
targetFolderId = getString(Cmd.OB_DRIVE_FILE_ID, minLen=0)
elif myarg == 'targetfoldername':
targetFolderNameLocation = Cmd.Location()
targetFolderName = getString(Cmd.OB_DRIVE_FILE_NAME, minLen=0)
elif myarg == 'targetuserfoldername':
targetUserFolderPattern = getString(Cmd.OB_DRIVE_FILE_NAME, minLen=0)
elif myarg == 'targetuserorphansfoldername':
targetUserOrphansFolderPattern = getString(Cmd.OB_DRIVE_FILE_NAME, minLen=0)
elif myarg == 'select':
fileIdEntity = getDriveFileEntity()
buildTree = False
elif myarg == 'mergewithtarget':
mergeWithTarget = getBoolean()
elif myarg == 'createshortcutsfornonmovablefiles':
createShortcutsForNonmovableFiles = getBoolean()
elif myarg == 'skipids':
skipFileIdEntity = getDriveFileEntity()
elif myarg == 'preview':
csvPF = CSVPrintFile(['OldOwner', 'NewOwner', 'type', 'id', 'name', 'role'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
if not nonOwnerRetainRoleBody:
nonOwnerRetainRoleBody = ownerRetainRoleBody
if not OBY.orderBy:
OBY.SetItems('folder,createdTime')
targetUser, targetDrive = buildGAPIServiceObject(API.DRIVE3, targetUser)
if not targetDrive:
return
try:
result = callGAPI(targetDrive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='storageQuota,user(permissionId)')
if result['storageQuota'].get('limit'):
targetDriveFree = int(result['storageQuota']['limit'])-int(result['storageQuota']['usageInDrive'])
else:
targetDriveFree = None
targetPermissionId = result['user']['permissionId']
result = callGAPI(targetDrive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id,name')
targetRootId = result['id']
if not targetFolderId and not targetFolderName:
targetFolderId = targetRootId
targetFolderName = result['name']
else:
if targetFolderId:
targetFolder = callGAPI(targetDrive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=targetFolderId, fields='id,name,mimeType,ownedByMe')
if targetFolder['mimeType'] != MIMETYPE_GA_FOLDER:
Cmd.SetLocation(targetFolderIdLocation)
usageErrorExit(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(Ent.USER), targetUser,
Ent.Singular(Ent.DRIVE_FOLDER_ID), targetFolderId,
Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER))],
'\n'))
if not targetFolder['ownedByMe']:
Cmd.SetLocation(targetFolderIdLocation)
usageErrorExit(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(Ent.USER), targetUser,
Ent.Singular(Ent.DRIVE_FOLDER_ID), targetFolderId,
Msg.NOT_OWNED_BY.format(targetUser)],
'\n'))
targetFolderId = targetFolder['id']
targetFolderName = targetFolder['name']
elif targetFolderName:
result = callGAPIpages(targetDrive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
q=MY_NON_TRASHED_FOLDER_NAME.format(escapeDriveFileName(targetFolderName)),
fields='nextPageToken,files(id)')
if not result:
Cmd.SetLocation(targetFolderNameLocation)
usageErrorExit(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(Ent.USER), targetUser,
Ent.Singular(Ent.DRIVE_FOLDER), targetFolderName,
Msg.DOES_NOT_EXIST],
'\n'))
targetFolderId = result[0]['id']
except GAPI.fileNotFound:
Cmd.SetLocation(targetFolderIdLocation)
usageErrorExit(formatKeyValueList(Ind.Spaces(),
[Ent.Singular(Ent.USER), targetUser,
Ent.Singular(Ent.DRIVE_FOLDER_ID), targetFolderId,
Msg.DOES_NOT_EXIST],
'\n'))
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(targetUser, str(e))
return
targetWriterPermissionsBody = {'role': 'writer', 'type': 'user', 'emailAddress': targetUser}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
if buildTree:
sourceUser, sourceDrive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not sourceDrive:
continue
else:
sourceUser, sourceDrive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FOLDER)
if jcount == 0:
continue
sourceUserName, _ = splitEmailAddress(sourceUser)
try:
result = callGAPI(sourceDrive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='storageQuota,user(permissionId)')
sourceDriveSize = int(result['storageQuota']['usageInDrive'])
sourcePermissionId = result['user']['permissionId']
sourceRootId = callGAPI(sourceDrive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
if (targetDriveFree is not None) and (targetDriveFree < sourceDriveSize):
printWarningMessage(TARGET_DRIVE_SPACE_ERROR_RC,
(f'{Msg.NO_TRANSFER_LACK_OF_DISK_SPACE} '
f'{formatKeyValueList("", ["Source drive size", formatFileSize(sourceDriveSize), "Target drive free", formatFileSize(targetDriveFree)], "")}'))
continue
printKeyValueList(['Source drive size', formatFileSize(sourceDriveSize),
'Target drive free', formatFileSize(targetDriveFree) if targetDriveFree is not None else 'UNLIMITED'])
if targetDriveFree is not None:
targetDriveFree = targetDriveFree-sourceDriveSize # prep targetDriveFree for next user
if not csvPF:
targetIds[TARGET_PARENT_ID] = _buildTargetUserFolder()
if targetIds[TARGET_PARENT_ID] is None:
return
Ind.Increment()
if skipFileIdEntity['query'] or skipFileIdEntity[ROOT]:
_validateUserGetFileIDs(origUser, i, count, skipFileIdEntity, drive=sourceDrive)
if buildTree:
topSourceId = sourceRootId
parentIdMap = {sourceRootId: targetIds[TARGET_PARENT_ID]}
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, Ent.TypeName(Ent.SOURCE_USER, user), i, count)
feed = callGAPIpages(sourceDrive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy, q=NON_TRASHED,
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
fileTree = buildFileTree(feed, sourceDrive)
del feed
filesTransferred = set()
_transferDriveFilesFromTree(fileTree[sourceRootId], i, count)
if fileTree[ORPHANS]['children']:
if not csvPF:
_buildTargetUserOrphansFolder()
_transferDriveFilesFromTree(fileTree[ORPHANS], i, count)
if not csvPF:
Act.Set(Act.RETAIN)
filesTransferred = set()
_manageRoleRetentionDriveFilesFromTree(fileTree[sourceRootId], i, count)
if fileTree[ORPHANS]['children']:
_manageRoleRetentionDriveFilesFromTree(fileTree[ORPHANS], i, count)
else:
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileTree = {}
parentIdMap = {sourceRootId: targetIds[TARGET_PARENT_ID]}
Act.Set(Act.TRANSFER_OWNERSHIP)
try:
fileEntry = callGAPI(sourceDrive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId,
fields='id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails')
entityType = _getEntityMimeType(fileEntry)
if fileId in skipFileIdEntity['list']:
entityActionNotPerformedWarning([Ent.USER, sourceUser, entityType, f'{fileEntry["name"]} ({fileId})'],
Msg.IN_SKIPIDS, j, jcount)
continue
entityPerformActionItemValue([Ent.USER, sourceUser], entityType, f'{fileEntry["name"]} ({fileId})', j, jcount)
if not mergeWithTarget:
topSourceId = None
for parentId in fileEntry.get('parents', []):
parentIdMap[parentId] = targetIds[TARGET_PARENT_ID]
else:
topSourceId = fileId
parentIdMap[fileId] = targetIds[TARGET_PARENT_ID]
_identifyDriveFileAndChildren(fileEntry, i, count)
filesTransferred = set()
_transferDriveFileAndChildren(fileTree[fileId], i, count, j, jcount, True)
if not csvPF:
Act.Set(Act.RETAIN)
filesTransferred = set()
_manageRoleRetentionDriveFileAndChildren(fileTree[fileId], i, count, j, jcount, True)
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, sourceUser, Ent.DRIVE_FILE_OR_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
break
Ind.Decrement()
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Files to Transfer')
def validateUserGetPermissionId(user, i=0, count=0, drive=None):
if drive is None:
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if drive:
try:
result = callGAPI(drive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='user(permissionId)')
return (drive, result['user']['permissionId'])
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return (None, None)
def getPermissionIdForEmail(user, i, count, email):
currentSvcAcctAPI = GM.Globals[GM.CURRENT_SVCACCT_API]
currentSvcAcctAPIScopes = GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]
_, drive = buildGAPIServiceObject(API.DRIVE2, user, i, count)
GM.Globals[GM.CURRENT_SVCACCT_API] = currentSvcAcctAPI
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = currentSvcAcctAPIScopes
if drive:
try:
return callGAPI(drive.permissions(), 'getIdForEmail',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
email=email, fields='id')['id']
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
entityActionNotPerformedWarning([Ent.USER, user], Msg.UNABLE_TO_GET_PERMISSION_ID.format(email), i, count)
systemErrorExit(GM.Globals[GM.SYSEXITRC], None)
return None
# gam <UserTypeEntity> transfer ownership <DriveFileEntity> <UserItem>
# [<DriveFileParentAttribute>] [includetrashed] [norecursion [<Boolean>]]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [preview] [filepath] [pathdelimiter <Character>] [buildtree]
# [enforceexpansiveaccess [<Boolean>]]
# [todrive <ToDriveAttribute>*]
def transferOwnership(users):
def _identifyFilesToTransfer(fileEntry):
for childFileId in fileEntry['children']:
if childFileId in filesTransferred:
continue
filesTransferred.add(childFileId)
childEntry = fileTree.get(childFileId)
if childEntry:
childEntryInfo = childEntry['info']
if includeTrashed or not childEntryInfo['trashed']:
if childEntryInfo['ownedByMe']:
filesToTransfer[childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
_identifyFilesToTransfer(childEntry)
def _identifyChildrenToTransfer(fileEntry, user, i, count):
q = WITH_PARENTS.format(fileEntry['id'])
setGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, query=q)
pageMessage = getPageMessageForWhom(clearLastGotMsgLen=False)
try:
children = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=pageMessage, noFinalize=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy, q=q,
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return
for childEntryInfo in children:
childFileId = childEntryInfo['id']
if filepath:
fileTree[childFileId] = {'info': childEntryInfo}
if childFileId in filesTransferred:
continue
filesTransferred.add(childFileId)
if includeTrashed or not childEntryInfo['trashed']:
if childEntryInfo['ownedByMe']:
filesToTransfer[childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
_identifyChildrenToTransfer(childEntryInfo, user, i, count)
fileIdEntity = getDriveFileEntity()
body = {}
newOwner = getEmailAddress()
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
changeParents = enforceExpansiveAccess = filepath = includeTrashed = noRecursion = False
pathDelimiter = '/'
csvPF = fileTree = None
addParents = ''
parentBody = {}
parentParms = initDriveFileAttributes()
buildTree = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'includetrashed':
includeTrashed = True
elif myarg == 'norecursion':
noRecursion = getBoolean()
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'filepath':
filepath = True
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'buildtree':
buildTree = True
elif myarg == 'preview':
csvPF = CSVPrintFile(['OldOwner', 'NewOwner', 'type', 'id', 'name'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getDriveFileParentAttribute(myarg, parentParms):
changeParents = True
elif myarg == 'enforceexpansiveaccess':
enforceExpansiveAccess = getBoolean()
else:
unknownArgumentExit()
Act.Set(Act.TRANSFER_OWNERSHIP)
targetDrive, permissionId = validateUserGetPermissionId(newOwner)
if not permissionId:
return
if changeParents:
if not _getDriveFileParentInfo(targetDrive, newOwner, 0, 0, parentBody, parentParms):
return
addParents = ','.join(parentBody['parents'])
if csvPF:
if filepath:
csvPF.AddTitles('paths')
else:
filepath = False
body = {'role': 'owner'}
bodyAdd = {'role': 'writer', 'type': 'user', 'emailAddress': newOwner}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER)
if jcount == 0:
continue
if filepath:
filePathInfo = initFilePathInfo(pathDelimiter)
filesTransferred = set()
if buildTree:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count)
try:
feed = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy,
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
fileTree = buildFileTree(feed, drive)
del feed
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
else:
fileTree = {}
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
kvList = [Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId]
if buildTree:
fileEntry = fileTree.get(fileId)
if not fileEntry:
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
continue
fileEntryInfo = fileEntry['info']
else:
try:
fileEntryInfo = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='id,name,parents,mimeType,ownedByMe,trashed,shortcutDetails')
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if filepath:
fileTree[fileId] = {'info': fileEntryInfo}
entityType = _getEntityMimeType(fileEntryInfo)
entityPerformActionItemValue([Ent.USER, user], entityType, f'{fileEntryInfo["name"]} ({fileId})', j, jcount)
if fileId in filesTransferred:
continue
filesTransferred.add(fileId)
filesToTransfer = {}
if includeTrashed or not fileEntryInfo['trashed']:
if fileEntryInfo['ownedByMe'] and fileEntryInfo['name'] != MY_DRIVE:
filesToTransfer[fileId] = {'name': fileEntryInfo['name'], 'type': entityType}
if changeParents:
filesToTransfer[fileId]['addParents'] = addParents
filesToTransfer[fileId]['removeParents'] = ','.join(fileEntryInfo.get('parents', []))
if fileEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT and entityType != Ent.DRIVE_SHORTCUT:
filesToTransfer[fileId]['shortcutDetails'] = fileEntryInfo['shortcutDetails']
if fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and not noRecursion:
if buildTree:
_identifyFilesToTransfer(fileEntry)
else:
_identifyChildrenToTransfer(fileEntryInfo, user, i, count)
if csvPF:
for xferFileId, fileInfo in iter(filesToTransfer.items()):
row = {'OldOwner': user, 'NewOwner': newOwner, 'type': Ent.Singular(fileInfo['type']), 'id': xferFileId, 'name': fileInfo['name']}
if filepath:
addFilePathsToRow(drive, fileTree, fileTree[xferFileId]['info'], filePathInfo, csvPF, row)
csvPF.WriteRow(row)
continue
Ind.Increment()
kcount = len(filesToTransfer)
entityPerformActionNumItemsModifier([Ent.USER, user], kcount, Ent.DRIVE_FILE_OR_FOLDER, f'{Act.MODIFIER_TO} {Ent.Singular(Ent.USER)}: {newOwner}', i, count)
Ind.Increment()
k = 0
for xferFileId, fileInfo in iter(filesToTransfer.items()):
k += 1
entityType = fileInfo['type']
fileDesc = f'{fileInfo["name"]} ({xferFileId})'
kvList = [Ent.USER, user, entityType, fileDesc]
try:
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
if changeParents:
removeParents = fileInfo.get('removeParents', '')
if removeParents:
callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
fileId=xferFileId, removeParents=removeParents, fields='', supportsAllDrives=True)
action = Act.Get()
Act.Set(Act.REMOVE)
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.DRIVE_FOLDER, fileInfo['removeParents']], k, kcount)
Act.Set(action)
callGAPI(drive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.USER, newOwner], k, kcount)
else:
if changeParents and entityType != Ent.DRIVE_SHORTCUT:
callGAPI(drive.files(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
fileId=xferFileId, supportsAllDrives=True)
action = Act.Get()
Act.Set(Act.DELETE_SHORTCUT)
entityActionPerformed(kvList, k, kcount)
Act.Set(action)
else:
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.USER, newOwner], k, kcount)
except GAPI.permissionNotFound:
# this might happen if target user isn't explicitly in ACL (i.e. shared with anyone)
try:
callGAPI(drive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST],
fileId=xferFileId, sendNotificationEmail=False, body=bodyAdd, fields='')
callGAPI(drive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.USER, newOwner], k, kcount)
except GAPI.invalidSharingRequest as e:
entityActionFailedWarning(kvList, Ent.TypeNameMessage(Ent.PERMISSION_ID, permissionId, str(e)), k, kcount)
continue
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning(kvList+[Ent.PERMISSION_ID, permissionId], k, kcount)
continue
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, k, kcount)
continue
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, k, kcount)
continue
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
kvList = [Ent.USER, newOwner, entityType, fileDesc]
try:
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
if changeParents and 'addParents' in fileInfo:
if entityType != Ent.DRIVE_FILE_SHORTCUT:
callGAPI(targetDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
fileId=xferFileId, addParents=fileInfo['addParents'], fields='', supportsAllDrives=True)
action = Act.Get()
Act.Set(Act.ADD)
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], k, kcount)
Act.Set(action)
else:
if changeParents and 'addParents' in fileInfo and entityType != Ent.DRIVE_SHORTCUT:
body = {'name': fileInfo['name'], 'mimeType': MIMETYPE_GA_SHORTCUT,
'parents': [fileInfo['addParents']], 'shortcutDetails': {'targetId': fileInfo['shortcutDetails']['targetId']}}
callGAPI(targetDrive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID,
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
body=body, fields='id', supportsAllDrives=True)
Act.Set(Act.CREATE_SHORTCUT)
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], k, kcount)
Act.Set(action)
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, k, kcount)
except (GAPI.forbidden, GAPI.cannotAddParent, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
GAPI.invalid, GAPI.badRequest, GAPI.unknownError) as e:
entityActionFailedWarning(kvList, str(e), k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(newOwner, str(e), 0, 0)
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Files to Transfer Ownership')
# gam <UserTypeEntity> claim ownership <DriveFileEntity>
# [<DriveFileParentAttribute>] [includetrashed]
# [skipids <DriveFileEntity>] [onlyUsers|skipusers <UserTypeEntity>] [subdomains <DomainNameEntity>]
# [restricted [<Boolean>]] [writerscanshare|writerscantshare [<Boolean>]]
# [keepuser | (retainrole reader|commenter|writer|editor|none)] [noretentionmessages]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [preview] [filepath] [pathdelimiter <Character>] [buildtree]
# [enforceexpansiveaccess [<Boolean>]]
# [todrive <ToDriveAttribute>*]
def claimOwnership(users):
def _identifyFilesToClaim(fileEntry):
for childFileId in fileEntry['children']:
if childFileId in filesTransferred or childFileId in skipFileIdEntity['list']:
continue
filesTransferred.add(childFileId)
childEntry = fileTree.get(childFileId)
if childEntry:
childEntryInfo = childEntry['info']
if includeTrashed or not childEntryInfo['trashed']:
owner = childEntryInfo['owners'][0]['emailAddress']
if (not childEntryInfo['ownedByMe']) and ((not checkOwner) or (checkOnly and owner in onlyOwners) or (checkSkip and owner not in skipOwners)):
oldOwnerPermissionIds[owner] = childEntryInfo['owners'][0]['permissionId']
filesToClaim.setdefault(owner, {})
if childFileId not in filesToClaim[owner]:
filesToClaim[owner][childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
_identifyFilesToClaim(childEntry)
def _identifyChildrenToClaim(fileEntry, user, i, count):
q = WITH_PARENTS.format(fileEntry['id'])
setGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, query=q)
pageMessage = getPageMessageForWhom(clearLastGotMsgLen=False)
try:
children = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=pageMessage, noFinalize=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy, q=q,
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId))',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return
for childEntryInfo in children:
childFileId = childEntryInfo['id']
if childFileId in filesTransferred or childFileId in skipFileIdEntity['list']:
continue
filesTransferred.add(childFileId)
if includeTrashed or not childEntryInfo['trashed']:
if filepath:
fileTree[childFileId] = {'info': childEntryInfo}
owner = childEntryInfo['owners'][0]['emailAddress']
if (not childEntryInfo['ownedByMe']) and ((not checkOwner) or (checkOnly and owner in onlyOwners) or (checkSkip and owner not in skipOwners)):
oldOwnerPermissionIds[owner] = childEntryInfo['owners'][0]['permissionId']
filesToClaim.setdefault(owner, {})
if childFileId not in filesToClaim[owner]:
filesToClaim[owner][childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
_identifyChildrenToClaim(childEntryInfo, user, i, count)
def _processRetainedRole(user, i, count, oldOwner, entityType, ofileId, fileDesc, l, lcount):
oldOwnerPermissionId = oldOwnerPermissionIds[oldOwner]
Act.Set(Act.RETAIN)
try:
if sourceRetainRoleBody['role'] != 'none':
if sourceRetainRoleBody['role'] != 'writer':
callGAPI(sourceDrive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=ofileId, permissionId=oldOwnerPermissionId, body=sourceRetainRoleBody, fields='')
else:
callGAPI(sourceDrive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST],
fileId=ofileId, permissionId=oldOwnerPermissionId)
if showRetentionMessages:
entityActionPerformed([Ent.USER, oldOwner, entityType, fileDesc, Ent.ROLE, sourceRetainRoleBody['role']], l, lcount)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, oldOwner, entityType, fileDesc, Ent.PERMISSION_ID, oldOwnerPermissionId], l, lcount)
except (GAPI.badRequest, GAPI.insufficientFilePermissions, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning([Ent.USER, oldOwner, entityType, fileDesc], str(e), l, lcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Act.Set(Act.CLAIM_OWNERSHIP)
fileIdEntity = getDriveFileEntity()
skipFileIdEntity = initDriveFileEntity()
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
body = {}
checkOnly = checkSkip = False
onlyOwners = set()
skipOwners = set()
subdomains = []
enforceExpansiveAccess = filepath = includeTrashed = False
pathDelimiter = '/'
addParents = ''
parentBody = {}
parentParms = initDriveFileAttributes()
sourceRetainRoleBody = {'role': 'writer'}
showRetentionMessages = True
oldOwnerPermissionIds = {}
csvPF = fileTree = None
buildTree = changeParents = False
bodyShare = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'keepuser':
sourceRetainRoleBody['role'] = 'writer'
elif myarg == 'retainrole':
sourceRetainRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
elif myarg == 'noretentionmessages':
showRetentionMessages = False
elif myarg == 'skipids':
skipFileIdEntity = getDriveFileEntity()
elif myarg == 'onlyusers':
_, userList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
checkOnly = True
onlyOwners = set(userList)
elif myarg == 'skipusers':
_, userList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
checkSkip = len(userList) > 0
skipOwners = set(userList)
elif myarg == 'subdomains':
subdomains = getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)
elif myarg == 'includetrashed':
includeTrashed = True
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg == 'enforceexpansiveaccess':
enforceExpansiveAccess = getBoolean()
elif myarg == 'restricted':
bodyShare['copyRequiresWriterPermission'] = getBoolean()
elif myarg == 'writerscanshare':
bodyShare['writersCanShare'] = getBoolean()
elif myarg == 'writerscantshare':
bodyShare['writersCanShare'] = not getBoolean()
elif myarg == 'filepath':
filepath = True
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
elif myarg == 'buildtree':
buildTree = True
elif myarg == 'preview':
csvPF = CSVPrintFile(['OldOwner', 'NewOwner', 'type', 'id', 'name'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getDriveFileParentAttribute(myarg, parentParms):
changeParents = True
else:
unknownArgumentExit()
if checkOnly and checkSkip:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('onlyusers', 'skipusers'))
checkOwner = checkOnly or checkSkip
Act.Set(Act.CLAIM_OWNERSHIP)
if csvPF:
if filepath:
csvPF.AddTitles('paths')
else:
filepath = False
body = {'role': 'owner'}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
if not drive:
continue
_, permissionId = validateUserGetPermissionId(user, i, count, drive)
if not permissionId:
continue
if changeParents:
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
return
addParents = ','.join(parentBody['parents'])
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
if jcount == 0:
continue
if filepath:
filePathInfo = initFilePathInfo(pathDelimiter)
filesTransferred = set()
bodyAdd = {'role': 'writer', 'type': 'user', 'emailAddress': user}
if skipFileIdEntity['query'] or skipFileIdEntity[ROOT]:
_validateUserGetFileIDs(origUser, i, count, skipFileIdEntity, drive=drive)
if buildTree:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count)
try:
feed = callGAPIpages(drive.files(), 'list', 'files',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
retryReasons=[GAPI.UNKNOWN_ERROR],
orderBy=OBY.orderBy,
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId))',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
fileTree = buildFileTree(feed, drive)
del feed
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
else:
fileTree = {}
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
filesToClaim = {}
kvList = [Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId]
if buildTree:
fileEntry = fileTree.get(fileId)
if not fileEntry:
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
continue
fileEntryInfo = fileEntry['info']
else:
try:
fileEntryInfo = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId,
fields='id,name,parents,mimeType,ownedByMe,trashed,shortcutDetails,owners(emailAddress,permissionId)')
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if filepath:
fileTree[fileId] = {'info': fileEntryInfo}
entityType = _getEntityMimeType(fileEntryInfo)
if fileId in skipFileIdEntity['list']:
entityActionNotPerformedWarning([Ent.USER, user, entityType, f'{fileEntryInfo["name"]} ({fileId})'],
Msg.IN_SKIPIDS, j, jcount)
continue
entityPerformActionItemValue([Ent.USER, user], entityType, f'{fileEntryInfo["name"]} ({fileId})', j, jcount)
if fileId in filesTransferred:
continue
filesTransferred.add(fileId)
if fileId not in skipFileIdEntity['list'] and (includeTrashed or not fileEntryInfo['trashed']):
owner = fileEntryInfo['owners'][0]['emailAddress']
if (not fileEntryInfo['ownedByMe']) and ((not checkOwner) or (checkOnly and owner in onlyOwners) or (checkSkip and owner not in skipOwners)):
oldOwnerPermissionIds[owner] = fileEntryInfo['owners'][0]['permissionId']
filesToClaim.setdefault(owner, {})
if fileId not in filesToClaim[owner]:
filesToClaim[owner][fileId] = {'name': fileEntryInfo['name'], 'type': entityType}
if changeParents:
filesToClaim[owner][fileId]['addParents'] = addParents
filesToClaim[owner][fileId]['removeParents'] = ','.join(fileEntryInfo.get('parents', []))
if fileEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT and entityType != Ent.DRIVE_SHORTCUT:
filesToClaim[owner][fileId]['shortcutDetails'] = fileEntryInfo['shortcutDetails']
if fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
if buildTree:
_identifyFilesToClaim(fileEntry)
else:
_identifyChildrenToClaim(fileEntryInfo, user, i, count)
if csvPF:
for oldOwner in filesToClaim:
for claimFileId, fileInfo in iter(filesToClaim[oldOwner].items()):
row = {'NewOwner': user, 'OldOwner': oldOwner, 'type': Ent.Singular(fileInfo['type']), 'id': claimFileId, 'name': fileInfo['name']}
if filepath:
addFilePathsToRow(drive, fileTree, fileTree[claimFileId]['info'], filePathInfo, csvPF, row)
csvPF.WriteRow(row)
continue
Ind.Increment()
kcount = len(filesToClaim)
entityPerformActionNumItems([Ent.USER, user], kcount, Ent.USER, i, count)
Ind.Increment()
k = 0
for oldOwner in filesToClaim:
k += 1
_, userDomain = splitEmailAddress(oldOwner)
lcount = len(filesToClaim[oldOwner])
if userDomain == GC.Values[GC.DOMAIN] or userDomain in subdomains:
_, sourceDrive = buildGAPIServiceObject(API.DRIVE3, oldOwner, k, kcount)
if not sourceDrive:
continue
entityPerformActionNumItemsModifier([Ent.USER, user], lcount, Ent.DRIVE_FILE_OR_FOLDER,
f'{Act.MODIFIER_FROM} {Ent.Singular(Ent.USER)}: {oldOwner}', k, kcount)
Ind.Increment()
l = 0
for xferFileId, fileInfo in iter(filesToClaim[oldOwner].items()):
l += 1
entityType = fileInfo['type']
fileDesc = f'{fileInfo["name"]} ({xferFileId})'
kvList = [Ent.USER, oldOwner, entityType, fileDesc]
try:
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
if bodyShare:
callGAPI(sourceDrive.files(), 'update',
fileId=xferFileId, body=bodyShare, fields='')
if changeParents:
removeParents = fileInfo.get('removeParents', '')
if removeParents:
callGAPI(sourceDrive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
fileId=xferFileId, removeParents=removeParents, fields='', supportsAllDrives=True)
action = Act.Get()
Act.Set(Act.REMOVE)
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None,
[Ent.DRIVE_FOLDER, removeParents], l, lcount)
Act.Set(action)
callGAPI(sourceDrive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
kvList = [Ent.USER, user, entityType, fileDesc]
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.USER, oldOwner], l, lcount)
_processRetainedRole(user, i, count, oldOwner, entityType, xferFileId, fileDesc, l, lcount)
else:
if changeParents and entityType != Ent.DRIVE_SHORTCUT:
callGAPI(sourceDrive.files(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
fileId=xferFileId, supportsAllDrives=True)
action = Act.Get()
Act.Set(Act.DELETE_SHORTCUT)
entityActionPerformed(kvList, l, lcount)
Act.Set(action)
else:
kvList = [Ent.USER, user, entityType, fileDesc]
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.USER, oldOwner], l, lcount)
except GAPI.permissionNotFound:
# if claimer not in ACL (file might be visible for all with link)
try:
callGAPI(sourceDrive.permissions(), 'create',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST],
fileId=xferFileId, sendNotificationEmail=False, body=bodyAdd, fields='')
callGAPI(sourceDrive.permissions(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
enforceExpansiveAccess=enforceExpansiveAccess,
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.USER, oldOwner], l, lcount)
_processRetainedRole(user, i, count, oldOwner, entityType, xferFileId, fileDesc, l, lcount)
except GAPI.invalidSharingRequest as e:
entityActionFailedWarning(kvList, Ent.TypeNameMessage(Ent.PERMISSION_ID, permissionId, str(e)), l, lcount)
continue
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning(kvList+[Ent.PERMISSION_ID, permissionId], l, lcount)
continue
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, l, lcount)
continue
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
entityActionFailedWarning(kvList, str(e), l, lcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, l, lcount)
continue
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
entityActionFailedWarning(kvList, str(e), l, lcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
kvList = [Ent.USER, user, entityType, fileDesc]
try:
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
if changeParents and 'addParents' in fileInfo:
callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
fileId=xferFileId, addParents=fileInfo['addParents'], fields='', supportsAllDrives=True)
action = Act.Get()
Act.Set(Act.ADD)
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], l, lcount)
Act.Set(action)
else:
if changeParents and 'addParents' in fileInfo and entityType != Ent.DRIVE_SHORTCUT:
body = {'name': fileInfo['name'], 'mimeType': MIMETYPE_GA_SHORTCUT,
'parents': [fileInfo['addParents']], 'shortcutDetails': {'targetId': fileInfo['shortcutDetails']['targetId']}}
callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID,
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
body=body, fields='id', supportsAllDrives=True)
Act.Set(Act.CREATE_SHORTCUT)
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], l, lcount)
Act.Set(action)
except GAPI.fileNotFound:
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, l, lcount)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.cannotAddParent) as e:
entityActionFailedWarning(kvList, str(e), l, lcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Ind.Decrement()
else:
entityPerformActionModifierNumItemsModifier([Ent.USER, user], 'Not Performed', kcount, Ent.DRIVE_FILE_OR_FOLDER,
f'{Act.MODIFIER_FROM} {Ent.Singular(Ent.USER)}: {oldOwner}', j, jcount)
Ind.Increment()
l = 0
for xferFileId, fileInfo in iter(filesToClaim[oldOwner].items()):
l += 1
entityActionNotPerformedWarning([Ent.USER, user, fileInfo['type'], f'{fileInfo["name"]} ({xferFileId})'],
Msg.USER_IN_OTHER_DOMAIN.format(Ent.Singular(Ent.USER), oldOwner), l, lcount)
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Files to Claim Ownership')
# gam <UserTypeEntity> print emptydrivefolders [todrive <ToDriveAttribute>*]
# [select <DriveFileEntity>]
# [pathdelimiter <Character>]
def printEmptyDriveFolders(users):
def _checkChildDriveFolderContents(drive, fileEntry, user, i, count, pathList):
query = WITH_PARENTS.format(fileEntry ['id'])
try:
children = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query, fields='nextPageToken,files(id,name,mimeType,ownedByMe)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **fileIdEntity['shareddrive'])
if not children:
if sharedDriveId or fileEntry.get('ownedByMe', False):
row = {'User': user, 'id': fileEntry['id'], 'name': pathDelimiter.join(pathList)}
if sharedDriveId:
row['driveId'] = sharedDriveId
csvPF.WriteRow(row)
return
for childEntryInfo in children:
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
_checkChildDriveFolderContents(drive, childEntryInfo, user, i, count, pathList+[childEntryInfo['name']])
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(query), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
csvPF = CSVPrintFile(['User', 'id', 'name']) if Act.csvFormat() else None
fileIdEntity = {}
pathDelimiter = '/'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'select':
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': True})
fileIdEntity = getDriveFileEntity(DLP=DLP)
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
else:
unknownArgumentExit()
if not fileIdEntity:
fileIdEntity = initDriveFileEntity()
cleanFileIDsList(fileIdEntity, [ROOT])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
fileId = sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
if not sharedDriveId:
fileId = fileIdEntity['list'][0]
try:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FOLDER, user, i, count)
fileEntryInfo = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND, GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=[GAPI.UNKNOWN_ERROR],
fileId=fileId, fields='id,name,mimeType,ownedByMe,driveId', supportsAllDrives=True)
if 'driveId' in fileEntryInfo:
sharedDriveId = fileEntryInfo['driveId']
fileIdEntity['shareddrive'] = {'driveId': sharedDriveId, 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
csvPF.AddTitles(['driveId'])
csvPF.MoveTitlesToEnd(['name'])
pathList = [f'{SHARED_DRIVES}/{_getSharedDriveNameFromId(sharedDriveId)}']
else:
pathList = [fileEntryInfo['name']]
mimeType = fileEntryInfo['mimeType']
if mimeType != MIMETYPE_GA_FOLDER:
entityValueList = [Ent.USER, user, _getEntityMimeType(fileEntryInfo), fileEntryInfo['name']]
entityActionNotPerformedWarning(entityValueList, Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_FOLDER), i, count)
continue
_checkChildDriveFolderContents(drive, fileEntryInfo, user, i, count, pathList)
except (GAPI.fileNotFound, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileId], str(e), i, count)
except GAPI.teamDriveMembershipRequired as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Empty Folders')
# gam <UserTypeEntity> delete emptydrivefolders
# [select <DriveFileEntity>]
# [<SharedDriveEntity>]
# [pathdelimiter <Character>]
def deleteEmptyDriveFolders(users):
def _deleteEmptyChildDriveFolders(drive, fileEntry, user, i, count, pathList, atTop):
query = WITH_PARENTS.format(fileEntry ['id'])
try:
children = callGAPIpages(drive.files(), 'list', 'files',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.BAD_REQUEST],
retryReasons=[GAPI.UNKNOWN_ERROR],
q=query, fields='nextPageToken,files(id,name,mimeType,ownedByMe)',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **fileIdEntity['shareddrive'])
numChildren = len(children)
for childEntryInfo in children:
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
numChildren -= _deleteEmptyChildDriveFolders(drive, childEntryInfo, user, i, count, pathList+[childEntryInfo['name']], False)
if numChildren == 0 and not atTop:
try:
callGAPI(drive.files(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
fileId=fileEntry['id'], supportsAllDrives=True)
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER_ID, fileEntry['id'],
Ent.DRIVE_FOLDER, pathDelimiter.join(pathList)], i, count)
return 1
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileEntry['name']], str(e), i, count)
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(query), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
return 0
Act.Set(Act.DELETE_EMPTY)
fileIdEntity = {}
pathDelimiter = '/'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'select':
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': True})
fileIdEntity = getDriveFileEntity(DLP=DLP)
elif myarg == 'pathdelimiter':
pathDelimiter = getCharacter()
else:
fileIdEntity = getSharedDriveEntity()
if not fileIdEntity:
fileIdEntity = initDriveFileEntity()
cleanFileIDsList(fileIdEntity, [ROOT])
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
printEntityKVList([Ent.USER, user],
[f'{Act.ToPerform()} {Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER)}'],
i, count)
Ind.Increment()
fileId = sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
if not sharedDriveId:
fileId = fileIdEntity['list'][0]
try:
printGettingAllEntityItemsForWhom(Ent.DRIVE_FOLDER, user, i, count)
fileEntryInfo = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND, GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
retryReasons=[GAPI.UNKNOWN_ERROR],
fileId=fileId, fields='id,name,mimeType,ownedByMe,driveId', supportsAllDrives=True)
if 'driveId' in fileEntryInfo:
sharedDriveId = fileEntryInfo['driveId']
fileIdEntity['shareddrive'] = {'driveId': sharedDriveId, 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
pathList = [f'{SHARED_DRIVES}/{_getSharedDriveNameFromId(sharedDriveId)}']
else:
pathList = [fileEntryInfo['name']]
mimeType = fileEntryInfo['mimeType']
if mimeType != MIMETYPE_GA_FOLDER:
entityValueList = [Ent.USER, user, _getEntityMimeType(fileEntryInfo), fileEntryInfo['name']]
entityActionNotPerformedWarning(entityValueList, Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_FOLDER), i, count)
continue
_deleteEmptyChildDriveFolders(drive, fileEntryInfo, user, i, count, pathList, True)
except (GAPI.fileNotFound, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileId], str(e), i, count)
except GAPI.teamDriveMembershipRequired as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Ind.Decrement()
# gam <UserTypeEntity> empty drivetrash [<SharedDriveEntity>]
def emptyDriveTrash(users):
if Cmd.ArgumentsRemaining():
fileIdEntity = getSharedDriveEntity()
else:
fileIdEntity = {}
checkForExtraneousArguments()
kwargs = {'driveId': None}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if not fileIdEntity:
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
else:
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
kwargs['driveId'] = fileIdEntity['shareddrive']['driveId']
try:
callGAPI(drive.files(), 'emptyTrash',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INSUFFICIENT_FILE_PERMISSIONS],
**kwargs)
entityActionPerformed([Ent.USER, user, Ent.DRIVE_TRASH, kwargs['driveId']], i, count)
except (GAPI.notFound, GAPI.insufficientFilePermissions) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, kwargs['driveId']], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
def _getDriveFileACLPrintKeysTimeObjects():
printKeys = ['id', 'type', 'emailAddress', 'domain', 'role', 'permissionDetails',
'expirationTime', 'photoLink', 'allowFileDiscovery', 'deleted',
'pendingOwner', 'view']
timeObjects = ['expirationTime']
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
_mapDrive3TitlesToDrive2(printKeys, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
_mapDrive3TitlesToDrive2(timeObjects, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
return (printKeys, set(timeObjects))
# DriveFileACL commands utilities
def _showDriveFilePermissionJSON(user, fileId, fileName, createdTime, permission, timeObjects):
_mapDrivePermissionNames(permission)
row = {'Owner': user, 'id': fileId, 'permission': permission}
if createdTime is not None:
row['createdTime'] = createdTime
if fileId != fileName:
row['name'] = fileName
printLine(json.dumps(cleanJSON(row, timeObjects=timeObjects), ensure_ascii=False, sort_keys=True))
def _showDriveFilePermissionsJSON(user, fileId, fileName, createdTime, permissions, timeObjects):
for permission in permissions:
_mapDrivePermissionNames(permission)
row = {'Owner': user, 'id': fileId, 'permissions': permissions}
if createdTime is not None:
row['createdTime'] = createdTime
if fileId != fileName:
row['name'] = fileName
printLine(json.dumps(cleanJSON(row, timeObjects=timeObjects), ensure_ascii=False, sort_keys=True))
def _showDriveFilePermission(permission, printKeys, timeObjects, i=0, count=0):
if permission.get('displayName'):
name = permission['displayName']
elif 'id' in permission:
if permission['id'] == 'anyone':
name = 'Anyone'
elif permission['id'] == 'anyoneWithLink':
name = 'Anyone with Link'
else:
name = permission['id']
else:
name = 'Permission'
_mapDrivePermissionNames(permission)
printKeyValueListWithCount([name], i, count)
Ind.Increment()
for key in printKeys:
value = permission.get(key)
if value is None:
continue
if key == 'permissionDetails':
printKeyValueList([key, ''])
Ind.Increment()
for detail in value:
printKeyValueList(['role', detail['role']])
Ind.Increment()
printKeyValueList(['type', detail['permissionType']])
if 'additionalRoles' in detail:
printKeyValueList(['additionalRoles', ','.join(detail['additionalRoles'])])
printKeyValueList(['inherited', detail['inherited']])
if detail['inherited']:
printKeyValueList(['inheritedFrom', detail.get('inheritedFrom', UNKNOWN)])
Ind.Decrement()
Ind.Decrement()
elif key not in timeObjects:
printKeyValueList([key, value])
else:
printKeyValueList([key, formatLocalTime(value)])
Ind.Decrement()
def _showDriveFilePermissions(entityType, fileName, permissions, printKeys, timeObjects, j, jcount):
kcount = len(permissions)
entityPerformActionNumItems([entityType, fileName], kcount, Ent.PERMITTEE, j, jcount)
Ind.Increment()
k = 0
for permission in permissions:
k += 1
_showDriveFilePermission(permission, printKeys, timeObjects, k, kcount)
Ind.Decrement()
def _checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess):
if useDomainAdminAccess and fileIdEntity['nonDomainAdminAccess']:
Cmd.SetLocation(fileIdEntity['location'])
usageErrorExit(Msg.INVALID_FILE_SELECTION_WITH_ADMIN_ACCESS)
# gam [<UserTypeEntity>] create drivefileacl <DriveFileEntity> [asadmin]
# anyone|(user <UserItem>)|(group <GroupItem>)|(domain <DomainName>) (role <DriveFileACLRole>)]
# [withlink|(allowfilediscovery|discoverable [<Boolean>])] [expiration <Time>]
# (mappermissionsdomain <DomainName> <DomainName>)*
# [moveToNewOwnersRoot [<Boolean>]]
# [updatesheetprotectedranges [<Boolean>]]
# [sendemail] [emailmessage <String>]
# [showtitles] [nodetails|(csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]])]
def createDriveFileACL(users, useDomainAdminAccess=False):
moveToNewOwnersRoot = False
sendNotificationEmail = showTitles = _transferOwnership = updateSheetProtectedRanges = False
roleLocation = withLinkLocation = expirationLocation = None
emailMessage = None
showDetails = True
csvPF = None
FJQC = FormatJSONQuoteChar(csvPF)
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
fileIdEntity = getDriveFileEntity()
body = {}
body['type'] = permType = getChoice(DRIVEFILE_ACL_PERMISSION_TYPES)
if permType != 'anyone':
if permType != 'domain':
body['emailAddress'] = permissionId = getEmailAddress()
else:
body['domain'] = permissionId = getString(Cmd.OB_DOMAIN_NAME)
else:
permissionId = 'anyone'
mapPermissionsDomains = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'withlink':
withLinkLocation = Cmd.Location()
body['allowFileDiscovery'] = False
elif myarg in {'allowfilediscovery', 'discoverable'}:
withLinkLocation = Cmd.Location()
body['allowFileDiscovery'] = getBoolean()
elif myarg == 'role':
roleLocation = Cmd.Location()
body['role'] = getChoice(DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
if body['role'] == 'owner':
sendNotificationEmail = _transferOwnership = True
elif myarg == 'mappermissionsdomain':
oldDomain = getString(Cmd.OB_DOMAIN_NAME).lower()
mapPermissionsDomains[oldDomain] = getString(Cmd.OB_DOMAIN_NAME).lower()
elif myarg == 'enforcesingleparent':
deprecatedArgument(myarg)
elif myarg == 'movetonewownersroot':
moveToNewOwnersRoot = getBoolean()
elif myarg in {'expiration', 'expires'}:
expirationLocation = Cmd.Location()
body['expirationTime'] = getTimeOrDeltaFromNow()
elif myarg == 'sendemail':
sendNotificationEmail = True
elif myarg == 'emailmessage':
sendNotificationEmail = True
emailMessage = getString(Cmd.OB_STRING)
elif myarg == 'showtitles':
showTitles = True
elif myarg == 'updatesheetprotectedranges':
updateSheetProtectedRanges = getBoolean()
elif myarg == 'nodetails':
showDetails = False
elif myarg == 'csv':
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall')
FJQC.SetCsvPF(csvPF)
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
FJQC.GetFormatJSONQuoteChar(myarg)
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
if 'role' not in body:
missingArgumentExit(f'role {formatChoiceList(DRIVEFILE_ACL_ROLES_MAP)}')
if mapPermissionsDomains:
if permType != 'anyone':
if permType != 'domain':
atLoc = permissionId.find('@')
if atLoc != -1:
mappedDomain = mapPermissionsDomains.get(permissionId[atLoc+1:], None)
if mappedDomain:
body['emailAddress'] = permissionId = f"{permissionId[:atLoc]}@{mappedDomain}"
else:
mappedDomain = mapPermissionsDomains.get(permissionId, None)
if mappedDomain:
body['domain'] = permissionId = mappedDomain
_validateACLOwnerType(roleLocation, body)
_validateACLAttributes('allowfilediscovery/withlink', withLinkLocation, body, 'allowFileDiscovery', ['anyone', 'domain'])
_validateACLAttributes('expiration', expirationLocation, body, 'expirationTime', ['user', 'group'])
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
if csvPF:
if showTitles:
csvPF.AddTitles(fileNameTitle)
csvPF.SetSortAllTitles()
if FJQC.formatJSON:
csvPF.SetJSONTitles(csvPF.titlesList+['JSON'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=Ent.DRIVE_FILE_OR_FOLDER_ACL if not csvPF else None,
useDomainAdminAccess=useDomainAdminAccess)
if jcount == 0:
continue
sheet = None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles or updateSheetProtectedRanges:
fileName, entityType, mimeType = _getDriveFileNameFromId(drive, fileId, combineTitleId=not csvPF)
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
if not sheet:
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
break
permission = callGAPI(drive.permissions(), 'create',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
moveToNewOwnersRoot=moveToNewOwnersRoot,
useDomainAdminAccess=useDomainAdminAccess,
fileId=fileId, sendNotificationEmail=sendNotificationEmail, emailMessage=emailMessage,
transferOwnership=_transferOwnership, body=body, fields='*', supportsAllDrives=True)
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
_updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileName, True, permission)
if csvPF:
baserow = {'Owner': user, 'id': fileId}
if showTitles:
baserow[fileNameTitle] = fileName
row = baserow.copy()
_mapDrivePermissionNames(permission)
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
else:
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
if showDetails:
_showDriveFilePermission(permission, printKeys, timeObjects)
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.cannotSetExpiration, GAPI.cannotSetExpirationOnAnyoneOrDomain,
GAPI.expirationDateNotAllowedForSharedDriveMembers, GAPI.expirationDatesMustBeInTheFuture,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
GAPI.ownerOnTeamDriveItemNotSupported,
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility,
GAPI.invalidSharingRequest, GAPI.fileNeverWritable, GAPI.abusiveContentRestriction) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], str(e), j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Drive File ACLs')
def doCreateDriveFileACL():
createDriveFileACL([_getAdminEmail()], True)
# gam [<UserTypeEntity>] update drivefileacl <DriveFileEntity> <DriveFilePermissionIDorEmail> [asadmin]
# (role <DriveFileACLRole>) [expiration <Time>] [removeexpiration [<Boolean>]]
# [updatesheetprotectedranges [<Boolean>]] [enforceexpansiveaccess [<Boolean>]]
# [showtitles] [nodetails|(csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]])]
def updateDriveFileACLs(users, useDomainAdminAccess=False):
fileIdEntity = getDriveFileEntity()
isEmail, permissionId = getPermissionId()
enforceExpansiveAccess = None
removeExpiration = showTitles = updateSheetProtectedRanges = False
showDetails = True
csvPF = None
FJQC = FormatJSONQuoteChar(csvPF)
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'role':
body['role'] = getChoice(DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
elif myarg in {'expiration', 'expires'}:
body['expirationTime'] = getTimeOrDeltaFromNow()
elif myarg == 'removeexpiration':
removeExpiration = getBoolean()
elif myarg == 'showtitles':
showTitles = True
elif myarg == 'updatesheetprotectedranges':
updateSheetProtectedRanges = getBoolean()
elif myarg == 'enforceexpansiveaccess':
enforceExpansiveAccess = getBoolean()
elif myarg == 'nodetails':
showDetails = False
elif myarg == 'csv':
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall')
FJQC.SetCsvPF(csvPF)
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'transferownership':
deprecatedArgument(myarg)
getBoolean()
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
FJQC.GetFormatJSONQuoteChar(myarg)
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
if 'role' not in body:
missingArgumentExit(f'role {formatChoiceList(DRIVEFILE_ACL_ROLES_MAP)}')
updateKwargs = {'useDomainAdminAccess': useDomainAdminAccess}
if enforceExpansiveAccess is not None:
updateKwargs['enforceExpansiveAccess'] = enforceExpansiveAccess
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
if csvPF and showTitles:
csvPF.AddTitles(fileNameTitle)
csvPF.SetSortAllTitles()
if FJQC.formatJSON:
csvPF.SetJSONTitles(csvPF.titlesList+['JSON'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=Ent.DRIVE_FILE_OR_FOLDER_ACL if not csvPF else None,
useDomainAdminAccess=useDomainAdminAccess)
if jcount == 0:
continue
if isEmail:
permissionId = getPermissionIdForEmail(user, i, count, permissionId)
if not permissionId:
return
isEmail = False
sheet = None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles or updateSheetProtectedRanges:
fileName, entityType, mimeType = _getDriveFileNameFromId(drive, fileId, combineTitleId=not csvPF)
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
if not sheet:
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
break
permission = callGAPI(drive.permissions(), 'update',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_UPDATE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
**updateKwargs,
fileId=fileId, permissionId=permissionId, removeExpiration=removeExpiration,
transferOwnership=body.get('role', '') == 'owner', body=body, fields='*', supportsAllDrives=True)
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
_updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileName, True, permission)
if csvPF:
baserow = {'Owner': user, 'id': fileId}
if showTitles:
baserow[fileNameTitle] = fileName
row = baserow.copy()
_mapDrivePermissionNames(permission)
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
else:
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
if showDetails:
_showDriveFilePermission(permission, printKeys, timeObjects)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.cannotSetExpiration, GAPI.cannotSetExpirationOnAnyoneOrDomain,
GAPI.expirationDateNotAllowedForSharedDriveMembers, GAPI.expirationDatesMustBeInTheFuture,
GAPI.badRequest, GAPI.invalidOwnershipTransfer, GAPI.cannotRemoveOwner,
GAPI.fileNeverWritable, GAPI.ownershipChangeAcrossDomainNotPermitted, GAPI.sharingRateLimitExceeded,
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges,
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.organizerOnNonTeamDriveItemNotSupported, GAPI.fileOrganizerOnNonTeamDriveNotSupported,
GAPI.cannotUpdatePermission, GAPI.cannotModifyInheritedTeamDrivePermission, GAPI.fieldNotWritable) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
except (GAPI.notFound, GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.ownerOnTeamDriveItemNotSupported,
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive, GAPI.fileOrganizerOnFoldersInSharedDriveOnly) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Drive File ACLs')
def doUpdateDriveFileACLs():
updateDriveFileACLs([_getAdminEmail()], True)
# gam [<UserTypeEntity>] create permissions <DriveFileEntity> <DriveFilePermissionsEntity> [asadmin]
# [expiration <Time>] [sendmail] [emailmessage <String>]
# [moveToNewOwnersRoot [<Boolean>]]
# <PermissionMatch>* [<PermissionMatchAction>]
def createDriveFilePermissions(users, useDomainAdminAccess=False):
def convertJSONPermissions(jsonPermissions):
permissions = []
for permission in PM.GetMatchingPermissions(jsonPermissions):
if permission.get('deleted', False):
continue
if permission['type'] in {'anyone', 'domain'}:
permItem = permission['type'] if permission['allowFileDiscovery'] else permission['type']+'withlink'
if permission['type'] == 'domain':
permItem += ':'+permission['domain']
else:
permItem = permission['type']+':'+permission['emailAddress']
permissions.append(permItem+';'+permission['role'].lower())
return permissions
def _makePermissionBody(permission):
body = {}
try:
scope, role = permission.split(';', 1)
scope = scope.lower()
role = role.lower()
if scope in {'anyone', 'anyonewithlink'}:
body['type'] = 'anyone'
body['allowFileDiscovery'] = scope != 'anyonewithlink'
else:
body['type'], value = scope.split(':', 1)
if body['type'] == 'domainwithlink':
body['allowFileDiscovery'] = False
body['type'] = 'domain'
if body['type'] not in DRIVEFILE_ACL_PERMISSION_TYPES[1:]:
return None
if body['type'] != 'domain':
body['emailAddress'] = value
else:
body['domain'] = value
body['role'] = DRIVEFILE_ACL_ROLES_MAP.get(role)
if not body['role']:
return None
if expiration:
body['expirationTime'] = expiration
return body
except ValueError:
return None
def _callbackCreatePermission(request_id, _, exception):
ri = request_id.splitlines()
if int(ri[RI_J]) == 1:
entityPerformActionNumItems([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY]], int(ri[RI_JCOUNT]), Ent.PERMITTEE, int(ri[RI_I]), int(ri[RI_COUNT]))
Ind.Increment()
if exception is None:
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason not in GAPI.DEFAULT_RETRY_REASONS+[GAPI.SERVICE_LIMIT]:
if reason in GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS:
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], message, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
Ind.Decrement()
return
waitOnFailure(1, 10, reason, message)
try:
callGAPI(drive.permissions(), 'create',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS, retryReasons=[GAPI.SERVICE_LIMIT],
useDomainAdminAccess=useDomainAdminAccess,
fileId=ri[RI_ENTITY], sendNotificationEmail=sendNotificationEmail, emailMessage=emailMessage,
body=_makePermissionBody(ri[RI_ITEM]), fields='', supportsAllDrives=True)
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.cannotSetExpiration, GAPI.cannotSetExpirationOnAnyoneOrDomain,
GAPI.expirationDateNotAllowedForSharedDriveMembers, GAPI.expirationDatesMustBeInTheFuture,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
GAPI.ownerOnTeamDriveItemNotSupported,
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility,
GAPI.invalidSharingRequest, GAPI.fileNeverWritable, GAPI.abusiveContentRestriction,
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.notFound as e:
entityActionFailedWarning([Ent.SHAREDDRIVE, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
Ind.Decrement()
moveToNewOwnersRoot = False
sendNotificationEmail = False
emailMessage = expiration = None
fileIdEntity = getDriveFileEntity()
if not checkArgumentPresent('json'):
permissions = getEntityList(Cmd.OB_DRIVE_FILE_PERMISSION_ENTITY)
jsonData = None
PM = None
else:
permissions = []
jsonData = getJSON([])
PM = PermissionMatch()
PM.SetDefaultMatch(False, {'role': 'owner'})
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'enforcesingleparent':
deprecatedArgument(myarg)
elif myarg == 'movetonewownersroot':
moveToNewOwnersRoot = getBoolean()
elif myarg in {'expiration', 'expires'}:
expiration = getTimeOrDeltaFromNow()
elif myarg == 'sendemail':
sendNotificationEmail = True
elif myarg == 'emailmessage':
sendNotificationEmail = True
emailMessage = getString(Cmd.OB_STRING)
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif PM and PM.ProcessArgument(myarg):
pass
else:
unknownArgumentExit()
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
if jsonData:
if 'permission' in jsonData:
permissions = convertJSONPermissions([jsonData['permission']])
elif 'permissions' in jsonData:
permissions = convertJSONPermissions(jsonData['permissions'])
permissionsLists = permissions if isinstance(permissions, dict) else None
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
if not drive:
continue
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_OR_FOLDER_ACL, Act.MODIFIER_TO, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
if jcount == 0:
continue
try:
callGAPI(drive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='kind')
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
Ind.Increment()
svcargs = dict([('fileId', None), ('sendNotificationEmail', sendNotificationEmail), ('emailMessage', emailMessage),
('useDomainAdminAccess', useDomainAdminAccess),
('body', None), ('fields', ''), ('supportsAllDrives', True)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(drive.permissions(), 'create')
dbatch = drive.new_batch_http_request(callback=_callbackCreatePermission)
bcount = 0
j = 0
for fileId in fileIdEntity['list']:
j += 1
if permissionsLists:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
permissions = permissionsLists[fileId]
else:
permissions = permissionsLists[origUser][fileId]
kcount = len(permissions)
if kcount == 0:
continue
k = 0
for permission in permissions:
k += 1
svcparms = svcargs.copy()
svcparms['fileId'] = fileId
svcparms['body'] = _makePermissionBody(permission)
if not svcparms['body']:
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, fileId, Ent.PERMITTEE, permission], Msg.INVALID, k, kcount)
continue
if svcparms['body']['role'] == 'owner':
svcparms['moveToNewOwnersRoot'] = moveToNewOwnersRoot
svcparms['transferOwnership'] = svcparms['sendNotificationEmail'] = True
dbatch.add(method(**svcparms), request_id=batchRequestID(fileId, j, jcount, k, kcount, permission))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = drive.new_batch_http_request(callback=_callbackCreatePermission)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
def doCreatePermissions():
createDriveFilePermissions([_getAdminEmail()], True)
# gam [<UserTypeEntity>] delete drivefileacl <DriveFileEntity> <DriveFilePermissionIDorEmail> [asadmin]
# [updatesheetprotectedranges [<Boolean>]] [enforceexpansiveaccess [<Boolean>]]
# [showtitles]
def deleteDriveFileACLs(users, useDomainAdminAccess=False):
fileIdEntity = getDriveFileEntity()
isEmail, permissionId = getPermissionId()
enforceExpansiveAccess = None
showTitles = updateSheetProtectedRanges = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'showtitles':
showTitles = getBoolean()
elif myarg == 'updatesheetprotectedranges':
updateSheetProtectedRanges = getBoolean()
elif myarg == 'enforceexpansiveaccess':
enforceExpansiveAccess = getBoolean()
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
unknownArgumentExit()
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
deleteKwargs = {'useDomainAdminAccess': useDomainAdminAccess}
if enforceExpansiveAccess is not None:
deleteKwargs['enforceExpansiveAccess'] = enforceExpansiveAccess
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER_ACL, useDomainAdminAccess=useDomainAdminAccess)
if jcount == 0:
continue
if isEmail:
permissionId = getPermissionIdForEmail(user, i, count, permissionId)
if not permissionId:
return
isEmail = False
sheet = None
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles or updateSheetProtectedRanges:
fileName, entityType, mimeType = _getDriveFileNameFromId(drive, fileId)
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
permission = callGAPI(drive.permissions(), 'get',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.PERMISSION_NOT_FOUND, GAPI.INSUFFICIENT_ADMINISTRATOR_PRIVILEGES],
useDomainAdminAccess=useDomainAdminAccess,
fileId=fileId, permissionId=permissionId, fields='type,emailAddress,domain,role', supportsAllDrives=True)
if not sheet:
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
break
callGAPI(drive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
**deleteKwargs,
fileId=fileId, permissionId=permissionId, supportsAllDrives=True)
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
_updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileName, False, permission)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
def doDeleteDriveFileACLs():
deleteDriveFileACLs([_getAdminEmail()], True)
# gam [<UserTypeEntity>] delete permissions <DriveFileEntity> <DriveFilePermissionIDEntity> [asadmin]
# <PermissionMatch>* [<PermissionMatchAction>]
# [enforceexpansiveaccess [<Boolean>]]
def deletePermissions(users, useDomainAdminAccess=False):
def convertJSONPermissions(jsonPermissions):
permissionIds = []
for permission in PM.GetMatchingPermissions(jsonPermissions):
if permission.get('role', '') == 'owner':
continue
permissionIds.append(permission['id'])
return permissionIds
def _callbackDeletePermissionId(request_id, _, exception):
ri = request_id.splitlines()
if int(ri[RI_J]) == 1:
entityPerformActionNumItems([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY]], int(ri[RI_JCOUNT]), Ent.PERMISSION_ID, int(ri[RI_I]), int(ri[RI_COUNT]))
Ind.Increment()
if exception is None:
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
http_status, reason, message = checkGAPIError(exception)
if reason not in GAPI.DEFAULT_RETRY_REASONS+[GAPI.SERVICE_LIMIT]:
if reason == GAPI.PERMISSION_NOT_FOUND:
entityDoesNotHaveItemWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
elif reason in GAPI.DRIVE3_DELETE_ACL_THROW_REASONS:
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], message, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
errMsg = getHTTPError({}, http_status, reason, message)
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
Ind.Decrement()
return
waitOnFailure(1, 10, reason, message)
try:
callGAPI(drive.permissions(), 'delete',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS,
retryReasons=[GAPI.SERVICE_LIMIT],
useDomainAdminAccess=useDomainAdminAccess, enforceExpansiveAccess=enforceExpansiveAccess,
fileId=ri[RI_ENTITY], permissionId=ri[RI_ITEM], supportsAllDrives=True)
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.permissionNotFound, GAPI.cannotDeletePermission,
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
Ind.Decrement()
fileIdEntity = getDriveFileEntity()
if not checkArgumentPresent('json'):
permissionIds = getEntityList(Cmd.OB_DRIVE_FILE_PERMISSION_ID_ENTITY)
jsonData = None
PM = None
else:
permissionIds = []
jsonData = getJSON([])
PM = PermissionMatch()
PM.SetDefaultMatch(False, {'role': 'owner'})
enforceExpansiveAccess = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif PM and PM.ProcessArgument(myarg):
pass
elif myarg == 'enforceexpansiveaccess':
enforceExpansiveAccess = getBoolean()
else:
unknownArgumentExit()
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
if jsonData:
if 'permission' in jsonData:
permissionIds = convertJSONPermissions([jsonData['permission']])
elif 'permissions' in jsonData:
permissionIds = convertJSONPermissions(jsonData['permissions'])
permissionIdsLists = permissionIds if isinstance(permissionIds, dict) else None
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
if not drive:
continue
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_OR_FOLDER_ACL, Act.MODIFIER_FROM, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
if jcount == 0:
continue
try:
callGAPI(drive.about(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='kind')
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
Ind.Increment()
svcargs = dict([('fileId', None), ('permissionId', None), ('useDomainAdminAccess', useDomainAdminAccess), ('supportsAllDrives', True)]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(drive.permissions(), 'delete')
dbatch = drive.new_batch_http_request(callback=_callbackDeletePermissionId)
bcount = 0
j = 0
for fileId in fileIdEntity['list']:
j += 1
if permissionIdsLists:
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
permissionIds = permissionIdsLists[fileId]
else:
permissionIds = permissionIdsLists[origUser][fileId]
kcount = len(permissionIds)
if kcount == 0:
continue
k = 0
for permissionId in permissionIds:
k += 1
svcparms = svcargs.copy()
svcparms['fileId'] = fileId
svcparms['permissionId'] = permissionId
dbatch.add(method(**svcparms), request_id=batchRequestID(fileId, j, jcount, k, kcount, permissionId))
bcount += 1
if bcount >= GC.Values[GC.BATCH_SIZE]:
executeBatch(dbatch)
dbatch = drive.new_batch_http_request(callback=_callbackDeletePermissionId)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
def doDeletePermissions():
deletePermissions([_getAdminEmail()], True)
# gam [<UserTypeEntity>] info drivefileacl <DriveFileEntity> <DriveFilePermissionIDorEmail> [asadmin]
# [showtitles] [formatjson]
def infoDriveFileACLs(users, useDomainAdminAccess=False):
fileIdEntity = getDriveFileEntity()
isEmail, permissionId = getPermissionId()
showTitles = False
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'showtitles':
showTitles = getBoolean()
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
FJQC.GetFormatJSON(myarg)
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=[Ent.DRIVE_FILE_OR_FOLDER_ACL, None][FJQC.formatJSON],
useDomainAdminAccess=useDomainAdminAccess)
if jcount == 0:
continue
if isEmail:
permissionId = getPermissionIdForEmail(user, i, count, permissionId)
if not permissionId:
return
isEmail = False
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
try:
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles:
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId, not FJQC.formatJSON, useDomainAdminAccess)
permission = callGAPI(drive.permissions(), 'get',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.PERMISSION_NOT_FOUND, GAPI.INSUFFICIENT_ADMINISTRATOR_PRIVILEGES],
useDomainAdminAccess=useDomainAdminAccess,
fileId=fileId, permissionId=permissionId, fields='*', supportsAllDrives=True)
if not FJQC.formatJSON:
entityPerformActionNumItems([entityType, fileName], jcount, Ent.PERMITTEE)
Ind.Increment()
_showDriveFilePermission(permission, printKeys, timeObjects, j, jcount)
Ind.Decrement()
else:
_showDriveFilePermissionJSON(user, fileId, fileName, None, permission, timeObjects)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
GAPI.badRequest, GAPI.insufficientAdministratorPrivileges) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
except GAPI.permissionNotFound:
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
def doInfoDriveFileACLs():
infoDriveFileACLs([_getAdminEmail()], True)
DRIVEFILE_PERMISSIONS_FOR_VIEW_CHOICES = ['published']
def getDriveFilePermissionsFields(myarg, fieldsList):
if myarg in DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP:
fieldsList.append(DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP[myarg])
elif myarg == 'basicpermissions':
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS[:-1])
elif myarg == 'fields':
for field in _getFieldsList():
if field in DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP:
fieldsList.append(DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP[field])
elif field == 'basicpermissions':
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS[:-1])
else:
invalidChoiceExit(field, DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP, True)
else:
return False
return True
# gam [<UserTypeEntity>] print drivefileacls <DriveFileEntity> [todrive <ToDriveAttribute>*]
# (role|roles <DriveFileACLRoleList>)*
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
# [includepermissionsforview published]
# [oneitemperrow] [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
# [showtitles|(addtitle <String>)]]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [formatjson [quotechar <Character>]] [asadmin]
# gam [<UserTypeEntity>] show drivefileacls <DriveFileEntity>
# (role|roles <DriveFileACLRoleList>)*
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
# [includepermissionsforview published]
# [oneitemperrow] [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
# [showtitles|(addtitle <String>)]]
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
# [formatjson] [asadmin]
def printShowDriveFileACLs(users, useDomainAdminAccess=False):
def _printPermissionRow(baserow, permission):
row = baserow.copy()
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fileIdEntity = getDriveFileEntity()
aclRolesMap = DRIVEFILE_ACL_ROLES_MAP.copy()
if fileIdEntity['shareddrivename'] or fileIdEntity['shareddriveadminquery'] or fileIdEntity['shareddrivefilequery'] or fileIdEntity['shareddrive']:
aclRolesMap['owner'] = 'organizer'
addTitle = None
roles = set()
oneItemPerRow = pmselect = showTitles = False
includePermissionsForView = None
fieldsList = []
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
PM = PermissionMatch()
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'role', 'roles'}:
roles |= getACLRoles(aclRolesMap)
elif myarg == 'oneitemperrow':
oneItemPerRow = True
elif myarg == 'orderby':
OBY.GetChoice()
elif myarg in {'showtitles', 'addtitle'}:
if myarg == 'showtitles':
showTitles = True
addTitle = None
else:
addTitle = getString(Cmd.OB_STRING)
showTitles = False
if csvPF:
csvPF.AddTitles(fileNameTitle)
csvPF.SetSortAllTitles()
elif getDriveFilePermissionsFields(myarg, fieldsList):
pass
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif myarg == 'pmselect':
pmselect = True
elif myarg == 'pmfilter': # Ignore, this is the default behavior
pass
elif PM.ProcessArgument(myarg):
pass
elif myarg == 'includepermissionsforview':
includePermissionsForView = getChoice(DRIVEFILE_PERMISSIONS_FOR_VIEW_CHOICES)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
if fieldsList:
if roles:
fieldsList.append('role')
fields = getItemFieldsFromFieldsList('permissions', fieldsList, True)
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][csvPF is not None or FJQC.formatJSON],
orderBy=OBY.orderBy, useDomainAdminAccess=useDomainAdminAccess)
if jcount == 0:
continue
Ind.Increment()
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileName = fileId
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
if showTitles:
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId, not (csvPF or FJQC.formatJSON), useDomainAdminAccess)
elif addTitle:
fileName = f'{addTitle} ({fileId})'
entityType = Ent.DRIVE_FILE_OR_FOLDER
try:
permissions = callGAPIpages(drive.permissions(), 'list', 'permissions',
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS+[GAPI.NOT_FOUND],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
useDomainAdminAccess=useDomainAdminAccess,
includePermissionsForView=includePermissionsForView,
fileId=fileId, fields=fields, supportsAllDrives=True)
for permission in permissions:
permission.pop('teamDrivePermissionDetails', None)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid) as e:
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
continue
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if pmselect:
if not PM.CheckPermissionMatches(permissions):
continue
else:
permissions = PM.GetMatchingPermissions(permissions)
if roles:
matchingPermissions = []
for permission in permissions:
if roles and permission['role'] not in roles:
continue
matchingPermissions.append(permission)
permissions = matchingPermissions
if not csvPF:
if not FJQC.formatJSON:
_showDriveFilePermissions(entityType, fileName, permissions, printKeys, timeObjects, j, jcount)
else:
if oneItemPerRow:
for permission in permissions:
_showDriveFilePermissionJSON(user, fileId, fileName, None, permission, timeObjects)
else:
_showDriveFilePermissionsJSON(user, fileId, fileName, None, permissions, timeObjects)
else:
baserow = {'Owner': user, 'id': fileId}
if showTitles or addTitle:
baserow[fileNameTitle] = fileName
if oneItemPerRow:
for permission in permissions:
_mapDrivePermissionNames(permission)
pdetails = permission.pop('permissionDetails', [])
if not pdetails:
_printPermissionRow(baserow, permission)
else:
for pdetail in pdetails:
permission['permissionDetails'] = pdetail
_printPermissionRow(baserow, permission)
else:
row = baserow.copy()
for permission in permissions:
_mapDrivePermissionNames(permission)
flattenJSON({'permissions': permissions}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
baserow['JSON'] = json.dumps(cleanJSON({'permissions': permissions}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(baserow)
Ind.Decrement()
if csvPF:
if not oneItemPerRow:
csvPF.SetIndexedTitles(['permissions'])
csvPF.writeCSVfile('Drive File ACLs')
def doPrintShowDriveFileACLs():
printShowDriveFileACLs([_getAdminEmail()], True)
DRIVELABELS_PROJECTION_CHOICE_MAP = {'basic': 'LABEL_VIEW_BASIC', 'full': 'LABEL_VIEW_FULL'}
DRIVELABELS_PERMISSION_ROLE_MAP = {
'applier': 'APPLIER',
'editor': 'EDITOR',
'organizer': 'ORGANIZER',
'organiser': 'ORGANIZER',
'reader': 'READER',
}
DRIVELABELS_TIME_OBJECTS = {'createTime', 'publishTime', 'disableTime', 'revisionCreateTime'}
def _getDisplayDriveLabelsParameters(myarg, parameters):
if myarg in DRIVELABELS_PROJECTION_CHOICE_MAP:
parameters['view'] = DRIVELABELS_PROJECTION_CHOICE_MAP[myarg]
elif myarg == 'language':
parameters['languageCode'] = getLanguageCode(BCP47_LANGUAGE_CODES_MAP)
elif myarg in ADMIN_ACCESS_OPTIONS:
parameters['useAdminAccess'] = True
elif myarg == 'publishedonly':
parameters['publishedOnly'] = getBoolean()
elif myarg == 'minimumrole':
parameters['minimumRole'] = getChoice(DRIVELABELS_PERMISSION_ROLE_MAP, mapChoice=True)
else:
return False
return True
def normalizeDriveLabelID(driveLabelID):
atLoc = driveLabelID.find('@')
if atLoc != -1:
driveLabelID = driveLabelID[:atLoc]
if driveLabelID.startswith('labels/'):
return driveLabelID[7:]
return driveLabelID
def normalizeDriveLabelName(driveLabelName):
if driveLabelName.startswith('labels/'):
return driveLabelName
return f'labels/{driveLabelName}'
def validateDriveLabelName(name, kvList, j, jcount, permName=False):
name = normalizeDriveLabelName(name)
# Label name
if not permName:
mg = re.match(r'^(labels/[^/]+)$', name)
if not mg:
entityActionNotPerformedWarning(kvList, 'Expected labels/<String>', j, jcount)
return None
return name
# Label permission name
mg = re.match(r'^(labels/[^/]+)/permissions/(?:audiences|groups|people)/.+$', name)
if not mg:
entityActionNotPerformedWarning(kvList, 'Expected labels/<String>/permissions/(audiences|groups|people)/<String>', j, jcount)
return (None, None)
return (name, mg.group(1))
def _showDriveLabel(label, j, jcount, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(label, timeObjects=DRIVELABELS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CLASSIFICATION_LABEL_NAME, f'{label["name"]}'], j, jcount)
Ind.Increment()
showJSON(None, label, timeObjects=DRIVELABELS_TIME_OBJECTS, dictObjectsKey={'fields': 'id', 'choices': 'id'})
Ind.Decrement()
# gam [<UserTypeEntity>] info classificationlabels <ClassificationLabelNameEntity>
# [[basic|full] [languagecode <BCP47LanguageCode>]
# [formatjson] [asadmin]
def infoDriveLabels(users, useAdminAccess=False):
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL, shlexSplit=True)
FJQC = FormatJSONQuoteChar()
parameters = {'useAdminAccess': useAdminAccess}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getDisplayDriveLabelsParameters(myarg, parameters):
pass
else:
FJQC.GetFormatJSON(myarg)
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, labelNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
api=api, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in labelNames:
j += 1
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name]
name = validateDriveLabelName(name, kvList, j, jcount, False)
if name is None:
continue
try:
label = callGAPI(drive.labels(), 'get',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR],
name=name, **parameters)
_showDriveLabel(label, j, jcount, FJQC)
except GAPI.notFound as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.internalError) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
def doInfoDriveLabels():
infoDriveLabels([_getAdminEmail()], True)
# gam [<UserTypeEntity>] print classificationlabels> [todrive <ToDriveAttribute>*]
# [basic|full] [languagecode <BCP47LanguageCode>]
# [publishedonly [<Boolean>]] [minimumrole applier|editor|organizer|reader]
# [formatjson [quotechar <Character>]] [asadmin]
# gam [<UserTypeEntity>] show classificationlabels
# [basic|full] [languagecode <BCP47LanguageCode>]
# [publishedonly [<Boolean>]] [minimumrole applier|editor|organizer|reader]
# [formatjson] [asadmin]
def printShowDriveLabels(users, useAdminAccess=False):
csvPF = CSVPrintFile(['User', 'name', 'description', 'id'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
parameters = {'useAdminAccess': useAdminAccess}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getDisplayDriveLabelsParameters(myarg, parameters):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(['User', 'name', 'JSON'])
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(api, user, i, count)
if not drive:
continue
if csvPF:
printGettingAllEntityItemsForWhom(Ent.CLASSIFICATION_LABEL, user, i, count)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
try:
labels = callGAPIpages(drive.labels(), 'list', 'labels',
pageMessage=pageMessage,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED],
**parameters, fields='nextPageToken,labels', pageSize=200)
if not csvPF:
jcount = len(labels)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CLASSIFICATION_LABEL, i, count)
Ind.Increment()
j = 0
for label in labels:
j += 1
_showDriveLabel(label, j, jcount, FJQC)
Ind.Decrement()
else:
for label in labels:
row = flattenJSON(label, flattened={'User': user}, timeObjects=DRIVELABELS_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user, 'name': label['name']}
row['JSON'] = json.dumps(cleanJSON(label, timeObjects=DRIVELABELS_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, user, Ent.CLASSIFICATION_LABEL, None], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Classification Labels')
def doPrintShowDriveLabels():
printShowDriveLabels([_getAdminEmail()], True)
def _showDriveLabelPermission(labelperm, j, jcount, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(labelperm), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CLASSIFICATION_LABEL_PERMISSION_NAME, f'{labelperm["name"]}'], j, jcount)
Ind.Increment()
showJSON(None, labelperm)
Ind.Decrement()
# gam [<UserTypeEntity>] create classificationlabelpermission <ClassificationLabelNameEntity>
# (user <UserItem>) | (group <GroupItem) | (audience <String>)
# role applier|editor|organizer|reader
# [nodetails|formatjson] [asadmin]
def createDriveLabelPermissions(users, useAdminAccess=False):
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL_PERMISSION, shlexSplit=True)
FJQC = FormatJSONQuoteChar()
parameters = {'useAdminAccess': useAdminAccess}
body = {}
showDetails = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ADMIN_ACCESS_OPTIONS:
parameters['useAdminAccess'] = True
elif myarg == 'role':
body['role'] = getChoice(DRIVELABELS_PERMISSION_ROLE_MAP, mapChoice=True)
elif myarg in {'user', 'group'}:
email = getEmailAddress(returnUIDprefix='id:')
body['email'], status = convertUIDtoEmailAddressWithType(email, emailTypes=[myarg])
if status == 'unknown':
Cmd.Backup()
usageErrorExit(Msg.ENTITY_DOES_NOT_EXIST.format(email))
elif myarg == 'audience':
audience = getString(Cmd.OB_STRING)
if not audience.startswith('audiences/'):
audience = 'audiences/'+audience
body['audience'] = audience
elif myarg == 'nodetails':
showDetails = False
else:
FJQC.GetFormatJSON(myarg)
if 'role' not in body:
missingArgumentExit(f'role {"|".join(DRIVELABELS_PERMISSION_ROLE_MAP.keys())}')
if 'email' not in body and 'audience' not in body:
missingArgumentExit('(user <UserItem>) | (group <GroupItem) | (audience <String>)')
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, labelNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
api=api, showAction=not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in labelNames:
j += 1
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name, Ent.CLASSIFICATION_LABEL_PERMISSION, None]
name = validateDriveLabelName(name, kvList, j, jcount, False)
if name is None:
continue
try:
labelperm = callGAPI(drive.labels().permissions(), 'create',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.NOT_FOUND,
GAPI.INVALID, GAPI.INTERNAL_ERROR],
parent=name, body=body, **parameters)
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_PERMISSION, labelperm['name']]
if not FJQC.formatJSON:
entityActionPerformed(kvList, j, jcount)
if showDetails:
Ind.Increment()
_showDriveLabelPermission(labelperm, j, jcount, FJQC)
Ind.Decrement()
except (GAPI.permissionDenied, GAPI.notFound, GAPI.invalid, GAPI.internalError) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
def doCreateDriveLabelPermissions():
createDriveLabelPermissions([_getAdminEmail()], True)
# gam [<UserTypeEntity>] delete classificationlabelpermission <ClassificationLabelNameEntity>
# (user <UserItem>) | (group <GroupItem) | (audience <String>)
# [asadmin]
# gam [<UserTypeEntity>] remove classificationlabelpermission <ClassificationLabelPermissionNameEntity>
# [asadmin]
def deleteDriveLabelPermissions(users, useAdminAccess=False):
doDelete = Act.Get() == Act.DELETE
if doDelete:
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL, shlexSplit=True)
else:
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_PERMISSION_NAME, Ent.CLASSIFICATION_LABEL_PERMISSION, shlexSplit=True)
parameters = {'useAdminAccess': useAdminAccess, 'requests': [None]}
labelperm = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ADMIN_ACCESS_OPTIONS:
parameters['useAdminAccess'] = True
elif doDelete and myarg in {'user', 'group'}:
labelperm = ['people/', 'groups/'][myarg == 'group']+convertEmailAddressToUID(getEmailAddress(), cd=None, emailType=myarg, savedLocation=None)
elif doDelete and myarg == 'audience':
audience = getString(Cmd.OB_STRING)
if not audience.startswith('audiences/'):
audience = 'audiences/'+audience
labelperm = audience
else:
unknownArgumentExit()
if doDelete and not labelperm:
missingArgumentExit('(user <UserItem>) | (group <GroupItem) | (audience <String>)')
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, labelPermNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
api=api, showAction=True)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in labelPermNames:
j += 1
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_PERMISSION_NAME, name]
if doDelete:
parent = validateDriveLabelName(name, kvList, j, jcount, False)
if parent is None:
continue
name = parent+'/permissions/'+labelperm
else:
name, parent = validateDriveLabelName(name, kvList, j, jcount, True)
if name is None:
continue
kvList[-1] = name
parameters['requests'][0] = {'name': name}
try:
callGAPI(drive.labels().permissions(), 'batchDelete',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.INVALID, GAPI.NOT_FOUND],
parent=parent, body=parameters)
entityActionPerformed(kvList, j, jcount)
except (GAPI.permissionDenied, GAPI.invalid, GAPI.notFound) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
def doDeleteDriveLabelPermissions():
deleteDriveLabelPermissions([_getAdminEmail()], True)
# gam [<UserTypeEntity>] print classificationlabelpermissions <ClassificationLabelNameEntity> [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]] [asadmin]
# gam [<UserTypeEntity>] show classificationlabelpermissions <ClassificationLabelNameEntity>
# [formatjson] [asadmin]
def printShowDriveLabelPermissions(users, useAdminAccess=False):
csvPF = CSVPrintFile(['User', 'name', 'email', 'role', 'person', 'group', 'audience'], 'sortall') if Act.csvFormat() else None
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL_PERMISSION, shlexSplit=True)
FJQC = FormatJSONQuoteChar(csvPF)
parameters = {'useAdminAccess': useAdminAccess}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in ADMIN_ACCESS_OPTIONS:
parameters['useAdminAccess'] = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(['User', 'name', 'JSON'])
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, labelNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
api=api, showAction=FJQC is None or not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in labelNames:
j += 1
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name, Ent.CLASSIFICATION_LABEL_PERMISSION, None]
name = validateDriveLabelName(name, kvList, j, jcount, False)
if name is None:
continue
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name]
if csvPF:
printGettingAllEntityItemsForWhom(Ent.CLASSIFICATION_LABEL_PERMISSION, name, j, jcount)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
try:
labelperms = callGAPIpages(drive.labels().permissions(), 'list', 'labelPermissions',
pageMessage=pageMessage,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.NOT_FOUND],
parent=name, **parameters, fields='nextPageToken,labelPermissions', pageSize=200)
if not csvPF:
jcount = len(labelperms)
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, jcount, Ent.CLASSIFICATION_LABEL_PERMISSION, i, count)
Ind.Increment()
j = 0
for labelperm in labelperms:
j += 1
_showDriveLabelPermission(labelperm, j, jcount, FJQC)
Ind.Decrement()
else:
for labelperm in labelperms:
row = flattenJSON(labelperm, flattened={'User': user})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user, 'name': labelperm['name']}
row['JSON'] = json.dumps(cleanJSON(labelperm),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
except (GAPI.permissionDenied, GAPI.notFound) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Classification Label Permissions')
def doPrintShowDriveLabelPermissions():
printShowDriveLabelPermissions([_getAdminEmail()], True)
DRIVELABEL_FIELD_TYPE_MAP = {
'text': 'setTextValues',
'selection': 'setSelectionValues',
'integer': 'setIntegerValues',
'date': 'setDateValues',
'user': 'setUserValues',
}
# gam <UserTypeEntity> process filedrivelabels <DriveFileEntity>
# (addlabel <ClassificationLabelIDList>)*
# (deletelabel <ClassificationLabelIDList>)*
# (addlabelfield <ClassificationLabelID> <ClassificationLabelFieldID>
# (text <String>)|selection <ClassificationLabelSelectionIDList>)|
# (integer <Number>)|(date <Date>)|(user <EmailAddressList>))*
# (deletelabelfield <ClassificationLabelID> <ClassificationLabelFieldID>)*
# [nodetails]
def processFileDriveLabels(users):
fileIdEntity = getDriveFileEntity()
actionList = {'addlabel': {'action': Act.CREATE, 'list': []},
'deletelabel': {'action': Act.DELETE, 'list': []},
'addlabelfield': {'action': Act.CREATE, 'list': []},
'deletelabelfield': {'action': Act.DELETE, 'list': []},
}
showDetails = True
kcount = 0
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'addlabel':
labelIds = getEntityList(Cmd.OB_CLASSIFICATION_LABEL_ID, shlexSplit=True)
for labelId in labelIds:
actionList[myarg]['list'].append({'labelModifications': [{'labelId': normalizeDriveLabelID(labelId)}]})
kcount += 1
elif myarg == 'deletelabel':
labelIds = getEntityList(Cmd.OB_CLASSIFICATION_LABEL_ID, shlexSplit=True)
for labelId in labelIds:
actionList[myarg]['list'].append({'labelModifications': [{'labelId': normalizeDriveLabelID(labelId), 'removeLabel': True}]})
kcount += 1
elif myarg == 'addlabelfield':
labelId = normalizeDriveLabelID(getString(Cmd.OB_CLASSIFICATION_LABEL_ID))
fieldId = getString(Cmd.OB_CLASSIFICATION_LABEL_FIELD_ID)
fieldType = getChoice(DRIVELABEL_FIELD_TYPE_MAP, mapChoice=True)
if fieldType == 'setTextValues':
valueList = [getString(Cmd.OB_STRING, minLen=0)]
elif fieldType == 'setSelectionValues':
valueList = convertEntityToList(getString(Cmd.OB_CLASSIFICATION_LABEL_SELECTION_ID_LIST, minLen=0), shlexSplit=True)
elif fieldType == 'setIntegerValues':
valueList = [getInteger()]
elif fieldType == 'setDateValues':
valueList = [getYYYYMMDD()]
else: #elif fieldType == 'setUserValues':
valueList = convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0))
actionList[myarg]['list'].append({'labelModifications': [{'labelId': labelId,
'fieldModifications': [{'fieldId': fieldId, fieldType: valueList}]}]})
kcount += 1
elif myarg == 'deletelabelfield':
labelId = normalizeDriveLabelID(getString(Cmd.OB_CLASSIFICATION_LABEL_ID))
fieldId = getString(Cmd.OB_CLASSIFICATION_LABEL_FIELD_ID)
actionList[myarg]['list'].append({'labelModifications': [{'labelId': labelId,
'fieldModifications': [{'fieldId': fieldId, 'unsetValues': True}]}]})
kcount += 1
elif myarg == 'nodetails':
showDetails = False
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=None)
if jcount == 0:
continue
Act.Set(Act.PROCESS)
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.CLASSIFICATION_LABEL, Act.MODIFIER_FOR, jcount, Ent.DRIVE_FILE_OR_FOLDER)
Ind.Increment()
j = 0
userError = False
for fileId in fileIdEntity['list']:
j += 1
k = 0
kvList = [Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId]
Act.Set(Act.PROCESS)
entityPerformActionNumItems(kvList, kcount, Ent.CLASSIFICATION_LABEL)
Ind.Increment()
for operation in ['deletelabelfield', 'deletelabel', 'addlabel', 'addlabelfield']:
Act.Set(actionList[operation]['action'])
for action in actionList[operation]['list']:
k += 1
xkvList = kvList.copy()
xkvList.extend([Ent.CLASSIFICATION_LABEL_ID, action['labelModifications'][0]['labelId']])
if 'fieldModifications' in action['labelModifications'][0]:
xkvList.extend([Ent.CLASSIFICATION_LABEL_FIELD_ID, action['labelModifications'][0]['fieldModifications'][0]['fieldId']])
try:
label = callGAPI(drive.files(), 'modifyLabels',
throwReasons=GAPI.DRIVE3_MODIFY_LABEL_THROW_REASONS,
fileId=fileId, body=action)
entityActionPerformed(xkvList, k, kcount)
if showDetails:
Ind.Increment()
showJSON(None, label, timeObjects=DRIVELABELS_TIME_OBJECTS, dictObjectsKey={'fields': 'id', 'modifiedLabels': 'id'})
Ind.Decrement()
except GAPI.fileNotFound as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
break
except (GAPI.notFound, GAPI.forbidden, GAPI.internalError,
GAPI.fileNeverWritable, GAPI.applyLabelForbidden,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.invalidInput, GAPI.badRequest,
GAPI.labelMutationUnknownField, GAPI.labelMutationIllegalSelection, GAPI.labelMutationForbidden,
GAPI.labelMultipleValuesForSingularField) as e:
entityActionFailedWarning(xkvList, str(e), k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
userError = True
break
Ind.Decrement()
if userError:
break
Ind.Decrement()
# gam print ownership <DriveFileID>|(drivefilename <DriveFileName>) [todrive <ToDriveAttribute>*]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam show ownership <DriveFileID>|(drivefilename <DriveFileName>)
# [formatjson]
def doPrintShowOwnership():
rep = buildGAPIObject(API.REPORTS)
customerId = GC.Values[GC.CUSTOMER_ID]
if customerId == GC.MY_CUSTOMER:
customerId = None
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
csvPF = CSVPrintFile('Owner') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
addCSVData = {}
showComplete = False
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
mycmd = myarg.lower().replace('_', '').replace('-', '')
if mycmd == 'id':
fileId = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
elif mycmd == 'drivefilename':
entityType = Ent.DRIVE_FILE_OR_FOLDER
fileId = getString(Cmd.OB_DRIVE_FILE_NAME, checkBlank=True)
elif mycmd.find(':') != -1:
kw, fileId = myarg.split(':', 1)
kw = kw.lower().replace('_', '').replace('-', '')
if fileId.isspace():
Cmd.Backup()
blankArgumentExit(Cmd.OB_DRIVE_FILE_ID)
if kw == 'id':
pass
elif kw == 'drivefilename':
entityType = Ent.DRIVE_FILE_OR_FOLDER
else:
Cmd.Backup()
invalidArgumentExit(Cmd.OB_DRIVE_FILE_ID)
else:
fileId = myarg
if not fileId:
Cmd.Backup()
invalidArgumentExit(Cmd.OB_DRIVE_FILE_ID)
if entityType == Ent.DRIVE_FILE_OR_FOLDER_ID:
filters = f'doc_id=={fileId}'
else:
filters = f'doc_title=={fileId}'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and not FJQC.formatJSON:
csvPF.AddTitles(['id', fileNameTitle, 'type', 'ownerIsSharedDrive', 'driveId', 'event'])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
csvPF.SetSortAllTitles()
foundIds = {}
try:
feed = callGAPIpages(rep.activities(), 'list', 'items',
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.AUTH_ERROR],
applicationName='drive', userKey='all', customerId=customerId,
filters=filters, fields='nextPageToken,items(events(name,parameters))')
except GAPI.badRequest:
systemErrorExit(BAD_REQUEST_RC, Msg.BAD_REQUEST)
except GAPI.invalid as e:
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
except GAPI.authError:
accessErrorExit(None)
for activity in feed:
events = activity.pop('events')
for event in events:
fileInfo = {'event': event['name']}
for item in event.get('parameters', []):
if item['name'] == 'primary_event':
if not item['boolValue']:
break
elif item['name'] == 'doc_id':
if item['value'] in foundIds:
break
fileInfo['id'] = item['value']
elif event['name'] == 'change_owner' and item['name'] == 'new_owner':
fileInfo['Owner'] = item['value']
elif event['name'] != 'change_owner' and item['name'] == 'owner':
fileInfo['Owner'] = item['value']
elif item['name'] == 'doc_title':
fileInfo[fileNameTitle] = item['value']
elif item['name'] == 'doc_type':
fileInfo['type'] = item['value']
elif item['name'] == 'owner_is_shared_drive':
fileInfo['ownerIsSharedDrive'] = item['boolValue']
elif item['name'] == 'shared_drive_id':
fileInfo['driveId'] = item['value']
else:
if 'Owner' in fileInfo and 'id' in fileInfo:
foundIds[fileInfo['id']] = True
if not csvPF:
if not FJQC.formatJSON:
printEntityKVList([Ent.OWNER, fileInfo['Owner']],
['id', fileInfo['id'], fileNameTitle, fileInfo.get(fileNameTitle, ''),
'type', fileInfo.get('type', ''),
'ownerIsSharedDrive', fileInfo.get('ownerIsSharedDrive', False),
'driveId', fileInfo.get('driveId', ''),
'event', fileInfo['event']])
else:
printLine(json.dumps(cleanJSON(fileInfo), ensure_ascii=False, sort_keys=True))
else:
if addCSVData:
fileInfo.update(addCSVData)
row = flattenJSON(fileInfo)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(fileInfo), ensure_ascii=False, sort_keys=True)})
if entityType == Ent.DRIVE_FILE_OR_FOLDER_ID:
showComplete = True
break
if showComplete:
break
if showComplete:
break
if not foundIds:
entityActionFailedWarning([entityType, fileId], Msg.NOT_FOUND)
if csvPF:
csvPF.writeCSVfile('Drive File Ownership')
def _getSharedDriveTheme(myarg, body):
if myarg in {'theme', 'themeid'}:
body.pop('backgroundImageFile', None)
body.pop('colorRgb', None)
body['themeId'] = getString(Cmd.OB_STRING, checkBlank=True)
elif myarg == 'customtheme':
body.pop('themeId', None)
body['backgroundImageFile'] = {
'id': getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True),
'xCoordinate': getFloat(minVal=0.0, maxVal=1.0),
'yCoordinate': getFloat(minVal=0.0, maxVal=1.0),
'width': getFloat(minVal=0.0, maxVal=1.0)
}
elif myarg in {'color', 'colour'}:
body.pop('themeId', None)
body['colorRgb'] = getColor()
else:
return False
return True
SHAREDDRIVE_RESTRICTIONS_MAP = {
'adminmanagedrestrictions': 'adminManagedRestrictions',
'allowcontentmanagerstosharefolders': 'sharingFoldersRequiresOrganizerPermission',
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
'domainusersonly': 'domainUsersOnly',
'drivemembersonly': 'driveMembersOnly',
'sharingfoldersrequiresorganizerpermission': 'sharingFoldersRequiresOrganizerPermission',
'teammembersonly': 'driveMembersOnly',
}
def _getSharedDriveRestrictions(myarg, body):
def _setRestriction(restriction):
body.setdefault('restrictions', {})
if restriction != 'allowcontentmanagerstosharefolders':
body['restrictions'][SHAREDDRIVE_RESTRICTIONS_MAP[restriction]] = getBoolean()
else:
body['restrictions'][SHAREDDRIVE_RESTRICTIONS_MAP[restriction]] = not getBoolean()
if myarg.startswith('restrictions.'):
_, subField = myarg.split('.', 1)
if subField in SHAREDDRIVE_RESTRICTIONS_MAP:
_setRestriction(subField)
return True
invalidChoiceExit(subField, SHAREDDRIVE_RESTRICTIONS_MAP, True)
if myarg in SHAREDDRIVE_RESTRICTIONS_MAP:
_setRestriction(myarg)
return True
return False
def _moveSharedDriveToOU(orgUnit, orgUnitId, driveId, user, i, count, ci, returnIdOnly):
action = Act.Get()
name = f'orgUnits/-/memberships/shared_drive;{driveId}'
if ci is None:
ci = buildGAPIObject(API.CLOUDIDENTITY_ORGUNITS_BETA)
cibody = {'customer': _getCustomersCustomerIdWithC(),
'destinationOrgUnit': f'orgUnits/{orgUnitId[3:]}'}
try:
callGAPI(ci.orgUnits().memberships(), 'move',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN,
GAPI.INVALID_ARGUMENT, GAPI.ABORTED],
name=name, body=cibody)
if not returnIdOnly:
Act.Set(Act.MOVE)
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE, driveId], Act.MODIFIER_TO,
f'{Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnit}', i, count)
except (GAPI.notFound, GAPI.forbidden, GAPI.aborted, GAPI.badRequest, GAPI.internalError,
GAPI.noManageTeamDriveAdministratorPrivilege, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Act.Set(action)
return ci
# gam <UserTypeEntity> create shareddrive <Name> [asadmin]
# [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
# (<SharedDriveRestrictionsFieldName> <Boolean>)*
# [hide|hidden <Boolean>] [ou|org|orgunit <OrgUnitItem>]
# [errorretries <Integer>] [updateinitialdelay <Integer>] [updateretrydelay <Integer>]
# [movetoorgunitdelay <Integer>]
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) | returnidonly]
def createSharedDrive(users, useDomainAdminAccess=False):
def waitingForCreationToComplete(sleep_time):
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.SHAREDDRIVE), sleep_time))
time.sleep(sleep_time)
requestId = str(uuid.uuid4())
body = {'name': getString(Cmd.OB_NAME, checkBlank=True)}
updateBody = {}
csvPF = None
addCSVData = {}
hide = returnIdOnly = False
orgUnit = orgUnitId = ci = None
errorRetries = 5
updateInitialDelay = 10
updateRetryDelay = 10
moveToOrgUnitDelay = 20
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getSharedDriveTheme(myarg, body):
pass
elif _getSharedDriveRestrictions(myarg, updateBody):
pass
elif myarg in {'hide', 'hidden'}:
hide = getBoolean()
elif myarg in {'ou', 'org', 'orgunit'}:
orgUnit, orgUnitId = getOrgUnitId()
elif myarg == 'returnidonly':
returnIdOnly = True
elif myarg == 'csv':
csvPF = CSVPrintFile()
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif myarg == 'errorretries':
errorRetries = getInteger(minVal=0, maxVal=10)
elif myarg == 'updateinitialdelay':
updateInitialDelay = getInteger(minVal=0, maxVal=60)
elif myarg == 'updateretrydelay':
updateRetryDelay = getInteger(minVal=0, maxVal=60)
elif myarg == 'movetoorgunitdelay':
moveToOrgUnitDelay = getInteger(minVal=0, maxVal=60)
else:
unknownArgumentExit()
if csvPF:
csvPF.SetTitles(['User', 'name', 'id'])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
for field in ['backgroundImageFile', 'colorRgb']:
if field in body:
updateBody[field] = body.pop(field)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
doUpdate = False
Act.Set(Act.CREATE)
retry = 0
while True:
try:
shareddrive = callGAPI(drive.drives(), 'create',
bailOnTransientError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.TRANSIENT_ERROR, GAPI.TEAMDRIVE_ALREADY_EXISTS,
GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_FILE_PERMISSIONS,
GAPI.DUPLICATE, GAPI.BAD_REQUEST, GAPI.USER_CANNOT_CREATE_TEAMDRIVES],
requestId=requestId, body=body, fields='id')
driveId = shareddrive['id']
if returnIdOnly:
writeStdout(f'{driveId}\n')
elif not csvPF:
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_NAME, body['name'], Ent.SHAREDDRIVE_ID, driveId], i, count)
else:
row = {'User': user, 'name': body['name'], 'id': driveId}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
doUpdate = True
break
except (GAPI.transientError, GAPI.teamDriveAlreadyExists) as e:
retry += 1
if retry > errorRetries:
entityActionFailedWarning([Ent.USER, user, Ent.REQUEST_ID, requestId], str(e), i, count)
break
requestId = str(uuid.uuid4())
except GAPI.duplicate:
entityActionFailedWarning([Ent.USER, user, Ent.REQUEST_ID, requestId], Msg.DUPLICATE, i, count)
break
except (GAPI.insufficientPermissions, GAPI.insufficientFilePermissions, GAPI.badRequest, GAPI.userCannotCreateTeamDrives) as e:
entityActionFailedWarning([Ent.USER, user, Ent.REQUEST_ID, requestId], str(e), i, count)
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
if not (doUpdate or updateBody or hide or orgUnit):
continue
waitingForCreationToComplete(updateInitialDelay)
created = False
retry = 0
while not created:
try:
callGAPI(drive.drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
useDomainAdminAccess=useDomainAdminAccess,
driveId=driveId, fields='id')
created = True
break
except GAPI.notFound as e:
retry += 1
if retry > errorRetries:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
break
waitingForCreationToComplete(updateRetryDelay)
if not created:
continue
try:
if updateBody:
Act.Set(Act.UPDATE)
try:
callGAPI(drive.drives(), 'update',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN,
GAPI.NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE,
GAPI.BAD_REQUEST, GAPI.INTERNAL_ERROR, GAPI.PERMISSION_DENIED,
GAPI.FILE_NOT_FOUND],
useDomainAdminAccess=useDomainAdminAccess, driveId=driveId, body=updateBody)
if not returnIdOnly and not csvPF:
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
except GAPI.fileNotFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId,
Ent.DRIVE_FILE, body.get('backgroundImageFile', {}).get('id', UNKNOWN)],
str(e), i, count)
if hide:
Act.Set(Act.HIDE)
callGAPI(drive.drives(), 'hide',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
driveId=driveId)
if not returnIdOnly and not csvPF:
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
if orgUnit:
waitingForCreationToComplete(moveToOrgUnitDelay)
ci = _moveSharedDriveToOU(orgUnit, orgUnitId, driveId, user, i, count, ci, returnIdOnly or csvPF)
except (GAPI.notFound, GAPI.forbidden, GAPI.badRequest, GAPI.noManageTeamDriveAdministratorPrivilege) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('SharedDrives')
# gam create shareddrive <Name>
# [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
# (<SharedDriveRestrictionsFieldName> <Boolean>)*
# [hide|hidden <Boolean>]
# [errorretries <Integer>] [updateinitialdelay <Integer>] [updateretrydelay <Integer>]
# [movetoorgunitdelay <Integer>]
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) | returnidonly]
def doCreateSharedDrive():
createSharedDrive([_getAdminEmail()], True)
# gam <UserTypeEntity> update shareddrive <SharedDriveEntity> [asadmin] [name <Name>]
# [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
# (<SharedDriveRestrictionsFieldName> <Boolean>)*
# [hide|hidden <Boolean>] [ou|org|orgunit <OrgUnitItem>]
def updateSharedDrive(users, useDomainAdminAccess=False):
fileIdEntity = getSharedDriveEntity()
body = {}
hide = None
orgUnit = orgUnitId = ci = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['name'] = getString(Cmd.OB_NAME, checkBlank=True)
elif myarg in {'ou', 'org', 'orgunit'}:
orgUnit, orgUnitId = getOrgUnitId()
elif _getSharedDriveTheme(myarg, body):
pass
elif _getSharedDriveRestrictions(myarg, body):
pass
elif myarg in {'hide', 'hidden'}:
hide = getBoolean()
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
if not drive:
continue
try:
driveId = fileIdEntity['shareddrive']['driveId']
if body:
result = callGAPI(drive.drives(), 'update',
bailOnInternalError=True,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
GAPI.NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE,
GAPI.INTERNAL_ERROR, GAPI.FILE_NOT_FOUND],
useDomainAdminAccess=useDomainAdminAccess, driveId=driveId, body=body, fields='name')
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_NAME, result['name'], Ent.SHAREDDRIVE_ID, driveId], i, count)
if hide is not None:
if hide:
Act.Set(Act.HIDE)
function = 'hide'
else:
Act.Set(Act.UNHIDE)
function = 'unhide'
callGAPI(drive.drives(), function,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
driveId=driveId)
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
if orgUnit:
ci = _moveSharedDriveToOU(orgUnit, orgUnitId, driveId, user, i, count, ci, False)
except (GAPI.notFound, GAPI.forbidden, GAPI.badRequest, GAPI.internalError,
GAPI.noManageTeamDriveAdministratorPrivilege) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
except GAPI.fileNotFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId,
Ent.DRIVE_FILE, body.get('backgroundImageFile', {}).get('id', UNKNOWN)],
str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Act.Set(Act.UPDATE)
def doUpdateSharedDrive():
updateSharedDrive([_getAdminEmail()], True)
# gam <UserTypeEntity> delete shareddrive <SharedDriveEntity>
# [asadmin [allowitemdeletion]
def deleteSharedDrive(users):
fileIdEntity = getSharedDriveEntity()
allowItemDeletion = useDomainAdminAccess = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'nukefromorbit', 'allowitemdeletion'}:
allowItemDeletion = useDomainAdminAccess = True
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
try:
driveId = fileIdEntity['shareddrive']['driveId']
callGAPI(drive.drives(), 'delete',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN,
GAPI.CANNOT_DELETE_RESOURCE_WITH_CHILDREN, GAPI.INSUFFICIENT_FILE_PERMISSIONS,
GAPI.NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE],
driveId=driveId, allowItemDeletion=allowItemDeletion, useDomainAdminAccess=useDomainAdminAccess)
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
except (GAPI.notFound, GAPI.forbidden,
GAPI.cannotDeleteResourceWithChildren, GAPI.insufficientFilePermissions,
GAPI.noManageTeamDriveAdministratorPrivilege) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
# gam delete shareddrive <SharedDriveEntity> [allowitemdeletion]
def doDeleteSharedDrive():
deleteSharedDrive([_getAdminEmail()])
# gam <UserTypeEntity> hide/unhide shareddrive <SharedDriveEntity>
def hideUnhideSharedDrive(users):
fileIdEntity = getSharedDriveEntity()
checkForExtraneousArguments()
function = 'hide' if Act.Get() == Act.HIDE else 'unhide'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
if not drive:
continue
try:
driveId = fileIdEntity['shareddrive']['driveId']
callGAPI(drive.drives(), function,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
driveId=driveId)
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
except (GAPI.notFound, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
# gam hide/unhide shareddrive <SharedDriveEntity>
def doHideUnhideSharedDrive():
hideUnhideSharedDrive([_getAdminEmail()])
SHAREDDRIVE_FIELDS_CHOICE_MAP = {
'backgroundimagefile': 'backgroundImageFile',
'backgroundimagelink': 'backgroundImageLink',
'capabilities': 'capabilities',
'colorrgb': 'colorRgb',
'createddate': 'createdTime',
'createdtime': 'createdTime',
'id': 'id',
'hidden': 'hidden',
'name': 'name',
'restrictions': 'restrictions',
'themeid': 'themeId',
}
SHAREDDRIVE_LIST_FIELDS_CHOICE_MAP = {
'org': 'orgUnitId',
'orgunit': 'orgUnitId',
'orgunitid': 'orgUnitId',
'ou': 'orgUnitId',
}
SHAREDDRIVE_TIME_OBJECTS = {'createdTime'}
SHAREDDRIVE_ROLES_CAPABILITIES_MAP = {
'commenter': {'canComment': True, 'canEdit': False},
'reader': {'canComment': False, 'canEdit': False},
'writer': {'canEdit': True, 'canTrashChildren': False},
'fileOrganizer': {'canTrashChildren': True, 'canManageMembers': False},
'organizer': {'canManageMembers': True},
}
SHAREDDRIVE_API_GUI_ROLES_MAP = {
'commenter': 'Commenter',
'fileOrganizer': 'Content manager',
'organizer': 'Manager',
'reader': 'Viewer',
'writer': 'Contributor',
'unknown': 'Unknown'
}
def _getSharedDriveRole(shareddrive):
if 'capabilities' not in shareddrive:
return None
for role, capabilities in iter(SHAREDDRIVE_ROLES_CAPABILITIES_MAP.items()):
match = True
for capability in capabilities:
if capabilities[capability] != shareddrive['capabilities'].get(capability, ''):
match = False
break
if match:
break
else:
role = 'unknown'
return role
def _showSharedDrive(user, shareddrive, j, jcount, FJQC):
def _showCapabilitiesRestrictions(field):
if field in shareddrive:
printKeyValueList([field, ''])
Ind.Increment()
for capability in sorted(shareddrive[field]):
printKeyValueList([capability, shareddrive[field][capability]])
Ind.Decrement()
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(shareddrive, timeObjects=SHAREDDRIVE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.USER, user, Ent.SHAREDDRIVE, f'{shareddrive["name"]} ({shareddrive["id"]})'], j, jcount)
Ind.Increment()
printEntity([Ent.SHAREDDRIVE_ID, shareddrive['id']])
printEntity([Ent.SHAREDDRIVE_NAME, shareddrive['name']])
if 'hidden' in shareddrive:
printKeyValueList(['hidden', shareddrive['hidden']])
if 'createdTime' in shareddrive:
printKeyValueList(['createdTime', formatLocalTime(shareddrive['createdTime'])])
for setting in ['backgroundImageLink', 'colorRgb', 'themeId', 'orgUnit', 'orgUnitId']:
if setting in shareddrive:
printKeyValueList([setting, shareddrive[setting]])
if 'role' in shareddrive:
printKeyValueList(['role', shareddrive['role']])
_showCapabilitiesRestrictions('capabilities')
_showCapabilitiesRestrictions('restrictions')
Ind.Decrement()
# gam <UserTypeEntity> info shareddrive <SharedDriveEntity>
# [asadmin]
# [fields <SharedDriveFieldNameList>]
# [guiroles [<Boolean>]] [formatjson]
def infoSharedDrive(users, useDomainAdminAccess=False):
fileIdEntity = getSharedDriveEntity()
fieldsList = []
FJQC = FormatJSONQuoteChar()
guiRoles = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif getFieldsList(myarg, SHAREDDRIVE_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
pass
elif myarg == 'guiroles':
guiRoles = getBoolean()
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList) if fieldsList else '*'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
if not drive:
continue
try:
driveId = fileIdEntity['shareddrive']['driveId']
shareddrive = callGAPI(drive.drives(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
useDomainAdminAccess=useDomainAdminAccess,
driveId=driveId, fields=fields)
role = _getSharedDriveRole(shareddrive)
if role:
shareddrive['role'] = role if not guiRoles else SHAREDDRIVE_API_GUI_ROLES_MAP[role]
_showSharedDrive(user, shareddrive, i, count, FJQC)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
def doInfoSharedDrive():
infoSharedDrive([_getAdminEmail()], True)
SHAREDDRIVE_ACL_ROLES_MAP = {
'commenter': 'commenter',
'contentmanager': 'fileOrganizer',
'contributor': 'writer',
'editor': 'writer',
'fileorganizer': 'fileOrganizer',
'fileorganiser': 'fileOrganizer',
'manager': 'organizer',
'organizer': 'organizer',
'organiser': 'organizer',
'owner': 'organizer',
'read': 'reader',
'reader': 'reader',
'viewer': 'reader',
'writer': 'writer',
}
# gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*]
# [asadmin [shareddriveadminquery|query <QuerySharedDrive>]]
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
# (role|roles <SharedDriveACLRoleList>)*
# [fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
# [guiroles [<Boolean>]] [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show shareddrives
# [asadmin [shareddriveadminquery|query <QuerySharedDrive>]]
# [matchname <REMatchPattrn>] [orgunit|org|ou <OrgUnitPath>]
# (role|roles <SharedDriveACLRoleLIst>)*
# [fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
# [guiroles [<Boolean>]] [formatjson]
def printShowSharedDrives(users, useDomainAdminAccess=False):
def stripNonShowFields(shareddrive):
if orgUnitIdToPathMap:
td_ouid = shareddrive.get('orgUnitId')
if td_ouid:
shareddrive['orgUnit'] = orgUnitIdToPathMap.get(f'id:{td_ouid}', UNKNOWN)
if not showFields:
return shareddrive
sshareddrive = {}
for field in showFields:
if field in shareddrive:
sshareddrive[field] = shareddrive[field]
return sshareddrive
csvPF = CSVPrintFile(['User', 'id', 'name'], ['User', 'id', 'name', 'role']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
roles = set()
cd = orgUnitId = query = matchPattern = None
showFields = set()
fieldsList = []
SHAREDDRIVE_FIELDS_CHOICE_MAP.update(SHAREDDRIVE_LIST_FIELDS_CHOICE_MAP)
showOrgUnitPaths = True
orgUnitIdToPathMap = None
guiRoles = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'teamdriveadminquery', 'shareddriveadminquery', 'query'}:
queryLocation = Cmd.Location()
query = getString(Cmd.OB_QUERY, minLen=0) or None
if query:
query = mapQueryRelativeTimes(query, ['createdTime'])
elif myarg == 'matchname':
matchPattern = getREPattern(re.IGNORECASE)
elif myarg in {'ou', 'org', 'orgunit'}:
orgLocation = Cmd.Location()
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
_, orgUnitId = getOrgUnitId(cd)
orgUnitId = orgUnitId[3:]
elif myarg in {'role', 'roles'}:
roles |= getACLRoles(SHAREDDRIVE_ACL_ROLES_MAP)
elif myarg == 'checkgroups':
pass
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif getFieldsList(myarg, SHAREDDRIVE_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
pass
elif myarg == 'noorgunits':
showOrgUnitPaths = not getBoolean()
elif myarg == 'guiroles':
guiRoles = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if query and not useDomainAdminAccess:
Cmd.SetLocation(queryLocation-1)
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_PERFORM_SHARED_DRIVE_QUERIES)
if orgUnitId is not None and not useDomainAdminAccess:
Cmd.SetLocation(orgLocation-1)
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_SPECIFY_SHARED_DRIVE_ORGUNIT)
if fieldsList:
showFields = set(fieldsList)
if csvPF and not useDomainAdminAccess:
if fieldsList:
showFields.add('role')
csvPF.AddTitle('role')
if FJQC.formatJSON:
csvPF.AddJSONTitles(['role'])
csvPF.MoveJSONTitlesToEnd(['JSON'])
if showOrgUnitPaths and useDomainAdminAccess and ((not showFields) or ('orgUnitId' in showFields)):
orgUnitIdToPathMap = getOrgUnitIdToPathMap(cd)
if showFields:
showFields.add('orgUnit')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if useDomainAdminAccess:
printGettingAllAccountEntities(Ent.SHAREDDRIVE, query)
pageMessage = getPageMessage()
else:
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, user, i, count, query)
pageMessage = getPageMessageForWhom()
try:
feed = callGAPIpages(drive.drives(), 'list', 'drives',
pageMessage=pageMessage,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.QUERY_REQUIRES_ADMIN_CREDENTIALS,
GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE,
GAPI.FILE_NOT_FOUND],
q=query, useDomainAdminAccess=useDomainAdminAccess,
fields='*', pageSize=100)
except (GAPI.invalidQuery, GAPI.invalid, GAPI.queryRequiresAdminCredentials,
GAPI.noListTeamDrivesAdministratorPrivilege, GAPI.fileNotFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
matchedFeed = []
if not useDomainAdminAccess:
for shareddrive in feed:
if matchPattern is not None and matchPattern.match(shareddrive['name']) is None:
continue
role = _getSharedDriveRole(shareddrive)
if not roles or role in roles:
shareddrive['role'] = role if not guiRoles else SHAREDDRIVE_API_GUI_ROLES_MAP[role]
matchedFeed.append(shareddrive)
elif matchPattern is not None or orgUnitId is not None:
for shareddrive in feed:
if ((matchPattern is not None and matchPattern.match(shareddrive['name']) is None) or
(orgUnitId is not None and orgUnitId != shareddrive.get('orgUnitId'))):
continue
matchedFeed.append(shareddrive)
else:
matchedFeed = feed
jcount = len(matchedFeed)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE, i, count)
Ind.Increment()
j = 0
for shareddrive in matchedFeed:
j += 1
shareddrive = stripNonShowFields(shareddrive)
_showSharedDrive(user, shareddrive, j, jcount, FJQC)
Ind.Decrement()
else:
for shareddrive in matchedFeed:
shareddrive = stripNonShowFields(shareddrive)
if FJQC.formatJSON:
row = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name']}
if not useDomainAdminAccess:
row['role'] = shareddrive['role'] if not guiRoles else SHAREDDRIVE_API_GUI_ROLES_MAP[shareddrive['role']]
row['JSON'] = json.dumps(cleanJSON(shareddrive, timeObjects=SHAREDDRIVE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)
csvPF.WriteRow(row)
else:
csvPF.WriteRowTitles(flattenJSON(shareddrive, flattened={'User': user}, timeObjects=SHAREDDRIVE_TIME_OBJECTS))
if csvPF:
csvPF.writeCSVfile('SharedDrives')
def doPrintShowSharedDrives():
printShowSharedDrives([_getAdminEmail()], True)
# gam print oushareddrives [todrive <ToDriveAttribute>*]
# [ou|org|orgunit <OrgUnitPath>]
# [formatjson [quotechar <Character>]]
# gam show oushareddrives
# [ou|org|orgunit <OrgUnitPath>]
# [formatjson]
def doPrintShowOrgunitSharedDrives():
def _getOrgUnitSharedDriveInfo(shareddrive):
shareddrive['driveId'] = shareddrive['name'].rsplit(';')[1]
shareddrive['driveName'] = _getSharedDriveNameFromId(shareddrive['driveId'])
shareddrive['orgUnitPath'] = orgUnitPath
def _showOrgUnitSharedDrive(shareddrive, j, jcount, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(shareddrive), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.NAME, f'{shareddrive["name"]}'], j, jcount)
Ind.Increment()
printEntity([Ent.TYPE, shareddrive['type']])
printEntity([Ent.MEMBER, shareddrive['member']])
printEntity([Ent.MEMBER_URI, shareddrive['memberUri']])
printEntity([Ent.SHAREDDRIVE_ID, shareddrive['driveId']])
printEntity([Ent.SHAREDDRIVE_NAME, shareddrive['driveName']])
printEntity([Ent.ORGANIZATIONAL_UNIT, shareddrive['orgUnit']])
Ind.Decrement()
ci = buildGAPIObject(API.CLOUDIDENTITY_ORGUNITS_BETA)
cd = buildGAPIObject(API.DIRECTORY)
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
if not GM.Globals[GM.ADMIN_DRIVE]:
return
csvPF = CSVPrintFile(['name', 'type', 'member', 'memberUri', 'driveId', 'driveName', 'orgUnitPath']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
orgUnitPath = '/'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'ou', 'org', 'orgunit'}:
orgUnitPath = getString(Cmd.OB_ORGUNIT_ITEM)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(['name', 'JSON'])
orgUnitPath, orgUnitId = getOrgUnitId(cd, orgUnitPath)
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
sds = callGAPIpages(ci.orgUnits().memberships(), 'list', 'orgMemberships',
pageMessage=getPageMessageForWhom(),
parent=f'orgUnits/{orgUnitId[3:]}',
customer=_getCustomersCustomerIdWithC(),
filter="type == 'shared_drive'")
jcount = len(sds)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.SHAREDDRIVE)
Ind.Increment()
j = 0
for shareddrive in sds:
j += 1
_getOrgUnitSharedDriveInfo(shareddrive)
_showOrgUnitSharedDrive(shareddrive, j, jcount, FJQC)
Ind.Decrement()
else:
for shareddrive in sds:
_getOrgUnitSharedDriveInfo(shareddrive)
if FJQC.formatJSON:
row = {'name': shareddrive['name']}
row['JSON'] = json.dumps(cleanJSON(shareddrive), ensure_ascii=False, sort_keys=True)
csvPF.WriteRow(row)
else:
csvPF.WriteRowTitles(flattenJSON(shareddrive))
if csvPF:
csvPF.writeCSVfile('OrgUnit {orgUnitPath} SharedDrives')
# gam [<UserTypeEntity>] copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
# gam [<UserTypeEntity>] sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
# [asadmin]
# [showpermissionsmessages [<Boolean>]]
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
# (mappermissionsdomain <DomainName> <DomainName>)*
def copySyncSharedDriveACLs(users, useDomainAdminAccess=False):
copyMoveOptions = initCopyMoveOptions(True)
srcFileIdEntity = getSharedDriveEntity()
checkArgumentPresent(['to', 'with'], True)
tgtFileIdEntity = getSharedDriveEntity()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getCopyMoveOptions(myarg, copyMoveOptions):
pass
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
else:
unknownArgumentExit()
copyMoveOptions['useDomainAdminAccess'] = useDomainAdminAccess
copyMoveOptions['copyTopFolderNonInheritedPermissions'] =\
COPY_NONINHERITED_PERMISSIONS_ALWAYS if Act.Get() == Act.COPY else COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS
copyMoveOptions['copyMergeWithParentFolderPermissions'] = True
copyMoveOptions['copyTopFolderInheritedPermissions'] = False
copyMoveOptions['destParentType'] = DEST_PARENT_SHAREDDRIVE_ROOT
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = _validateUserSharedDrive(user, i, count, srcFileIdEntity)
if not drive:
continue
if not srcFileIdEntity.get('shareddrivename'):
srcFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(srcFileIdEntity['shareddrive']['driveId'])
if tgtFileIdEntity.get('shareddrivename'):
if not _convertSharedDriveNameToId(drive, user, i, count, tgtFileIdEntity, useDomainAdminAccess):
continue
tgtFileIdEntity['shareddrive']['corpora'] = 'drive'
else:
tgtFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(tgtFileIdEntity['shareddrive']['driveId'])
statistics = _initStatistics()
copyMoveOptions['sourceDriveId'] = srcFileIdEntity['shareddrive']['driveId']
copyMoveOptions['destDriveId'] = tgtFileIdEntity['shareddrive']['driveId']
entityPerformActionModifierItemValueList([Ent.USER, user, Ent.SHAREDDRIVE, srcFileIdEntity['shareddrivename']],
f"{Ent.Plural(Ent.PERMISSION)} {Act.MODIFIER_TO}",
[Ent.SHAREDDRIVE, tgtFileIdEntity['shareddrivename']], i, count)
_copyPermissions(drive, user, i, count, 0, 0,
Ent.SHAREDDRIVE, srcFileIdEntity['shareddrive']['driveId'], srcFileIdEntity['shareddrivename'],
tgtFileIdEntity['shareddrive']['driveId'], tgtFileIdEntity['shareddrivename'],
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, True,
'copyTopFolderInheritedPermissions',
'copyTopFolderNonInheritedPermissions',
False)
def doCopySyncSharedDriveACLs():
copySyncSharedDriveACLs([_getAdminEmail()], True)
SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP = {
'true': 1,
'false': 0,
'only': -1,
}
# gam [<UserTypeEntity>] print shareddriveacls [todrive <ToDriveAttribute>*]
# [asadmin] [shareddriveadminquery|query <QuerySharedDrive>]
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
# [user|group <EmailAddress> [checkgroups]] (role|roles <SharedDriveACLRoleList>)*
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
# [oneitemperrow] [maxitems <Integer>]
# [shownopermissionsdrives false|true|only]
# [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam [<UserTypeEntity>] show shareddriveacls
# [asadmin] [shareddriveadminquery|query <QuerySharedDrive>]
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
# [user|group <EmailAddress> [checkgroups]] (role|roles <SharedDriveACLRoleList>)*
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
# [oneitemperrow] [maxitems <Integer>]
# [shownopermissionsdrives false|true|only]
# [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
# [formatjson]
def printShowSharedDriveACLs(users, useDomainAdminAccess=False):
def _printPermissionRow(baserow, permission):
row = baserow.copy()
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
csvPF = CSVPrintFile(['User', 'id', 'name', 'createdTime'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
roles = set()
checkGroups = oneItemPerRow = pmselect = False
showNoPermissionsDrives = SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP['false']
fieldsList = []
cd = emailAddress = orgUnitId = query = matchPattern = permtype = None
PM = PermissionMatch()
maxItems = 0
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'teamdriveadminquery', 'shareddriveadminquery', 'query'}:
queryLocation = Cmd.Location()
query = getString(Cmd.OB_QUERY, minLen=0) or None
if query:
query = mapQueryRelativeTimes(query, ['createdTime'])
elif myarg == 'matchname':
matchPattern = getREPattern(re.IGNORECASE)
elif myarg in {'ou', 'org', 'orgunit'}:
orgLocation = Cmd.Location()
if cd is None:
cd = buildGAPIObject(API.DIRECTORY)
_, orgUnitId = getOrgUnitId(cd)
orgUnitId = orgUnitId[3:]
elif myarg in {'user', 'group'}:
permtype = myarg
emailAddress = getEmailAddress(noUid=True)
elif myarg in {'role', 'roles'}:
roles |= getACLRoles(SHAREDDRIVE_ACL_ROLES_MAP)
elif myarg == 'checkgroups':
checkGroups = True
elif myarg == 'oneitemperrow':
oneItemPerRow = True
elif myarg == 'maxitems':
maxItems = getInteger(minVal=0)
elif getDriveFilePermissionsFields(myarg, fieldsList):
pass
elif myarg in ADMIN_ACCESS_OPTIONS:
useDomainAdminAccess = True
elif PM.ProcessArgument(myarg):
pass
elif myarg == 'pmselect':
pmselect = True
elif myarg == 'shownopermissionsdrives':
showNoPermissionsDrives = getChoice(SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP, mapChoice=True)
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if query and not useDomainAdminAccess:
Cmd.SetLocation(queryLocation-1)
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_PERFORM_SHARED_DRIVE_QUERIES)
if orgUnitId is not None and not useDomainAdminAccess:
Cmd.SetLocation(orgLocation-1)
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_SPECIFY_SHARED_DRIVE_ORGUNIT)
if fieldsList:
if permtype is not None:
fieldsList.extend(['type', 'emailAddress'])
if roles:
fieldsList.append('role')
fields = getItemFieldsFromFieldsList('permissions', fieldsList, True)
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
if checkGroups:
if emailAddress:
cd = buildGAPIObject(API.DIRECTORY)
try:
groups = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=emailAddress, orderBy='email', fields='nextPageToken,groups(email)')
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, emailAddress)
return
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
groupsSet = {group['email'] for group in groups}
else:
checkGroups = False
if csvPF and addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if FJQC.formatJSON:
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
csvPF.MoveJSONTitlesToEnd(['JSON'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
feed = None
if permtype == 'user':
_, userdrive = buildGAPIServiceObject(API.DRIVE3, emailAddress, displayError=False)
if userdrive is not None:
try:
feed = callGAPIpages(userdrive.drives(), 'list', 'drives',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fields='nextPageToken,drives(id,name,createdTime,orgUnitId)', pageSize=100)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
pass
if feed is None:
if useDomainAdminAccess:
printGettingAllAccountEntities(Ent.SHAREDDRIVE, query)
pageMessage = getPageMessage()
else:
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, user, i, count, query)
pageMessage = getPageMessageForWhom()
try:
feed = callGAPIpages(drive.drives(), 'list', 'drives',
pageMessage=pageMessage,
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
GAPI.QUERY_REQUIRES_ADMIN_CREDENTIALS,
GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE],
q=query, useDomainAdminAccess=useDomainAdminAccess,
fields='nextPageToken,drives(id,name,createdTime,orgUnitId)', pageSize=100)
except (GAPI.invalidQuery, GAPI.invalid, GAPI.queryRequiresAdminCredentials, GAPI.noListTeamDrivesAdministratorPrivilege) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], str(e), i, count)
continue
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
continue
matchFeed = []
jcount = len(feed)
j = 0
for shareddrive in feed:
j += 1
if ((matchPattern is not None and matchPattern.match(shareddrive['name']) is None) or
(orgUnitId is not None and orgUnitId != shareddrive.get('orgUnitId'))):
continue
printGettingAllEntityItemsForWhom(Ent.PERMISSION, shareddrive['name'], j, jcount)
shareddrive['createdTime'] = formatLocalTime(shareddrive['createdTime'])
shareddrive['permissions'] = []
try:
permissions = callGAPIpages(drive.permissions(), 'list', 'permissions',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
useDomainAdminAccess=useDomainAdminAccess,
fileId=shareddrive['id'], fields=fields, supportsAllDrives=True)
if not permissions:
if showNoPermissionsDrives == 0: # no permissions and showNoPermissionDrives False - ignore
continue
matchFeed.append(shareddrive) # no permissions and showNoPermissionDrives Only/True - keep
continue
if showNoPermissionsDrives < 0: # permissions and showNoPermissionDrives Only/True - ignore
continue
if pmselect:
if not PM.CheckPermissionMatches(permissions):
continue
else:
permissions = PM.GetMatchingPermissions(permissions)
for permission in permissions:
if roles and permission['role'] not in roles:
continue
permission.pop('teamDrivePermissionDetails', None)
if permtype is None:
shareddrive['permissions'].append(permission)
elif permission['type'] == permtype and permission['emailAddress'] == emailAddress:
shareddrive['permissions'].append(permission)
elif checkGroups and permission['emailAddress'] in groupsSet:
shareddrive['permissions'].append(permission)
if shareddrive['permissions']:
numItems = len(shareddrive['permissions'])
if numItems > maxItems > 0:
shareddrive['permissions'] = shareddrive['permissions'][0:maxItems]
matchFeed.append(shareddrive)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
GAPI.unknownError, GAPI.invalid):
pass
jcount = len(matchFeed)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE, i, count)
Ind.Increment()
j = 0
for shareddrive in matchFeed:
j += 1
if not FJQC.formatJSON:
_showDriveFilePermissions(Ent.SHAREDDRIVE, f'{shareddrive["name"]} ({shareddrive["id"]}) - {shareddrive["createdTime"]}',
shareddrive['permissions'], printKeys, timeObjects, j, jcount)
else:
if oneItemPerRow and shareddrive['permissions']:
for permission in shareddrive['permissions']:
_showDriveFilePermissionJSON(user, shareddrive['id'], shareddrive['name'], shareddrive['createdTime'], permission, timeObjects)
else:
_showDriveFilePermissionsJSON(user, shareddrive['id'], shareddrive['name'], shareddrive['createdTime'], shareddrive['permissions'], timeObjects)
Ind.Decrement()
elif matchFeed:
if oneItemPerRow:
for shareddrive in matchFeed:
baserow = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name'], 'createdTime': shareddrive['createdTime']}
if addCSVData:
baserow.update(addCSVData)
if shareddrive['permissions']:
for permission in shareddrive['permissions']:
_mapDrivePermissionNames(permission)
pdetails = permission.pop('permissionDetails', [])
if not pdetails:
_printPermissionRow(baserow, permission)
else:
for pdetail in pdetails:
permission['permissionDetails'] = pdetail
_printPermissionRow(baserow, permission)
else:
if not FJQC.formatJSON:
csvPF.WriteRowTitles(baserow)
elif csvPF.CheckRowTitles(baserow):
baserow['JSON'] = json.dumps({})
csvPF.WriteRowNoFilter(baserow)
else:
for shareddrive in matchFeed:
baserow = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name'], 'createdTime': shareddrive['createdTime']}
if addCSVData:
baserow.update(addCSVData)
row = baserow.copy()
if shareddrive['permissions']:
for permission in shareddrive['permissions']:
_mapDrivePermissionNames(permission)
flattenJSON({'permissions': shareddrive['permissions']}, flattened=row, timeObjects=timeObjects)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
baserow['JSON'] = json.dumps(cleanJSON({'permissions': shareddrive['permissions']}, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(baserow)
else:
if not FJQC.formatJSON:
csvPF.WriteRowTitles(baserow)
elif csvPF.CheckRowTitles(baserow):
baserow['JSON'] = json.dumps({})
csvPF.WriteRowNoFilter(baserow)
if csvPF:
if not oneItemPerRow:
csvPF.SetIndexedTitles(['permissions'])
csvPF.writeCSVfile('SharedDrive ACLs')
def doPrintShowSharedDriveACLs():
printShowSharedDriveACLs([_getAdminEmail()], True)
LOOKERSTUDIO_ASSETTYPE_CHOICE_MAP = {
'report': ['REPORT'],
'datasource': ['DATA_SOURCE'],
'all': ['REPORT', 'DATA_SOURCE'],
}
def initLookerStudioAssetSelectionParameters():
return ({'owner': None, 'title': None, 'includeTrashed': False}, {'assetTypes': ['REPORT']})
def getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
if myarg in {'assettype', 'assettypes'}:
assetTypes['assetTypes'] = getChoice(LOOKERSTUDIO_ASSETTYPE_CHOICE_MAP, mapChoice=True)
elif myarg == 'title':
parameters['title'] = getString(Cmd.OB_STRING)
elif myarg == 'owner':
parameters['owner'] = getEmailAddress(noUid=True)
elif myarg == 'includetrashed':
parameters['includeTrashed'] = True
else:
return False
return True
def _validateUserGetLookerStudioAssetIds(user, i, count, entity):
if entity:
if entity['dict']:
entityList = [{'name': item, 'title': item} for item in entity['dict'][user]]
else:
entityList = [{'name': item, 'title': item} for item in entity['list']]
else:
entityList = []
user, ds = buildGAPIServiceObject(API.LOOKERSTUDIO, user, i, count)
if not ds:
return (user, None, None, 0)
return (user, ds, entityList, len(entityList))
def _getLookerStudioAssetByID(ds, user, i, count, assetId):
printGettingAllEntityItemsForWhom(Ent.LOOKERSTUDIO_ASSET, user, i, count)
try:
return callGAPI(ds.assets(), 'get',
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
name=f'assets/{assetId}')
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userLookerStudioServiceNotEnabledWarning(user, i, count)
return None
def _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, fields, orderBy=None):
assets = []
for assetType in assetTypes['assetTypes']:
entityType = Ent.LOOKERSTUDIO_ASSET_REPORT if assetType == 'REPORT' else Ent.LOOKERSTUDIO_ASSET_DATASOURCE
printGettingAllEntityItemsForWhom(entityType, user, i, count)
parameters['assetTypes'] = assetType
try:
assets.extend(callGAPIpages(ds.assets(), 'search', 'assets',
pageMessage=getPageMessage(),
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
**parameters, orderBy=orderBy, fields=fields))
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
return (None, 0)
except GAPI.serviceNotAvailable:
userLookerStudioServiceNotEnabledWarning(user, i, count)
return (None, 0)
return (assets, len(assets))
LOOKERSTUDIO_ASSETS_ORDERBY_CHOICE_MAP = {
'title': 'title'
}
LOOKERSTUDIO_ASSETS_TIME_OBJECTS = {'updateTime', 'updateByMeTime', 'createTime', 'lastViewByMeTime'}
# gam <UserTypeEntity> print lookerstudioassets [todrive <ToDriveAttribute>*]
# [([assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)]
# [stripcrsfromtitle]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show lookerstudioassets
# [([assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)]
# [stripcrsfromtitle]
# [formatjson]
def printShowLookerStudioAssets(users):
def _printAsset(asset, user):
if stripCRsFromTitle:
asset['title'] = _stripControlCharsFromName(asset['title'])
row = flattenJSON(asset, flattened={'User': user})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'User': user, 'title': asset['title'],
'JSON': json.dumps(cleanJSON(asset, timeObjects=LOOKERSTUDIO_ASSETS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
def _showAsset(asset):
if stripCRsFromTitle:
asset['title'] = _stripControlCharsFromName(asset['title'])
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(asset, timeObjects=LOOKERSTUDIO_ASSETS_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
return
printEntity([Ent.LOOKERSTUDIO_ASSET, asset['title']], j, jcount)
Ind.Increment()
showJSON(None, asset, timeObjects=LOOKERSTUDIO_ASSETS_TIME_OBJECTS)
Ind.Decrement()
csvPF = CSVPrintFile(['User', 'title']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(LOOKERSTUDIO_ASSETS_ORDERBY_CHOICE_MAP, ascendingKeyword='ascending', descendingKeyword='')
parameters, assetTypes = initLookerStudioAssetSelectionParameters()
assetIdEntity = None
stripCRsFromTitle = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
pass
elif myarg in {'assetid', 'assetids'}:
assetIdEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.LOOKERSTUDIO_ASSETID)
elif myarg == 'stripcrsfromtitle':
stripCRsFromTitle = True
elif myarg == 'orderby':
OBY.GetChoice()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, ds, assets, jcount = _validateUserGetLookerStudioAssetIds(user, i, count, assetIdEntity)
if not ds:
continue
if assetIdEntity is None:
assets, jcount = _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, 'nextPageToken,assets', OBY.orderBy)
if assets is None:
continue
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LOOKERSTUDIO_ASSET, i, count)
Ind.Increment()
j = 0
for asset in assets:
j += 1
if assetIdEntity:
asset = _getLookerStudioAssetByID(ds, user, i, count, asset['name'])
if asset:
_showAsset(asset)
Ind.Decrement()
else:
for asset in assets:
if assetIdEntity:
asset = _getLookerStudioAssetByID(ds, user, i, count, asset['name'])
if asset:
_printAsset(asset, user)
if csvPF:
csvPF.writeCSVfile('Looker Studio Assets')
def _showLookerStudioPermissions(user, asset, permissions, j, jcount, FJQC):
if FJQC is not None and FJQC.formatJSON:
permissions['User'] = user
permissions['assetId'] = asset['name']
printLine(json.dumps(cleanJSON(permissions), ensure_ascii=False, sort_keys=False))
return
permissions = permissions['permissions']
if permissions:
printEntity([Ent.LOOKERSTUDIO_ASSET, asset['title'], Ent.LOOKERSTUDIO_PERMISSION, ''], j, jcount)
for role in ['OWNER', 'EDITOR', 'VIEWER']:
members = permissions.get(role, {}).get('members', [])
if members:
lrole = role.lower()
Ind.Increment()
for member in members:
printKeyValueList([lrole, member])
Ind.Decrement()
LOOKERSTUDIO_VIEW_PERMISSION_ROLE_CHOICE_MAP = {
'editor': 'EDITOR',
'owner': 'OWNER',
'viewer': 'VIEWER',
}
LOOKERSTUDIO_ADD_UPDATE_PERMISSION_ROLE_CHOICE_MAP = {
'editor': 'EDITOR',
'viewer': 'VIEWER',
}
LOOKERSTUDIO_DELETE_PERMISSION_ROLE_CHOICE_MAP = {
'any': None,
'editor': None,
'owner': None,
'viewer': None,
}
LOOKERSTUDIO_PERMISSION_MODIFIER_MAP = {
Act.ADD: Act.MODIFIER_TO,
Act.DELETE: Act.MODIFIER_FROM,
Act.UPDATE: Act.MODIFIER_FOR
}
# gam <UserTypeEntity> add lookerstudiopermissions
# [([assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)]
# (role editor|viewer <LookerStudioPermissionEntity>)+
# [nodetails]
# gam <UserTypeEntity> delete lookerstudiopermissions
# ([[assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)]
# (role any <LookerStudioPermissionEntity>)+
# [nodetails]
# gam <UserTypeEntity> update lookerstudiopermissions
# [([assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)]
# (role editor|viewer <LookerStudioPermissionEntity>)+
# [nodetails]
def processLookerStudioPermissions(users):
action = Act.Get()
if action == Act.CREATE:
action = Act.ADD
modifier = LOOKERSTUDIO_PERMISSION_MODIFIER_MAP[action]
parameters, assetTypes = initLookerStudioAssetSelectionParameters()
permissions = {}
assetIdEntity = None
showDetails = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
pass
elif myarg in {'assetid', 'assetids'}:
assetIdEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.LOOKERSTUDIO_ASSETID)
elif myarg == 'role':
permissions.setdefault('permissions', {})
if action in {Act.ADD, Act.UPDATE}:
role = getChoice(LOOKERSTUDIO_ADD_UPDATE_PERMISSION_ROLE_CHOICE_MAP, mapChoice=True)
else:
role = getChoice(LOOKERSTUDIO_DELETE_PERMISSION_ROLE_CHOICE_MAP, mapChoice=True)
permissions['permissions'].setdefault(role, {'members': []})
permissions['permissions'][role]['members'].extend(getEntityList(Cmd.OB_LOOKERSTUDIO_PERMISSION_ENTITY))
elif myarg == 'nodetails':
showDetails = False
else:
unknownArgumentExit()
if not permissions:
if action in {Act.ADD, Act.UPDATE}:
missingArgumentExit('role editor|owner|viewer members')
else:
missingArgumentExit('members')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, ds, assets, jcount = _validateUserGetLookerStudioAssetIds(user, i, count, assetIdEntity)
if not ds:
continue
if assetIdEntity is None:
assets, jcount = _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, 'nextPageToken,assets(name,title)', None)
if assets is None:
continue
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.LOOKERSTUDIO_PERMISSION, modifier, jcount, Ent.LOOKERSTUDIO_ASSET, i, count)
j = 0
for asset in assets:
j += 1
for role in permissions['permissions']:
try:
body = {'name': asset['name'], 'members': permissions['permissions'][role]['members']}
if action in {Act.DELETE, Act.UPDATE}:
results = callGAPI(ds.assets().permissions(), 'revokeAllPermissions',
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
name=asset['name'], body=body)
if action in {Act.ADD, Act.UPDATE}:
body['role'] = role
results = callGAPI(ds.assets().permissions(), 'addMembers',
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
name=asset['name'], body=body)
entityActionPerformed([Ent.USER, user, Ent.LOOKERSTUDIO_ASSET, asset['title'], Ent.LOOKERSTUDIO_PERMISSION, ''], j, jcount)
if showDetails:
_showLookerStudioPermissions(user, asset, results, j, jcount, None)
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LOOKERSTUDIO_ASSET, asset['title']], str(e), j, jcount)
continue
except GAPI.serviceNotAvailable:
userLookerStudioServiceNotEnabledWarning(user, i, count)
break
# gam <UserTypeEntity> print lookerstudiopermissions [todrive <ToDriveAttribute>*]
# [([assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)]
# [role editor|owner|viewer]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show lookerstudiopermissions
# [([assettype report|datasource|all] [title <String>]
# [owner <Emailddress>] [includetrashed]
# [orderby title [ascending|descending]]) |
# (assetids <LookerStudioAssetIDEntity>)[
# [role editor|owner|viewer]
# [formatjson]
def printShowLookerStudioPermissions(users):
def _printLookerStudioPermissions(user, asset, permissions):
row = flattenJSON(permissions, flattened={'User': user, 'assetId': asset['name']},
simpleLists=['members'], delimiter=delimiter)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'User': user, 'assetId': asset['name'],
'JSON': json.dumps(cleanJSON(permissions), ensure_ascii=False, sort_keys=True)})
csvPF = CSVPrintFile(['User', 'assetId']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
parameters, assetTypes = initLookerStudioAssetSelectionParameters()
assetIdEntity = None
role = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
pass
elif myarg in {'assetid', 'assetids'}:
assetIdEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.LOOKERSTUDIO_ASSETID)
elif myarg == 'role':
role = getChoice(LOOKERSTUDIO_VIEW_PERMISSION_ROLE_CHOICE_MAP, mapChoice=True)
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, ds, assets, jcount = _validateUserGetLookerStudioAssetIds(user, i, count, assetIdEntity)
if not ds:
continue
if assetIdEntity is None:
assets, jcount = _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, 'nextPageToken,assets(name,title)', None)
if assets is None:
continue
if not csvPF and not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LOOKERSTUDIO_ASSET, i, count)
j = 0
for asset in assets:
j += 1
try:
permissions = callGAPI(ds.assets(), 'getPermissions',
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
name=asset['name'], role=role)
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LOOKERSTUDIO_ASSET, asset['title']], str(e), j, jcount)
continue
except GAPI.serviceNotAvailable:
userLookerStudioServiceNotEnabledWarning(user, i, count)
break
if not csvPF:
Ind.Increment()
_showLookerStudioPermissions(user, asset, permissions, j, jcount, FJQC)
Ind.Decrement()
else:
_printLookerStudioPermissions(user, asset, permissions)
if csvPF:
csvPF.writeCSVfile('Looker Studio Permissions')
def _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count):
roleLower = role.lower()
if roleLower in GROUP_ROLES_MAP:
return (GROUP_ROLES_MAP[roleLower], userGroupLists[origUser][role])
entityActionNotPerformedWarning([Ent.USER, user, Ent.ROLE, role], Msg.INVALID_ROLE.format(','.join(sorted(GROUP_ROLES_MAP))), i, count)
return (None, None)
def _addUserToGroups(cd, user, addGroupsSet, addGroups, i, count):
jcount = len(addGroupsSet)
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_TO, jcount, Ent.GROUP, i, count)
Ind.Increment()
j = 0
for group in sorted(addGroupsSet):
j += 1
role = addGroups[group]['role']
body = {'email': user, 'role': role}
if addGroups[group]['delivery_settings'] != DELIVERY_SETTINGS_UNDEFINED:
body['delivery_settings'] = addGroups[group]['delivery_settings']
try:
callGAPI(cd.members(), 'insert',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.DUPLICATE, GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND,
GAPI.INVALID_MEMBER, GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED,
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, body=body, fields='')
entityActionPerformed([Ent.GROUP, group, role, user], j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, group, j, jcount)
except (GAPI.duplicate, GAPI.cyclicMembershipsNotAllowed, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.GROUP, group, role, user], str(e), j, jcount)
except GAPI.conflict:
entityActionPerformedMessage([Ent.GROUP, group, role, user], Msg.ACTION_MAY_BE_DELAYED, j, jcount)
except (GAPI.memberNotFound, GAPI.resourceNotFound, GAPI.invalidMember) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
Ind.Decrement()
# gam <UserTypeEntity> add group|groups
# ([<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)+
def addUserToGroups(users):
cd = buildGAPIObject(API.DIRECTORY)
baseRole = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
baseDeliverySettings = getDeliverySettings()
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
if not isinstance(groupKeys, dict):
userGroupLists = None
addGroups = {}
for group in groupKeys:
addGroups[normalizeEmailAddressOrUID(group)] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
while Cmd.ArgumentsRemaining():
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
deliverySettings = getDeliverySettings()
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
addGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': deliverySettings}
else:
userGroupLists = groupKeys
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user = normalizeEmailAddressOrUID(user)
if userGroupLists:
roleList = [baseRole]
if not subkeyRoleField:
groupKeys = userGroupLists[origUser]
else:
roleList = userGroupLists[origUser]
addGroups = {}
for role in roleList:
if subkeyRoleField:
role, groupKeys = _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count)
if role is None:
continue
for group in groupKeys:
addGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': DELIVERY_SETTINGS_UNDEFINED}
_addUserToGroups(cd, user, set(addGroups), addGroups, i, count)
def _deleteUserFromGroups(cd, user, deleteGroupsSet, deleteGroups, i, count):
jcount = len(deleteGroupsSet)
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_FROM, jcount, Ent.GROUP, i, count)
Ind.Increment()
j = 0
for group in sorted(deleteGroupsSet):
j += 1
role = deleteGroups[group]['role']
try:
callGAPI(cd.members(), 'delete',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER,
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, memberKey=user)
entityActionPerformed([Ent.GROUP, group, role, user], j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, group, j, jcount)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, group], str(e), j, jcount)
except GAPI.conflict:
entityActionPerformedMessage([Ent.GROUP, group, role, user], Msg.ACTION_MAY_BE_DELAYED, j, jcount)
Ind.Decrement()
def _getUserGroupOptionalDomainCustomerId():
if checkArgumentPresent('domain'):
return {'domain': getString(Cmd.OB_DOMAIN_NAME).lower()}
if checkArgumentPresent('customerid'):
return {'customer': getString(Cmd.OB_CUSTOMER_ID)}
return {}
def _setUserGroupArgs(user, kwargs):
if 'customer' in kwargs:
if "'" not in user:
kwargs['query'] = f'memberKey={user}'
else:
quser = user.replace("'", "\\'")
kwargs['query'] = f'memberKey={quser}'
else:
kwargs['userKey'] = user
def checkUserGroupMatchPattern(groupEmail, matchPattern):
if not matchPattern['not']:
if not matchPattern['pattern'].match(groupEmail):
return False
else:
if matchPattern['pattern'].match(groupEmail):
return False
return True
# gam <UserTypeEntity> delete group|groups
# [(domain <DomainName>)|(customerid <CustomerID>)|
# (emailmatchpattern [not] <REMatchPattern>)|<GroupEntity>]
def deleteUserFromGroups(users):
cd = buildGAPIObject(API.DIRECTORY)
groupKeys = None
matchPattern = {}
kwargs = _getUserGroupOptionalDomainCustomerId()
if not kwargs:
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
deleteGroups = {}
if Cmd.ArgumentsRemaining():
if not checkArgumentPresent('emailmatchpattern'):
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
userGroupLists = groupKeys if isinstance(groupKeys, dict) else None
for group in groupKeys:
deleteGroups[normalizeEmailAddressOrUID(group)] = {'role': Ent.MEMBER}
else:
matchPattern = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE)}
checkForExtraneousArguments()
else:
checkForExtraneousArguments()
role = Ent.MEMBER
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user = normalizeEmailAddressOrUID(user)
if groupKeys is None:
_setUserGroupArgs(user, kwargs)
try:
result = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
continue
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
deleteGroups = {}
for group in result:
if not matchPattern or checkUserGroupMatchPattern(group['email'], matchPattern):
deleteGroups[group['email']] = {'role': role}
elif userGroupLists:
userGroupKeys = userGroupLists[origUser]
deleteGroups = {}
for group in userGroupKeys:
deleteGroups[normalizeEmailAddressOrUID(group)] = {'role': role}
_deleteUserFromGroups(cd, user, set(deleteGroups), deleteGroups, i, count)
def _updateUserGroups(cd, user, updateGroupsSet, updateGroups, i, count):
jcount = len(updateGroupsSet)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP, i, count)
Ind.Increment()
j = 0
for group in sorted(updateGroupsSet):
j += 1
role = updateGroups[group]['role']
body = {'email': user, 'role': role}
if updateGroups[group]['delivery_settings'] != DELIVERY_SETTINGS_UNDEFINED:
body['delivery_settings'] = updateGroups[group]['delivery_settings']
try:
callGAPI(cd.members(), 'patch',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=group, memberKey=user, body=body, fields='')
entityActionPerformed([Ent.GROUP, group, role, user], j, jcount)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, group, j, jcount)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, group], str(e), j, jcount)
Ind.Decrement()
# gam <UserTypeEntity> update group|groups
# [(domain <DomainName>)|(customerid <CustomerID>)]) [<GroupRole>] [[delivery] <DeliverySetting>]
# ([<GroupRole>] [[delivery] <DeliverySetting>] [<GroupEntity>])*
def updateUserGroups(users):
cd = buildGAPIObject(API.DIRECTORY)
groupKeys = None
kwargs = _getUserGroupOptionalDomainCustomerId()
baseRole = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
baseDeliverySettings = getDeliverySettings()
if not kwargs:
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
if Cmd.ArgumentsRemaining():
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
if not isinstance(groupKeys, dict):
userGroupLists = None
updateGroups = {}
for group in groupKeys:
updateGroups[normalizeEmailAddressOrUID(group)] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
while Cmd.ArgumentsRemaining():
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
deliverySettings = getDeliverySettings()
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
updateGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': deliverySettings}
else:
userGroupLists = groupKeys
checkForExtraneousArguments()
else:
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user = normalizeEmailAddressOrUID(user)
if groupKeys is None:
_setUserGroupArgs(user, kwargs)
try:
result = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
continue
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
updateGroups = {}
for group in result:
updateGroups[group['email']] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
elif userGroupLists:
roleList = [baseRole]
if not subkeyRoleField:
groupKeys = userGroupLists[origUser]
else:
roleList = userGroupLists[origUser]
updateGroups = {}
for role in roleList:
if subkeyRoleField:
role, groupKeys = _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count)
if role is None:
continue
for group in groupKeys:
updateGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': DELIVERY_SETTINGS_UNDEFINED}
_updateUserGroups(cd, user, set(updateGroups), updateGroups, i, count)
# gam <UserTypeEntity> sync group|groups
# [(domain <DomainName>)|(customerid <CustomerID>)]
# [<GroupRole>] [[delivery] <DeliverySetting>] (<GroupEntity>)*
def syncUserWithGroups(users):
cd = buildGAPIObject(API.DIRECTORY)
kwargs = _getUserGroupOptionalDomainCustomerId()
if not kwargs:
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
baseRole = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
baseDeliverySettings = getDeliverySettings()
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
if not isinstance(groupKeys, dict):
userGroupLists = None
syncGroups = {}
for group in groupKeys:
syncGroups[normalizeEmailAddressOrUID(group)] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
while Cmd.ArgumentsRemaining():
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
deliverySettings = getDeliverySettings()
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
syncGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': deliverySettings}
else:
userGroupLists = groupKeys
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user = normalizeEmailAddressOrUID(user)
if userGroupLists:
roleList = [baseRole]
if not subkeyRoleField:
groupKeys = userGroupLists[origUser]
else:
roleList = userGroupLists[origUser]
syncGroups = {}
for role in roleList:
if subkeyRoleField:
role, groupKeys = _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count)
if role is None:
continue
for group in groupKeys:
syncGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': DELIVERY_SETTINGS_UNDEFINED}
currGroups = {}
_setUserGroupArgs(user, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
continue
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
for groupEntity in entityList:
groupEmail = groupEntity['email']
try:
result = callGAPI(cd.members(), 'get',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=groupEmail, memberKey=user, fields='role,delivery_settings')
currGroups[groupEmail] = {'role': result.get('role', Ent.MEMBER),
'delivery_settings': result.get('delivery_settings', DELIVERY_SETTINGS_UNDEFINED)}
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, groupEmail, i, count)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), i, count)
currGroupsSet = set(currGroups)
syncGroupsSet = set(syncGroups)
removeGroupsSet = currGroupsSet-syncGroupsSet
addGroupsSet = syncGroupsSet-currGroupsSet
updateGroupsSet = set()
for group in currGroupsSet.intersection(syncGroupsSet):
if (syncGroups[group]['role'] != currGroups[group]['role'] or
(syncGroups[group]['delivery_settings'] != currGroups[group]['delivery_settings'] and
syncGroups[group]['delivery_settings'] != DELIVERY_SETTINGS_UNDEFINED)):
updateGroupsSet.add(group)
if removeGroupsSet or addGroupsSet or updateGroupsSet:
if removeGroupsSet:
Act.Set(Act.REMOVE)
_deleteUserFromGroups(cd, user, removeGroupsSet, currGroups, i, count)
if addGroupsSet:
Act.Set(Act.ADD)
_addUserToGroups(cd, user, addGroupsSet, syncGroups, i, count)
if updateGroupsSet:
Act.Set(Act.UPDATE)
_updateUserGroups(cd, user, updateGroupsSet, syncGroups, i, count)
else:
printEntityKVList([Ent.USER, user], [Msg.NO_CHANGES], i, count)
def _getUserGroupDomainCustomerId(myarg, kwargs):
if myarg == 'domain':
kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower()
kwargs.pop('customer', None)
elif myarg == 'customerid':
kwargs['customer'] = getString(Cmd.OB_CUSTOMER_ID)
kwargs.pop('domain', None)
else:
return False
return True
# gam <UserTypeEntity> check group|groups
# [roles <GroupRoleList>] [includederivedmembership] [csv] <GroupEntity>
def checkUserInGroups(users):
def _setCheckError():
sysRC['sysRC'] = CHECK_USER_GROUPS_ERROR_RC
def _checkMember(result):
role = result.get('role', Ent.MEMBER)
if role in rolesSet:
if not csvPF:
printEntity([Ent.USER, user, Ent.GROUP, groupEmail, Ent.ROLE, role], j, jcount)
else:
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': role})
else:
if not csvPF:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail, Ent.ROLE, role], Msg.ROLE_NOT_IN_SET.format(rolesSet), j, jcount)
else:
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': notMemberOrRole})
_setCheckError()
cd = buildGAPIObject(API.DIRECTORY)
csvPF = None
groupKeys = []
checkGroupsSet = set()
rolesSet = set()
includeDerivedMembership = False
sysRC = {'sysRC' : 0}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'role', 'roles'}:
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
if role in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[role])
else:
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
elif myarg == 'includederivedmembership':
includeDerivedMembership = True
elif myarg == 'csv':
csvPF = CSVPrintFile(['user', 'group', 'role'])
else:
Cmd.Backup()
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
if not rolesSet:
rolesSet = ALL_GROUP_ROLES
notMemberOrRole = Msg.NOT_AN_ENTITY.format('|'.join(rolesSet))
userGroupLists = groupKeys if isinstance(groupKeys, dict) else None
for group in groupKeys:
checkGroupsSet.add(normalizeEmailAddressOrUID(group))
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user = normalizeEmailAddressOrUID(user)
if userGroupLists:
userGroupKeys = userGroupLists[origUser]
checkGroupsSet = set()
for group in userGroupKeys:
checkGroupsSet.add(normalizeEmailAddressOrUID(group))
jcount = len(checkGroupsSet)
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_IN, jcount, Ent.GROUP, i, count)
Ind.Increment()
j = 0
for groupEmail in sorted(checkGroupsSet):
j += 1
if not includeDerivedMembership:
try:
result = callGAPI(cd.members(), 'get',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=groupEmail, memberKey=user, fields='role')
_checkMember(result)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
_setCheckError()
except GAPI.memberNotFound:
if not csvPF:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], Msg.NOT_A_MEMBER, j, jcount)
else:
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': notMemberOrRole})
_setCheckError()
except (GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
_setCheckError()
else:
try:
result = callGAPIpages(cd.members(), 'list', 'members',
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
includeDerivedMembership=includeDerivedMembership,
groupKey=groupEmail, fields='nextPageToken,members(email,role,type)', maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
for member in result:
if member['type'] != Ent.TYPE_USER or member['email'].lower() != user:
continue
_checkMember(member)
break
else:
if not csvPF:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], Msg.NOT_A_MEMBER, j, jcount)
else:
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': notMemberOrRole})
_setCheckError()
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
_setCheckError()
except (GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
_setCheckError()
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('User Check Groups')
setSysExitRC(sysRC['sysRC'])
# gam <UserTypeEntity> print groups [todrive <ToDriveAttribute>*]
# [(domain <DomainName>)|(customerid <CustomerID>)]
# [roles <GroupRoleList>] [countsonly|totalonly|nodetails]
# gam <UserTypeEntity> show groups
# [(domain <DomainName>)|(customerid <CustomerID>)]
# [roles <GroupRoleList>] [countsonly|totalonly|nodetails]
def printShowUserGroups(users):
cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile(['User', 'Group', 'Role', 'Status', 'Delivery'], 'sortall') if Act.csvFormat() else None
rolesSet = set()
countsOnly = noDetails = totalOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getUserGroupDomainCustomerId(myarg, kwargs):
pass
elif myarg in {'role', 'roles'}:
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
if role in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[role])
else:
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
elif myarg == 'countsonly':
countsOnly = True
elif myarg == 'totalonly':
countsOnly = totalOnly = True
elif myarg == 'nodetails':
noDetails = True
else:
unknownArgumentExit()
if not rolesSet:
rolesSet = ALL_GROUP_ROLES
allRoles = rolesSet == ALL_GROUP_ROLES
if noDetails:
if csvPF:
titles = ['User', 'Group']
csvPF.SetTitles(titles)
csvPF.SetSortTitles([])
elif countsOnly:
zeroCounts = {'User': None, 'Total': 0}
if not totalOnly:
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
if role in rolesSet:
zeroCounts[role] = 0
if csvPF:
titles = ['User', 'Total']
if not totalOnly:
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
if role in rolesSet:
titles.append(role)
csvPF.SetTitles(titles)
csvPF.SetSortTitles([])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
if csvPF:
printGettingAllEntityItemsForWhom(Ent.GROUP, user, i, count)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
_setUserGroupArgs(user, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=pageMessage,
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
continue
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
if kwargs.get('domain'):
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
return
accessErrorExit(cd)
jcount = len(entityList)
if totalOnly:
if not csvPF:
printEntityKVList([Ent.USER, user], ['Total', jcount])
else:
csvPF.WriteRow({'User': user, 'Total': jcount})
continue
if not csvPF:
if allRoles:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP, i, count)
else:
entityPerformActionModifierNumItems([Ent.USER, user], Msg.MAXIMUM_OF, jcount, Ent.GROUP, i, count)
if noDetails:
Ind.Increment()
j = 0
for groupEntity in entityList:
j += 1
groupEmail = groupEntity['email']
if not csvPF:
printEntity([Ent.GROUP, groupEmail], j, jcount)
else:
csvPF.WriteRow({'User': user, 'Group': groupEmail})
Ind.Decrement()
continue
if countsOnly:
userCounts = zeroCounts.copy()
userCounts['User'] = user
Ind.Increment()
j = 0
for groupEntity in entityList:
j += 1
groupEmail = groupEntity['email']
try:
result = callGAPI(cd.members(), 'get',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=groupEmail, memberKey=user, fields='role,status,delivery_settings')
role = result.get('role', Ent.MEMBER)
status = result.get('status', 'UNKNOWN')
delivery_settings = result.get('delivery_settings', '')
if role in rolesSet:
if countsOnly:
userCounts[role] += 1
elif not csvPF:
printEntity([Ent.GROUP, groupEmail, Ent.ROLE, role, Ent.STATUS, status, Ent.DELIVERY, delivery_settings], j, jcount)
else:
csvPF.WriteRow({'User': user, 'Group': groupEmail, 'Role': role, 'Status': status, 'Delivery': delivery_settings})
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
if countsOnly:
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
if role in rolesSet:
userCounts['Total'] += userCounts[role]
if not csvPF:
kvList = ['Total', userCounts['Total']]
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
if role in rolesSet:
kvList.extend([role, userCounts[role]])
printEntityKVList([Ent.USER, user], kvList)
else:
csvPF.WriteRow(userCounts)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('User Groups')
# gam <UserTypeEntity> print grouptree [todrive <ToDriveAttribute>*]
# [(domain <DomainName>)|(customerid <CustomerID>)]
# [roles <GroupRoleList>]
# [showparentsaslist [<Boolean>]] [delimiter <Character>]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show grouptree
# [(domain <DomainName>)|(customerid <CustomerID>)]
# [roles <GroupRoleList>]
# [formatjson]
def printShowGroupTree(users):
cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile(['User', 'Group', 'Name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showParentsAsList = False
rolesSet = set()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getUserGroupDomainCustomerId(myarg, kwargs):
pass
elif myarg in {'role', 'roles'}:
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
if role in GROUP_ROLES_MAP:
rolesSet.add(GROUP_ROLES_MAP[role])
else:
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
elif csvPF and myarg == 'delimiter':
delimiter = getCharacter()
elif csvPF and myarg == 'showparentsaslist':
showParentsAsList = getBoolean()
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if csvPF:
if rolesSet:
csvPF.AddTitles('Role')
if not FJQC.formatJSON:
if not showParentsAsList:
csvPF.SetIndexedTitles(['parents'])
else:
csvPF.AddTitles(['ParentsCount', 'Parents', 'ParentsName'])
else:
if rolesSet:
csvPF.AddJSONTitles('Role')
csvPF.AddJSONTitles('JSON')
allRoles = rolesSet == ALL_GROUP_ROLES
groupParents = {}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
_setUserGroupArgs(user, kwargs)
try:
groups = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
entityUnknownWarning(Ent.USER, user, i, count)
continue
j = 0
jcount = len(groups)
if not csvPF and not FJQC.formatJSON:
if allRoles:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP_TREE, i, count)
else:
entityPerformActionModifierNumItems([Ent.USER, user], Msg.MAXIMUM_OF, jcount, Ent.GROUP_TREE, i, count)
Ind.Increment()
for group in groups:
j += 1
groupEmail = group['email']
if groupEmail not in groupParents:
getGroupParents(cd, groupParents, groupEmail, group['name'], kwargs)
if rolesSet:
try:
result = callGAPI(cd.members(), 'get',
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
groupKey=groupEmail, memberKey=user, fields='role')
role = result.get('role', Ent.MEMBER)
if role not in rolesSet:
continue
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
continue
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
continue
else:
role = None
if not FJQC.formatJSON:
if not csvPF:
showGroupParents(groupParents, groupEmail, role, j, jcount)
else:
row = {'User': user, 'Group': groupEmail, 'Name': group['name'], 'parents': []}
if role is not None:
row['Role'] = role
printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList)
else:
groupInfo = {'email': groupEmail, 'name': group['name'], 'parents': []}
if role is not None:
groupInfo['role'] = role
addJsonGroupParents(groupParents, groupInfo, groupEmail)
if not csvPF:
printLine(json.dumps(cleanJSON(groupInfo), ensure_ascii=False, sort_keys=True))
else:
row = flattenJSON(groupInfo)
if csvPF.CheckRowTitles(row):
row = {'User': user, 'Group': groupEmail, 'Name': group['name']}
if rolesSet:
row['Role'] = role
row['JSON'] = json.dumps(cleanJSON(groupInfo),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('User Group Trees')
# gam <UserTypeEntity> print groupslist [todrive <ToDriveAttribute>*]
# [(domain <DomainName>)|(customerid <CustomerID>)]
# [delimiter <Character>] [quotechar <Character>]
def printUserGroupsList(users):
cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile(['User', 'Groups', 'GroupsList'])
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getUserGroupDomainCustomerId(myarg, kwargs):
pass
elif myarg == 'delimiter':
delimiter = getCharacter()
else:
FJQC.GetQuoteChar(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
printGettingAllEntityItemsForWhom(Ent.GROUP, user, i, count)
_setUserGroupArgs(user, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
continue
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
if kwargs.get('domain'):
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
return
accessErrorExit(cd)
csvPF.WriteRow({'User': user, 'Groups': len(entityList), 'GroupsList': delimiter.join([group['email'] for group in entityList])})
csvPF.writeCSVfile('User GroupsList')
# License command utilities
LICENSE_PRODUCT_SKUIDS = 'productSkuIds'
LICENSE_PREVIEW_TITLES = ['user', 'productId', 'skuId', 'action', 'message']
def getLicenseParameters(operation):
lic = buildGAPIObject(API.LICENSING)
parameters = {LICENSE_PRODUCT_SKUIDS: [], 'csvPF': None, 'preview': False, 'syncOperation': 'addremove', 'syncACLsMode': None, 'archive': False}
skuLocation = Cmd.Location()
if operation != 'patch':
parameters[LICENSE_PRODUCT_SKUIDS] = getGoogleSKUList(allowUnknownProduct=True)
else:
parameters[LICENSE_PRODUCT_SKUIDS] = [getGoogleSKU()]
if checkArgumentPresent(['product', 'productid']):
productId = getGoogleProduct()
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
productSku = (productId, productSku[1])
if operation == 'patch':
checkArgumentPresent('from')
productId, oldSkuId = getGoogleSKU()
if not productId:
invalidChoiceExit(oldSkuId, SKU.getSortedSKUList(), True)
skuId = parameters[LICENSE_PRODUCT_SKUIDS][0][1]
parameters[LICENSE_PRODUCT_SKUIDS] = [(productId, skuId, oldSkuId)]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if operation == 'sync' and myarg in {'addonly', 'removeonly'}:
parameters['syncOperation'] = myarg
elif operation == 'sync' and myarg in {'allskus', 'onesku'}:
parameters['syncACLsMode'] = myarg
elif myarg == 'preview':
parameters['preview'] = True
elif myarg == 'actioncsv':
titles = LICENSE_PREVIEW_TITLES
if operation == 'patch':
titles.insert(2, 'oldskuId')
parameters['csvPF'] = CSVPrintFile(titles)
elif operation == 'patch' and myarg == 'archive':
if skuId not in SKU.ARCHIVABLE_SKUS:
usageErrorExit(Msg.SKU_HAS_NO_MATCHING_ARCHIVED_USER_SKU.format(skuId))
parameters['archive'] = True
else:
unknownArgumentExit()
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
if not productSku[0]:
Cmd.SetLocation(skuLocation)
invalidChoiceExit(productSku[1], SKU.getSortedSKUList(), False)
if operation == 'sync':
if len(parameters[LICENSE_PRODUCT_SKUIDS]) > 1:
if parameters['syncACLsMode'] is None:
missingArgumentExit('allskus|onesku')
if parameters['syncACLsMode'] == 'onesku':
# With onesku, all SKU productIds must be the same
baseProductId = parameters[LICENSE_PRODUCT_SKUIDS][0][0]
baseSkuId = parameters[LICENSE_PRODUCT_SKUIDS][0][1]
for i in range(1, len(parameters[LICENSE_PRODUCT_SKUIDS])):
productId = parameters[LICENSE_PRODUCT_SKUIDS][i][0]
if baseProductId != productId:
skuId = parameters[LICENSE_PRODUCT_SKUIDS][i][1]
Cmd.SetLocation(skuLocation)
usageErrorExit(Msg.ALL_SKU_PRODUCTIDS_MUST_MATCH.format(f'{baseProductId}:{baseSkuId}', f'{productId}:{skuId}'))
else:
parameters['syncACLsMode'] = 'allskus'
return (lic, parameters)
def _writeLicenseAction(productId, skuId, oldSkuId, parameters, user, action, message):
actionName = Act.PerformedName(action) if message == Act.SUCCESS else Act.ToPerformName(action)
if action != Act.UPDATE:
parameters['csvPF'].WriteRow({'user': user,
'productId': productId,
'skuId': skuId,
'action': actionName,
'message': message})
else:
parameters['csvPF'].WriteRow({'user': user,
'productId': productId,
'oldskuId': oldSkuId,
'skuId': skuId,
'action': actionName,
'message': message})
def _createLicenses(lic, productId, skuId, parameters, jcount, users, i, count, returnDoneSet=False):
Act.Set([Act.ADD, Act.ADD_PREVIEW][parameters['preview']])
if parameters['preview']:
message = Act.PREVIEW
noAvailableLicenses = False
doneSet = set()
if not returnDoneSet:
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.TO_LC, jcount, Ent.USER, i, count)
else:
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.TO_MAXIMUM_OF, jcount, Ent.USER, i, count)
Ind.Increment()
j = 0
for user in users:
j += 1
if returnDoneSet:
origUser = user
doneSet.add(user)
user = normalizeEmailAddressOrUID(user)
if not noAvailableLicenses:
try:
if not parameters['preview']:
callGAPI(lic.licenseAssignments(), 'insert',
bailOnInternalError=True,
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
productId=productId, skuId=skuId, body={'userId': user}, fields='')
message = Act.SUCCESS
entityActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], j, jcount)
except (GAPI.internalError, GAPI.duplicate, GAPI.invalid,
GAPI.forbidden, GAPI.serviceNotAvailable) as e:
message = str(e)
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
except (GAPI.conditionNotMet, GAPI.backendError) as e:
message = str(e)
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
if ("there aren't enough available licenses" in message.lower() or
"backend error" in message.lower()):
noAvailableLicenses = True
if returnDoneSet:
doneSet.remove(origUser)
break
except GAPI.userNotFound as e:
message = str(e)
entityUnknownWarning(Ent.USER, user, j, jcount)
else:
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
if parameters['csvPF']:
_writeLicenseAction(productId, skuId, None, parameters, user, Act.ADD, message)
Ind.Decrement()
if returnDoneSet:
return doneSet
# gam <UserTypeEntity> create license <SKUIDList> [product|productid <ProductID>] [preview] [actioncsv]
def createLicense(users):
lic, parameters = getLicenseParameters('insert')
_, jcount, users = getEntityArgument(users)
count = len(parameters[LICENSE_PRODUCT_SKUIDS])
i = 0
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
i += 1
_createLicenses(lic, productSku[0], productSku[1], parameters, jcount, users, i, count)
if parameters['csvPF']:
parameters['csvPF'].writeCSVfile('Create Licenses')
# gam <UserTypeEntity> update license <SKUID> [product|productid <ProductID>] [from] <SKUID>
# [preview] [actioncsv] [archive]
def updateLicense(users):
lic, parameters = getLicenseParameters('patch')
j, jcount, users = getEntityArgument(users)
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][parameters['preview']])
cd = None
if parameters['preview']:
message = Act.PREVIEW
elif parameters['archive']:
cd = buildGAPIObject(API.DIRECTORY)
productId, skuId, oldSkuId = parameters[LICENSE_PRODUCT_SKUIDS][0]
body = {'skuId': skuId}
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.FOR, jcount, Ent.USER)
Ind.Increment()
for user in users:
j += 1
user = normalizeEmailAddressOrUID(user)
try:
if not parameters['preview']:
callGAPI(lic.licenseAssignments(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.NOT_FOUND, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
productId=productId, skuId=oldSkuId, userId=user, body=body, fields='')
message = Act.SUCCESS
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.skuIdToDisplayName(skuId)],
Act.MODIFIER_FROM, SKU.skuIdToDisplayName(oldSkuId), j, jcount)
except (GAPI.internalError, GAPI.notFound, GAPI.conditionNotMet, GAPI.invalid,
GAPI.forbidden, GAPI.backendError, GAPI.serviceNotAvailable) as e:
message = str(e)
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(oldSkuId)], message, j, jcount)
except GAPI.userNotFound as e:
message = str(e)
entityUnknownWarning(Ent.USER, user, j, jcount)
if parameters['archive'] and message == Act.SUCCESS:
Act.Set(Act.ARCHIVE)
try:
callGAPI(cd.users(), 'update',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES],
retryReasons=[GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES],
userKey=user, body={'archived': True})
entityActionPerformed([Ent.USER, user], j, jcount)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, j, jcount)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
GAPI.insufficientArchivedUserLicenses) as e:
entityActionFailedWarning([Ent.USER, user], str(e), j, jcount)
Act.Set(Act.UPDATE)
if parameters['csvPF']:
_writeLicenseAction(productId, skuId, oldSkuId, parameters, user, Act.UPDATE, message)
Ind.Decrement()
if parameters['csvPF']:
parameters['csvPF'].writeCSVfile('Update Licenses')
def _deleteLicenses(lic, productId, skuId, parameters, jcount, users, i, count):
Act.Set([Act.DELETE, Act.DELETE_PREVIEW][parameters['preview']])
if parameters['preview']:
message = Act.PREVIEW
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.FROM_LC, jcount, Ent.USER, i, count)
Ind.Increment()
j = 0
for user in users:
j += 1
user = normalizeEmailAddressOrUID(user)
try:
if not parameters['preview']:
callGAPI(lic.licenseAssignments(), 'delete',
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.NOT_FOUND, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
productId=productId, skuId=skuId, userId=user)
message = Act.SUCCESS
entityActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], j, jcount)
except (GAPI.internalError, GAPI.notFound, GAPI.conditionNotMet, GAPI.invalid,
GAPI.forbidden, GAPI.backendError, GAPI.serviceNotAvailable) as e:
message = str(e)
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
except GAPI.userNotFound as e:
message = str(e)
entityUnknownWarning(Ent.USER, user, j, jcount)
if parameters['csvPF']:
_writeLicenseAction(productId, skuId, None, parameters, user, Act.DELETE, message)
Ind.Decrement()
# gam <UserTypeEntity> delete license <SKUIDList> [product|productid <ProductID>] [preview] [actioncsv]
def deleteLicense(users):
lic, parameters = getLicenseParameters('delete')
_, jcount, users = getEntityArgument(users)
count = len(parameters[LICENSE_PRODUCT_SKUIDS])
i = 0
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
i += 1
_deleteLicenses(lic, productSku[0], productSku[1], parameters, jcount, users, i, count)
if parameters['csvPF']:
parameters['csvPF'].writeCSVfile('Delete Licenses')
# gam <UserTypeEntity> sync license <SKUIDList> [product|productid <ProductID>]
# [addonly|removeonly] [allskus|onesku] [preview] [actioncsv]
def syncLicense(users):
lic, parameters = getLicenseParameters('sync')
_, _, users = getEntityArgument(users)
usersSet = set()
currentLicenses = {}
for user in users:
usersSet.add(normalizeEmailAddressOrUID(user))
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
skuId = productSku[1]
currentLicenses[skuId] = set(getItemsToModify(Cmd.ENTITY_LICENSES, skuId))
count = len(parameters[LICENSE_PRODUCT_SKUIDS])
if parameters['syncACLsMode'] != 'onesku':
i = 0
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
i += 1
if parameters['syncOperation'] != 'addonly':
deleteSet = currentLicenses[skuId]-usersSet
_deleteLicenses(lic, productSku[0], productSku[1], parameters, len(deleteSet), deleteSet, i, count)
if parameters['syncOperation'] != 'removeonly':
addSet = usersSet-currentLicenses[skuId]
_createLicenses(lic, productSku[0], productSku[1], parameters, len(addSet), addSet, i, count)
else: #parameters['syncACLsMode'] == 'onesku':
if parameters['syncOperation'] != 'addonly':
i = 0
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
i += 1
deleteSet = currentLicenses[productSku[1]]-usersSet
_deleteLicenses(lic, productSku[0], productSku[1], parameters, len(deleteSet), deleteSet, i, count)
if parameters['syncOperation'] != 'removeonly':
addSet = usersSet.copy()
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
addSet = addSet-currentLicenses[productSku[1]]
i = 0
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
if not addSet:
break
i += 1
addSet -= _createLicenses(lic, productSku[0], productSku[1], parameters, len(addSet), addSet, i, count, returnDoneSet=True)
if addSet:
productId = productSku[0]
skuIds = []
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
skuIds.append(productSku[1])
skuIdsList = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER].join(skuIds)
message = Msg.NO_AVAILABLE_LICENSES
for user in addSet:
user = normalizeEmailAddressOrUID(user)
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, skuIdsList], message)
if parameters['csvPF']:
_writeLicenseAction(productId, skuIdsList, None, parameters, user, Act.ADD, message)
if parameters['csvPF']:
parameters['csvPF'].writeCSVfile('Sync Licenses')
# gam <UserTypeEntity> update photo
# ([<FileNamePattern>] |
# ([drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]) |
# (gphoto <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity>))
# #user# and #email" will be replaced with user email address #username# will be replaced by portion of email address in front of @
# in <FileNamePattern> and <DriveFileNameEntity>
def updatePhoto(users):
cd = buildGAPIObject(API.DIRECTORY)
baseFileIdEntity = drive = owner = None
sourceFolder = os.getcwd()
if Cmd.NumArgumentsRemaining() == 1:
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
else:
filenamePattern = '#email#.jpg'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'drivedir':
sourceFolder = GC.Values[GC.DRIVE_DIR]
elif myarg == 'sourcefolder':
sourceFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(sourceFolder):
entityDoesNotExistExit(Ent.DIRECTORY, sourceFolder)
elif myarg == 'filename':
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
elif myarg == 'gphoto':
owner, drive = buildGAPIServiceObject(API.DRIVE3, getEmailAddress())
if not drive:
return
baseFileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
else:
unknownArgumentExit()
p = re.compile('^(ht|f)tps?://.*$')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, userName, _ = splitEmailAddressOrUID(user)
filename = _substituteForUser(filenamePattern, user, userName)
if baseFileIdEntity is not None:
fileIdEntity = baseFileIdEntity.copy()
if fileIdEntity['query'] is not None:
fileIdEntity['query'] = _substituteForUser(fileIdEntity['query'], user, userName)
_, _, jcount = _validateUserGetFileIDs(owner, 0, 0, fileIdEntity, drive=drive, entityType=None)
if jcount == 0:
entityItemValueListActionNotPerformedWarning([Ent.USER, user], [Ent.OWNER, owner],
Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)), i, count)
continue
if jcount > 1:
entityItemValueListActionNotPerformedWarning([Ent.USER, user], [Ent.OWNER, owner],
Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])), i, count)
continue
fb = TemporaryFile(mode='wb+')
filename = fileIdEntity['list'][0]
request = drive.files().get_media(fileId=filename)
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
done = False
while not done:
_, done = downloader.next_chunk()
fb.seek(0)
image_data = fb.read()
fb.close()
elif p.match(filename):
try:
status, image_data = getHttpObj().request(filename, 'GET')
if status['status'] != '200':
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, i, count)
continue
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
continue
else:
if sourceFolder is not None:
filename = os.path.join(sourceFolder, filename)
try:
with open(os.path.expanduser(filename), 'rb') as f:
image_data = f.read()
except (OSError, IOError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
continue
body = {'photoData': base64.urlsafe_b64encode(image_data).decode(UTF8)}
try:
callGAPI(cd.users().photos(), 'update',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT, GAPI.CONDITION_NOT_MET],
userKey=user, body=body, fields='')
entityActionPerformed([Ent.USER, user, Ent.PHOTO, filename], i, count)
except (GAPI.invalidInput, GAPI.conditionNotMet) as e:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
except (GAPI.userNotFound, GAPI.forbidden):
entityUnknownWarning(Ent.USER, user, i, count)
# gam <UserTypeEntity> delete photo
def deletePhoto(users):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
try:
callGAPI(cd.users().photos(), 'delete',
bailOnInternalError=True,
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND, GAPI.INTERNAL_ERROR],
userKey=user)
entityActionPerformed([Ent.USER, user, Ent.PHOTO, ''], i, count)
except (GAPI.photoNotFound, GAPI.internalError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, ''], str(e), i, count)
except (GAPI.userNotFound, GAPI.forbidden):
entityUnknownWarning(Ent.USER, user, i, count)
def getPhoto(users, profileMode):
cd = buildGAPIObject(API.DIRECTORY)
targetFolder = os.getcwd()
filenamePattern = '#email#.#ext#'
noDefault = returnURLonly = False
writeFileData = showPhotoData = True
size = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'drivedir':
targetFolder = GC.Values[GC.DRIVE_DIR]
elif myarg == 'targetfolder':
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
elif myarg == 'filename':
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
elif myarg == 'nofile':
writeFileData = False
elif myarg == 'noshow':
showPhotoData = False
elif profileMode and myarg == 'returnurlonly':
returnURLonly = True
elif myarg == 'nodefault':
noDefault = True
elif profileMode and myarg == 'size':
size = getInteger(minVal=50)
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if profileMode:
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
else:
user = normalizeEmailAddressOrUID(user)
_, userName, _ = splitEmailAddressOrUID(user)
filename = os.path.join(targetFolder, _substituteForUser(filenamePattern, user, userName))
try:
if not showPhotoData:
entityPerformActionNumItems([Ent.USER, user], 1, Ent.PHOTO, i, count)
if not profileMode:
photo = callGAPI(cd.users().photos(), 'get',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND],
userKey=user)
if showPhotoData:
writeStdout(photo['photoData']+'\n')
photo_data = base64.urlsafe_b64decode(photo['photoData'])
else:
result = callGAPI(people.people(), 'get',
throwReasons=[GAPI.NOT_FOUND],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName='people/me', personFields='photos')
default = False
url = None
for photo in result.get('photos', []):
if photo['metadata']['source']['type'] == 'PROFILE':
default = photo.get('default', False)
url = photo['url']
break
if not url:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], Msg.PROFILE_PHOTO_NOT_FOUND, i, count)
continue
if noDefault and default:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], Msg.PROFILE_PHOTO_IS_DEFAULT, i, count)
continue
if returnURLonly:
writeStdout(f'{url}\n')
continue
if size:
url = re.sub(r"=s\d+$", f"=s{size}", url)
try:
status, photo_data = getHttpObj().request(url, 'GET')
if status['status'] != '200':
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, i, count)
continue
if showPhotoData:
writeStdout(base64.encodebytes(photo_data).decode(UTF8))
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
continue
if writeFileData:
if photo_data[:3] == b'\xff\xd8\xff':
extension = 'jpg'
elif photo_data[:8] == b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a':
extension = 'png'
elif photo_data[:6] == b'\x47\x49\x46\x38\x37\x61' or photo_data[:6] == b'\x47\x49\x46\x38\x39\x61':
extension = 'gif'
elif photo_data[:2] == b'\x42\x4d':
extension= 'bmp'
elif photo_data[:4] == b'\x49\x49\x2A\x00' or photo_data[:4] == b'\x4D\x4D\x00\x2A':
extension= 'tif'
else:
extension = 'img'
filenameExt = filename.replace('#ext#', extension)
status, e = writeFileReturnError(filenameExt, photo_data, mode='wb')
if status:
if not showPhotoData:
entityActionPerformed([Ent.USER, user, Ent.PHOTO, filenameExt], i, count)
else:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filenameExt], str(e), i, count)
except (GAPI.notFound, GAPI.photoNotFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], str(e), i, count)
except (GAPI.userNotFound, GAPI.forbidden):
entityUnknownWarning(Ent.USER, user, i, count)
# gam <UserTypeEntity> get photo [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]]
# [noshow] [nofile]
def getUserPhoto(users):
getPhoto(users, False)
# gam <UserTypeEntity> get profilephoto [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
# [noshow] [nofile] [returnurlonly] [nodefault] [size <Integer>]
def getProfilePhoto(users):
getPhoto(users, True)
PROFILE_SHARING_CHOICE_MAP = {
'share': True,
'shared': True,
'unshare': False,
'unshared': False,
}
def _setShowProfile(users, function, **kwargs):
cd = buildGAPIObject(API.DIRECTORY)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
try:
result = callGAPI(cd.users(), function,
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN],
userKey=user, fields='includeInGlobalAddressList', **kwargs)
printEntity([Ent.USER, user, Ent.PROFILE_SHARING_ENABLED, result.get('includeInGlobalAddressList', UNKNOWN)], i, count)
except (GAPI.userNotFound, GAPI.forbidden):
entityUnknownWarning(Ent.USER, user, i, count)
# gam <UserTypeEntity> profile share|shared|unshare|unshared
def setProfile(users):
body = {'includeInGlobalAddressList': getChoice(PROFILE_SHARING_CHOICE_MAP, mapChoice=True)}
_setShowProfile(users, 'update', body=body)
# gam <UserTypeEntity> show profile
def showProfile(users):
_setShowProfile(users, 'get')
# gam <UserTypeEntity> create sheet
# ((json [charset <Charset>] <SpreadsheetJSONCreateRequest>) |
# (json file <FileName> [charset <Charset>]))
# [<DriveFileParentAttribute>]
# [formatjson] [returnidonly]
def createSheet(users):
parameters = initDriveFileAttributes()
parentBody = {}
changeParents = returnIdOnly = False
addParents = ''
removeParents = ROOT
body = {}
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'json':
body = getJSON([])
elif getDriveFileParentAttribute(myarg, parameters):
changeParents = True
elif myarg == 'returnidonly':
returnIdOnly = True
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
continue
if changeParents:
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parameters):
continue
addParents = ','.join(parentBody['parents'])
try:
result = callGAPI(sheet.spreadsheets(), 'create',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
body=body)
spreadsheetId = result['spreadsheetId']
if not returnIdOnly and not FJQC.formatJSON:
entityActionPerformed([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], i, count)
parentId = ROOT
parentMsg = Act.SUCCESS
if changeParents:
try:
callGAPI(drive.files(), 'update',
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
fileId=result['spreadsheetId'],
addParents=addParents, removeParents=removeParents, fields='', supportsAllDrives=True)
parentId = addParents
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition,
GAPI.cannotAddParent) as e:
parentMsg = f'{ERROR_PREFIX}{addParents}: {str(e)}'
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
parentMsg = f'{ERROR_PREFIX}{addParents}: {str(e)}'
if returnIdOnly:
writeStdout(f'{spreadsheetId}\n')
continue
if FJQC.formatJSON:
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "parentId": "{parentId}", '\
'"parentAssignment": "{parentMsg}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
continue
Ind.Increment()
for field in ['spreadsheetId', 'spreadsheetUrl']:
printKeyValueList([field, result[field]])
printKeyValueList(['parentId', parentId])
printKeyValueList(['parentAssignment', parentMsg])
for field in ['properties', 'sheets', 'namedRanges', 'developerMetadata']:
if field in result:
showJSON(field, result[field])
Ind.Decrement()
except (GAPI.notFound, GAPI.forbidden, GAPI.internalError,
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, ''], str(e), i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
def _validateUserGetSpreadsheetIDs(user, i, count, fileIdEntity, showEntityType):
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.SPREADSHEET if showEntityType else None)
if jcount == 0:
return (user, None, 0)
user, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
if not sheet:
return (user, None, 0)
return (user, sheet, jcount)
# gam <UserTypeEntity> update sheet <DriveFileEntity>
# ((json [charset <Charset>] <SpreadsheetJSONUpdateRequest>) |
# (json file <FileName> [charset <Charset>]))
# [formatjson]
def updateSheets(users):
spreadsheetIdEntity = getDriveFileEntity()
body = {}
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'json':
body = getJSON([])
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for spreadsheetId in spreadsheetIdEntity['list']:
j += 1
try:
result = callGAPI(sheet.spreadsheets(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, body=body)
if FJQC.formatJSON:
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
continue
entityActionPerformed([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], j, jcount)
Ind.Increment()
for field in ['replies', 'updatedSpreadsheet']:
if field in result:
showJSON(field, result[field])
Ind.Decrement()
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
SPREADSHEET_FIELDS_CHOICE_MAP = {
'developermetadata': 'developerMetadata',
'namedranges': 'namedRanges',
'properties': 'properties',
'sheets': 'sheets',
'spreadsheetid': 'spreadsheetId',
'spreadsheeturl': 'spreadsheetUrl',
}
SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP = {
'properties': 'sheets.properties',
'data': 'sheets.data',
'merges': 'sheets.merges',
'conditionalformats': 'sheets.conditionalFormats',
'filterviews': 'sheets.filterViews',
'protectedranges': 'sheets.protectedRanges',
'basicfilter': 'sheets.basicFilter',
'charts': 'sheets.charts',
'bandedranges': 'sheets.bandedRanges',
'developermetadata': 'sheets.developerMetadata',
'rowgroups': 'sheets.rowGroups',
'columngroups': 'sheets.columnGroups',
'slicers': 'sheets.slicers',
}
# gam <UserTypeEntity> info|show sheet <DriveFileEntity>
# [fields <SpreadsheetFieldList>] [sheetsfields <SpreadsheetSheetsFieldList>]
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
# [includegriddata [<Boolean>]] [shownames]
# [formatjson]
# gam <UserTypeEntity> print sheet <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [fields <SpreadsheetFieldList>] [sheetsfields <SpreadsheetSheetsFieldList>]
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
# [includegriddata [<Boolean>]] [shownames]
# [formatjson [quotechar <Character>]]
def infoPrintShowSheets(users):
csvPF = CSVPrintFile(['User', 'spreadsheetId'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
spreadsheetIdEntity = getDriveFileEntity()
fieldsList = []
ranges = []
includeGridData = showSheetNames = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'range':
ranges.append(getString(Cmd.OB_SPREADSHEET_RANGE))
elif myarg == 'rangelist':
ranges.extend(convertEntityToList(getString(Cmd.OB_SPREADSHEET_RANGE_LIST), shlexSplit=True))
elif myarg == 'includegriddata':
includeGridData = getBoolean()
elif getFieldsList(myarg, SPREADSHEET_FIELDS_CHOICE_MAP, fieldsList, initialField='spreadsheetId'):
pass
elif myarg == 'sheetsfields':
for field in _getFieldsList():
if field in SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP:
fieldsList.append(SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP[field])
else:
invalidChoiceExit(field, SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP, True)
elif myarg == 'shownames':
showSheetNames = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and showSheetNames:
csvPF.AddTitles('spreadsheetName')
csvPF.SetSortAllTitles()
if FJQC.formatJSON:
csvPF.AddJSONTitles('spreadsheetName')
csvPF.MoveJSONTitlesToEnd(['JSON'])
if includeGridData and fieldsList:
fieldsList.append(SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP['data'])
fields = getFieldsFromFieldsList(fieldsList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
if jcount == 0:
continue
if showSheetNames:
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
Ind.Increment()
j = 0
for spreadsheetId in spreadsheetIdEntity['list']:
j += 1
try:
result = callGAPI(sheet.spreadsheets(), 'get',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, ranges=ranges, includeGridData=includeGridData, fields=fields)
if not includeGridData and 'sheets' in result:
for usheet in result['sheets']:
usheet.pop('data', None)
if showSheetNames:
try:
spreadsheetName = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=spreadsheetId, fields='name', supportsAllDrives=True)['name']
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
spreadsheetName = spreadsheetId
if not csvPF:
if FJQC.formatJSON:
baserow = {'User': user, 'spreadsheetId': spreadsheetId}
if showSheetNames:
baserow['spreadsheetName'] = spreadsheetName
baserow['JSON'] = result
printLine(json.dumps(baserow, ensure_ascii=False, sort_keys=False)+'\n')
continue
if showSheetNames:
printEntity([Ent.SPREADSHEET, f'{spreadsheetName}({spreadsheetId})'], j, jcount)
else:
printEntity([Ent.SPREADSHEET, spreadsheetId], j, jcount)
Ind.Increment()
if 'spreadsheetUrl' in result:
printKeyValueList(['spreadsheetUrl', result['spreadsheetUrl']])
for field in ['properties', 'sheets', 'namedRanges', 'developerMetadata', 'dataSources', 'dataSourceSchedules']:
if field in result:
if field != 'sheets':
showJSON(field, result[field])
else:
jcount = len(result[field])
j = 0
for usheet in result[field]:
j += 1
printEntity([Ent.SHEET, usheet.get('properties', {}).get('title', '')], j, jcount)
Ind.Increment()
showJSON(None, usheet)
Ind.Decrement()
Ind.Decrement()
else:
baserow = {'User': user, 'spreadsheetId': spreadsheetId}
if showSheetNames:
baserow['spreadsheetName'] = spreadsheetName
row = flattenJSON(result, flattened=baserow.copy())
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
baserow['JSON'] = json.dumps(cleanJSON(result), ensure_ascii=False, sort_keys=False)
csvPF.WriteRowNoFilter(baserow)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Spreadsheet')
SHEET_VALUE_INPUT_OPTIONS_MAP = {
'raw': 'RAW',
'userentered': 'USER_ENTERED',
}
SHEET_DIMENSIONS_MAP = {
'rows': 'ROWS',
'columns': 'COLUMNS',
}
SHEET_VALUE_RENDER_OPTIONS_MAP = {
'formula': 'FORMULA',
'formattedvalue': 'FORMATTED_VALUE',
'unformattedvalue': 'UNFORMATTED_VALUE',
}
SHEET_DATETIME_RENDER_OPTIONS_MAP = {
'serialnumber': 'SERIAL_NUMBER',
'formattedstring': 'FORMATTED_STRING',
}
SHEET_INSERT_DATA_OPTIONS_MAP = {
'overwrite': 'OVERWRITE',
'insertrows': 'INSERT_ROWS',
}
def _getSpreadsheetRangesValues(append):
spreadsheetRangesValues = []
kwargs = {
'valueInputOption': 'USER_ENTERED',
'includeValuesInResponse': False,
'responseValueRenderOption': 'FORMATTED_VALUE',
'responseDateTimeRenderOption': 'FORMATTED_STRING',
}
if append:
kwargs['insertDataOption'] = 'INSERT_ROWS'
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'json':
if append and spreadsheetRangesValues:
usageErrorExit(Msg.ONLY_ONE_JSON_RANGE_ALLOWED)
spreadsheetRangeValue = getJSON([])
if isinstance(spreadsheetRangeValue, dict) and 'valueRanges' in spreadsheetRangeValue:
spreadsheetRangesValues.extend(spreadsheetRangeValue['valueRanges'])
elif isinstance(spreadsheetRangeValue, list):
spreadsheetRangesValues.extend(spreadsheetRangeValue)
else:
spreadsheetRangesValues.append(spreadsheetRangeValue)
if append and len(spreadsheetRangesValues) > 1:
Cmd.Backup()
usageErrorExit(Msg.ONLY_ONE_JSON_RANGE_ALLOWED)
elif myarg in SHEET_VALUE_INPUT_OPTIONS_MAP:
kwargs['valueInputOption'] = SHEET_VALUE_INPUT_OPTIONS_MAP[myarg]
elif myarg == 'includevaluesinresponse':
kwargs['includeValuesInResponse'] = getBoolean()
elif myarg in SHEET_VALUE_RENDER_OPTIONS_MAP:
kwargs['responseValueRenderOption'] = SHEET_VALUE_RENDER_OPTIONS_MAP[myarg]
elif myarg in SHEET_DATETIME_RENDER_OPTIONS_MAP:
kwargs['responseDateTimeRenderOption'] = SHEET_DATETIME_RENDER_OPTIONS_MAP[myarg]
elif append and myarg in SHEET_INSERT_DATA_OPTIONS_MAP:
kwargs['insertDataOption'] = SHEET_INSERT_DATA_OPTIONS_MAP[myarg]
else:
FJQC.GetFormatJSON(myarg)
return (kwargs, spreadsheetRangesValues, FJQC)
def _showValueRange(valueRange):
Ind.Increment()
printKeyValueList(['majorDimension', valueRange['majorDimension']])
printKeyValueList(['range', valueRange['range']])
printKeyValueList(['value', '{'+f'"values": {json.dumps(valueRange.get("values", []), ensure_ascii=False, sort_keys=False)}'+'}'])
Ind.Decrement()
def _showUpdateValuesResponse(result, k, kcount):
printKeyValueListWithCount(['updatedRange', result['updatedRange']], k, kcount)
Ind.Increment()
for field in ['updatedRows', 'updatedColumns', 'updatedCells']:
printKeyValueList([field, result[field]])
if 'updatedData' in result:
printKeyValueList(['updatedData', ''])
_showValueRange(result['updatedData'])
Ind.Decrement()
# gam <UserTypeEntity> append sheetrange <DriveFileEntity>
# ((json [charset <Charset>] <SpreadsheetJSONRangeValues>|<SpreadsheetJSONRangeValuesList>) |
# (json file <FileName> [charset <Charset>]))
# [overwrite|insertrows]
# [raw|userentered] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
# [includevaluesinresponse [<Boolean>]] [formatjson]
def appendSheetRanges(users):
spreadsheetIdEntity = getDriveFileEntity()
kwargs, spreadsheetRangesValues, FJQC = _getSpreadsheetRangesValues(True)
kcount = len(spreadsheetRangesValues)
body = spreadsheetRangesValues[0] if kcount > 0 else {}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for spreadsheetId in spreadsheetIdEntity['list']:
j += 1
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
Ind.Increment()
k = 1
try:
result = callGAPI(sheet.spreadsheets().values(), 'append',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, range=body['range'], body=body, **kwargs)
if FJQC.formatJSON:
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
continue
for field in ['tableRange']:
if field in result:
printKeyValueList([field, result[field]])
_showUpdateValuesResponse(result['updates'], k, kcount)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> update sheetrange <DriveFileEntity>
# ((json [charset <Charset>] <SpreadsheetJSONRangeValues>|<SpreadsheetJSONRangeValuesList>)+
# (json file <FileName> [charset <Charset>]))+
# [raw|userentered] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
# [includevaluesinresponse [<Boolean>]] [formatjson]
def updateSheetRanges(users):
spreadsheetIdEntity = getDriveFileEntity()
body, spreadsheetRangesValues, FJQC = _getSpreadsheetRangesValues(False)
body['data'] = spreadsheetRangesValues
kcount = len(spreadsheetRangesValues)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for spreadsheetId in spreadsheetIdEntity['list']:
j += 1
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
Ind.Increment()
try:
result = callGAPI(sheet.spreadsheets().values(), 'batchUpdate',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, body=body)
if FJQC.formatJSON:
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
continue
for field in ['totalUpdatedRows', 'totalUpdatedColumns', 'totalUpdatedCells', 'totalUpdatedSheets']:
printKeyValueList([field, result[field]])
k = 0
for response in result.get('responses', []):
k += 1
_showUpdateValuesResponse(response, k, kcount)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> clear sheetrange <DriveFileEntity>
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
# [formatjson]
def clearSheetRanges(users):
spreadsheetIdEntity = getDriveFileEntity()
body = {'ranges': []}
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'range':
body['ranges'].append(getString(Cmd.OB_SPREADSHEET_RANGE))
elif myarg == 'rangelist':
body['ranges'].extend(convertEntityToList(getString(Cmd.OB_SPREADSHEET_RANGE_LIST), shlexSplit=True))
else:
FJQC.GetFormatJSON(myarg)
kcount = len(body['ranges'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for spreadsheetId in spreadsheetIdEntity['list']:
j += 1
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
Ind.Increment()
try:
result = callGAPIitems(sheet.spreadsheets().values(), 'batchClear', 'clearedRanges',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, body=body)
if FJQC.formatJSON:
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps({"clearedRanges": result}, ensure_ascii=False, sort_keys=False)}'+'}')
continue
k = 0
for clearedRange in result:
k += 1
printKeyValueListWithCount(['range', clearedRange], k, kcount)
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> print sheetrange <DriveFileEntity> [todrive <ToDriveAttribute>*]
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
# [rows|columns] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
# [formatjson [quotechar <Character>] [valuerangesonly [<Boolean>]]]
# gam <UserTypeEntity> show sheetrange <DriveFileEntity>
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
# [rows|columns] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
# [formatjson [valuerangesonly [<Boolean>]]]
def printShowSheetRanges(users):
csvPF = CSVPrintFile(['User', 'spreadsheetId'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
spreadsheetIdEntity = getDriveFileEntity()
spreadsheetRanges = []
kwargs = {
'majorDimension': 'ROWS',
'valueRenderOption': 'FORMATTED_VALUE',
'dateTimeRenderOption': 'FORMATTED_STRING',
}
valueRangesOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'range':
spreadsheetRanges.append(getString(Cmd.OB_SPREADSHEET_RANGE))
elif myarg == 'rangelist':
spreadsheetRanges.extend(convertEntityToList(getString(Cmd.OB_SPREADSHEET_RANGE_LIST), shlexSplit=True))
elif myarg == 'valuerangesonly':
valueRangesOnly = getBoolean()
elif myarg in SHEET_DIMENSIONS_MAP:
kwargs['majorDimension'] = SHEET_DIMENSIONS_MAP[myarg]
elif myarg in SHEET_VALUE_RENDER_OPTIONS_MAP:
kwargs['valueRenderOption'] = SHEET_VALUE_RENDER_OPTIONS_MAP[myarg]
elif myarg in SHEET_DATETIME_RENDER_OPTIONS_MAP:
kwargs['dateTimeRenderOption'] = SHEET_DATETIME_RENDER_OPTIONS_MAP[myarg]
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and FJQC.formatJSON and valueRangesOnly:
csvPF.SetJSONTitles(['JSON'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not csvPF and not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for spreadsheetId in spreadsheetIdEntity['list']:
j += 1
try:
result = callGAPI(sheet.spreadsheets().values(), 'batchGet',
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
spreadsheetId=spreadsheetId, ranges=spreadsheetRanges, fields='valueRanges', **kwargs)
valueRanges = result.get('valueRanges', [])
kcount = len(valueRanges)
if not csvPF:
if FJQC.formatJSON:
if not valueRangesOnly:
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
else:
printLine(json.dumps(result.get('valueRanges', []), ensure_ascii=False, sort_keys=False))
continue
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
Ind.Increment()
k = 0
for valueRange in valueRanges:
k += 1
printKeyValueListWithCount(['range', valueRange['range']], k, kcount)
_showValueRange(valueRange)
Ind.Decrement()
else:
if kcount:
row = flattenJSON(result, flattened={'User': user, 'spreadsheetId': spreadsheetId})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
if not valueRangesOnly:
csvPF.WriteRowNoFilter({'User': user, 'spreadsheetId': spreadsheetId,
'JSON': json.dumps(result, ensure_ascii=False, sort_keys=False)})
else:
csvPF.WriteRowNoFilter({'JSON': json.dumps(result.get('valueRanges', []), ensure_ascii=False, sort_keys=False)})
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
break
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Spreadsheet')
# Token commands utilities
def commonClientIds(clientId):
if clientId == 'gasmo':
return '1095133494869.apps.googleusercontent.com'
return clientId
# gam <UserTypeEntity> delete token|tokens|3lo|oauth clientid <ClientID>
def deleteTokens(users):
cd = buildGAPIObject(API.DIRECTORY)
checkArgumentPresent('clientid', required=True)
clientId = commonClientIds(getString(Cmd.OB_CLIENT_ID))
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
try:
callGAPI(cd.tokens(), 'get',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
userKey=user, clientId=clientId, fields='')
callGAPI(cd.tokens(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
userKey=user, clientId=clientId)
entityActionPerformed([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], i, count)
except (GAPI.notFound, GAPI.resourceNotFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], str(e), i, count)
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden):
entityUnknownWarning(Ent.USER, user, i, count)
TOKENS_FIELDS_TITLES = ['clientId', 'displayText', 'anonymous', 'nativeApp', 'userKey', 'scopes']
TOKENS_AGGREGATE_FIELDS_TITLES = ['clientId', 'displayText', 'anonymous', 'nativeApp', 'users', 'scopes']
TOKENS_AGGREGATE_ORDERBY_CHOICE_MAP = {
'clientid': 'clientId',
'id': 'clientId',
'displaytext': 'displayText',
'appname': 'displayText',
}
TOKENS_TITLE_MAP = {
'clientId': 'Client ID',
'displayText': 'App Name',
'user': 'user',
}
def _printShowTokens(entityType, users):
def _printToken(token):
row = {}
for item in token:
if item != 'scopes':
row[item] = token.get(item, '')
else:
row[item] = delimiter.join(token.get('scopes', []))
csvPF.WriteRow(row)
def _showToken(token, keyTitle, keyField, j, jcount):
printKeyValueListWithCount([keyTitle, token[keyField]], j, jcount)
Ind.Increment()
for item in sorted(token):
if item not in {keyField, 'scopes'}:
printKeyValueList([item, token.get(item, '')])
item = 'scopes'
printKeyValueList([item, None])
Ind.Increment()
for it in sorted(token.get(item, [])):
printKeyValueList([it])
Ind.Decrement()
Ind.Decrement()
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile() if Act.csvFormat() else None
clientId = None
aggregateUsersBy = ''
orderBy = 'clientId'
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
aggregateTokensById = {}
tokenNameIdMap = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'clientid':
clientId = commonClientIds(getString(Cmd.OB_CLIENT_ID))
elif myarg == 'orderby':
orderBy = getChoice(TOKENS_AGGREGATE_ORDERBY_CHOICE_MAP, mapChoice=True)
elif myarg == 'aggregateusersby':
aggregateUsersBy = getChoice(TOKENS_AGGREGATE_ORDERBY_CHOICE_MAP, mapChoice=True)
if aggregateUsersBy == 'displayText':
tokenNameIdMap = {}
elif myarg == 'usertokencounts':
aggregateUsersBy = 'user'
elif myarg == 'delimiter':
delimiter = getCharacter()
elif not entityType:
Cmd.Backup()
entityType, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
else:
unknownArgumentExit()
if not entityType:
users = getItemsToModify(Cmd.ENTITY_ALL_USERS_NS, None)
if csvPF:
if not aggregateUsersBy:
csvPF.SetTitles(['user']+TOKENS_FIELDS_TITLES)
elif aggregateUsersBy != 'user':
csvPF.SetTitles(TOKENS_AGGREGATE_FIELDS_TITLES)
else:
csvPF.SetTitles(['user', 'tokenCount'])
else:
if not aggregateUsersBy:
tokenTitle = TOKENS_TITLE_MAP[orderBy]
else:
tokenTitle = TOKENS_TITLE_MAP[aggregateUsersBy]
fields = ','.join(TOKENS_FIELDS_TITLES)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
try:
if csvPF or aggregateUsersBy:
printGettingEntityItemForWhom(Ent.ACCESS_TOKEN, user, i, count)
if clientId:
results = [callGAPI(cd.tokens(), 'get',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
userKey=user, clientId=clientId, fields=fields)]
else:
results = callGAPIitems(cd.tokens(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN, GAPI.BAD_REQUEST],
userKey=user, fields=f'items({fields})')
jcount = len(results)
if not aggregateUsersBy:
if not csvPF:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.ACCESS_TOKEN, i, count)
Ind.Increment()
j = 0
for token in sorted(results, key=lambda k: k[orderBy]):
j += 1
_showToken(token, tokenTitle, orderBy, j, jcount)
Ind.Decrement()
else:
if results:
for token in sorted(results, key=lambda k: k[orderBy]):
row = {'user': user, 'scopes': delimiter.join(token.get('scopes', []))}
for item in token:
if item != 'scopes':
row[item] = token.get(item, '')
csvPF.WriteRow(row)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'user': user})
elif aggregateUsersBy != 'user':
if results:
for token in results:
tokcid = token['clientId']
if tokcid not in aggregateTokensById:
token.pop('userKey', None)
token['users'] = 0
aggregateTokensById[tokcid] = token
aggregateTokensById[tokcid]['users'] += 1
if tokenNameIdMap is not None:
tokname = token['displayText']
if tokname not in tokenNameIdMap:
tokenNameIdMap[tokname] = set()
tokenNameIdMap[tokname].add(tokcid)
else: # aggregateUsersBy == 'user':
aggregateTokensById[user] = jcount
except (GAPI.notFound, GAPI.resourceNotFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], str(e), i, count)
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
entityUnknownWarning(Ent.USER, user, i, count)
if aggregateUsersBy == 'clientId':
if not csvPF:
jcount = len(aggregateTokensById)
performActionNumItems(jcount, Ent.ACCESS_TOKEN)
Ind.Increment()
j = 0
for _, token in sorted(iter(aggregateTokensById.items())):
j += 1
_showToken(token, tokenTitle, aggregateUsersBy, j, jcount)
Ind.Decrement()
else:
for _, token in sorted(iter(aggregateTokensById.items())):
_printToken(token)
elif aggregateUsersBy == 'displayText':
if not csvPF:
jcount = len(aggregateTokensById)
performActionNumItems(jcount, Ent.ACCESS_TOKEN)
Ind.Increment()
j = 0
for _, tokenIds in sorted(iter(tokenNameIdMap.items())):
for tokcid in sorted(tokenIds):
j += 1
_showToken(aggregateTokensById[tokcid], tokenTitle, aggregateUsersBy, j, jcount)
Ind.Decrement()
else:
for _, tokenIds in sorted(iter(tokenNameIdMap.items())):
for tokcid in sorted(tokenIds):
_printToken(aggregateTokensById[tokcid])
else: # aggregateUsersBy == 'user':
if not csvPF:
jcount = len(aggregateTokensById)
j = 0
for user, count in sorted(iter(aggregateTokensById.items())):
j += 1
printEntityKVList([Ent.USER, user], [Ent.Plural(Ent.ACCESS_TOKEN), count], j, jcount)
else:
for user, count in sorted(iter(aggregateTokensById.items())):
csvPF.WriteRow({'user': user, 'tokenCount': count})
if csvPF:
csvPF.writeCSVfile('OAuth Tokens')
# gam <UserTypeEntity> print tokens|token [todrive <ToDriveAttribute>*] [clientid <ClientID>]
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
# gam <UserTypeEntity> show tokens|token|3lo|oauth [clientid <ClientID>]
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
def printShowTokens(users):
_printShowTokens(Cmd.ENTITY_USERS, users)
# gam print tokens|token [todrive <ToDriveAttribute>*] [clientid <ClientID>]
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
# [<UserTypeEntity>]
# gam show tokens|token [clientid <ClientID>]
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
# [<UserTypeEntity>]
def doPrintShowTokens():
_printShowTokens(None, None)
# gam <UserTypeEntity> deprovision|deprov [popimap] [signout] [turnoff2sv]
def deprovisionUser(users):
cd = buildGAPIObject(API.DIRECTORY)
disablePopImap = signout = turnoff2sv = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'popimap':
disablePopImap = True
elif myarg == 'signout':
signout = True
elif myarg == 'turnoff2sv':
turnoff2sv = True
else:
unknownArgumentExit()
if disablePopImap:
imapBody = _imapDefaults(False)
popBody = _popDefaults(False)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user = normalizeEmailAddressOrUID(user)
userSuspended = checkUserSuspended(cd, user, Ent.USER, i, count)
if userSuspended is None:
continue
try:
printGettingEntityItemForWhom(Ent.APPLICATION_SPECIFIC_PASSWORD, user, i, count)
asps = callGAPIitems(cd.asps(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user, fields='items(codeId)')
codeIds = [asp['codeId'] for asp in asps]
jcount = len(codeIds)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.APPLICATION_SPECIFIC_PASSWORD, i, count)
if jcount > 0:
Ind.Increment()
j = 0
for codeId in codeIds:
j += 1
try:
callGAPI(cd.asps(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER, GAPI.FORBIDDEN],
userKey=user, codeId=codeId)
entityActionPerformed([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], j, jcount)
except (GAPI.invalid, GAPI.invalidParameter, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], str(e), j, jcount)
Ind.Decrement()
#
printGettingEntityItemForWhom(Ent.BACKUP_VERIFICATION_CODES, user, i, count)
try:
codes = callGAPIitems(cd.verificationCodes(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user, fields='items(verificationCode)')
jcount = len(codes)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.BACKUP_VERIFICATION_CODES, i, count)
if jcount > 0:
if not userSuspended:
callGAPI(cd.verificationCodes(), 'invalidate',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT],
userKey=user)
entityActionPerformed([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], i, count)
else:
entityActionNotPerformedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None],
Msg.IS_SUSPENDED_NO_BACKUPCODES, i, count)
except (GAPI.invalid, GAPI.invalidInput) as e:
entityActionFailedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], str(e), i, count)
#
printGettingEntityItemForWhom(Ent.ACCESS_TOKEN, user, i, count)
tokens = callGAPIitems(cd.tokens(), 'list', 'items',
throwReasons=[GAPI.USER_NOT_FOUND],
userKey=user, fields='items(clientId)')
jcount = len(tokens)
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.ACCESS_TOKEN, i, count)
if jcount > 0:
Ind.Increment()
j = 0
for token in tokens:
j += 1
clientId = token['clientId']
try:
callGAPI(cd.tokens(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.NOT_FOUND],
userKey=user, clientId=clientId)
entityActionPerformed([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], j, jcount)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], str(e), j, jcount)
Ind.Decrement()
#
if turnoff2sv:
Act.Set(Act.TURNOFF2SV)
try:
callGAPI(cd.twoStepVerification(), 'turnOff',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN],
userKey=user)
entityActionPerformed([Ent.USER, user], i, count)
except GAPI.invalid as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
#
if signout:
Act.Set(Act.SIGNOUT)
callGAPI(cd.users(), 'signOut',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.DOMAIN_NOT_FOUND,
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN],
userKey=user)
entityActionPerformed([Ent.USER, user], i, count)
#
Act.Set(Act.DEPROVISION)
if disablePopImap:
_setImap(user, imapBody, i, count)
_setPop(user, popBody, i, count)
#
entityActionPerformed([Ent.USER, user], i, count)
except GAPI.userNotFound:
entityUnknownWarning(Ent.USER, user, i, count)
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
# gam <UserTypeEntity> watch gmail [maxmessages <Integer>]
def watchGmail(users):
maxMessages = 100
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'maxmessages':
maxMessages = getInteger(minVal=1)
else:
unknownArgumentExit()
project = f'projects/{_getCurrentProjectId()}'
gamTopics = project+'/topics/gam-pubsub-gmail-'
gamSubscriptions = project+'/subscriptions/gam-pubsub-gmail-'
pubsub = buildGAPIObject(API.PUBSUB)
topics = callGAPIpages(pubsub.projects().topics(), 'list', items='topics',
project=project)
for atopic in topics:
if atopic['name'].startswith(gamTopics):
topic = atopic['name']
break
else:
topic = gamTopics+str(uuid.uuid4())
callGAPI(pubsub.projects().topics(), 'create',
name=topic)
body = {'policy': {'bindings': [{'members': ['serviceAccount:gmail-api-push@system.gserviceaccount.com'], 'role': 'roles/pubsub.editor'}]}}
callGAPI(pubsub.projects().topics(), 'setIamPolicy',
resource=topic, body=body)
subscriptions = callGAPIpages(pubsub.projects().topics().subscriptions(), 'list', items='subscriptions',
topic=topic)
for asubscription in subscriptions:
if asubscription.startswith(gamSubscriptions):
subscription = asubscription
break
else:
subscription = gamSubscriptions+str(uuid.uuid4())
callGAPI(pubsub.projects().subscriptions(), 'create',
name=subscription, body={'topic': topic})
gmails = {}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
gmails[user] = {'g': gmail}
callGAPI(gmails[user]['g'].users(), 'watch',
userId='me', body={'topicName': topic})
gmails[user]['seen_historyId'] = callGAPI(gmails[user]['g'].users(), 'getProfile',
userId='me', fields='historyId')['historyId']
entityPerformActionNumItems([Ent.EVENT, 'gmail'], count, Ent.USER)
while True:
results = callGAPI(pubsub.projects().subscriptions(), 'pull',
subscription=subscription, body={'maxMessages': maxMessages})
if 'receivedMessages' in results:
ackIds = []
update_history = []
for message in results['receivedMessages']:
if 'data' in message['message']:
try:
decoded_message = json.loads(base64.b64decode(message['message']['data']))
if 'historyId' in decoded_message:
update_history.append(decoded_message['emailAddress'])
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
pass
if 'ackId' in message:
ackIds.append(message['ackId'])
if ackIds:
callGAPI(pubsub.projects().subscriptions(), 'acknowledge',
subscription=subscription, body={'ackIds': ackIds})
if update_history:
for a_user in update_history:
results = callGAPI(gmails[a_user]['g'].users().history(), 'list',
userId='me', startHistoryId=gmails[a_user]['seen_historyId'])
if 'history' in results:
for history in results['history']:
if list(history) == ['messages', 'id']:
continue
if 'labelsAdded' in history:
Act.Set(Act.ADD)
for labelling in history['labelsAdded']:
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, labelling['message']['id'],
Ent.LABEL, ', '.join(labelling['labelIds'])])
if 'labelsRemoved' in history:
Act.Set(Act.REMOVE)
for labelling in history['labelsRemoved']:
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, labelling['message']['id'],
Ent.LABEL, ', '.join(labelling['labelIds'])])
if 'messagesAdded' in history:
Act.Set(Act.CREATE)
for adding in history['messagesAdded']:
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, adding['message']['id'],
Ent.LABEL, ', '.join(adding['message']['labelIds'])])
if 'messagesDeleted' in history:
Act.Set(Act.DELETE)
for deleting in history['messagesDeleted']:
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, deleting['message']['id']])
gmails[a_user]['seen_historyId'] = results['historyId']
# gam <UserTypeEntity> print gmailprofile [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show gmailprofile
def printShowGmailProfile(users):
csvPF = CSVPrintFile(['emailAddress'], 'sortall') if Act.csvFormat() else None
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.GMAIL_PROFILE, user, i, count)
try:
results = callGAPI(gmail.users(), 'getProfile',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
if not csvPF:
kvList = []
for item in ['historyId', 'messagesTotal', 'threadsTotal']:
kvList += [item, results[item]]
printEntityKVList([Ent.USER, user], kvList, i, count)
else:
csvPF.WriteRowTitles(results)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Gmail Profiles')
def _getUserGmailLabels(gmail, user, i, count, fields):
try:
labels = callGAPI(gmail.users().labels(), 'list',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me', fields=fields)
if not labels:
labels = {'labels': []}
return labels
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
return None
def _getLabelId(labels, labelName):
for label in labels['labels']:
if (labelName.upper() == label['id']) or (labelName.lower() in {label['id'].lower(), label['name'].lower()}):
return label['id']
return None
def _getLabelName(labels, labelId):
for label in labels['labels']:
if label['id'] == labelId:
return label['name']
return labelId
def _getLabelSet(labels):
labelSet = {}
for ulabel in labels['labels']:
if ulabel['type'] != LABEL_TYPE_SYSTEM:
labelSet[ulabel['name']] = ulabel['id']
return labelSet
LABEL_LABEL_LIST_VISIBILITY_CHOICE_MAP = {
'hide': 'labelHide',
'show': 'labelShow',
'showifunread': 'labelShowIfUnread',
}
LABEL_MESSAGE_LIST_VISIBILITY_CHOICES = ['hide', 'show']
LABEL_TYPE_SYSTEM = 'system'
LABEL_TYPE_USER = 'user'
def getLabelAttributes(myarg, body):
if myarg == 'labellistvisibility':
body['labelListVisibility'] = getChoice(LABEL_LABEL_LIST_VISIBILITY_CHOICE_MAP, mapChoice=True)
elif myarg == 'messagelistvisibility':
body['messageListVisibility'] = getChoice(LABEL_MESSAGE_LIST_VISIBILITY_CHOICES)
elif myarg in {'backgroundcolor', 'backgroundcolour'}:
body.setdefault('color', {})
body['color']['backgroundColor'] = getLabelColor(LABEL_BACKGROUND_COLORS)
elif myarg in {'textcolor', 'textcolour'}:
body.setdefault('color', {})
body['color']['textColor'] = getLabelColor(LABEL_TEXT_COLORS)
else:
unknownArgumentExit()
def checkLabelColor(body):
if 'color' not in body:
return
if 'backgroundColor' in body['color']:
if 'textColor' in body['color']:
return
missingArgumentExit('textcolor <LabelColorHex>')
missingArgumentExit('backgroundcolor <LabelColorHex>')
def buildLabelPath(gmail, user, i, count, body, label, labelSet, l=0, lcount=0):
label = label.strip('/')
if label in labelSet:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.DUPLICATE, l, lcount)
return
labelParts = label.split('/')
invalid = False
for j, labelPart in enumerate(labelParts):
labelParts[j] = labelPart.strip()
if not labelParts[j]:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.INVALID, l, lcount)
invalid = True
break
if invalid:
return
decrement = False
duplicate = True
labelPath = ''
j = 0
for k, labelPart in enumerate(labelParts):
if labelPath != '':
labelPath += '/'
labelPath += labelPart
if labelPath not in labelSet:
if duplicate:
jcount = len(labelParts)-k
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LABEL, i, count)
Ind.Increment()
decrement = True
duplicate = False
j += 1
body['name'] = labelPath
try:
newLabel = callGAPI(gmail.users().labels(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.DUPLICATE],
userId='me', body=body, fields='id,name')
labelSet[newLabel['name']] = newLabel['id']
entityActionPerformed([Ent.USER, user, Ent.LABEL, labelPath], j, jcount)
except GAPI.duplicate:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, labelPath], Msg.DUPLICATE, j, jcount)
break
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
if duplicate:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, labelPath], Msg.DUPLICATE, l, lcount)
if decrement:
Ind.Decrement()
def createLabels(users, labelEntity):
body = {}
buildPath = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'buildpath':
buildPath = getBoolean()
else:
getLabelAttributes(myarg, body)
checkLabelColor(body)
if not isinstance(labelEntity, dict):
userLabelList = None
labelList = labelEntity
else:
userLabelList = labelEntity
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if userLabelList:
labelList = userLabelList[origUser]
lcount = len(labelList)
if buildPath:
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not labels:
continue
labelSet = _getLabelSet(labels)
entityPerformActionNumItems([Ent.USER, user], lcount, Ent.LABEL, i, count)
Ind.Increment()
l = 0
for label in labelList:
l += 1
body['name'] = label
if not buildPath:
try:
callGAPI(gmail.users().labels(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.DUPLICATE, GAPI.INVALID, GAPI.PERMISSION_DENIED],
userId='me', body=body, fields='')
entityActionPerformed([Ent.USER, user, Ent.LABEL, label], l, lcount)
except GAPI.duplicate:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.DUPLICATE, l, lcount)
except (GAPI.invalid, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], str(e), l, lcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
else:
buildLabelPath(gmail, user, i, count, body, label, labelSet, l, lcount)
Ind.Decrement()
# gam <UserTypeEntity> [create] label|labels <String>
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
# [buildpath [<Boolean>]]
def createLabel(users):
createLabels(users, getStringReturnInList(Cmd.OB_LABEL_NAME))
# gam <UserTypeEntity> create labellist <LabelNameEntity>
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
# [buildpath [<Boolean>]]
def createLabelList(users):
createLabels(users, getEntityList(Cmd.OB_LABEL_NAME_LIST, shlexSplit=True))
# gam <UserTypeEntity> update labelsettings <LabelName> [name <String>]
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
def updateLabelSettings(users):
label_name = getString(Cmd.OB_LABEL_NAME)
label_name_lower = label_name.lower()
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
else:
getLabelAttributes(myarg, body)
checkLabelColor(body)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name)')
if not labels:
continue
try:
for label in labels['labels']:
if label['name'].lower() == label_name_lower:
result = callGAPI(gmail.users().labels(), 'patch',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
userId='me', id=label['id'], body=body, fields='name')
entityActionPerformed([Ent.USER, user, Ent.LABEL, result['name']], i, count)
break
else:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label_name], Msg.DOES_NOT_EXIST, i, count)
except (GAPI.notFound, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label_name], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> update labelid <LabelID> [name <String>]
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
def updateLabelSettingsById(users):
labelId = getString(Cmd.OB_LABEL_ID)
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'name':
body['name'] = getString(Cmd.OB_STRING)
else:
getLabelAttributes(myarg, body)
checkLabelColor(body)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().labels(), 'patch',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
userId='me', id=labelId, body=body, fields='name')
entityActionPerformed([Ent.USER, user, Ent.LABEL_ID, labelId, Ent.LABEL, result['name']], i, count)
except (GAPI.notFound, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL_ID, labelId], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
#
def cleanLabelQuery(labelQuery):
for ch in '/ (){}':
labelQuery = labelQuery.replace(ch, '-')
return labelQuery.lower()
# gam <UserTypeEntity> update label|labels
# [search <REMatchPattern>] [replace <RESubstitution>] [merge [keepoldlabel]]
# search defaults to '^Inbox/(.*)$' which will find all labels in the Inbox
# replace defaults to '%s'
def updateLabels(users):
search = '^Inbox/(.*)$'
pattern = re.compile(search, re.IGNORECASE)
replace = '%s'
keepOldLabel = merge = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'search':
search = getString(Cmd.OB_RE_PATTERN)
pattern = validateREPattern(search, re.IGNORECASE)
elif myarg == 'replace':
replaceLocation = Cmd.Location()
replace = getString(Cmd.OB_RE_SUBSTITUTION)
elif myarg == 'merge':
merge = True
elif myarg == 'keepoldlabel':
keepOldLabel = True
else:
unknownArgumentExit()
# Validate that number of substitions in replace matches the number of groups in pattern
useRegexSub = replace.find('%s') == -1
if useRegexSub:
Cmd.SetLocation(replaceLocation)
validateREPatternSubstitution(pattern, replace)
else:
if pattern.groups != replace.count('%s'):
Cmd.SetLocation(replaceLocation)
usageErrorExit(Msg.MISMATCH_SEARCH_REPLACE_SUBFIELDS.format(pattern.groups, search, replace.count('%s'), replace))
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not labels:
continue
try:
labelMatches = 0
for label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
if label['type'] == LABEL_TYPE_SYSTEM:
continue
match_result = pattern.match(label['name'])
if match_result is not None:
labelMatches += 1
if useRegexSub:
newLabelName = pattern.sub(replace, label['name'])
else:
newLabelName = replace % match_result.groups()
newLabelNameLower = newLabelName.lower()
try:
Act.Set(Act.RENAME)
callGAPI(gmail.users().labels(), 'patch',
throwReasons=[GAPI.ABORTED, GAPI.DUPLICATE],
userId='me', id=label['id'], body={'name': newLabelName}, fields='')
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.LABEL, label['name']], Act.MODIFIER_TO, newLabelName, i, count)
except (GAPI.aborted, GAPI.duplicate):
if merge:
Act.Set(Act.MERGE)
entityPerformActionModifierNewValue([Ent.USER, user, Ent.LABEL, label['name']], Act.MODIFIER_WITH, newLabelName, i, count)
messagesToRelabel = callGAPIpages(gmail.users().messages(), 'list', 'messages',
userId='me', q=f'label:{cleanLabelQuery(label["name"])}')
Act.Set(Act.RELABEL)
jcount = len(messagesToRelabel)
Ind.Increment()
if jcount > 0:
for new_label in labels['labels']:
if new_label['name'].lower() == newLabelNameLower:
body = {'addLabelIds': [new_label['id']]}
break
j = 0
for message in messagesToRelabel:
j += 1
callGAPI(gmail.users().messages(), 'modify',
userId='me', id=message['id'], body=body, fields='')
entityActionPerformed([Ent.USER, user, Ent.MESSAGE, message['id']], j, jcount)
else:
printEntityKVList([Ent.USER, user],
[Msg.NO_MESSAGES_WITH_LABEL, label['name']],
i, count)
Ind.Decrement()
if not keepOldLabel:
callGAPI(gmail.users().labels(), 'delete',
userId='me', id=label['id'])
Act.Set(Act.DELETE)
entityActionPerformed([Ent.USER, user, Ent.LABEL, label['name']], i, count)
else:
entityActionNotPerformedWarning([Ent.USER, user, Ent.LABEL, newLabelName], Msg.ALREADY_EXISTS_USE_MERGE_ARGUMENT, i, count)
if labels and (labelMatches == 0):
printEntityKVList([Ent.USER, user],
[Msg.NO_LABELS_MATCH, search],
i, count)
except (GAPI.serviceNotAvailable, GAPI.badRequest):
userGmailServiceNotEnabledWarning(user, i, count)
def _validateLabelList(user, i, count, labels, labelList, userOnly):
validLabels = []
for label in labelList:
label_name_lower = label.lower()
if label_name_lower[:6] == 'regex:':
labelPattern = validateREPattern(label[6:])
else:
labelPattern = None
if label.upper() == '--ALL_LABELS--':
count = len(labels['labels'])
for delLabel in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
if (not userOnly or delLabel['type'] != LABEL_TYPE_SYSTEM):
validLabels.append(delLabel)
elif labelPattern:
for delLabel in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
if (not userOnly or delLabel['type'] != LABEL_TYPE_SYSTEM) and labelPattern.match(delLabel['name']):
validLabels.append(delLabel)
else:
for delLabel in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
if (not userOnly or delLabel['type'] != LABEL_TYPE_SYSTEM) and label_name_lower == delLabel['name'].lower():
validLabels.append(delLabel)
break
else:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.DOES_NOT_EXIST, i, count)
return (validLabels, len(validLabels))
def deleteLabels(users, labelEntity):
def _handleProcessGmailError(exception, ri):
http_status, reason, message = checkGAPIError(exception)
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], Ent.LABEL, labelIdToNameMap[ri[RI_ITEM]]], formatHTTPError(http_status, reason, message), int(ri[RI_J]), int(ri[RI_JCOUNT]))
def _callbackDeleteLabel(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
entityActionPerformed([Ent.USER, ri[RI_ENTITY], Ent.LABEL, labelIdToNameMap[ri[RI_ITEM]]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
_handleProcessGmailError(exception, ri)
if not isinstance(labelEntity, dict):
userLabelList = None
labelList = labelEntity
else:
userLabelList = labelEntity
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if userLabelList:
labelList = userLabelList[origUser]
try:
printGettingAllEntityItemsForWhom(Ent.LABEL, user, i, count)
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not labels:
continue
delLabels, jcount = _validateLabelList(user, i, count, labels, labelList, True)
labelIdToNameMap = {}
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LABEL, i, count)
if jcount == 0:
continue
Ind.Increment()
svcargs = dict([('userId', 'me'), ('id', None), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(gmail.users().labels(), 'delete')
dbatch = gmail.new_batch_http_request(callback=_callbackDeleteLabel)
bcount = 0
j = 0
for del_me in delLabels:
j += 1
svcparms = svcargs.copy()
svcparms['id'] = del_me['id']
labelIdToNameMap[del_me['id']] = del_me['name']
dbatch.add(method(**svcparms), request_id=batchRequestID(user, i, count, j, jcount, del_me['id']))
bcount += 1
if bcount == 10:
executeBatch(dbatch)
dbatch = gmail.new_batch_http_request(callback=_callbackDeleteLabel)
bcount = 0
if bcount > 0:
dbatch.execute()
Ind.Decrement()
except (GAPI.serviceNotAvailable, GAPI.badRequest):
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> delete label|labels <LabelName>|regex:<REMatchPattern>
def deleteLabel(users):
deleteLabels(users, getStringReturnInList(Cmd.OB_LABEL_NAME))
# gam <UserTypeEntity> delete labellist <LabelNameEntity>
def deleteLabelList(users):
deleteLabels(users, getEntityList(Cmd.OB_LABEL_NAME_LIST, shlexSplit=True))
def deleteLabelIds(users, labelEntity):
if not isinstance(labelEntity, dict):
userLabelList = None
labelList = labelEntity
else:
userLabelList = labelEntity
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if userLabelList:
labelList = userLabelList[origUser]
lcount = len(labelList)
entityPerformActionNumItems([Ent.USER, user], lcount, Ent.LABEL, i, count)
Ind.Increment()
l = 0
for labelId in labelList:
l += 1
try:
callGAPI(gmail.users().labels(), 'delete',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID,
GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
userId='me', id=labelId)
entityActionPerformed([Ent.USER, user, Ent.LABEL_ID, labelId], l, lcount)
except (GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL_ID, labelId], str(e), l, lcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> delete labelid <LabelID>
def deleteLabelId(users):
deleteLabelIds(users, getStringReturnInList(Cmd.OB_LABEL_ID))
# gam <UserTypeEntity> delete labelidlist <LabelIDEntity>
def deleteLabelIdList(users):
deleteLabelIds(users, getEntityList(Cmd.OB_LABEL_ID_LIST))
PRINT_LABELS_TITLES = ['User', 'type', 'name', 'id']
SHOW_LABELS_DISPLAY_CHOICES = ['allfields', 'basename', 'fullname']
LABEL_DISPLAY_FIELDS_LIST = ['type', 'id', 'labelListVisibility', 'messageListVisibility', 'color']
LABEL_COUNTS_FIELDS_LIST = ['messagesTotal', 'messagesUnread', 'threadsTotal', 'threadsUnread']
LABEL_COUNTS_FIELDS = ','.join(LABEL_COUNTS_FIELDS_LIST)
# gam <UserTypeEntity> print labels|label [todrive <ToDriveAttribute>*]
# [onlyuser|useronly [<Boolean>]] [showcounts [<Boolean>]]
# [labellist <LabelNameEntity>]
# gam <UserTypeEntity> show labels|label
# [onlyuser|useronly [<Boolean>]] [showcounts [<Boolean>]]
# [nested [<Boolean>]] [display allfields|basename|fullname]
# [labellist <LabelNameEntity>]
def printShowLabels(users):
def _buildLabelTree(labels):
def _checkChildLabel(label):
labelItemList = label.split('/')
i = len(labelItemList)-1
while i > 0:
parent = '/'.join(labelItemList[:i])
base = '/'.join(labelItemList[i:])
if parent in labelTree:
if label in labelTree:
labelTree[label]['info']['base'] = base
labelTree[parent]['children'].append(labelTree.pop(label))
_checkChildLabel(parent)
return
i -= 1
labelTree = {}
for label in labels['labels']:
if not onlyUser or (label['type'] != LABEL_TYPE_SYSTEM):
label['base'] = label['name']
labelTree[label['name']] = {'info': label, 'children': []}
labelList = sorted(list(labelTree), reverse=True)
for label in labelList:
_checkChildLabel(label)
return labelTree
def _printLabel(label):
if not displayAllFields:
if not showCounts:
printKeyValueList([label[nameField]])
else:
counts = callGAPI(gmail.users().labels(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me', id=label['id'],
fields=LABEL_COUNTS_FIELDS)
kvlist = [label[nameField], 'Counts']
for a_key in LABEL_COUNTS_FIELDS_LIST:
kvlist.extend([a_key, counts[a_key]])
printKeyValueList(kvlist)
else:
printKeyValueList([label[nameField]])
Ind.Increment()
for a_key in LABEL_DISPLAY_FIELDS_LIST:
if a_key in label:
if a_key != 'color':
printKeyValueList([a_key, label[a_key]])
else:
printKeyValueList(['backgroundColor', label[a_key]['backgroundColor']])
printKeyValueList(['textColor', label[a_key]['textColor']])
if showCounts:
counts = callGAPI(gmail.users().labels(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me', id=label['id'],
fields=LABEL_COUNTS_FIELDS)
for a_key in LABEL_COUNTS_FIELDS_LIST:
printKeyValueList([a_key, counts[a_key]])
Ind.Decrement()
def _printFlatLabel(label):
_printLabel(label['info'])
if label['children']:
for child in sorted(label['children'], key=lambda k: k['info']['name']):
_printFlatLabel(child)
def _printNestedLabel(label):
_printLabel(label['info'])
if label['children']:
Ind.Increment()
if displayAllFields:
printKeyValueList(['nested', len(label['children'])])
Ind.Increment()
for child in sorted(label['children'], key=lambda k: k['info']['name']):
_printNestedLabel(child)
Ind.Decrement()
else:
for child in sorted(label['children'], key=lambda k: k['info']['name']):
_printNestedLabel(child)
Ind.Decrement()
csvPF = CSVPrintFile(PRINT_LABELS_TITLES, 'sortall') if Act.csvFormat() else None
onlyUser = showCounts = showNested = False
displayAllFields = True
nameField = 'name'
labelEntity = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'onlyuser', 'useronly'}:
onlyUser = getBoolean()
elif myarg == 'showcounts':
showCounts = getBoolean()
elif not csvPF and myarg == 'nested':
showNested = getBoolean()
elif not csvPF and myarg == 'display':
fields = getChoice(SHOW_LABELS_DISPLAY_CHOICES)
nameField = 'name' if fields != 'basename' else 'base'
displayAllFields = fields == 'allfields'
elif myarg == 'labellist':
labelEntity = getEntityList(Cmd.OB_LABEL_NAME_LIST, shlexSplit=True)
else:
unknownArgumentExit()
if not isinstance(labelEntity, dict):
userLabelList = None
labelList = labelEntity
else:
userLabelList = labelEntity
i, count, users = getEntityArgument(users)
for user in users:
i += 1
origUser = user
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if userLabelList:
labelList = userLabelList[origUser]
if csvPF:
printGettingEntityItemForWhom(Ent.LABEL, user, i, count)
labels = _getUserGmailLabels(gmail, user, i, count, 'labels')
if not labels:
continue
if not labelList:
jcount = len(labels['labels'])
if (jcount > 0) and onlyUser:
for label in labels['labels']:
if label['type'] == LABEL_TYPE_SYSTEM:
jcount -= 1
else:
labels['labels'], jcount = _validateLabelList(user, i, count, labels, labelList, onlyUser)
try:
if not csvPF:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LABEL, i, count)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if not csvPF:
labelTree = _buildLabelTree(labels)
Ind.Increment()
if not showNested:
for label, _ in sorted(iter(labelTree.items()), key=lambda k: (k[1]['info']['type'], k[1]['info']['name'])):
_printFlatLabel(labelTree[label])
else:
for label, _ in sorted(iter(labelTree.items()), key=lambda k: (k[1]['info']['type'], k[1]['info']['name'])):
_printNestedLabel(labelTree[label])
Ind.Decrement()
else:
for label in sorted(labels['labels'], key=lambda k: (k['type'], k['name'])):
if not onlyUser or label['type'] != LABEL_TYPE_SYSTEM:
if showCounts:
counts = callGAPI(gmail.users().labels(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me', id=label['id'],
fields=LABEL_COUNTS_FIELDS)
for a_key in LABEL_COUNTS_FIELDS_LIST:
label[a_key] = counts[a_key]
csvPF.WriteRowTitles(flattenJSON(label, flattened={'User': user}))
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Labels')
GMAIL_SYSTEM_LABELS = {
'CHAT': 'CHAT',
'DRAFT': 'DRAFT',
'IMPORTANT': 'IMPORTANT',
'INBOX': 'INBOX',
'SENT': 'SENT',
'SPAM': 'SPAM',
'STARRED': 'STARRED',
'TRASH': 'TRASH',
'UNREAD': 'UNREAD',
}
GMAIL_CATEGORY_LABELS = {
'CATEGORY_PERSONAL': 'CATEGORY_PERSONAL',
'CATEGORY_SOCIAL': 'CATEGORY_SOCIAL',
'CATEGORY_PROMOTIONS': 'CATEGORY_PROMOTIONS',
'CATEGORY_UPDATES': 'CATEGORY_UPDATES',
'CATEGORY_FORUMS': 'CATEGORY_FORUMS',
}
def _initLabelNameMap(userGmailLabels):
labelNameMap = {}
labelNameMap.update(GMAIL_SYSTEM_LABELS)
labelNameMap.update(GMAIL_CATEGORY_LABELS)
for label in userGmailLabels['labels']:
if label['type'] == 'system':
labelNameMap[label['id']] = label['id']
else:
labelNameMap[label['name']] = labelNameMap[label['name'].upper()] = label['id']
return labelNameMap
def _convertLabelNamesToIds(gmail, user, i, count, bodyLabels, labelNameMap, addLabel):
labelIds = []
for label in bodyLabels:
if label in labelNameMap:
labelIds.append(labelNameMap[label])
continue
if label.upper() in labelNameMap:
labelIds.append(labelNameMap[label.upper()])
continue
if not addLabel:
entityListDoesNotExistWarning([Ent.USER, user, Ent.LABEL, label], i, count)
continue
try:
results = callGAPI(gmail.users().labels(), 'create',
throwReasons=[GAPI.INVALID],
userId='me', body={'labelListVisibility': 'labelShow', 'messageListVisibility': 'show', 'name': label}, fields='id')
except GAPI.invalid as e:
action = Act.Get()
Act.Set(Act.CREATE)
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], str(e), i, count)
Act.Set(action)
continue
labelNameMap[label] = labelNameMap[label.upper()] = results['id']
labelIds.append(results['id'])
if label.find('/') != -1:
# make sure to create parent labels for proper nesting
parent_label = label[:label.rfind('/')]
while True:
if (not parent_label in labelNameMap) and (not parent_label.upper() in labelNameMap):
result = callGAPI(gmail.users().labels(), 'create',
userId='me', body={'name': parent_label}, fields='id')
labelNameMap[parent_label] = labelNameMap[parent_label.upper()] = result['id']
if parent_label.find('/') == -1:
break
parent_label = parent_label[:parent_label.rfind('/')]
return labelIds
MESSAGES_MAX_TO_KEYWORDS = {
Act.ARCHIVE: 'maxtoarchive',
Act.DELETE: 'maxtodelete',
Act.EXPORT: 'maxtoexport',
Act.FORWARD: 'maxtoforward',
Act.MODIFY: 'maxtomodify',
Act.PRINT: 'maxtoprint',
Act.SHOW: 'maxtoshow',
Act.SPAM: 'maxtospam',
Act.TRASH: 'maxtotrash',
Act.UNTRASH: 'maxtountrash',
}
def _initMessageThreadParameters(entityType, doIt, maxToProcess):
listType = 'messages' if entityType == Ent.MESSAGE else 'threads'
return {'currLabelOp': 'and', 'prevLabelOp': 'and', 'labelGroupOpen': False,
'query': '', 'queryTimes': {},
'entityType': entityType, 'messageEntity': None, 'doIt': doIt, 'quick': True,
'labelMatchPattern': None, 'senderMatchPattern': None,
'maxToProcess': maxToProcess, 'maxItems': 0, 'maxMessagesPerThread': 0,
'maxToKeywords': [MESSAGES_MAX_TO_KEYWORDS[Act.Get()], 'maxtoprocess'],
'listType': listType, 'fields': f'nextPageToken,{listType}(id)'}
LABEL_QUERY_REPLACEMENT_CHARACTERS = ' &()"|{}/'
def _getMessageSelectParameters(myarg, parameters):
if myarg == 'query':
parameters['query'] += f' ({getString(Cmd.OB_QUERY)})'
elif myarg.startswith('querytime'):
parameters['queryTimes'][myarg] = getDateOrDeltaFromNow().replace('-', '/')
elif myarg == 'matchlabel':
labelTemp = getString(Cmd.OB_LABEL_NAME).lower()
labelName = ''
for c in labelTemp:
labelName += c if c not in LABEL_QUERY_REPLACEMENT_CHARACTERS else '-'
if not parameters['labelGroupOpen']:
parameters['query'] += '('
parameters['labelGroupOpen'] = True
parameters['query'] += f' label:{labelName}'
elif myarg in {'or', 'and'}:
parameters['prevLabelOp'] = parameters['currLabelOp']
parameters['currLabelOp'] = myarg
if parameters['labelGroupOpen'] and parameters['currLabelOp'] != parameters['prevLabelOp']:
parameters['query'] += ')'
parameters['labelGroupOpen'] = False
if parameters['currLabelOp'] == 'or':
parameters['query'] += ' OR '
elif myarg == 'labelmatchpattern':
parameters['labelMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'sendermatchpattern':
parameters['senderMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'ids':
parameters['messageEntity'] = getUserObjectEntity(Cmd.OB_MESSAGE_ID, parameters['entityType'])
elif myarg == 'quick':
parameters['quick'] = True
elif myarg == 'notquick':
parameters['quick'] = False
elif myarg == 'doit':
parameters['doIt'] = True
elif myarg in parameters['maxToKeywords']:
parameters['maxToProcess'] = getInteger(minVal=0)
elif myarg == 'maxmessagesperthread':
parameters['maxMessagesPerThread'] = getInteger(minVal=0)
else:
return False
return True
MESSAGE_TIME_QUERY_PATTERN = re.compile(r'(after:|before:|older:|newer:)(\d{4})/(\d{2})/(\d{2})')
def _mapMessageQueryDates(parameters):
query = parameters['query']
pos = 0
while True:
mg = MESSAGE_TIME_QUERY_PATTERN.search(query, pos)
if not mg:
break
try:
dt = datetime.datetime(int(mg.groups()[1]), int(mg.groups()[2]), int(mg.groups()[3]), tzinfo=GC.Values[GC.TIMEZONE])
query = query[:mg.start(2)]+str(int(datetime.datetime.timestamp(dt)))+query[mg.end(4):]
except ValueError:
pass
pos = mg.end()
parameters['query'] = query
def _finalizeMessageSelectParameters(parameters, queryOrIdsRequired):
if parameters['query']:
if parameters['labelGroupOpen']:
parameters['query'] += ')'
if parameters['queryTimes']:
for queryTimeName, queryTimeValue in iter(parameters['queryTimes'].items()):
parameters['query'] = parameters['query'].replace(f'#{queryTimeName}#', queryTimeValue)
_mapMessageQueryDates(parameters)
elif queryOrIdsRequired and parameters['messageEntity'] is None:
missingArgumentExit('query|matchlabel|ids')
else:
parameters['query'] = None
parameters['maxItems'] = parameters['maxToProcess'] if parameters['quick'] and not parameters['labelMatchPattern'] else 0
# gam <UserTypeEntity> archive messages <GroupItem>
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
def archiveMessages(users):
def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0):
if not csvPF:
entityActionFailedWarning([Ent.USER, user, entityType, idsList], errMsg, j, jcount)
else:
csvPF.WriteRow({'User': user, entityHeader: idsList, 'action': Act.Failed(), 'error': errMsg})
entityType = Ent.MESSAGE
entityHeader = 'id'
parameters = _initMessageThreadParameters(entityType, False, 0)
group = getEmailAddress()
csvPF = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getMessageSelectParameters(myarg, parameters):
pass
elif myarg == 'csv':
csvPF = CSVPrintFile(['User', entityHeader, 'action', 'error'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
_finalizeMessageSelectParameters(parameters, False)
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
gm = buildGAPIObject(API.GROUPSMIGRATION)
cd = buildGAPIObject(API.DIRECTORY)
try:
group = callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
groupKey=group, fields='email')['email']
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
entityDoesNotExistExit(Ent.GROUP, group)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, messageIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
if not gmail:
continue
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
_, gm = buildGAPIServiceObject(API.GROUPSMIGRATION, user, i, count)
if not gm:
continue
service = gmail.users().messages()
try:
if parameters['messageEntity'] is None:
printGettingAllEntityItemsForWhom(entityType, user, i, count)
listResult = callGAPIpages(service, 'list', parameters['listType'],
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', q=parameters['query'], fields=parameters['fields'],
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
messageIds = [message['id'] for message in listResult]
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(messageIds)
if jcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if parameters['messageEntity'] is None:
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
continue
if not parameters['doIt']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
continue
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
Ind.Increment()
j = 0
for messageId in messageIds:
j += 1
try:
message = callGAPI(service, 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
userId='me', id=messageId, format='raw')
stream = io.BytesIO()
stream.write(base64.urlsafe_b64decode(str(message['raw'])))
try:
callGAPI(gm.archive(), 'insert',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID,
GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN],
retryReasons=[GAPI.NOT_FOUND],
groupId=group, media_body=googleapiclient.http.MediaIoBaseUpload(stream, mimetype='message/rfc822', resumable=True))
if not csvPF:
entityActionPerformed([Ent.USER, user, entityType, messageId], j, jcount)
else:
csvPF.WriteRow({'User': user, entityHeader: messageId, 'action': Act.Performed()})
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
except GAPI.notFound as e:
_processMessageFailed(user, messageId, str(e), j, jcount)
break
except (GAPI.badRequest, GAPI.invalid, GAPI.failedPrecondition, GAPI.forbidden,
googleapiclient.errors.MediaUploadSizeError) as e:
_processMessageFailed(user, messageId, str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
except (GAPI.notFound, GAPI.invalidMessageId) as e:
_processMessageFailed(user, messageId, str(e), j, jcount)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile(f'{Act.ToPerform()} Messages')
def _processMessagesThreads(users, entityType):
def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0):
if not csvPF:
entityActionFailedWarning([Ent.USER, user, entityType, idsList], errMsg, j, jcount)
else:
csvPF.WriteRow({'User': user, entityHeader: idsList, 'action': Act.Failed(), 'error': errMsg})
def _batchDeleteModifyMessages(gmail, function, user, jcount, messageIds, body):
mcount = 0
bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE])
while bcount > 0:
body['ids'] = messageIds[mcount:mcount+bcount]
idsCount = min(5, bcount)
idsList = ','.join(body['ids'][0:idsCount])
if bcount > 5:
idsList += ',...'
try:
callGAPI(gmail.users().messages(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_MESSAGE_ID, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
userId='me', body=body)
for messageId in body['ids']:
mcount += 1
if not csvPF:
entityActionPerformed([Ent.USER, user, entityType, messageId], mcount, jcount)
else:
csvPF.WriteRow({'User': user, entityHeader: messageId, 'action': Act.Performed()})
except GAPI.serviceNotAvailable:
mcount += bcount
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
_processMessageFailed(user, idsList, f'{str(e)} ({mcount+1}-{mcount+bcount}/{jcount})')
mcount += bcount
except GAPI.invalidMessageId:
_processMessageFailed(user, idsList, f'{Msg.INVALID_MESSAGE_ID} ({mcount+1}-{mcount+bcount}/{jcount})')
mcount += bcount
except GAPI.failedPrecondition:
_processMessageFailed(user, idsList, f'{Msg.FAILED_PRECONDITION} ({mcount+1}-{mcount+bcount}/{jcount})')
mcount += bcount
bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE])
_GMAIL_ERROR_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
GAPI.INVALID_MESSAGE_ID: Msg.INVALID_MESSAGE_ID,
GAPI.FAILED_PRECONDITION: Msg.FAILED_PRECONDITION}
def _callbackProcessMessage(request_id, _, exception):
ri = request_id.splitlines()
if exception is None:
if not csvPF:
entityActionPerformed([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
csvPF.WriteRow({'User': ri[RI_ENTITY], entityHeader: ri[RI_ITEM], 'action': Act.Performed()})
else:
http_status, reason, message = checkGAPIError(exception)
_processMessageFailed(ri[RI_ENTITY], ri[RI_ITEM], getHTTPError(_GMAIL_ERROR_REASON_TO_MESSAGE_MAP, http_status, reason, message), int(ri[RI_J]), int(ri[RI_JCOUNT]))
def _batchProcessMessagesThreads(service, function, user, jcount, messageIds, **kwargs):
svcargs = dict([('userId', 'me'), ('id', None), ('fields', '')]+list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST])
method = getattr(service, function)
dbatch = gmail.new_batch_http_request(callback=_callbackProcessMessage)
bcount = 0
j = 0
for messageId in messageIds:
j += 1
svcparms = svcargs.copy()
svcparms['id'] = messageId
dbatch.add(method(**svcparms), request_id=batchRequestID(user, 0, 0, j, jcount, svcparms['id']))
bcount += 1
if bcount == GC.Values[GC.EMAIL_BATCH_SIZE]:
executeBatch(dbatch)
dbatch = gmail.new_batch_http_request(callback=_callbackProcessMessage)
bcount = 0
if bcount > 0:
dbatch.execute()
parameters = _initMessageThreadParameters(entityType, False, 1)
includeSpamTrash = False
function = {Act.DELETE: 'delete', Act.MODIFY: 'modify', Act.SPAM: 'spam', Act.TRASH: 'trash', Act.UNTRASH: 'untrash'}[Act.Get()]
labelNameMap = {}
addLabelNames = []
addLabelIds = []
removeLabelNames = []
removeLabelIds = []
csvPF = None
entityHeader = 'id' if entityType == Ent.MESSAGE else 'threadId'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getMessageSelectParameters(myarg, parameters):
pass
elif (function == 'modify') and (myarg == 'addlabel'):
addLabelNames.append(getString(Cmd.OB_LABEL_NAME))
elif (function == 'modify') and (myarg == 'removelabel'):
removeLabelNames.append(getString(Cmd.OB_LABEL_NAME))
elif myarg == 'csv':
csvPF = CSVPrintFile(['User', entityHeader, 'action', 'error'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
if function == 'modify' and not addLabelNames and not removeLabelNames:
missingArgumentExit('(addlabel <LabelName>)|(removelabel <LabelName>)')
_finalizeMessageSelectParameters(parameters, True)
includeSpamTrash = Act.Get() in [Act.DELETE, Act.MODIFY, Act.UNTRASH]
if function == 'spam':
function = 'modify'
addLabelIds = ['SPAM']
removeLabelIds = ['INBOX']
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, messageIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
if not gmail:
continue
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
if addLabelNames or removeLabelNames:
userGmailLabels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not userGmailLabels:
continue
labelNameMap = _initLabelNameMap(userGmailLabels)
addLabelIds = _convertLabelNamesToIds(gmail, user, i, count, addLabelNames, labelNameMap, True)
removeLabelIds = _convertLabelNamesToIds(gmail, user, i, count, removeLabelNames, labelNameMap, False)
if not addLabelIds and not removeLabelIds:
entityActionNotPerformedWarning([Ent.USER, user], Msg.NO_LABELS_TO_PROCESS, i, count)
continue
try:
if parameters['messageEntity'] is None:
printGettingAllEntityItemsForWhom(Ent.MESSAGE, user, i, count)
listResult = callGAPIpages(service, 'list', parameters['listType'],
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
messageIds = [message['id'] for message in listResult]
else:
# Need to get authorization set up for batch
callGAPI(gmail.users(), 'getProfile',
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', fields='')
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(messageIds)
if jcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if parameters['messageEntity'] is None:
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
continue
if not parameters['doIt']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
continue
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
Ind.Increment()
if function == 'delete' and entityType == Ent.MESSAGE:
_batchDeleteModifyMessages(gmail, 'batchDelete', user, jcount, messageIds, {'ids': []})
elif function == 'modify' and entityType == Ent.MESSAGE:
_batchDeleteModifyMessages(gmail, 'batchModify', user, jcount, messageIds, {'ids': [], 'addLabelIds': addLabelIds, 'removeLabelIds': removeLabelIds})
else:
if addLabelIds or removeLabelIds:
kwargs = {'body': {'addLabelIds': addLabelIds, 'removeLabelIds': removeLabelIds}}
else:
kwargs = {}
_batchProcessMessagesThreads(service, function, user, jcount, messageIds, **kwargs)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile(f'{Act.ToPerform()} Messages')
# gam <UserTypeEntity> delete message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> modify message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>)
# (addlabel <LabelName>)* (removelabel <LabelName>)*
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> spam message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> trash message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> untrash message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
def processMessages(users):
_processMessagesThreads(users, Ent.MESSAGE)
# gam <UserTypeEntity> delete thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <ThreadIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> modify thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <ThreadIDEntity>)
# (addlabel <LabelName>)* (removelabel <LabelName>)*
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> spam thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <ThreadIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> trash thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
# gam <UserTypeEntity> untrash thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <ThreadIDEntity>)
# [csv [todrive <ToDriveAttribute>*]]
def processThreads(users):
_processMessagesThreads(users, Ent.THREAD)
def exportMessagesThreads(users, entityType):
parameters = _initMessageThreadParameters(entityType, False, 1)
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
targetNamePattern = None
includeSpamTrash = overwrite = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getMessageSelectParameters(myarg, parameters):
pass
elif myarg == 'targetfolder':
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
elif myarg == 'targetname':
targetNamePattern = getString(Cmd.OB_FILE_NAME)
elif myarg == 'overwrite':
overwrite = getBoolean()
else:
unknownArgumentExit()
_finalizeMessageSelectParameters(parameters, True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, entityIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
if not gmail:
continue
_, userName, _ = splitEmailAddressOrUID(user)
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
try:
if parameters['messageEntity'] is None:
printGettingAllEntityItemsForWhom(entityType, user, i, count)
listResult = callGAPIpages(service, 'list', parameters['listType'],
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
entityIds = [entity['id'] for entity in listResult]
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(entityIds)
if jcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if parameters['messageEntity'] is None:
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
continue
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
Ind.Increment()
j = 0
for entityId in entityIds:
j += 1
if entityType == Ent.THREAD:
try:
result = callGAPI(gmail.users().threads(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
userId='me', id=entityId, fields='messages(id)')
messageIds = [message['id'] for message in result['messages']]
kcount = len(messageIds)
entityPerformActionNumItems([Ent.USER, user, Ent.THREAD, entityId], kcount, Ent.MESSAGE, j, jcount)
Ind.Increment()
k = 0
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
except (GAPI.notFound, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.THREAD, entityId], str(e), j, jcount)
continue
else:
messageIds = [entityId]
kcount = jcount
k = j-1
for messageId in messageIds:
k += 1
try:
result = callGAPI(gmail.users().messages(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
userId='me', id=messageId, format='raw')
if targetName:
msgName = targetName.replace('#id#', messageId)
else:
msgName = f'Msg-{messageId}.eml'
filename, _ = uniqueFilename(targetFolder, msgName, overwrite)
status, e = writeFileReturnError(filename, base64.urlsafe_b64decode(str(result['raw'])), mode='wb')
if status:
entityActionPerformed([Ent.MESSAGE, filename])
else:
entityActionFailedWarning([Ent.MESSAGE, filename], str(e))
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
except (GAPI.notFound, GAPI.invalidMessageId) as e:
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], str(e), k, kcount)
continue
if entityType == Ent.THREAD:
Ind.Decrement()
Ind.Decrement()
# gam <UserTypeEntity> export message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [max_to_export <Number>])|(ids <MessageIDEntity>)
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
def exportMessages(users):
exportMessagesThreads(users, Ent.MESSAGE)
# gam <UserTypeEntity> export thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [max_to_export <Number>])|(ids <ThreadIDEntity>)
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
def exportThreads(users):
exportMessagesThreads(users, Ent.THREAD)
HEADER_ENCODE_PATTERN = re.compile(r'=\?([^?]*?)\?[qQbB]\?(.*?)\?=', re.VERBOSE | re.MULTILINE)
def _decodeHeader(header):
header = header.encode(UTF8, 'replace').decode(UTF8)
while True:
mg = HEADER_ENCODE_PATTERN.search(header)
if not mg:
return header
try:
header = header[:mg.start()]+decode_header(mg.group())[0][0].decode(mg.group(1))+header[mg.end():]
except LookupError:
stderrWarningMsg(Msg.INVALID_CHARSET.format(mg.group(1)))
return header
# gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity>
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>)
# [subject <String>] [addorigfieldstosubject [<Boolean>]] [altcharset <String>]
# gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity>
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>)
# [subject <String>] [addorigfieldstosubject [<Boolean>]] [altcharset <String>]
def forwardMessagesThreads(users, entityType):
checkArgumentPresent({'recipient', 'recipients', 'to'})
recipients = getRecipients()
parameters = _initMessageThreadParameters(entityType, False, 1)
addOriginalFieldsToSubject = includeSpamTrash = False
subject = ''
encodings = [UTF8]
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _getMessageSelectParameters(myarg, parameters):
pass
elif myarg == 'subject':
subject = getString(Cmd.OB_STRING)
elif myarg == 'addorigfieldstosubject':
addOriginalFieldsToSubject = getBoolean()
elif myarg == 'altcharset':
encodings.append(getString(Cmd.OB_CHAR_SET))
else:
unknownArgumentExit()
_finalizeMessageSelectParameters(parameters, True)
msgTo = ','.join(recipients)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, entityIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
if not gmail:
continue
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
if parameters['messageEntity'] is None:
try:
printGettingAllEntityItemsForWhom(entityType, user, i, count)
listResult = callGAPIpages(service, 'list', parameters['listType'],
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
entityIds = [entity['id'] for entity in listResult]
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(entityIds)
if jcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
if parameters['messageEntity'] is None:
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
continue
if not parameters['doIt']:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
continue
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
Ind.Increment()
j = 0
for entityId in entityIds:
j += 1
if entityType == Ent.THREAD:
try:
result = callGAPI(gmail.users().threads(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
userId='me', id=entityId, fields='messages(id)')
messageIds = [message['id'] for message in result['messages']]
kcount = len(messageIds)
entityPerformActionNumItems([Ent.USER, user, Ent.THREAD, entityId], kcount, Ent.MESSAGE, j, jcount)
Ind.Increment()
k = 0
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
except (GAPI.notFound, GAPI.invalidMessageId) as e:
entityActionFailedWarning([Ent.USER, user, Ent.THREAD, entityId], str(e), j, jcount)
continue
else:
messageIds = [entityId]
kcount = jcount
k = j-1
for messageId in messageIds:
k += 1
try:
result = callGAPI(gmail.users().messages(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
userId='me', id=messageId, format='raw')
for encoding in encodings:
try:
message = message_from_string(base64.urlsafe_b64decode(str(result['raw'])).decode(encoding), policy=policySMTP)
break
except UnicodeDecodeError as e:
errMsg = str(e)
else:
entityActionNotPerformedWarning([Ent.RECIPIENT, msgTo, entityType, messageId], errMsg, k, kcount)
continue
if not subject:
msgSubject = f"Fwd: {_decodeHeader(message['Subject'])}"
else:
msgSubject = f"Subject: {subject}"
for header in ['To', 'Cc', 'Subject']:
if header in message:
del message[header]
message['To'] = msgTo
if addOriginalFieldsToSubject:
msgSubject += ' (Original'
for header in ['From', 'To', 'Date']:
if header in message:
msgSubject += f' {header}: {message[header]}'
msgSubject += ')'
message['Subject'] = msgSubject
try:
result = callGAPI(gmail.users().messages(), 'send',
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
userId='me', body={'raw': base64.urlsafe_b64encode(message.as_bytes()).decode(encoding)}, fields='id')
entityActionPerformedMessage([Ent.RECIPIENT, msgTo], f"{result['id']}", k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied, UnicodeEncodeError) as e:
entityActionFailedWarning([Ent.RECIPIENT, msgTo], str(e), k, kcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
except (GAPI.notFound, GAPI.invalidMessageId) as e:
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], str(e), k, kcount)
continue
if entityType == Ent.THREAD:
Ind.Decrement()
Ind.Decrement()
SMTP_HEADERS_MAP = {
'accept-language': 'Accept-Language',
'alternate-recipient': 'Alternate-Recipient',
'autoforwarded': 'Autoforwarded',
'autosubmitted': 'Autosubmitted',
'bcc': 'Bcc',
'cc': 'Cc',
'comments': 'Comments',
'content-alternative': 'Content-Alternative',
'content-base': 'Content-Base',
'content-description': 'Content-Description',
'content-disposition': 'Content-Disposition',
'content-duration': 'Content-Duration',
'content-id': 'Content-ID',
'content-identifier': 'Content-Identifier',
'content-language': 'Content-Language',
'content-location': 'Content-Location',
'content-md5': 'Content-MD5',
'content-return': 'Content-Return',
'content-transfer-encoding': 'Content-Transfer-Encoding',
'content-type': 'Content-Type',
'content-features': 'Content-features',
'conversion': 'Conversion',
'conversion-with-loss': 'Conversion-With-Loss',
'dl-expansion-history': 'DL-Expansion-History',
'date': 'Date',
'deferred-delivery': 'Deferred-Delivery',
'delivered-to': 'Delivered-To',
'delivery-date': 'Delivery-Date',
'discarded-x400-ipms-extensions': 'Discarded-X400-IPMS-Extensions',
'discarded-x400-mts-extensions': 'Discarded-X400-MTS-Extensions',
'disclose-recipients': 'Disclose-Recipients',
'disposition-notification-options': 'Disposition-Notification-Options',
'disposition-notification-to': 'Disposition-Notification-To',
'encoding': 'Encoding',
'encrypted': 'Encrypted',
'expires': 'Expires',
'expiry-date': 'Expiry-Date',
'from': 'From',
'generate-delivery-report': 'Generate-Delivery-Report',
'importance': 'Importance',
'in-reply-to': 'In-Reply-To',
'incomplete-copy': 'Incomplete-Copy',
'keywords': 'Keywords',
'language': 'Language',
'latest-delivery-time': 'Latest-Delivery-Time',
'list-archive': 'List-Archive',
'list-help': 'List-Help',
'list-id': 'List-ID',
'list-owner': 'List-Owner',
'list-post': 'List-Post',
'list-subscribe': 'List-Subscribe',
'list-unsubscribe': 'List-Unsubscribe',
'mime-version': 'MIME-Version',
'message-context': 'Message-Context',
'message-id': 'Message-ID',
'message-type': 'Message-Type',
'obsoletes': 'Obsoletes',
'original-encoded-information-types': 'Original-Encoded-Information-Types',
'original-message-id': 'Original-Message-ID',
'originator-return-address': 'Originator-Return-Address',
'pics-label': 'PICS-Label',
'prevent-nondelivery-report': 'Prevent-NonDelivery-Report',
'priority': 'Priority',
'received': 'Received',
'recipient': 'To',
'references': 'References',
'reply-by': 'Reply-By',
'reply-to': 'Reply-To',
'resent-bcc': 'Resent-Bcc',
'resent-cc': 'Resent-Cc',
'resent-date': 'Resent-Date',
'resent-from': 'Resent-From',
'resent-message-id': 'Resent-Message-ID',
'resent-reply-to': 'Resent-Reply-To',
'resent-sender': 'Resent-Sender',
'resent-to': 'Resent-To',
'return-path': 'Return-Path',
'sender': 'Sender',
'sensitivity': 'Sensitivity',
'subject': 'Subject',
'supersedes': 'Supersedes',
'to': 'To',
'x400-content-identifier': 'X400-Content-Identifier',
'x400-content-return': 'X400-Content-Return',
'x400-content-type': 'X400-Content-Type',
'x400-mts-identifier': 'X400-MTS-Identifier',
'x400-originator': 'X400-Originator',
'x400-received': 'X400-Received',
'x400-recipients': 'X400-Recipients',
'x400-trace': 'X400-Trace',
}
SMTP_ADDRESS_HEADERS = {
'Bcc',
'Cc',
'Delivered-To',
'From',
'Reply-To',
'Resent-Bcc',
'Resent-Cc',
'Resent-Reply-To',
'Resent-Sender',
'Resent-To',
'Sender',
'To',
}
SMTP_DATE_HEADERS = {
'date',
'delivery-date',
'expires',
'expiry-date',
'latest-delivery-time',
'reply-by',
'resent-date',
}
SMTP_NAME_ADDRESS_PATTERN = re.compile(r'^(.+?)\s*<(.+)>$')
IMPORT_INSERT = {'import', 'insert'}
def _draftImportInsertMessage(users, operation):
def _appendToHeader(header, value):
try:
header.append(value)
except UnicodeDecodeError:
header.append(value, UTF8)
labelNameMap = {}
addLabelNames = []
msgHTML = msgText = ''
msgHeaders = {}
tagReplacements = _initTagReplacements()
attachments = []
embeddedImages = []
internalDateSource = 'receivedTime'
deleted = processForCalendar = substituteForUserInHeaders = False
neverMarkSpam = True
emlFile = False
emlEncoding = 'ascii'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in SMTP_HEADERS_MAP:
if myarg == 'content-type':
unknownArgumentExit()
if myarg in SMTP_DATE_HEADERS:
msgDate, _, _ = getTimeOrDeltaFromNow(True)
msgHeaders[SMTP_HEADERS_MAP[myarg]] = formatdate(time.mktime(msgDate.timetuple()) + msgDate.microsecond/1E6, True)
if myarg == 'date':
internalDateSource = 'dateHeader'
else:
value = getString(Cmd.OB_STRING)
if (value.find('#user#') >= 0) or (value.find('#email#') >= 0) or (value.find('#username#') >= 0):
substituteForUserInHeaders = True
msgHeaders[SMTP_HEADERS_MAP[myarg]] = value
elif myarg == 'header':
header = getString(Cmd.OB_STRING, minLen=1)
if header.lower() == 'content-type':
unknownArgumentExit()
value = getString(Cmd.OB_STRING)
if (value.find('#user#') >= 0) or (value.find('#email#') >= 0) or (value.find('#username#') >= 0):
substituteForUserInHeaders = True
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = value
elif myarg in SORF_MSG_FILE_ARGUMENTS:
if 'html' in myarg:
msgHTML, _, _ = getStringOrFile(myarg)
else:
msgText, _, _ = getStringOrFile(myarg)
emlFile = False
elif myarg == 'emlfile':
filename = getString(Cmd.OB_FILE_NAME)
if checkArgumentPresent('charset'):
emlEncoding = getString(Cmd.OB_CHAR_SET)
msgText = readFile(filename, encoding=emlEncoding)
emlFile = True
internalDateSource = 'dateHeader'
if checkArgumentPresent('emlutf8'):
emlEncoding = UTF8
elif _getTagReplacement(myarg, tagReplacements, True):
pass
elif operation in IMPORT_INSERT and myarg == 'addlabel':
addLabelNames.append(getString(Cmd.OB_LABEL_NAME, minLen=1))
elif operation in IMPORT_INSERT and myarg == 'labels':
addLabelNames.extend(shlexSplitList(getString(Cmd.OB_LABEL_NAME_LIST)))
elif operation in IMPORT_INSERT and myarg == 'deleted':
deleted = getBoolean()
elif myarg == 'attach':
attachments.append((getFilename(), getCharSet()))
elif myarg == 'embedimage':
embeddedImages.append((getFilename(), getString(Cmd.OB_STRING)))
elif operation == 'import' and myarg == 'nevermarkspam':
neverMarkSpam = getBoolean()
elif operation == 'import' and myarg == 'checkspam':
neverMarkSpam = not getBoolean()
elif operation == 'import' and myarg == 'processforcalendar':
processForCalendar = getBoolean()
else:
unknownArgumentExit()
if not msgText and not msgHTML:
missingArgumentExit('textmessage|textfile|htmlmessage|htmlfile|empfile')
if not emlFile:
msgText = msgText.replace('\r', '').replace('\\n', '\n')
msgHTML = msgHTML.replace('\r', '').replace('\\n', '<br/>')
if not tagReplacements['tags']:
tmpText = msgText
tmpHTML = msgHTML
if operation != 'draft':
if not emlFile:
if 'To' not in msgHeaders:
msgHeaders['To'] = '#user#'
substituteForUserInHeaders = True
if 'From' not in msgHeaders:
msgHeaders['From'] = _getAdminEmail()
kwargs = {'internalDateSource': internalDateSource, 'deleted': deleted}
if operation == 'import':
function = 'import_'
kwargs.update({'neverMarkSpam': neverMarkSpam, 'processForCalendar': processForCalendar})
else: #'insert':
function = 'insert'
else:
function = 'create'
add_charset(UTF8, QP, QP, UTF8)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
userName, _ = splitEmailAddress(user)
if not emlFile:
if tagReplacements['tags']:
if tagReplacements['subs']:
_getTagReplacementFieldValues(user, i, count, tagReplacements)
tmpText = _processTagReplacements(tagReplacements, msgText)
tmpHTML = _processTagReplacements(tagReplacements, msgHTML)
if attachments or embeddedImages:
if tmpText and tmpHTML:
message = MIMEMultipart('alternative')
textpart = MIMEText(tmpText, 'plain', UTF8)
message.attach(textpart)
htmlpart = MIMEText(tmpHTML, 'html', UTF8)
message.attach(htmlpart)
elif tmpHTML:
message = MIMEMultipart()
htmlpart = MIMEText(tmpHTML, 'html', UTF8)
message.attach(htmlpart)
else:
message = MIMEMultipart()
textpart = MIMEText(tmpText, 'plain', UTF8)
message.attach(textpart)
_addAttachmentsToMessage(message, attachments)
_addEmbeddedImagesToMessage(message, embeddedImages)
else:
if tmpText and tmpHTML:
message = MIMEMultipart('alternative')
textpart = MIMEText(tmpText, 'plain', UTF8)
message.attach(textpart)
htmlpart = MIMEText(tmpHTML, 'html', UTF8)
message.attach(htmlpart)
elif tmpHTML:
message = MIMEText(tmpHTML, 'html', UTF8)
else:
message = MIMEText(tmpText, 'plain', UTF8)
for header, value in iter(msgHeaders.items()):
if substituteForUserInHeaders:
value = _substituteForUser(value, user, userName)
message[header] = Header()
if header in SMTP_ADDRESS_HEADERS:
match = SMTP_NAME_ADDRESS_PATTERN.match(value.strip())
if match:
_appendToHeader(message[header], match.group(1))
_appendToHeader(message[header], match.group(2))
else:
_appendToHeader(message[header], value)
else:
_appendToHeader(message[header], value)
tmpFile = TemporaryFile(mode='w+', encoding=UTF8)
g = Generator(tmpFile, False)
g.flatten(message)
tmpFile.seek(0)
body = {'raw': base64.urlsafe_b64encode(bytes(tmpFile.read(), UTF8)).decode()}
tmpFile.close()
else:
for header, value in iter(msgHeaders.items()):
if substituteForUserInHeaders:
value = _substituteForUser(value, user, userName)
msgText = re.sub(fr'(?sm)\n{header}:.+?(?=[\r\n]+[a-zA-Z0-9-]+:)', f'\n{header}: {value}', msgText, 1)
message_bytes = msgText.encode(emlEncoding)
base64_bytes = base64.b64encode(message_bytes)
body = {'raw': base64_bytes.decode(emlEncoding)}
try:
if operation != 'draft':
if addLabelNames:
userGmailLabels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not userGmailLabels:
continue
labelNameMap = _initLabelNameMap(userGmailLabels)
body['labelIds'] = _convertLabelNamesToIds(gmail, user, i, count, addLabelNames, labelNameMap, True)
else:
body['labelIds'] = ['INBOX']
result = callGAPI(gmail.users().messages(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me', body=body, fields='id', **kwargs)
else:
result = callGAPI(gmail.users().drafts(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
userId='me', body={'message': body}, fields='id')
entityActionPerformed([Ent.USER, user, Ent.MESSAGE, result['id']], i, count)
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> draft message
# <MessageContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
def draftMessage(users):
_draftImportInsertMessage(users, 'draft')
# gam <UserTypeEntity> import message
# <MessageContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# (addlabel <LabelName>)* [labels <LabelNameList>]
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [deleted [<Boolean>]] [nevermarkspam [<Boolean>]] [processforcalendar [<Boolean>]]
def importMessage(users):
_draftImportInsertMessage(users, 'import')
# gam <UserTypeEntity> insert message
# <MessageContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# (addlabel <LabelName>)* [labels <LabelNameList>]
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [deleted [<Boolean>]]
def insertMessage(users):
_draftImportInsertMessage(users, 'insert')
def printShowMessagesThreads(users, entityType):
def _getBodyData(payload, getOrigMsg):
data = headers = ''
for part in payload.get('parts', []):
if getOrigMsg:
if show_all_headers:
if not headers:
headers = '---------- Original message ----------\n'
for header in part.get('headers', []):
headers += header['name']+': '+_decodeHeader(header['value'])+'\n'
elif 'headers' in part:
for name in headersToShow:
for header in part['headers']:
if name == header['name'].lower():
if not headers:
headers = '---------- Original message ----------\n'
headers += SMTP_HEADERS_MAP.get(name, header['name'])+': '+_decodeHeader(header['value'])+'\n'
if headers:
headers += 'Body:\n'
data = Ind.INDENT_SPACES_PER_LEVEL
if part['mimeType'] == 'text/plain':
if 'data' in part['body']:
data += base64.urlsafe_b64decode(str(part['body']['data'])).decode(UTF8)+'\n'
elif show_html and part['mimeType'] == 'text/html':
if 'data' in part['body']:
data += base64.urlsafe_b64decode(str(part['body']['data'])).decode(UTF8)+'\n'
elif part['mimeType'] == 'text/rfc822-headers':
if 'data' in part['body']:
data += _decodeHeader(base64.urlsafe_b64decode(str(part['body']['data'])).decode(UTF8)+'\n')
else:
data += _getBodyData(part, part['mimeType'] == 'message/rfc822')
if getOrigMsg:
data = data.replace('\n', f'\n{Ind.INDENT_SPACES_PER_LEVEL}').rstrip()
return headers+data
def _getMessageBody(payload):
if 'attachmentId' not in payload.get('body', {}) and 'data' in payload.get('body', {}):
return base64.urlsafe_b64decode(str(payload['body']['data'])).decode(UTF8)
data = _getBodyData(payload, False)
if data:
return data
return 'Body not available'
ATTACHMENT_NAME_PATTERN = re.compile(r'^.*name="?(.*?)(?:"|;|$)')
CHARSET_NAME_PATTERN = re.compile(r'^.*charset="?(.*?)(?:"|;|$)')
def _showAttachmentMimeTypeSizeCharset(part, charset):
printKeyValueList(['mimeType', part['mimeType']])
printKeyValueList(['size', part['body']['size']])
if charset:
printKeyValueList(['charset', charset])
def _showSaveAttachments(messageId, payload, attachmentNamePattern, j, jcount):
for part in payload.get('parts', []):
if 'attachmentId' in part['body']:
for header in part['headers']:
if header['name'] in {'Content-Type', 'Content-Disposition'}:
mg = ATTACHMENT_NAME_PATTERN.match(header['value'])
if not mg:
continue
attachmentName = mg.group(1)
if (not attachmentNamePattern) or attachmentNamePattern.match(attachmentName):
charset = ''
if part['mimeType'] == 'text/plain':
mg = CHARSET_NAME_PATTERN.match(header['value'])
if mg:
charset = mg.group(1)
if (part['mimeType'] == 'text/plain' and not noshow_text_plain) or save_attachments or upload_attachments:
try:
result = callGAPI(gmail.users().messages().attachments(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND],
messageId=messageId, id=part['body']['attachmentId'], userId='me')
if 'data' in result:
if show_attachments:
printKeyValueList(['Attachment', attachmentName])
Ind.Increment()
if part['mimeType'] == 'text/plain':
try:
printKeyValueList([Ind.MultiLineText(base64.urlsafe_b64decode(str(result['data'])).decode(charset)+'\n')])
except (LookupError, UnicodeDecodeError, UnicodeError):
_showAttachmentMimeTypeSizeCharset(part, charset)
else:
_showAttachmentMimeTypeSizeCharset(part, charset)
Ind.Decrement()
if save_attachments:
filename, _ = uniqueFilename(targetFolder, cleanFilename(attachmentName), overwrite)
action = Act.Get()
Act.Set(Act.DOWNLOAD)
status, e = writeFileReturnError(filename, base64.urlsafe_b64decode(str(result['data'])), mode='wb')
if status:
entityActionPerformed([Ent.ATTACHMENT, filename])
else:
entityActionFailedWarning([Ent.ATTACHMENT, filename], str(e))
Act.Set(action)
if upload_attachments:
filename = cleanFilename(attachmentName)
uploadAttachmentBody.update({'name': filename, 'mimeType': part['mimeType']})
action = Act.Get()
Act.Set(Act.CREATE)
media_body = googleapiclient.http.MediaIoBaseUpload(io.BytesIO(base64.urlsafe_b64decode(str(result['data']))), mimetype=part['mimeType'], resumable=True)
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR,
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
GAPI.UPLOAD_TOO_LARGE, GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
media_body=media_body, body=uploadAttachmentBody, fields='id,name', supportsAllDrives=True)
entityModifierItemValueListActionPerformed([Ent.DRIVE_FILE, f"{result['name']}({result['id']})"],
Act.MODIFIER_WITH_CONTENT_FROM, [Ent.ATTACHMENT, filename], j, jcount)
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
GAPI.fileNotFound, GAPI.unknownError, GAPI.internalError,
GAPI.storageQuotaExceeded, GAPI.teamdrivesSharingRestrictionNotAllowed,
GAPI.teamdrivefileLimitExceeded, GAPI.teamdriveHierarchyTooDeep,
GAPI.uploadTooLarge, GAPI.teamdrivesShortcutFileNotSupported) as e:
entityModifierItemValueListActionFailedWarning([Ent.DRIVE_FILE, None],
Act.MODIFIER_WITH_CONTENT_FROM, [Ent.ATTACHMENT, filename], str(e), j, jcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Act.Set(action)
except (GAPI.serviceNotAvailable, GAPI.notFound):
pass
elif show_attachments:
printKeyValueList(['Attachment', attachmentName])
Ind.Increment()
_showAttachmentMimeTypeSizeCharset(part, charset)
Ind.Decrement()
break
else:
_showSaveAttachments(messageId, part, attachmentNamePattern, j, jcount)
def _initSenderLabelsMap(sender):
if sender not in senderLabelsMaps:
senderLabelsMaps[sender] = {'*None*': {'name': '*None*', 'count': 0, 'size': 0, 'type': LABEL_TYPE_USER, 'match': labelMatchPattern is None}}
for label in labels['labels']:
senderLabelsMaps[sender][label['id']] = {'name': label['name'], 'count': 0, 'size': 0, 'type': label['type'],
'match': True if not labelMatchPattern else labelMatchPattern.match(label['name']) is not None}
return senderLabelsMaps[sender]
def _getMatchMessageLabels(result, sender):
labelsMap = _initSenderLabelsMap(sender)
messageLabels = []
match = False
for labelId in result.get('labelIds', []):
if labelId in labelsMap:
match |= labelsMap[labelId]['match']
if not onlyUser or labelsMap[labelId]['type'] != LABEL_TYPE_SYSTEM:
messageLabels.append(labelsMap[labelId]['name'])
else:
messageLabels.append(labelId)
if labelMatchPattern and not match:
return None
return messageLabels
def _checkSenderMatch(result):
for header in result['payload'].get('headers', []):
sender = _decodeHeader(header['value'])
if header['name'] == 'Sender' and senderMatchPattern.match(sender):
return sender
return None
def _checkSenderMatchCount(result):
for header in result['payload'].get('headers', []):
sender = _decodeHeader(header['value'])
if header['name'] == 'Sender' and senderMatchPattern.match(sender):
senderCounts.setdefault(sender, {'count': 0, 'size': 0})
senderCounts[sender]['count'] += 1
senderCounts[sender]['size'] += result['sizeEstimate']
return sender
return None
def _qualifyMessage(user, result):
if senderMatchPattern:
sender = _checkSenderMatchCount(result)
if not sender:
return (False, None)
else:
sender = user
if show_labels or labelMatchPattern:
messageLabels = _getMatchMessageLabels(result, sender)
if messageLabels is None:
return (False, None)
return (True, messageLabels)
return (True, None)
def _convertDateTime(headerValue):
dateTimeValue = headerValue.replace('GMT', '+0000')
dateTimeValue = dateTimeValue.replace('UT', '+0000')
pLoc = dateTimeValue.find(' (')
if pLoc > 0:
dateTimeValue = dateTimeValue[:pLoc]
try:
dateTimeValue = datetime.datetime.strptime(dateTimeValue, RFC2822_TIME_FORMAT)
if dateHeaderConvertTimezone:
dateTimeValue = dateTimeValue.astimezone(GC.Values[GC.TIMEZONE])
return dateTimeValue.strftime(dateHeaderFormat)
except ValueError:
return headerValue
def _showMessage(user, result, j, jcount, checkMax=True):
if checkMax and parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
return
status, messageLabels = _qualifyMessage(user, result)
if not status:
return
printEntity([Ent.MESSAGE, result['id']], j, jcount)
Ind.Increment()
if show_snippet:
printKeyValueList(['Snippet', dehtml(result['snippet']).replace('\n', ' ')])
if show_all_headers:
for header in result['payload'].get('headers', []):
headerValue = _decodeHeader(header['value'])
if dateHeaderFormat and header['name'].lower() in SMTP_DATE_HEADERS:
headerValue = _convertDateTime(headerValue)
printKeyValueList([header['name'], headerValue])
else:
for name in headersToShow:
for header in result['payload'].get('headers', []):
if name == header['name'].lower():
headerValue = _decodeHeader(header['value'])
if dateHeaderFormat and name in SMTP_DATE_HEADERS:
headerValue = _convertDateTime(headerValue)
printKeyValueList([SMTP_HEADERS_MAP.get(name, header['name']), headerValue])
if show_date:
printKeyValueList(['Date', formatLocalTimestamp(result['internalDate'])])
if show_size:
printKeyValueList(['SizeEstimate', result['sizeEstimate']])
if show_labels:
printKeyValueList(['Labels', delimiter.join(messageLabels)])
if show_body:
printKeyValueList(['Body', None])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(_getMessageBody(result['payload']))])
Ind.Decrement()
if show_attachments or save_attachments or upload_attachments:
_showSaveAttachments(result['id'], result['payload'], attachmentNamePattern, j, jcount)
Ind.Decrement()
if checkMax:
parameters['messagesProcessed'] += 1
def _getAttachments(messageId, payload, attachmentNamePattern, attachments):
for part in payload.get('parts', []):
if 'attachmentId' in part['body']:
for header in part['headers']:
if header['name'] in {'Content-Type', 'Content-Disposition'}:
mg = ATTACHMENT_NAME_PATTERN.match(header['value'])
if not mg:
continue
attachmentName = mg.group(1)
charset = ''
if part['mimeType'] == 'text/plain':
mg = CHARSET_NAME_PATTERN.match(header['value'])
if mg:
charset = mg.group(1)
if (not attachmentNamePattern) or attachmentNamePattern.match(attachmentName):
attachments.append((attachmentName, part['mimeType'], part['body']['size'], charset))
break
else:
_getAttachments(messageId, part, attachmentNamePattern, attachments)
def _printMessage(user, result, checkMax=True):
if checkMax and parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
return
status, messageLabels = _qualifyMessage(user, result)
if not status:
return
row = {'User': user, 'threadId': result['threadId'], 'id': result['id']}
if show_snippet:
row['Snippet'] = dehtml(result['snippet']).replace('\n', ' ')
if show_all_headers:
headerCounts = {}
for header in result['payload'].get('headers', []):
headerCounts.setdefault(header['name'], 0)
headerValue = _decodeHeader(header['value'])
if dateHeaderFormat and header['name'].lower() in SMTP_DATE_HEADERS:
headerValue = _convertDateTime(headerValue)
if headerCounts[header['name']] == 0:
row[header['name']] = headerValue
else:
row[f'{header["name"]} {headerCounts[header["name"]]}'] = headerValue
headerCounts[header['name']] += 1
else:
for name in headersToShow:
j = 0
for header in result['payload'].get('headers', []):
if name == header['name'].lower():
headerValue = _decodeHeader(header['value'])
if dateHeaderFormat and name in SMTP_DATE_HEADERS:
headerValue = _convertDateTime(headerValue)
if j == 0:
row[SMTP_HEADERS_MAP.get(name, header['name'])] = headerValue
else:
row[f'{SMTP_HEADERS_MAP.get(name, header["name"])} {j}'] = headerValue
j += 1
if show_date:
row['Date'] = formatLocalTimestamp(result['internalDate'])
if show_size:
row['SizeEstimate'] = result['sizeEstimate']
if show_labels:
row['LabelsCount'] = len(messageLabels)
row['Labels'] = delimiter.join(messageLabels)
if show_body:
if not convertCRNL:
row['Body'] = _getMessageBody(result['payload'])
else:
row['Body'] = escapeCRsNLs(_getMessageBody(result['payload']))
if show_attachments:
attachments = []
_getAttachments(result['id'], result['payload'], attachmentNamePattern, attachments)
row['Attachments'] = len(attachments)
for i, attachment in enumerate(attachments):
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = attachment[0]
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType'] = attachment[1]
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}size'] = attachment[2]
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}charset'] = attachment[3]
csvPF.WriteRowTitles(row)
if checkMax:
parameters['messagesProcessed'] += 1
def _countMessageLabels(user, result):
if senderMatchPattern:
sender = _checkSenderMatch(result)
if not sender:
return False
else:
sender = user
labelsMap = _initSenderLabelsMap(sender)
labelIds = result.get('labelIds', [])
if labelIds:
for labelId in labelIds:
if labelId in labelsMap:
labelsMap[labelId]['count'] += 1
labelsMap[labelId]['size'] += result['sizeEstimate']
else:
labelsMap[labelId] = {'name': labelId, 'count': 1, 'size': result['sizeEstimate'], 'type': LABEL_TYPE_USER,
'match': True if not labelMatchPattern else labelMatchPattern.match(labelId) is not None}
elif not labelMatchPattern:
labelsMap['*None*']['count'] += 1
labelsMap['*None*']['size'] += result['sizeEstimate']
def _countMessages(_, result):
if senderMatchPattern and not _checkSenderMatchCount(result):
return
messageThreadCounts['messages'] += 1
messageThreadCounts['size'] += result['sizeEstimate']
def _showThread(user, result, j, jcount):
if parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
return
if senderMatchPattern:
for message in result['messages']:
if _checkSenderMatch(message):
break
else:
return
messageThreadCounts['threads'] += 1
printEntity([Ent.THREAD, result['id']], j, jcount)
Ind.Increment()
if show_snippet and 'snippet' in result:
printKeyValueList(['Snippet', dehtml(result['snippet']).replace('\n', ' ')])
kcount = len(result['messages'])
k = 0
for message in result['messages']:
k += 1
_showMessage(user, message, k, kcount, False)
if k == parameters['maxMessagesPerThread']:
break
Ind.Decrement()
parameters['messagesProcessed'] += 1
def _printThread(user, result):
if parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
return
if senderMatchPattern:
for message in result['messages']:
if _checkSenderMatch(message):
break
else:
return
k = 0
for message in result['messages']:
k += 1
_printMessage(user, message, False)
if k == parameters['maxMessagesPerThread']:
break
messageThreadCounts['threads'] += 1
parameters['messagesProcessed'] += 1
def _countThreadLabels(user, result):
for message in result['messages']:
_countMessageLabels(user, message)
def _countThreads(_, result):
if senderMatchPattern:
for message in result['messages']:
if _checkSenderMatchCount(message):
messageThreadCounts['size'] += message['sizeEstimate']
break
else:
return
else:
k = 0
for message in result['messages']:
k += 1
messageThreadCounts['size'] += message['sizeEstimate']
if k == parameters['maxMessagesPerThread']:
break
messageThreadCounts['threads'] += 1
_GMAIL_ERROR_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST, GAPI.INVALID_MESSAGE_ID: Msg.INVALID_MESSAGE_ID}
def _handleGmailError(exception, ri):
http_status, reason, message = checkGAPIError(exception)
errMsg = getHTTPError(_GMAIL_ERROR_REASON_TO_MESSAGE_MAP, http_status, reason, message)
if reason not in GAPI.DEFAULT_RETRY_REASONS:
if not csvPF:
printKeyValueListWithCount([Ent.Singular(entityType), ri[RI_ITEM], errMsg], int(ri[RI_J]), int(ri[RI_JCOUNT]))
setSysExitRC(ACTION_FAILED_RC)
else:
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
return
try:
response = callGAPI(service, 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
userId='me', id=ri[RI_ITEM], format=['metadata', 'full'][show_size or show_body or show_attachments or save_attachments or upload_attachments])
if countsOnly:
_callbacks['process'](ri[RI_ENTITY], response)
else:
if not csvPF:
_callbacks['process'](ri[RI_ENTITY], response, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
_callbacks['process'](ri[RI_ENTITY], response)
except GAPI.notFound:
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], Msg.DOES_NOT_EXIST, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.invalidMessageId:
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], Msg.INVALID_MESSAGE_ID, int(ri[RI_J]), int(ri[RI_JCOUNT]))
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
def _callbackShow(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
_callbacks['process'](ri[RI_ENTITY], response, int(ri[RI_J]), int(ri[RI_JCOUNT]))
else:
_handleGmailError(exception, ri)
def _callbackPrint(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
_callbacks['process'](ri[RI_ENTITY], response)
else:
_handleGmailError(exception, ri)
def _callbackCountLabels(request_id, response, exception):
ri = request_id.splitlines()
if exception is None:
_callbacks['process'](ri[RI_ENTITY], response)
else:
_handleGmailError(exception, ri)
def _batchPrintShowMessagesThreads(service, user, jcount, messageIds):
svcargs = dict([('userId', 'me'), ('id', None), ('format', ['metadata', 'full'][show_body or show_attachments or save_attachments or upload_attachments])]+GM.Globals[GM.EXTRA_ARGS_LIST])
if countsOnly:
if show_labels:
if not senderMatchPattern:
svcargs['fields'] = 'labelIds,sizeEstimate' if entityType == Ent.MESSAGE else 'messages(labelIds,sizeEstimate)'
else:
svcargs['fields'] = 'labelIds,sizeEstimate,payload' if entityType == Ent.MESSAGE else 'messages(labelIds,sizeEstimate,payload)'
else:
if not senderMatchPattern:
svcargs['fields'] = 'sizeEstimate' if entityType == Ent.MESSAGE else 'messages(sizeEstimate)'
else:
svcargs['fields'] = 'sizeEstimate,payload' if entityType == Ent.MESSAGE else 'messages(sizeEstimate,payload)'
method = getattr(service, 'get')
dbatch = gmail.new_batch_http_request(callback=_callbacks['batch'])
bcount = 0
j = 0
for messageId in messageIds:
j += 1
svcparms = svcargs.copy()
svcparms['id'] = messageId
dbatch.add(method(**svcparms), request_id=batchRequestID(user, 0, 0, j, jcount, svcparms['id']))
bcount += 1
if not labelMatchPattern and parameters['maxToProcess'] and j == parameters['maxToProcess']:
break
if bcount == GC.Values[GC.EMAIL_BATCH_SIZE]:
executeBatch(dbatch)
dbatch = gmail.new_batch_http_request(callback=_callbacks['batch'])
bcount = 0
if labelMatchPattern and parameters['messagesProcessed'] == parameters['maxToProcess']:
break
if bcount > 0:
dbatch.execute()
parameters = _initMessageThreadParameters(entityType, True, 0)
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
countsOnly = positiveCountsOnly = includeSpamTrash = onlyUser = overwrite = save_attachments = upload_attachments = False
show_all_headers = show_attachments = show_body = show_date = show_html = show_labels = show_size = show_snippet = False
noshow_text_plain = False
attachmentNamePattern = None
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
defaultHeaders = ['Date', 'Subject', 'From', 'Reply-To', 'To', 'Delivered-To', 'Content-Type', 'Message-ID']
headersToShow = [header.lower() for header in defaultHeaders]
csvPF = CSVPrintFile() if Act.csvFormat() else None
showMode = Act.Get() == Act.SHOW
dateHeaderFormat = ''
dateHeaderConvertTimezone = False
uploadAttachmentBody = {}
addCSVData = {}
parentParms = initDriveFileAttributes()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif _getMessageSelectParameters(myarg, parameters):
pass
elif myarg == 'headers':
headersToShow = getString(Cmd.OB_STRING, minLen=0).lower().replace(',', ' ').split()
show_all_headers = headersToShow and headersToShow[0] == 'all'
elif not showMode and myarg in {'convertcrnl', 'converttextnl', 'convertbodynl'}:
convertCRNL = True
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'showdate':
show_date = True
elif myarg == 'showbody':
show_body = True
elif myarg == 'showhtml':
show_html = True
elif myarg == 'showlabels':
show_labels = True
elif myarg == 'showsize':
show_size = True
elif myarg == 'showsnippet':
show_snippet = True
elif myarg == 'showattachments':
show_attachments = True
elif myarg == 'noshowtextplain':
noshow_text_plain = True
elif myarg == 'attachmentnamepattern':
attachmentNamePattern = getREPattern(re.IGNORECASE)
elif showMode and myarg == 'saveattachments':
save_attachments = True
elif showMode and myarg == 'targetfolder':
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
elif showMode and myarg == 'overwrite':
overwrite = getBoolean()
elif showMode and myarg == 'uploadattachments':
upload_attachments = True
elif showMode and getDriveFileParentAttribute(myarg, parentParms):
pass
elif myarg == 'includespamtrash':
includeSpamTrash = True
elif myarg == 'countsonly':
countsOnly = True
elif myarg == 'positivecountsonly':
countsOnly = positiveCountsOnly = True
elif myarg in {'onlyuser', 'useronly'}:
onlyUser = getBoolean()
elif myarg == 'dateheaderformat':
dateHeaderFormat = getString(Cmd.OB_STRING, minLen=0)
if dateHeaderFormat == 'iso':
dateHeaderFormat = IS08601_TIME_FORMAT
elif dateHeaderFormat == 'rfc2822':
dateHeaderFormat = RFC2822_TIME_FORMAT
elif myarg == 'dateheaderconverttimezone':
dateHeaderConvertTimezone = getBoolean()
if not dateHeaderFormat:
dateHeaderFormat = RFC2822_TIME_FORMAT
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
unknownArgumentExit()
labelMatchPattern = parameters['labelMatchPattern']
senderMatchPattern = parameters['senderMatchPattern']
if senderMatchPattern and not show_all_headers and 'sender' not in headersToShow:
headersToShow.append('sender')
_finalizeMessageSelectParameters(parameters, False)
if csvPF:
if countsOnly:
if show_labels:
if not senderMatchPattern:
sortTitles = ['User']
else:
sortTitles = ['User', 'Sender']
csvPF.SetIndexedTitles(['Labels'])
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessageLabels if entityType == Ent.MESSAGE else _countThreadLabels}
else:
if not senderMatchPattern:
sortTitles = ['User', parameters['listType']]
else:
sortTitles = ['User', 'Sender', parameters['listType']]
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessages if entityType == Ent.MESSAGE else _countThreads}
if show_size:
sortTitles.append('size')
if addCSVData:
sortTitles.extend(sorted(addCSVData.keys()))
csvPF.SetTitles(sortTitles)
else:
sortTitles = ['User', 'threadId', 'id']
sortTitles.extend(defaultHeaders)
if show_size:
sortTitles.append('SizeEstimate')
if show_labels:
sortTitles.extend(['LabelsCount', 'Labels'])
if show_snippet:
sortTitles.append('Snippet')
if show_body:
sortTitles.append('Body')
if addCSVData:
sortTitles.extend(sorted(addCSVData.keys()))
_callbacks = {'batch': _callbackPrint, 'process': _printMessage if entityType == Ent.MESSAGE else _printThread}
csvPF.SetTitles(sortTitles)
csvPF.SetSortTitles(sortTitles)
else:
if countsOnly:
if show_labels:
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessageLabels if entityType == Ent.MESSAGE else _countThreadLabels}
else:
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessages if entityType == Ent.MESSAGE else _countThreads}
else:
_callbacks = {'batch': _callbackShow, 'process': _showMessage if entityType == Ent.MESSAGE else _showThread}
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, messageIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
if not gmail:
continue
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
if upload_attachments:
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if not _getDriveFileParentInfo(drive, user, i, count, uploadAttachmentBody, parentParms):
continue
if show_labels or labelMatchPattern:
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not labels:
continue
senderLabelsMaps = {}
if not senderMatchPattern:
_initSenderLabelsMap(user)
messageThreadCounts = {'User': user, parameters['listType']: 0, 'size': 0}
if addCSVData:
messageThreadCounts.update(addCSVData)
senderCounts = {}
if save_attachments:
_, userName, _ = splitEmailAddressOrUID(user)
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
try:
if parameters['messageEntity'] is None:
printGettingAllEntityItemsForWhom(entityType, user, i, count)
listResult = callGAPIpages(service, 'list', parameters['listType'],
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
messageIds = [message['id'] for message in listResult]
else:
# Need to get authorization set up for batch
callGAPI(gmail.users(), 'getProfile',
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', fields='')
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(messageIds)
if jcount == 0:
setSysExitRC(NO_ENTITIES_FOUND_RC)
if countsOnly and not show_labels and not senderMatchPattern and not show_size:
if not positiveCountsOnly or jcount > 0:
if not csvPF:
printEntityKVList([Ent.USER, user], [parameters['listType'], jcount], i, count)
else:
row = {'User': user, parameters['listType']: jcount}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
continue
if not csvPF and not countsOnly:
if (parameters['messageEntity'] is not None or
((parameters['maxToProcess'] == 0 or jcount <= parameters['maxToProcess']) and
(not labelMatchPattern and not senderMatchPattern))):
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
elif not labelMatchPattern and not senderMatchPattern:
entityPerformActionNumItemsModifier([Ent.USER, user], parameters['maxToProcess'], entityType, f'of {jcount} Total {Ent.Plural(entityType)}', i, count)
else:
entityPerformActionModifierNumItemsModifier([Ent.USER, user], Msg.MAXIMUM_OF, parameters['maxToProcess'] or jcount, entityType,
f'of {jcount} Total {Ent.Plural(entityType)}', i, count)
if parameters['messageEntity'] is None and not labelMatchPattern and parameters['maxToProcess'] and (jcount > parameters['maxToProcess']):
jcount = parameters['maxToProcess']
parameters['messagesProcessed'] = 0
if not csvPF:
Ind.Increment()
_batchPrintShowMessagesThreads(service, user, jcount, messageIds)
Ind.Decrement()
else:
_batchPrintShowMessagesThreads(service, user, jcount, messageIds)
if countsOnly:
if show_labels:
if onlyUser or positiveCountsOnly or labelMatchPattern:
for sender in senderLabelsMaps:
userLabelsMap = {}
for labelId, label in iter(senderLabelsMaps[sender].items()):
if (label['match'] and
(not onlyUser or label['type'] != LABEL_TYPE_SYSTEM) and
(not positiveCountsOnly or label['count'] > 0)):
userLabelsMap[labelId] = label
senderLabelsMaps[sender] = userLabelsMap
if not csvPF:
for sender, labelsMap in sorted(iter(senderLabelsMaps.items())):
jcount = len(labelsMap)
kvlist = [Ent.USER, user]
if senderMatchPattern:
kvlist.extend([Ent.SENDER, sender])
entityPerformActionNumItems(kvlist, jcount, Ent.LABEL, i, count)
Ind.Increment()
j = 0
for label in sorted(iter(labelsMap.values()), key=lambda k: k['name']):
j += 1
if not show_size:
printEntityKVList([Ent.LABEL, label['name']], ['Count', label['count'], 'Type', label['type']], j, jcount)
else:
printEntityKVList([Ent.LABEL, label['name']], ['Count', label['count'], 'Size', label['size'], 'Type', label['type']], j, jcount)
Ind.Decrement()
else:
for sender, labelsMap in sorted(iter(senderLabelsMaps.items())):
row = {'User': user}
if senderMatchPattern:
row['Sender'] = sender
if not show_size:
for label in labelsMap.values():
label.pop('size', None)
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(flattenJSON({'Labels': sorted(iter(labelsMap.values()), key=lambda k: k['name'])}, flattened=row))
elif not senderMatchPattern:
v = messageThreadCounts[parameters['listType']]
if not positiveCountsOnly or v > 0:
if not csvPF:
if not show_size:
printEntityKVList([Ent.USER, user], [parameters['listType'], v], i, count)
else:
printEntityKVList([Ent.USER, user], [parameters['listType'], v, 'size', messageThreadCounts['size']], i, count)
else:
if not show_size:
messageThreadCounts.pop('size', None)
csvPF.WriteRow(messageThreadCounts)
else:
if not show_size:
if not csvPF:
for k, v in sorted(iter(senderCounts.items())):
if not positiveCountsOnly or v['count'] > 0:
printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count']], i, count)
else:
for k, v in sorted(iter(senderCounts.items())):
if not positiveCountsOnly or v['count'] > 0:
row = {'User': user, 'Sender': k, parameters['listType']: v['count']}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
else:
if not csvPF:
for k, v in sorted(iter(senderCounts.items())):
if not positiveCountsOnly or v['count'] > 0:
printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count'], 'size', v['size']], i, count)
else:
for k, v in sorted(iter(senderCounts.items())):
if not positiveCountsOnly or v['count'] > 0:
row = {'User': user, 'Sender': k, parameters['listType']: v['count'], 'size': v['size']}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRow(row)
if csvPF:
if not countsOnly:
csvPF.RemoveTitles(['SizeEstimate', 'LabelsCount', 'Labels', 'Snippet', 'Body'])
if show_size:
csvPF.AddTitle('SizeEstimate')
if show_labels:
csvPF.AddTitles(['LabelsCount', 'Labels'])
if show_snippet:
csvPF.AddTitle('Snippet')
if show_body:
csvPF.AddTitle('Body')
csvPF.SetSortAllTitles()
csvPF.writeCSVfile('Messages')
else:
csvPF.writeCSVfile('Message Counts' if not show_labels else 'Message Label Counts')
# gam <UserTypeEntity> print message|messages [todrive <ToDriveAttribute>*]
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
# [labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
# [convertcrnl] [delimiter <Character>]
# [countsonly|positivecountsonly] [useronly]
# [[attachmentnamepattern <REMatchPattern>]
# [showattachments [noshowtextplain]]]
# (addcsvdata <FieldName> <String>)*
# gam <UserTypeEntity> show message|messages
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
# [labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
# [countsonly|positivecountsonly] [useronly]
# [[attachmentnamepattern <REMatchPattern>]
# [showattachments [noshowtextplain]]
# [saveattachments [targetfolder <FilePath>] [overwrite [<Boolean>]]]
# [uploadattachments [<DriveFileParentAttribute>]]]
def printShowMessages(users):
printShowMessagesThreads(users, Ent.MESSAGE)
# gam <UserTypeEntity> print thread|threads [todrive <ToDriveAttribute>*]
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <ThreadIDEntity>)
# [labelmatchpattern <REMatchPattern>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
# [convertcrnl] [delimiter <Character>]
# [countsonly|positivecountsonly] [useronly]
# [[attachmentnamepattern <REMatchPattern>]
# [showattachments [noshowtextplain]]]
# (addcsvdata <FieldName> <String>)*
# gam <UserTypeEntity> show thread|threads
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <ThreadIDEntity>)
# [labelmatchpattern <REMatchPattern>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
# [countsonly|positivecountsonly] [useronly]
# [[attachmentnamepattern <REMatchPattern>]
# [showattachments [noshowtextplain]]
# [saveattachments [targetfolder <FilePath>] [overwrite [<Boolean>]]]
# [uploadattachments [<DriveFileParentAttribute>]]]
def printShowThreads(users):
printShowMessagesThreads(users, Ent.THREAD)
# gam <UserTypeEntity> create delegate|delegates [convertalias] <UserEntity>
# gam <UserTypeEntity> delete delegate|delegates [convertalias] <UserEntity>
def processDelegates(users):
cd = buildGAPIObject(API.DIRECTORY)
function = 'delete' if Act.Get() == Act.DELETE else 'create'
aliasAllowed = not checkArgumentPresent(['convertalias'])
delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, delegates, jcount = _validateUserGetObjectList(user, i, count, delegateEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for delegate in delegates:
j += 1
delegateEmail = convertUIDtoEmailAddress(delegate, cd=cd, emailTypes=['user', 'group'], aliasAllowed=aliasAllowed)
try:
if function == 'create':
callGAPI(gmail.users().settings().delegates(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION, GAPI.INVALID,
GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
userId='me', body={'delegateEmail': delegateEmail})
else:
callGAPI(gmail.users().settings().delegates(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_INPUT, GAPI.PERMISSION_DENIED],
userId='me', delegateEmail=delegateEmail)
entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
except (GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.invalid,
GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, delegateEmail], str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
Ind.Decrement()
# gam <UserTypeEntity> delegate to [convertalias] <UserEntity>
def delegateTo(users):
checkArgumentPresent('to', required=True)
processDelegates(users)
# gam <UserTypeEntity> update delegate|delegates [convertalias] [<UserEntity>]
def updateDelegates(users):
cd = buildGAPIObject(API.DIRECTORY)
aliasAllowed = not checkArgumentPresent(['convertalias'])
if Cmd.ArgumentsRemaining():
delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
checkForExtraneousArguments()
else:
delegateEntity = None
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if delegateEntity is None:
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().settings().delegates(), 'list',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me')
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, None], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
delegates = result.get('delegates', []) if result is not None else []
jcount = len(delegates)
entityPerformActionModifierNumItems([Ent.USER, user], Msg.MAXIMUM_OF, jcount, Ent.DELEGATE, i, count)
else:
user, gmail, delegates, jcount = _validateUserGetObjectList(user, i, count, delegateEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for delegate in delegates:
j += 1
if delegateEntity is not None or delegate['verificationStatus'] == 'accepted':
delegateEmail = delegate['delegateEmail'] if delegateEntity is None else convertUIDtoEmailAddress(delegate, cd=cd, aliasAllowed=aliasAllowed)
try:
callGAPI(gmail.users().settings().delegates(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
userId='me', body={'delegateEmail': delegateEmail, 'verificationStatus': 'accepted'})
entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
except GAPI.alreadyExists:
entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
except (GAPI.failedPrecondition, GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, delegateEmail], str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
Ind.Decrement()
# gam <UserTypeEntity> print delegates|delegate [todrive <ToDriveAttribute>*] [shownames]
# gam <UserTypeEntity> show delegates|delegate [shownames] [csv]
def printShowDelegates(users):
titlesList = ['User', 'delegateAddress', 'delegationStatus']
csvPF = CSVPrintFile() if Act.csvFormat() else None
cd = None
csvStyle = showNames = False
delegateNames = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif not csvPF and myarg == 'csv':
csvStyle = True
elif myarg == 'shownames':
cd = buildGAPIObject(API.DIRECTORY)
titlesList = ['User', 'delegateName', 'delegateAddress', 'delegationStatus']
showNames = True
else:
unknownArgumentExit()
if csvPF:
csvPF.AddTitles(titlesList)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingAllEntityItemsForWhom(Ent.DELEGATE, user, i, count)
try:
result = callGAPI(gmail.users().settings().delegates(), 'list',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
userId='me')
delegates = result.get('delegates', []) if result is not None else []
jcount = len(delegates)
if not csvPF:
if not csvStyle:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DELEGATE, i, count)
Ind.Increment()
j = 0
for delegate in delegates:
j += 1
status = delegate['verificationStatus']
delegateEmail = delegate['delegateEmail']
if cd:
printEntity([Ent.DELEGATE, _getDelegateName(cd, delegateEmail, delegateNames)], j, jcount)
Ind.Increment()
printKeyValueList(['Status', status])
printKeyValueList(['Delegate Email', delegateEmail])
Ind.Decrement()
else:
printEntity([Ent.DELEGATE, delegateEmail], j, jcount)
Ind.Increment()
printKeyValueList(['Status', status])
Ind.Decrement()
Ind.Decrement()
else:
j = 0
for delegate in delegates:
j += 1
status = delegate['verificationStatus']
delegateEmail = delegate['delegateEmail']
if cd:
writeStdout(f'{user},{_getDelegateName(cd, delegateEmail, delegateNames)},{status},{delegateEmail}\n')
else:
writeStdout(f'{user},{status},{delegateEmail}\n')
else:
if delegates:
if showNames:
for delegate in delegates:
csvPF.WriteRow({'User': user, 'delegateName': _getDelegateName(cd, delegate['delegateEmail'], delegateNames),
'delegateAddress': delegate['delegateEmail'], 'delegationStatus': delegate['verificationStatus']})
else:
for delegate in delegates:
csvPF.WriteRow({'User': user, 'delegateAddress': delegate['delegateEmail'],
'delegationStatus': delegate['verificationStatus']})
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, None], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Delegates')
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
'IMPORTANT': 'important',
'STARRED': 'star',
'TRASH': 'trash',
}
FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP = {
'IMPORTANT': 'notimportant',
'INBOX': 'archive',
'SPAM': 'neverspam',
'UNREAD': 'markread',
}
def _printFilter(user, userFilter, labels):
row = {'User': user, 'id': userFilter['id']}
if 'criteria' in userFilter:
for item in userFilter['criteria']:
if item in {'hasAttachment', 'excludeChats'}:
row[item] = item
elif item == 'size':
row[item] = f'size {userFilter["criteria"]["sizeComparison"]} {formatMaxMessageBytes(userFilter["criteria"][item], ONE_KILO_10_BYTES, ONE_MEGA_10_BYTES)}'
elif item == 'sizeComparison':
pass
else:
row[item] = f'{item} {userFilter["criteria"][item]}'
else:
row['error'] = 'NoCriteria'
if 'action' in userFilter:
for labelId in userFilter['action'].get('addLabelIds', []):
if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP:
row[FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]] = FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]
else:
row['label'] = f'label {_getLabelName(labels, labelId)}'
for labelId in userFilter['action'].get('removeLabelIds', []):
if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP:
row[FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]] = FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]
if userFilter['action'].get('forward'):
row['forward'] = f'forward {userFilter["action"]["forward"]}'
else:
row['error'] = 'NoActions'
return row
def _mapFilterLabelIdsToNames(userFilter, labels):
# Map user label IDs to label names
if 'action' in userFilter:
for field in ['addLabelIds', 'removeLabelIds']:
if field in userFilter['action']:
for i, labelId in enumerate(userFilter['action'][field]):
if labelId not in GMAIL_SYSTEM_LABELS and labelId not in GMAIL_CATEGORY_LABELS:
userFilter['action'][field][i] = _getLabelName(labels, labelId)
def _showFilter(userFilter, j, jcount, labels, FJQC=None):
if FJQC is not None and FJQC.formatJSON:
if labels['labels']:
_mapFilterLabelIdsToNames(userFilter, labels)
printLine(json.dumps(cleanJSON(userFilter), ensure_ascii=False, sort_keys=False))
return
printEntity([Ent.FILTER, userFilter['id']], j, jcount)
Ind.Increment()
printEntitiesCount(Ent.CRITERIA, None)
Ind.Increment()
if 'criteria' in userFilter:
for item in sorted(userFilter['criteria']):
if item in {'hasAttachment', 'excludeChats'}:
printKeyValueList([item])
elif item == 'size':
printKeyValueList([f'{item} {userFilter["criteria"]["sizeComparison"]} {formatMaxMessageBytes(userFilter["criteria"][item], ONE_KILO_10_BYTES, ONE_MEGA_10_BYTES)}'])
elif item == 'sizeComparison':
pass
else:
printKeyValueList([f'{item} "{userFilter["criteria"][item]}"'])
else:
printKeyValueList([ERROR, Msg.NO_FILTER_CRITERIA.format(Ent.Singular(Ent.FILTER))])
Ind.Decrement()
printEntitiesCount(Ent.ACTION, None)
Ind.Increment()
if 'action' in userFilter:
for labelId in sorted(userFilter['action'].get('addLabelIds', [])):
if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP:
printKeyValueList([FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]])
else:
printKeyValueList([f'label "{_getLabelName(labels, labelId)}"'])
for labelId in sorted(userFilter['action'].get('removeLabelIds', [])):
if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP:
printKeyValueList([FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]])
Ind.Decrement()
if userFilter['action'].get('forward'):
printEntity([Ent.FORWARDING_ADDRESS, userFilter['action']['forward']])
else:
printKeyValueList([ERROR, Msg.NO_FILTER_ACTIONS.format(Ent.Singular(Ent.FILTER))])
Ind.Decrement()
Ind.Decrement()
#
FILTER_CATEGORY_CHOICE_MAP = {
'personal': 'CATEGORY_PERSONAL',
'social': 'CATEGORY_SOCIAL',
'promotions': 'CATEGORY_PROMOTIONS',
'updates': 'CATEGORY_UPDATES',
'forums': 'CATEGORY_FORUMS',
}
FILTER_CRITERIA_CHOICE_MAP = {
'excludechats': 'excludeChats',
'from': 'from',
'hasattachment': 'hasAttachment',
'haswords': 'query',
'musthaveattachment': 'hasAttachment',
'negatedquery': 'negatedQuery',
'nowords': 'negatedQuery',
'query': 'query',
'size': 'size',
'subject': 'subject',
'to': 'to',
}
FILTER_ADD_LABEL_ACTIONS = ['important', 'star', 'trash']
FILTER_REMOVE_LABEL_ACTIONS = ['markread', 'notimportant', 'archive', 'neverspam']
FILTER_ACTION_CHOICES = FILTER_ADD_LABEL_ACTIONS+FILTER_REMOVE_LABEL_ACTIONS+['category', 'forward', 'label']
FILTER_ACTION_LABEL_MAP = {
'archive': 'INBOX',
'important': 'IMPORTANT',
'markread': 'UNREAD',
'neverspam': 'SPAM',
'notimportant': 'IMPORTANT',
'star': 'STARRED',
'trash': 'TRASH',
}
# gam <UserTypeEntity> [create]
# (filter <FilterCriteria>+ <FilterAction>+) |
# ((json [charset <Charset>] <String>) |
# (json file <FileName> [charset <Charset>]))
# [buildpath [<Boolean>]]
def createFilter(users):
body = {'criteria': {}, 'action': {'addLabelIds': [], 'removeLabelIds': []}}
buildPath = False
jsonData = None
categorySpecified = labelSpecified = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if jsonData is None and myarg in FILTER_CRITERIA_CHOICE_MAP:
myarg = FILTER_CRITERIA_CHOICE_MAP[myarg]
if myarg in {'from', 'to', 'subject', 'query', 'negatedQuery'}:
body['criteria'][myarg] = getString(Cmd.OB_STRING)
elif myarg in {'hasAttachment', 'excludeChats'}:
body['criteria'][myarg] = True
elif myarg == 'size':
body['criteria']['sizeComparison'] = getChoice(['larger', 'smaller'])
body['criteria'][myarg] = getMaxMessageBytes(ONE_KILO_10_BYTES, ONE_MEGA_10_BYTES)
elif jsonData is None and myarg in FILTER_ACTION_CHOICES:
if myarg in FILTER_ADD_LABEL_ACTIONS:
myarg = FILTER_ACTION_LABEL_MAP[myarg]
body['action']['addLabelIds'].append(myarg)
if (myarg == 'IMPORTANT') and (myarg in body['action']['removeLabelIds']):
body['action']['removeLabelIds'].remove(myarg)
elif myarg in FILTER_REMOVE_LABEL_ACTIONS:
myarg = FILTER_ACTION_LABEL_MAP[myarg]
body['action']['removeLabelIds'].append(myarg)
if (myarg == 'IMPORTANT') and (myarg in body['action']['addLabelIds']):
body['action']['addLabelIds'].remove(myarg)
elif myarg == 'forward':
body['action']['forward'] = getEmailAddress(noUid=True)
elif myarg == 'label':
label = getString(Cmd.OB_LABEL_NAME)
labelUpper = label.upper()
if labelUpper not in GMAIL_SYSTEM_LABELS:
if labelUpper not in GMAIL_CATEGORY_LABELS:
if not labelSpecified:
body['action']['addLabelIds'].append(label)
labelSpecified = True
else:
Cmd.Backup()
usageErrorExit(Msg.FILTER_CAN_ONLY_CONTAIN_ONE_USER_LABEL)
elif not categorySpecified:
body['action']['addLabelIds'].append(labelUpper)
categorySpecified = True
else:
Cmd.Backup()
usageErrorExit(Msg.FILTER_CAN_ONLY_CONTAIN_ONE_CATEGORY_LABEL)
else:
body['action']['addLabelIds'].append(labelUpper)
if (labelUpper == 'IMPORTANT') and (labelUpper in body['action']['removeLabelIds']):
body['action']['removeLabelIds'].remove(labelUpper)
elif myarg == 'category':
if not categorySpecified:
body['action']['addLabelIds'].append(getChoice(FILTER_CATEGORY_CHOICE_MAP, mapChoice=True))
categorySpecified = True
else:
Cmd.Backup()
usageErrorExit(Msg.FILTER_CAN_ONLY_CONTAIN_ONE_CATEGORY_LABEL)
else:
unknownArgumentExit()
elif myarg == 'json':
jsonData = getJSON([])
body['criteria'] = jsonData['criteria']
body['action'] = jsonData['action']
elif myarg == 'buildpath':
buildPath = getBoolean()
else:
unknownArgumentExit()
if not body['criteria']:
missingChoiceExit(FILTER_CRITERIA_CHOICE_MAP)
if not body['action'].get('addLabelIds') and not body['action'].get('removeLabelIds') and 'forward' not in body['action']:
missingChoiceExit(FILTER_ACTION_CHOICES)
addLabelIndicies = {}
for field in ['addLabelIds', 'removeLabelIds']:
for i, labelId in enumerate(body['action'].get(field, [])):
if labelId not in GMAIL_SYSTEM_LABELS and labelId not in GMAIL_CATEGORY_LABELS:
addLabelIndicies.setdefault(labelId, {})
addLabelIndicies[labelId][field] = i
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if addLabelIndicies:
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
if not labels:
continue
labelSet = _getLabelSet(labels)
try:
lcount = len(addLabelIndicies)
l = 0
for addLabelName, addLabelData in iter(addLabelIndicies.items()):
l += 1
retries = 3
for _ in range(1, retries+1):
addLabelId = _getLabelId(labels, addLabelName)
if addLabelId:
retries = 0
break
lbody = {'name': addLabelName}
if not buildPath:
try:
result = callGAPI(gmail.users().labels(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.DUPLICATE, GAPI.PERMISSION_DENIED],
userId='me', body=lbody, fields='id')
entityActionPerformed([Ent.USER, user, Ent.LABEL, addLabelName], l, lcount)
addLabelId = result['id']
labels['labels'].append({'id': result['id'], 'name': addLabelName})
retries = 0
break
except GAPI.duplicate:
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
labelSet = _getLabelSet(labels)
else:
buildLabelPath(gmail, user, i, count, lbody, addLabelName, labelSet, l, lcount)
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name, type)')
labelSet = _getLabelSet(labels)
if retries:
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, addLabelName], Msg.DUPLICATE, i, count)
continue
for field in ['addLabelIds', 'removeLabelIds']:
if field in addLabelData:
body['action'][field][addLabelData[field]] = addLabelId
result = callGAPI(gmail.users().settings().filters(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
userId='me', body=body, fields='id')
if result:
entityActionPerformed([Ent.USER, user, Ent.FILTER, result['id']], i, count)
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.FILTER, ''], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> delete filter <FilterIDEntity>
def deleteFilters(users):
filterIdEntity = getUserObjectEntity(Cmd.OB_FILTER_ID_ENTITY, Ent.FILTER)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, filterIds, jcount = _validateUserGetObjectList(user, i, count, filterIdEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for filterId in filterIds:
j += 1
try:
callGAPI(gmail.users().settings().filters(), 'delete',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
userId='me', id=filterId)
entityActionPerformed([Ent.USER, user, Ent.FILTER, filterId], j, jcount)
except (GAPI.notFound, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.FILTER, filterId], str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> info filters <FilterIDEntity> [labelidsonly] [formatjson]
def infoFilters(users):
labelIdsOnly = False
filterIdEntity = getUserObjectEntity(Cmd.OB_FILTER_ID_ENTITY, Ent.FILTER)
FJQC = FormatJSONQuoteChar()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'labelidsonly':
labelIdsOnly = True
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, filterIds, jcount = _validateUserGetObjectList(user, i, count, filterIdEntity,
showAction=FJQC is None or not FJQC.formatJSON)
if jcount == 0:
continue
if not labelIdsOnly:
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name)')
if not labels:
continue
else:
labels = {'labels': []}
Ind.Increment()
j = 0
for filterId in filterIds:
j += 1
try:
result = callGAPI(gmail.users().settings().filters(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND],
userId='me', id=filterId)
if not FJQC.formatJSON:
printEntityKVList([Ent.USER, user],
[Ent.Singular(Ent.FILTER), result['id']],
i, count)
Ind.Increment()
_showFilter(result, j, jcount, labels, FJQC)
Ind.Decrement()
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.FILTER, filterId], str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> print filters [labelidsonly] [todrive <ToDriveAttribute>*]
# [formatjson] [quotechar <Character>]
# gam <UserTypeEntity> show filters [labelidsonly] [formatjson]
def printShowFilters(users):
labelIdsOnly = False
csvPF = CSVPrintFile() if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'labelidsonly':
labelIdsOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg)
if csvPF:
csvPF.SetFormatJSON(False)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if not labelIdsOnly:
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name)')
if not labels:
continue
else:
labels = {'labels': []}
if csvPF:
printGettingEntityItemForWhom(Ent.FILTER, user, i, count)
try:
results = callGAPIitems(gmail.users().settings().filters(), 'list', 'filter',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
jcount = len(results)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.FILTER, i, count)
Ind.Increment()
j = 0
for userFilter in results:
j += 1
_showFilter(userFilter, j, jcount, labels, FJQC)
Ind.Decrement()
else:
if results:
for userFilter in results:
row = _printFilter(user, userFilter, labels)
if FJQC.formatJSON:
if labels['labels']:
_mapFilterLabelIdsToNames(userFilter, labels)
row['JSON'] = json.dumps(userFilter, ensure_ascii=False, sort_keys=False)
csvPF.WriteRowTitles(row)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.SetFormatJSON(False)
csvPF.SetSortTitles(['User', 'id', 'from', 'to', 'subject', 'query', 'negatedQuery', 'hasAttachment', 'excludeChats', 'size', 'forward',
'archive', 'important', 'label', 'markread', 'star'])
csvPF.SortTitles()
if FJQC.formatJSON:
csvPF.MoveTitlesToEnd(['JSON'])
csvPF.SetSortTitles([])
csvPF.writeCSVfile('Filters')
def findFormRequest(field, newRequest, ubody):
for request in ubody['requests']:
if field in request:
return request
ubody['requests'].append(newRequest)
return ubody['requests'][-1]
def updateFormInfoRequest(key, value, ubody):
request = findFormRequest('updateFormInfo',
{'updateFormInfo': {'info': {}, 'updateMask': []}},
ubody)
request['updateFormInfo']['info'][key] = value
request['updateFormInfo']['updateMask'].append(key)
def updateFormSettingsRequest(key, value, ubody):
request = findFormRequest('updateSettings',
{'updateSettings': {'settings': {'quizSettings': {}}, 'updateMask': []}},
ubody)
request['updateSettings']['settings']['quizSettings'][key] = value
request['updateSettings']['updateMask'].append(f'quizSettings.{key}')
def updateFormRequestUpdateMasks(ubody):
for request in ubody['requests']:
for k, v in request.items():
if k in {'updateFormInfo', 'updateSettings'}:
v['updateMask'] = ','.join(v['updateMask'])
break
# gam <UserTypeEntity> create form
# title <String> [description <String>] [isquiz [<Boolean>]] [<JSONData>]
# [drivefilename <DriveFileName>] [<DriveFileParentAttribute>]
# [(csv [todrive <ToDriveAttribute>*]) | returnidonly]
def createForm(users):
csvPF = None
returnIdOnly = False
title = ''
body = {'mimeType': MIMETYPE_GA_FORM}
ubody = {'includeFormInResponse': True, 'requests': []}
parentParms = initDriveFileAttributes()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'title':
title = getString(Cmd.OB_STRING)
updateFormInfoRequest(myarg, title, ubody)
elif myarg == 'description':
updateFormInfoRequest(myarg, getString(Cmd.OB_STRING, minLen=0), ubody)
elif myarg == 'isquiz':
updateFormSettingsRequest('isQuiz', getBoolean(), ubody)
elif myarg == 'json':
jsonData = getJSON([])
ubody['requests'].extend(jsonData.get('requests', []))
elif myarg == 'drivefilename':
body['name'] = getString(Cmd.OB_DRIVE_FILE_NAME)
elif getDriveFileParentAttribute(myarg, parentParms):
pass
elif myarg == 'returnidonly':
returnIdOnly = True
elif myarg == 'csv':
csvPF = CSVPrintFile(['User', 'formId', 'name', 'title', 'responderUri'])
elif csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
unknownArgumentExit()
if not title:
missingArgumentExit('title')
updateFormRequestUpdateMasks(ubody)
if 'name' not in body:
body['name'] = title
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if not _getDriveFileParentInfo(drive, user, i, count, body, parentParms):
continue
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
if not gform:
continue
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
body=body, fields='id,name', supportsAllDrives=True)
formId = result['id']
form = callGAPI(gform.forms(), 'batchUpdate',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
formId=formId, body=ubody)
if returnIdOnly:
writeStdout(f'{formId}\n')
elif not csvPF:
entityActionPerformed([Ent.USER, user, Ent.FORM, title,
Ent.DRIVE_FILE, f"{result['name']}({formId})"])
else:
csvPF.WriteRow({'User': user, 'formId': formId,
'name': form['form']['info']['documentTitle'],
'title': form['form']['info']['title'],
'responderUri': form['form']['responderUri']})
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.FORM, title, Ent.DRIVE_FILE, body['name']], str(e), i, count)
except GAPI.permissionDenied:
SvcAcctAPIDisabledExit()
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
if csvPF:
csvPF.writeCSVfile('Forms')
# gam <UserTypeEntity> update form <DriveFileEntity>
# [title <String>] [description <String>] [isquiz [Boolean>]] [<JSONData>]
def updateForm(users):
ubody = {'includeFormInResponse': False, 'requests': []}
fileIdEntity = getDriveFileEntity()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'title':
updateFormInfoRequest(myarg, getString(Cmd.OB_STRING), ubody)
elif myarg == 'description':
updateFormInfoRequest(myarg, getString(Cmd.OB_STRING, minLen=0), ubody)
elif myarg == 'isquiz':
updateFormSettingsRequest('isQuiz', getBoolean(), ubody)
elif myarg == 'json':
jsonData = getJSON([])
ubody['requests'].extend(jsonData.get('requests', []))
else:
unknownArgumentExit()
updateFormRequestUpdateMasks(ubody)
if not ubody['requests']:
return
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.FORM)
if jcount == 0:
continue
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
if not gform:
continue
Ind.Increment()
j = 0
for formId in fileIdEntity['list']:
j += 1
try:
callGAPI(gform.forms(), 'batchUpdate',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
formId=formId, body=ubody)
entityActionPerformed([Ent.USER, user, Ent.FORM, formId], j, jcount)
except (GAPI.notFound, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user, Ent.FORM, formId], str(e), j, jcount)
Ind.Decrement()
# gam <UserTypeEntity> print forms <DriveFileEntity> [todrive <ToDriveAttribute>*]
# (addcsvdata <FieldName> <String>)*
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show forms <DriveFileEntity>
# [formatjson]
def printShowForms(users):
csvPF = CSVPrintFile(['User', 'formId', 'name', 'title', 'description'], 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
fileIdEntity = getDriveFileEntity()
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF and addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if FJQC.formatJSON:
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
csvPF.MoveJSONTitlesToEnd(['JSON'])
csvPF.SetSortAllTitles()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.FORM, None][csvPF is not None or FJQC.formatJSON])
if jcount == 0:
continue
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
if not gform:
continue
Ind.Increment()
j = 0
for formId in fileIdEntity['list']:
j += 1
try:
result = callGAPI(gform.forms(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
formId=formId)
if not csvPF:
if not FJQC.formatJSON:
printEntity([Ent.FORM, result['formId']], j, jcount)
Ind.Increment()
showJSON(None, result)
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(result), ensure_ascii=False, sort_keys=True))
else:
info = result.pop('info')
baserow = {'User': user, 'formId': formId, 'name': info['documentTitle'],
'title': info.get('title', ''), 'description': info.get('description', '')}
if addCSVData:
baserow.update(addCSVData)
row = flattenJSON(result, flattened=baserow.copy())
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
result['info'] = info
baserow['JSON'] = json.dumps(cleanJSON(result), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(baserow)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.FORM, formId], str(e), j, jcount)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Forms')
FORM_RESPONSE_TIME_OBJECTS = {'createTime', 'lastSubmittedTime'}
# gam <UserTypeEntity> print formresponses <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [filtertime.* <Time>] [filter <String>]
# (addcsvdata <FieldName> <String>)*
# [countsonly|(formatjson [quotechar <Character>])]
# gam <UserTypeEntity> show formresponses <DriveFileEntity>
# [filtertime.* <Time>] [filter <String>]
# [countsonly|formatjson]
def printShowFormResponses(users):
csvPF = CSVPrintFile(['User', 'formId', 'responseId', 'createTime', 'lastSubmittedTime', 'respondentEmail', 'totalScore'],
'sortall', indexedTitles=['answers']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
frfilter = None
filterTimes = {}
fileIdEntity = getDriveFileEntity()
countsOnly = False
addCSVData = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg.startswith('filtertime'):
filterTimes[myarg] = getTimeOrDeltaFromNow()
elif myarg in {'filter', 'filters'}:
frfilter = getString(Cmd.OB_STRING)
elif myarg == 'countsonly':
countsOnly = True
elif csvPF and myarg == 'addcsvdata':
k = getString(Cmd.OB_STRING)
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if filterTimes and filter is not None:
for filterTimeName, filterTimeValue in iter(filterTimes.items()):
frfilter = frfilter.replace(f'#{filterTimeName}#', filterTimeValue)
if csvPF:
if countsOnly:
csvPF.SetTitles(['User', 'formId', 'responses'])
if addCSVData:
csvPF.AddTitles(sorted(addCSVData.keys()))
if FJQC.formatJSON:
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
csvPF.MoveJSONTitlesToEnd(['JSON'])
csvPF.SetSortAllTitles()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
entityType=[Ent.FORM, None][csvPF is not None or FJQC.formatJSON])
if jcount == 0:
continue
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
if not gform:
continue
Ind.Increment()
j = 0
for formId in fileIdEntity['list']:
j += 1
try:
results = callGAPIpages(gform.forms().responses(), 'list', 'responses',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
filter=frfilter, formId=formId)
kcount = len(results)
if countsOnly:
if not csvPF:
printEntityKVList([Ent.FORM, formId], [Ent.Plural(Ent.FORM_RESPONSE), kcount], j, jcount)
else:
row = {'User': user, 'formId': formId, 'responses': kcount}
if addCSVData:
row.update(addCSVData)
csvPF.WriteRowTitles(row)
continue
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.FORM, formId], kcount, Ent.FORM_RESPONSE, j, jcount)
Ind.Increment()
k = 0
for response in results:
k += 1
if not FJQC.formatJSON:
printEntity([Ent.FORM_RESPONSE, response['responseId']], k, kcount)
Ind.Increment()
showJSON(None, response, timeObjects=FORM_RESPONSE_TIME_OBJECTS)
Ind.Decrement()
else:
printLine(json.dumps(cleanJSON(response, timeObjects=FORM_RESPONSE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True))
Ind.Decrement()
else:
baserow = {'User': user, 'formId': formId}
if addCSVData:
baserow.update(addCSVData)
for response in results:
row = flattenJSON(response, flattened=baserow.copy())
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = baserow.copy()
row.update({'responseId': response['responseId'],
'createTime': response['createTime'],
'lastSubmittedTime': response['lastSubmittedTime'],
'respondentEmail': response.get('respondentEmail', ''),
'totalScore': response.get('totalScore', ''),
'JSON': json.dumps(cleanJSON(response, timeObjects=FORM_RESPONSE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
csvPF.WriteRowNoFilter(row)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.FORM, formId], str(e), j, jcount)
Ind.Decrement()
if csvPF:
csvPF.writeCSVfile('Form Responses')
EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP = {
'ARCHIVE': 'archive',
'DELETE': 'trash',
'KEEP': 'leaveInInBox',
'MARK_READ': 'markRead',
}
def _showForward(user, i, count, result):
if 'enabled' in result:
enabled = result['enabled']
kvList = [Ent.Singular(Ent.FORWARD_ENABLED), enabled]
if enabled:
kvList += [Ent.Singular(Ent.FORWARDING_ADDRESS), result['emailAddress']]
kvList += [Ent.Singular(Ent.ACTION), result['disposition']]
else:
enabled = result['enable'] == 'true'
kvList = [Ent.Singular(Ent.FORWARD_ENABLED), enabled]
if enabled:
kvList += [Ent.Singular(Ent.FORWARDING_ADDRESS), result['forwardTo']]
kvList += [Ent.Singular(Ent.ACTION), EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result['action']]]
printEntityKVList([Ent.USER, user], kvList, i, count)
EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP = {
'archive': 'archive',
'delete': 'trash',
'keep': 'leaveInInbox',
'leaveininbox': 'leaveInInbox',
'markread': 'markRead',
'trash': 'trash',
}
# gam <UserTypeEntity> forward <FalseValues>
# gam <UserTypeEntity> forward <TrueValues> keep|leaveininbox|archive|delete|trash|markread <EmailAddress>
def setForward(users):
if checkArgumentPresent([Cmd.ARG_MESSAGE, Cmd.ARG_MESSAGES]):
Act.Set(Act.FORWARD)
forwardMessagesThreads(users, Ent.MESSAGE)
return
if checkArgumentPresent([Cmd.ARG_THREAD, Cmd.ARG_THREADS]):
Act.Set(Act.FORWARD)
forwardMessagesThreads(users, Ent.THREAD)
return
enable = getBoolean(None)
body = {'enabled': enable}
if enable:
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP:
body['disposition'] = EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP[myarg]
elif myarg == 'confirm':
pass
elif myarg.find('@') != -1 or (not Cmd.ArgumentsRemaining() and 'emailAddress' not in body):
body['emailAddress'] = normalizeEmailAddressOrUID(Cmd.Previous())
else:
unknownArgumentExit()
if not body.get('disposition'):
missingChoiceExit(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP)
if not body.get('emailAddress'):
missingArgumentExit(Cmd.OB_EMAIL_ADDRESS)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().settings(), 'updateAutoForwarding',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
userId='me', body=body)
_showForward(user, i, count, result)
except (GAPI.invalid, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
if enable:
entityActionFailedWarning([Ent.USER, user, Ent.FORWARDING_ADDRESS, body['emailAddress']], str(e), i, count)
else:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> print forward [enabledonly] [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show forward
def printShowForward(users):
def _printForward(user, result, showDisabled):
if 'enabled' in result:
enabled = result['enabled']
if not enabled and not showDisabled:
return
row = {'User': user, 'forwardEnabled': enabled}
if enabled:
row['forwardTo'] = result['emailAddress']
row['disposition'] = result['disposition']
else:
enabled = result['enable'] == 'true'
if not enabled and not showDisabled:
return
row = {'User': user, 'forwardEnabled': enabled}
if enabled:
row['forwardTo'] = result['forwardTo']
row['disposition'] = EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result['action']]
csvPF.WriteRow(row)
csvPF = CSVPrintFile(['User', 'forwardEnabled', 'forwardTo', 'disposition']) if Act.csvFormat() else None
showDisabled = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif csvPF and myarg == 'enabledonly':
showDisabled = False
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.FORWARD_ENABLED, user, i, count)
try:
result = callGAPI(gmail.users().settings(), 'getAutoForwarding',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
userId='me')
if not csvPF:
_showForward(user, i, count, result)
else:
_printForward(user, result, showDisabled)
except GAPI.failedPrecondition as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Forward')
# Process ForwardingAddresses functions
def _showForwardingAddress(j, jcount, result):
printEntityKVList([Ent.FORWARDING_ADDRESS, result['forwardingEmail']], ['Verification Status', result['verificationStatus']], j, jcount)
def _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, function, **kwargs):
userDefined = True
try:
result = callGAPI(gmail.users().settings().forwardingAddresses(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS, GAPI.DUPLICATE,
GAPI.INVALID_ARGUMENT, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
userId='me', **kwargs)
if function == 'get':
_showForwardingAddress(j, count, result)
else:
entityActionPerformed([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], j, jcount)
except (GAPI.notFound, GAPI.alreadyExists, GAPI.duplicate, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
userDefined = False
return userDefined
# gam <UserTypeEntity> create forwardingaddresses <EmailAddressEntity>
def createForwardingAddresses(users):
emailAddressEntity = getUserObjectEntity(Cmd.OB_EMAIL_ADDRESS_ENTITY, Ent.FORWARDING_ADDRESS)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, emailAddresses, jcount = _validateUserGetObjectList(user, i, count, emailAddressEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for emailAddress in emailAddresses:
j += 1
emailAddress = normalizeEmailAddressOrUID(emailAddress, noUid=True)
body = {'forwardingEmail': emailAddress}
if not _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, 'create', body=body, fields=''):
break
Ind.Decrement()
def _deleteInfoForwardingAddreses(users, function):
emailAddressEntity = getUserObjectEntity(Cmd.OB_EMAIL_ADDRESS_ENTITY, Ent.FORWARDING_ADDRESS)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, emailAddresses, jcount = _validateUserGetObjectList(user, i, count, emailAddressEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for emailAddress in emailAddresses:
j += 1
emailAddress = normalizeEmailAddressOrUID(emailAddress, noUid=True)
if not _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, function, forwardingEmail=emailAddress):
break
Ind.Decrement()
# gam <UserTypeEntity> delete forwardingaddresses <EmailAddressEntity>
def deleteForwardingAddresses(users):
_deleteInfoForwardingAddreses(users, 'delete')
# gam <UserTypeEntity> info forwardingaddresses <EmailAddressEntity>
def infoForwardingAddresses(users):
_deleteInfoForwardingAddreses(users, 'get')
# gam <UserTypeEntity> print forwardingaddresses [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show forwardingaddresses
def printShowForwardingAddresses(users):
csvPF = CSVPrintFile(['User', 'forwardingEmail', 'verificationStatus']) if Act.csvFormat() else None
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.FORWARDING_ADDRESS, user, i, count)
try:
results = callGAPIitems(gmail.users().settings().forwardingAddresses(), 'list', 'forwardingAddresses',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
userId='me')
jcount = len(results)
if not csvPF:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.FORWARDING_ADDRESS, i, count)
Ind.Increment()
j = 0
for forward in results:
j += 1
_showForwardingAddress(j, jcount, forward)
Ind.Decrement()
else:
if results:
for forward in results:
csvPF.WriteRow({'User': user, 'forwardingEmail': forward['forwardingEmail'], 'verificationStatus': forward['verificationStatus']})
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
except GAPI.failedPrecondition as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Forwarding Addresses')
def _showImap(user, i, count, result):
enabled = result['enabled']
kvList = [Ent.Singular(Ent.IMAP_ENABLED), enabled]
for item in result:
if item != 'enabled':
kvList += [item, result[item]]
printEntityKVList([Ent.USER, user], kvList, i, count)
#
EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICE_MAP = {
'archive': 'archive',
'deleteforever': 'deleteForever',
'trash': 'trash',
}
EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES = ['0', '1000', '2000', '5000', '10000']
def _imapDefaults(enable):
return {'enabled': enable, 'autoExpunge': True, 'expungeBehavior': 'archive', 'maxFolderSize': 0}
def _setImap(user, body, i, count):
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if gmail:
try:
result = callGAPI(gmail.users().settings(), 'updateImap',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me', body=body)
_showImap(user, i, count, result)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> imap|imap4 <Boolean> [noautoexpunge] [expungebehavior archive|deleteforever|trash] [maxfoldersize 0|1000|2000|5000|10000]
def setImap(users):
body = _imapDefaults(getBoolean(None))
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'noautoexpunge':
body['autoExpunge'] = False
elif myarg == 'expungebehavior':
body['expungeBehavior'] = getChoice(EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICE_MAP, mapChoice=True)
elif myarg == 'maxfoldersize':
body['maxFolderSize'] = int(getChoice(EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES))
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
_setImap(user, body, i, count)
# gam <UserTypeEntity> print imap|imap4 [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show imap|imap4
def printShowImap(users):
csvPF = CSVPrintFile(['User', 'enabled']) if Act.csvFormat() else None
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().settings(), 'getImap',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
if not csvPF:
_showImap(user, i, count, result)
else:
csvPF.WriteRowTitles(flattenJSON(result, flattened={'User': user}))
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('IMAP')
def _showPop(user, i, count, result):
enabled = result['accessWindow'] != 'disabled'
kvList = [Ent.Singular(Ent.POP_ENABLED), enabled]
if enabled:
kvList += ['For', result['accessWindow'], Ent.Singular(Ent.ACTION), result['disposition']]
printEntityKVList([Ent.USER, user], kvList, i, count)
#
EMAILSETTINGS_POP_ENABLE_FOR_CHOICE_MAP = {
'allmail': 'allMail',
'fromnowon': 'fromNowOn',
'mailfromnowon': 'fromNowOn',
'newmail': 'fromNowOn',
}
def _popDefaults(enable):
return {'accessWindow': ['disabled', 'allMail'][enable], 'disposition': 'leaveInInbox'}
def _setPop(user, body, i, count):
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if gmail:
try:
result = callGAPI(gmail.users().settings(), 'updatePop',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me', body=body)
_showPop(user, i, count, result)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> pop|pop3 <Boolean> [for allmail|newmail|mailfromnowon|fromnowown] [action keep|leaveininbox|archive|delete|trash|markread]
def setPop(users):
body = _popDefaults(getBoolean(None))
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'for':
body['accessWindow'] = getChoice(EMAILSETTINGS_POP_ENABLE_FOR_CHOICE_MAP, mapChoice=True)
elif myarg == 'action':
body['disposition'] = getChoice(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP, mapChoice=True)
elif myarg == 'confirm':
pass
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
_setPop(user, body, i, count)
# gam <UserTypeEntity> print pop|pop3 [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show pop|pop3
def printShowPop(users):
csvPF = CSVPrintFile(['User', 'enabled']) if Act.csvFormat() else None
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().settings(), 'getPop',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
if not csvPF:
_showPop(user, i, count, result)
else:
csvPF.WriteRowTitles(flattenJSON(result, flattened={'User': user, 'enabled': result['accessWindow'] != 'disabled'}))
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('POP')
# gam <UserTypeEntity> language <Language>
def setLanguage(users):
language = getLanguageCode(LANGUAGE_CODES_MAP)
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().settings(), 'updateLanguage',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me', body={'displayLanguage': language})
entityActionPerformed([Ent.USER, user, Ent.LANGUAGE, result['displayLanguage']], i, count)
except GAPI.permissionDenied as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> print language [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show language
def printShowLanguage(users):
csvPF = CSVPrintFile(['User', 'displayLanguage']) if Act.csvFormat() else None
getTodriveOnly(csvPF)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
result = callGAPI(gmail.users().settings(), 'getLanguage',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
if not csvPF:
printEntity([Ent.USER, user, Ent.LANGUAGE, result['displayLanguage']], i, count)
else:
csvPF.WriteRowTitles(flattenJSON(result, flattened={'User': user}))
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Language')
SIG_REPLY_HTML = 0
SIG_REPLY_COMPACT = 1
SIG_REPLY_FORMAT = 2
SIG_REPLY_OPTIONS_MAP = {'html': SIG_REPLY_HTML, 'compact': SIG_REPLY_COMPACT, 'format': SIG_REPLY_FORMAT}
SMTPMSA_DISPLAY_FIELDS = ['host', 'port', 'securityMode']
def _showSendAs(result, j, jcount, sigReplyFormat, verifyOnly=False):
if result['displayName']:
printEntity([Ent.SENDAS_ADDRESS, f'{result["displayName"]} <{result["sendAsEmail"]}>'], j, jcount)
else:
printEntity([Ent.SENDAS_ADDRESS, f'<{result["sendAsEmail"]}>'], j, jcount)
Ind.Increment()
if result.get('replyToAddress'):
printKeyValueList(['ReplyTo', result['replyToAddress']])
printKeyValueList(['IsPrimary', result.get('isPrimary', False)])
printKeyValueList(['Default', result.get('isDefault', False)])
if not result.get('isPrimary', False):
printKeyValueList(['TreatAsAlias', result.get('treatAsAlias', False)])
if 'smtpMsa' in result:
for field in SMTPMSA_DISPLAY_FIELDS:
if field in result['smtpMsa']:
printKeyValueList([f'smtpMsa.{field}', result['smtpMsa'][field]])
if 'verificationStatus' in result:
printKeyValueList(['Verification Status', result['verificationStatus']])
signature = result.get('signature')
if verifyOnly:
printKeyValueList(['Signature', bool(signature)])
else:
if not signature:
signature = 'None'
if sigReplyFormat == SIG_REPLY_HTML:
printKeyValueList(['Signature', None])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(signature)])
Ind.Decrement()
elif sigReplyFormat == SIG_REPLY_FORMAT:
printKeyValueList(['Signature', None])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(dehtml(signature))])
Ind.Decrement()
else: # SIG_REPLY_COMPACT
printKeyValueList(['Signature', escapeCRsNLs(signature)])
Ind.Decrement()
def _processSignature(tagReplacements, signature, html):
if signature:
signature = signature.replace('\r', '').replace('\\n', '<br/>')
if tagReplacements['tags']:
signature = _processTagReplacements(tagReplacements, signature)
if not html:
signature = signature.replace('\n', '<br/>')
return signature
# Process SendAs functions
def _processSendAs(user, i, count, entityType, emailAddress, j, jcount, gmail, function,
sigReplyFormat, verifyOnly=False, **kwargs):
userDefined = True
try:
result = callGAPI(gmail.users().settings().sendAs(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS, GAPI.DUPLICATE,
GAPI.CANNOT_DELETE_PRIMARY_SENDAS, GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED,
GAPI.INSUFFICIENT_PERMISSIONS],
userId='me', **kwargs)
if function == 'get':
_showSendAs(result, j, jcount, sigReplyFormat, verifyOnly)
else:
entityActionPerformed([Ent.USER, user, entityType, emailAddress], j, jcount)
except (GAPI.notFound, GAPI.alreadyExists, GAPI.duplicate,
GAPI.cannotDeletePrimarySendAs, GAPI.invalidArgument,
GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.insufficientPermissions) as e:
entityActionFailedWarning([Ent.USER, user, entityType, emailAddress], str(e), j, jcount)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
userDefined = False
return userDefined
def getSendAsAttributes(myarg, body, tagReplacements):
if _getTagReplacement(myarg, tagReplacements, True):
pass
elif myarg == 'name':
body['displayName'] = getString(Cmd.OB_NAME, minLen=0)
elif myarg == 'replyto':
body['replyToAddress'] = getString(Cmd.OB_EMAIL_ADDRESS, minLen=0)
if len(body['replyToAddress']) > 0:
body['replyToAddress'] = normalizeEmailAddressOrUID(body['replyToAddress'], noUid=True, noLower=True)
elif myarg == 'default':
body['isDefault'] = True
elif myarg == 'treatasalias':
body['treatAsAlias'] = getBoolean()
else:
unknownArgumentExit()
SMTPMSA_PORTS = ['25', '465', '587']
SMTPMSA_SECURITY_MODES = ['none', 'ssl', 'starttls']
SMTPMSA_REQUIRED_FIELDS = ['host', 'port', 'username', 'password']
# gam <UserTypeEntity> [create] sendas <EmailAddress> [name] <String>
# [<SendAsContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
# [html [<Boolean>]] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
# [smtpmsa.host <SMTPHostName> smtpmsa.port 25|465|587
# smtpmsa.username <UserName> smtpmsa.password <Password>
# [smtpmsa.securitymode none|ssl|starttls]]
# gam <UserTypeEntity> update sendas <EmailAddress> [name <String>]
# [<SendAsContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
# [html [<Boolean>]] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
def createUpdateSendAs(users):
updateCmd = Act.Get() == Act.UPDATE
emailAddress = normalizeEmailAddressOrUID(getString(Cmd.OB_EMAIL_ADDRESS), noUid=True, noLower=True)
if not updateCmd:
body = {'sendAsEmail': emailAddress}
checkArgumentPresent(['name'])
body['displayName'] = getString(Cmd.OB_NAME)
else:
body = {}
signature = None
smtpMsa = {}
tagReplacements = _initTagReplacements()
html = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in SORF_SIG_FILE_ARGUMENTS:
signature, _, html = getStringOrFile(myarg)
elif myarg == 'html':
html = getBoolean()
elif not updateCmd and myarg.startswith('smtpmsa.'):
if myarg == 'smtpmsa.host':
smtpMsa['host'] = getString(Cmd.OB_SMTP_HOST_NAME)
elif myarg == 'smtpmsa.port':
smtpMsa['port'] = int(getChoice(SMTPMSA_PORTS))
elif myarg == 'smtpmsa.username':
smtpMsa['username'] = getString(Cmd.OB_USER_NAME)
elif myarg == 'smtpmsa.password':
smtpMsa['password'] = getString(Cmd.OB_PASSWORD)
elif myarg == 'smtpmsa.securitymode':
smtpMsa['securityMode'] = getChoice(SMTPMSA_SECURITY_MODES)
else:
unknownArgumentExit()
else:
getSendAsAttributes(myarg, body, tagReplacements)
if signature is not None and not tagReplacements['subs']:
body['signature'] = _processSignature(tagReplacements, signature, html)
if smtpMsa:
for field in SMTPMSA_REQUIRED_FIELDS:
if field not in smtpMsa:
missingArgumentExit(f'smtpmsa.{field}')
body['smtpMsa'] = smtpMsa
kwargs = {'body': body, 'fields': ''}
if updateCmd:
kwargs['sendAsEmail'] = emailAddress
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if signature is not None and tagReplacements['subs']:
_getTagReplacementFieldValues(user, i, count, tagReplacements)
kwargs['body']['signature'] = _processSignature(tagReplacements, signature, html)
_processSendAs(user, i, count, Ent.SENDAS_ADDRESS, emailAddress, i, count, gmail, ['create', 'patch'][updateCmd], False, **kwargs)
# gam <UserTypeEntity> delete sendas <EmailAddressEntity>
# gam <UserTypeEntity> info sendas <EmailAddressEntity> [compact|format|html]
def deleteInfoSendAs(users):
function = 'delete' if Act.Get() == Act.DELETE else 'get'
emailAddressEntity = getUserObjectEntity(Cmd.OB_EMAIL_ADDRESS_ENTITY, Ent.SENDAS_ADDRESS)
sigReplyFormat = SIG_REPLY_HTML
if function == 'get':
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in SIG_REPLY_OPTIONS_MAP:
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
else:
unknownArgumentExit()
else:
checkForExtraneousArguments()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail, emailAddresses, jcount = _validateUserGetObjectList(user, i, count, emailAddressEntity)
if jcount == 0:
continue
Ind.Increment()
j = 0
for emailAddress in emailAddresses:
j += 1
emailAddress = normalizeEmailAddressOrUID(emailAddress, noUid=True)
if not _processSendAs(user, i, count, Ent.SENDAS_ADDRESS, emailAddress, j, jcount, gmail, function, sigReplyFormat, sendAsEmail=emailAddress):
break
Ind.Decrement()
# gam <UserTypeEntity> print sendas [compact]
# [primary|default] [verifyonly] [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show sendas [compact|format|html]
# [primary|default] [verifyonly]
def printShowSendAs(users):
csvPF = CSVPrintFile(['User', 'displayName', 'sendAsEmail', 'replyToAddress',
'isPrimary', 'isDefault', 'treatAsAlias', 'verificationStatus'],
'sortall') if Act.csvFormat() else None
sigReplyFormat = SIG_REPLY_HTML
selection=None
verifyOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'primary':
selection = 'isPrimary'
elif myarg == 'default':
selection = 'isDefault'
elif myarg == 'verifyonly':
verifyOnly = True
elif (not csvPF and myarg in SIG_REPLY_OPTIONS_MAP) or (csvPF and myarg == 'compact'):
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.SENDAS_ADDRESS, user, i, count)
try:
results = callGAPIitems(gmail.users().settings().sendAs(), 'list', 'sendAs',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
jcount = len(results)
if not csvPF:
entityPerformActionNumItems([Ent.USER, user], jcount if selection is None else 1, Ent.SENDAS_ADDRESS, i, count)
Ind.Increment()
j = 0
for sendas in results:
j += 1
if (selection is None) or (sendas.get(selection, False)):
_showSendAs(sendas, j, jcount, sigReplyFormat, verifyOnly)
Ind.Decrement()
else:
if results:
for sendas in results:
if (selection is None) or (sendas.get(selection, False)):
row = {'User': user, 'isPrimary': False}
for item in sendas:
if item != 'smtpMsa':
if item == 'signature':
if verifyOnly:
row[item] = bool(sendas[item])
elif sigReplyFormat != SIG_REPLY_COMPACT:
row[item] = sendas[item]
else:
row[item] = sendas[item].replace('\r', '').replace('\n', '')
else:
row[item] = sendas[item]
else:
for field in SMTPMSA_DISPLAY_FIELDS:
if field in sendas[item]:
row[f'smtpMsa{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{field}'] = sendas[item][field]
csvPF.WriteRowTitles(row)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('SendAs')
# gam <UserTypeEntity> print signature [compact]
# [primary] [verifyonly] [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show signature|sig [compact|format|html]
# [primary] [verifyonly]
def printShowSignature(users):
csvPF = CSVPrintFile(['User', 'displayName', 'sendAsEmail', 'replyToAddress',
'isPrimary', 'isDefault', 'treatAsAlias', 'verificationStatus'],
'sortall') if Act.csvFormat() else None
sigReplyFormat = SIG_REPLY_HTML
selection = None
verifyOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'primary':
selection = 'isPrimary'
elif myarg == 'default':
selection = 'isDefault'
elif myarg == 'verifyonly':
verifyOnly = True
elif (not csvPF and myarg in SIG_REPLY_OPTIONS_MAP) or (csvPF and myarg == 'compact'):
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.SIGNATURE, user, i, count)
try:
if selection is None:
sendas = callGAPI(gmail.users().settings().sendAs(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me', sendAsEmail=user)
else:
results = callGAPIitems(gmail.users().settings().sendAs(), 'list', 'sendAs',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
for sendas in results:
if sendas.get(selection, False):
break
if not csvPF:
_showSendAs(sendas, 0, 0, sigReplyFormat, verifyOnly)
else:
row = {'User': user, 'isPrimary': False}
for item in sendas:
if item != 'smtpMsa':
if item == 'signature':
if verifyOnly:
row[item] = bool(sendas[item])
elif sigReplyFormat != SIG_REPLY_COMPACT:
row[item] = sendas[item]
else:
row[item] = sendas[item].replace('\r', '').replace('\n', '')
else:
row[item] = sendas[item]
else:
for field in SMTPMSA_DISPLAY_FIELDS:
if field in sendas[item]:
row[f'smtpMsa{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{field}'] = sendas[item][field]
csvPF.WriteRowTitles(row)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Signature')
# gam <UserTypeEntity> create smime file <FileName> [password <Password>]
# [sendas|sendasemail <EmailAddress>] [default]
def createSmime(users):
sendAsEmailBase = None
setDefault = False
body = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'file':
smimefile = getString(Cmd.OB_FILE_NAME)
smimeData = readFile(smimefile, mode='rb')
body['pkcs12'] = base64.urlsafe_b64encode(smimeData).decode(UTF8)
elif myarg == 'password':
body['encryptedKeyPassword'] = getString(Cmd.OB_PASSWORD)
elif myarg == 'default':
setDefault = True
elif myarg in {'sendas', 'sendasemail'}:
sendAsEmailBase = getEmailAddress(noUid=True)
else:
unknownArgumentExit()
if 'pkcs12' not in body:
missingArgumentExit('file')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
sendAsEmail = sendAsEmailBase if sendAsEmailBase else user
try:
Act.Set(Act.CREATE)
result = callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'insert',
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
userId='me', sendAsEmail=sendAsEmail, body=body, fields='id,issuerCn')
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, result['id']],
Act.MODIFIER_FROM, f'{Ent.Singular(Ent.ISSUER_CN)}: {result["issuerCn"]}', i, count)
if setDefault:
Act.Set(Act.UPDATE)
smimeId = result['id']
callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'setDefault',
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
userId='me', sendAsEmail=sendAsEmail, id=smimeId)
entityActionPerformedMessage([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, smimeId], Msg.DEFAULT_SMIME, i, count)
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
def _getSmimeIds(gmail, user, i, count, sendAsEmail, function):
try:
result = callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'list',
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
userId='me', sendAsEmail=sendAsEmail, fields='smimeInfo(id)')
smimes = result.get('smimeInfo', [])
jcount = len(smimes)
if jcount == 0:
entityActionNotPerformedWarning([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, None],
Msg.NO_ENTITIES_FOUND.format(Ent.Plural(Ent.SMIME_ID)), i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
elif jcount > 1:
entityActionNotPerformedWarning([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, None],
Msg.PLEASE_SELECT_ENTITY_TO_PROCESS.format(jcount, Ent.Plural(Ent.SMIME_ID), function, 'id <S/MIMEID>'),
i, count)
Ind.Increment()
j = 0
for smime in smimes:
j += 1
printEntityKVList([Ent.SMIME_ID, smime['id'], Ent.ISSUER_CN, smime['issuerCn']], ['Default', smime.get('isDefault', False)], j, jcount)
Ind.Decrement()
else:
return smimes[0]['id']
except GAPI.forbidden as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
return None
# gam <UserTypeEntity> update smime default
# [id <SmimeID>] [sendas|sendasemail <EmailAddress>]
def updateSmime(users):
smimeIdBase = None
sendAsEmailBase = None
setDefault = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'id':
smimeIdBase = getString(Cmd.OB_SMIME_ID)
elif myarg in {'sendas', 'sendasemail'}:
sendAsEmailBase = getEmailAddress(noUid=True)
elif myarg == 'default':
setDefault = True
else:
unknownArgumentExit()
if not setDefault:
missingArgumentExit('default')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
sendAsEmail = sendAsEmailBase if sendAsEmailBase else user
if not smimeIdBase:
smimeId = _getSmimeIds(gmail, user, i, count, sendAsEmail, 'update')
if not smimeId:
continue
else:
smimeId = smimeIdBase
try:
callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'setDefault',
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
userId='me', sendAsEmail=sendAsEmail, id=smimeId)
entityActionPerformedMessage([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, smimeId], Msg.DEFAULT_SMIME, i, count)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SMIME_ID, smimeId], str(e), i, count)
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> delete smime
# [id <SmimeID>] [sendas|sendasemail <EmailAddress>]
def deleteSmime(users):
smimeIdBase = None
sendAsEmailBase = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'id':
smimeIdBase = getString(Cmd.OB_SMIME_ID)
elif myarg in {'sendas', 'sendasemail'}:
sendAsEmailBase = getEmailAddress(noUid=True)
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
sendAsEmail = sendAsEmailBase if sendAsEmailBase else user
if not smimeIdBase:
smimeId = _getSmimeIds(gmail, user, i, count, sendAsEmail, 'delete')
if not smimeId:
continue
else:
smimeId = smimeIdBase
try:
callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'delete',
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
userId='me', sendAsEmail=sendAsEmail, id=smimeId)
entityActionPerformed([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, smimeId], i, count)
except GAPI.notFound as e:
entityActionFailedWarning([Ent.USER, user, Ent.SMIME_ID, smimeId], str(e), i, count)
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> print smimes [todrive <ToDriveAttribute>*]
# [primary|default|(sendas|sendasemail <EmailAddress>)]
# gam <UserTypeEntity> show smimes
# [primary|default|(sendas|sendasemail <EmailAddress>)]
def printShowSmimes(users):
csvPF = CSVPrintFile(['User', 'id', 'isDefault', 'issuerCn', 'expiration', 'encryptedKeyPassword', 'pem']) if Act.csvFormat() else None
selectField = 'sendAsEmail'
sendAsEmailBase = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'default', 'defaultonly'}:
selectField = 'isDefault'
elif myarg in {'primary', 'primaryonly'}:
selectField = 'isPrimary'
elif myarg in {'sendas', 'sendasemail'}:
sendAsEmailBase = getEmailAddress(noUid=True)
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
if sendAsEmailBase:
sendAsEmails = [sendAsEmailBase]
else:
results = callGAPIitems(gmail.users().settings().sendAs(), 'list', 'sendAs',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me', fields='sendAs(isDefault,isPrimary,sendAsEmail)')
sendAsEmails = [sendAs['sendAsEmail'] for sendAs in results if sendAs.get(selectField, False)]
jcount = len(sendAsEmails)
if not csvPF:
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.SMIME_ID, Act.MODIFIER_FROM, jcount, Ent.SENDAS_ADDRESS, i, count)
else:
printGettingEntityItemForWhom(Ent.SENDAS_ADDRESS, user, i, count)
if sendAsEmails:
j = 0
for sendAsEmail in sendAsEmails:
j += 1
smimes = callGAPIitems(gmail.users().settings().sendAs().smimeInfo(), 'list', 'smimeInfo',
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
userId='me', sendAsEmail=sendAsEmail)
kcount = len(smimes)
if not csvPF:
Ind.Increment()
printEntity([Ent.SENDAS_ADDRESS, sendAsEmail], j, jcount)
Ind.Increment()
k = 0
for smime in smimes:
k += 1
printEntity([Ent.SMIME_ID, smime['id']], k, kcount)
Ind.Increment()
printKeyValueList(['Default', smime.get('isDefault', False)])
printKeyValueList(['Issuer CN', smime['issuerCn']])
printKeyValueList(['Expiration', formatLocalTimestamp(smime['expiration'])])
printKeyValueList(['Password', smime.get('encryptedKeyPassword', '')])
printKeyValueList(['PEM', None])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(smime['pem'])])
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
Ind.Decrement()
elif smimes:
for smime in smimes:
smime['expiration'] = formatLocalTimestamp(smime['expiration'])
csvPF.WriteRowTitles(flattenJSON(smime, flattened={'User': user}))
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
elif csvPF and GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
except (GAPI.forbidden, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('S/MIME')
def _showCSEItem(result, entityType, keyField, timeObjects, i, count, FJQC):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(result, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True))
return
Ind.Increment()
printEntity([entityType, result[keyField]], i, count)
Ind.Increment()
showJSON(None, result, timeObjects=timeObjects)
Ind.Decrement()
Ind.Decrement()
def _initCSEKeyPairSkipObjects():
return {'pem', 'kaclsData'}
def _resetCSEKeyPairSkipObjects(myarg, skipObjects):
if myarg == 'showpem':
skipObjects.discard('pem')
elif myarg == 'showkaclsdata':
skipObjects.discard('kaclsData')
else:
return False
return True
def _stripCSEKeyPairSkipObjects(result, skipObjects):
if 'pem' in skipObjects:
result.pop('pem', None)
if 'kaclsData' in skipObjects:
for privateKeyMetadata in result.get('privateKeyMetadata', []):
if 'kaclsKeyMetadata' in privateKeyMetadata:
privateKeyMetadata['kaclsKeyMetadata'].pop('kaclsData', None)
CSE_IDENTITY_TIME_OBJECTS = {}
CSE_KEYPAIR_TIME_OBJECTS = {'disableTime'}
def _printShowCSEItems(users, entityType, keyField, timeObjects):
csvPF = CSVPrintFile(['User', keyField]) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
skipObjects = _initCSEKeyPairSkipObjects() if entityType == Ent.CSE_KEYPAIR else set()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif entityType == Ent.CSE_KEYPAIR and _resetCSEKeyPairSkipObjects(myarg, skipObjects):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
printGettingAllEntityItemsForWhom(entityType, user, i, count)
kvList = [Ent.USER, user, entityType, None]
try:
if entityType == Ent.CSE_IDENTITY:
results = callGAPIpages(gmail.users().settings().cse().identities(), 'list', 'cseIdentities',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me', fields='nextPageToken,cseIdentities')
else:
results = callGAPIpages(gmail.users().settings().cse().keypairs(), 'list', 'cseKeyPairs',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
userId='me', fields='nextPageToken,cseKeyPairs')
except GAPI.permissionDenied as e:
entityActionFailedWarning(kvList, str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(results)
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
j = 0
for result in results:
j += 1
if entityType == Ent.CSE_KEYPAIR:
_stripCSEKeyPairSkipObjects(result, skipObjects)
_showCSEItem(result, entityType, keyField, timeObjects, j, jcount, FJQC)
else:
for result in results:
row = flattenJSON(result, flattened={'User': user})
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'User': user, keyField: result[keyField],
'JSON': json.dumps(cleanJSON(result, skipObjects=skipObjects, timeObjects=timeObjects),
ensure_ascii=False, sort_keys=True)})
if csvPF:
csvPF.writeCSVfile(Ent.Plural(entityType))
CSE_IDENTITY_ACTION_FUNCTION_MAP = {
Act.CREATE: 'create',
Act.UPDATE: 'patch',
Act.DELETE: 'delete',
Act.INFO: 'get',
}
# gam <UserTypeEntity> create cseidentity
# (primarykeypairid <KeyPairID>) | (signingkeypairid <KeyPairID> encryptionkeypairid <KeyPairID>)
# [kpemail <EmailAddress>]
# [formatjson]
# gam <UserTypeEntity> update cseidentity
# (primarykeypairid <KeyPairID>) | (signingkeypairid <KeyPairID> encryptionkeypairid <KeyPairID>)
# [kpemail <EmailAddress>]
# [formatjson]
def createUpdateCSEIdentity(users):
function = CSE_IDENTITY_ACTION_FUNCTION_MAP[Act.Get()]
primaryKeyPairId = signingKeyPairId = encryptionKeyPairId = None
FJQC = FormatJSONQuoteChar()
kpEmail = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'primarykeypairid':
primaryKeyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
elif myarg == 'signingkeypairid':
signingKeyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
elif myarg == 'encryptionkeypairid':
encryptionKeyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
elif myarg == 'kpemail':
kpEmail = getEmailAddress(noUid=True)
else:
FJQC.GetFormatJSON(myarg)
if primaryKeyPairId:
if signingKeyPairId or encryptionKeyPairId:
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('primarykeypairid', 'signingkeypairid/encryptionkeypairid'))
identity = {'primaryKeyPairId': primaryKeyPairId, 'emailAddress': None}
keyPairId = primaryKeyPairId
elif signingKeyPairId or encryptionKeyPairId:
if not signingKeyPairId or not encryptionKeyPairId:
usageErrorExit(Msg.ARE_BOTH_REQUIRED.format('signingkeypairid', 'encryptionkeypairid'))
identity = {'signAndEncryptKeyPairs': {'signingKeyPairId': signingKeyPairId, 'encryptionKeyPairId': encryptionKeyPairId},
'emailAddress': None}
keyPairId = f'{signingKeyPairId}/{encryptionKeyPairId}'
else:
missingArgumentExit('primarykeypairid|(signingkeypairid & encryptionkeypairid)')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
identity['emailAddress'] = user if not kpEmail else kpEmail
kwargs = {'body': identity}
if function == 'patch':
kwargs['emailAddress'] = identity['emailAddress']
kvList = [Ent.USER, user, Ent.CSE_IDENTITY, identity['emailAddress'], Ent.CSE_KEYPAIR, keyPairId]
try:
result = callGAPI(gmail.users().settings().cse().identities(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS],
userId='me', **kwargs)
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
_showCSEItem(result, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS, i, count, FJQC)
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound, GAPI.alreadyExists) as e:
entityActionFailedWarning(kvList, str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> delete cseidentity [kpemail <EmailAddress>]
# gam <UserTypeEntity> info cseidentity [kpemail <EmailAddress>]
# [formatjson]
def processCSEIdentity(users):
function = CSE_IDENTITY_ACTION_FUNCTION_MAP[Act.Get()]
FJQC = FormatJSONQuoteChar()
kpEmail = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'kpemail':
kpEmail = getEmailAddress(noUid=True)
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
kwargs = {'cseEmailAddress': user if not kpEmail else kpEmail}
kvList = [Ent.USER, user, Ent.CSE_IDENTITY, kwargs['cseEmailAddress']]
try:
result = callGAPI(gmail.users().settings().cse().identities(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.NOT_FOUND],
userId='me', **kwargs)
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
if function != 'delete':
_showCSEItem(result, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS, i, count, FJQC)
except (GAPI.permissionDenied, GAPI.notFound) as e:
entityActionFailedWarning(kvList, str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> show cseidentities
# [formatjson]
# gam <UserTypeEntity> print cseidentities [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
def printShowCSEIdentities(users):
_printShowCSEItems(users, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS)
# gam <UserTypeEntity> create csekeypair [incertdir <FilePath>] [inkeydir <FilePath>]
# [addidentity [<Boolean>]] [kpemail <EmailAddress>]
# [showpem] [showkaclsdata] [formatjson|returnidonly]
def createCSEKeyPair(users):
def _getFolderPath(myarg):
filepath = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
if not os.path.isdir(filepath):
entityDoesNotExistExit(Ent.DIRECTORY, f'{myarg} {filepath}')
return filepath
FJQC = FormatJSONQuoteChar()
incertdir = GC.Values[GC.GMAIL_CSE_INCERT_DIR]
inkeydir = GC.Values[GC.GMAIL_CSE_INKEY_DIR]
addIdentity = returnIdOnly = False
kpEmail = None
skipObjects = _initCSEKeyPairSkipObjects()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'incertdir':
incertdir = _getFolderPath(myarg)
elif myarg == 'inkeydir':
inkeydir = _getFolderPath(myarg)
elif myarg == 'addidentity':
addIdentity = getBoolean()
elif myarg == 'kpemail':
kpEmail = getEmailAddress(noUid=True)
elif myarg == 'returnidonly':
returnIdOnly = True
elif _resetCSEKeyPairSkipObjects(myarg, skipObjects):
pass
else:
FJQC.GetFormatJSON(myarg)
if not incertdir:
missingArgumentExit('incertdir <FilePath>')
if not inkeydir:
missingArgumentExit('inkeydir <FilePath>')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
kvList = [Ent.USER, user, Ent.CSE_KEYPAIR, None]
smimeFilename = os.path.join(incertdir, user+'.p7pem')
if not os.path.isfile(smimeFilename):
entityActionNotPerformedWarning(kvList, Msg.FILE_NOT_FOUND.format(smimeFilename), i, count)
continue
smimeData = readFile(smimeFilename, mode='rb', continueOnError=True)
if smimeData is None:
continue
kaclFilename = os.path.join(inkeydir, user+'.wrap')
if not os.path.isfile(kaclFilename):
entityActionNotPerformedWarning(kvList, Msg.FILE_NOT_FOUND.format(kaclFilename), i, count)
continue
jsonData = readFile(kaclFilename, mode='r', encoding=UTF8, continueOnError=True)
if jsonData is None:
continue
try:
keyData = json.loads(jsonData)
key = 'kacls_url'
kaclsUri = keyData[key]
key = 'wrapped_private_key'
kaclsData = keyData[key]
cseKeyPair = {
'pkcs7': smimeData.decode(UTF8),
'privateKeyMetadata': [{'kaclsKeyMetadata': {'kaclsUri': kaclsUri, 'kaclsData': kaclsData}}]
}
except KeyError:
entityActionNotPerformedWarning(kvList, Msg.JSON_KEY_NOT_FOUND.format(key, kaclFilename), i, count)
continue
except (IndexError, SyntaxError, TypeError, ValueError) as e:
entityActionNotPerformedWarning(kvList, Msg.JSON_ERROR.format(str(e), kaclFilename) , i, count)
continue
try:
result = callGAPI(gmail.users().settings().cse().keypairs(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
userId='me', body=cseKeyPair)
keyPairId = result['keyPairId']
if not returnIdOnly:
kvList[-1] = keyPairId
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
_stripCSEKeyPairSkipObjects(result, skipObjects)
_showCSEItem(result, Ent.CSE_KEYPAIR, 'keyPairId', CSE_KEYPAIR_TIME_OBJECTS, i, count, FJQC)
elif not addIdentity:
writeStdout(f'{keyPairId}\n')
if addIdentity:
identity = {'keyPairId': keyPairId, 'emailAddress': user if not kpEmail else kpEmail}
kvList = [Ent.USER, user, Ent.CSE_KEYPAIR, keyPairId, Ent.CSE_IDENTITY, identity['emailAddress']]
result = callGAPI(gmail.users().settings().cse().identities(), 'create',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
userId='me', body=identity)
if not returnIdOnly:
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
_showCSEItem(result, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS, i, count, FJQC)
else:
writeStdout(f'{keyPairId}-{user}\n')
except (GAPI.permissionDenied, GAPI.alreadyExists) as e:
entityActionFailedWarning(kvList, str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
CSE_KEYPAIR_ACTION_FUNCTION_MAP = {
Act.DISABLE: 'disable',
Act.ENABLE: 'enable',
Act.OBLITERATE: 'obliterate',
Act.INFO: 'get',
}
# gam <UserTypeEntity> disable csekeypair <KeyPairID>
# [showpem] [showkaclsdata] [formatjson]
# gam <UserTypeEntity> enable csekeypair <KeyPairID>
# [showpem] [showkaclsdata] [formatjson]
# gam <UserTypeEntity> obliterate csekeypair <KeyPairID>
# gam <UserTypeEntity> info csekey3pair <KeyPairID>
# [showpem] [showkaclsdata] [formatjson]
def processCSEKeyPair(users):
function = CSE_KEYPAIR_ACTION_FUNCTION_MAP[Act.Get()]
keyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
FJQC = FormatJSONQuoteChar()
skipObjects = _initCSEKeyPairSkipObjects()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if _resetCSEKeyPairSkipObjects(myarg, skipObjects):
pass
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
kvList = [Ent.USER, user, Ent.CSE_KEYPAIR, keyPairId]
try:
result = callGAPI(gmail.users().settings().cse().keypairs(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.ALREADY_EXISTS],
userId='me', keyPairId=keyPairId)
if function != 'obliterate':
if not FJQC.formatJSON:
entityActionPerformed(kvList, i, count)
_stripCSEKeyPairSkipObjects(result, skipObjects)
_showCSEItem(result, Ent.CSE_KEYPAIR, 'keyPairId', CSE_KEYPAIR_TIME_OBJECTS, i, count, FJQC)
else:
entityActionPerformed(kvList, i, count)
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound, GAPI.failedPrecondition, GAPI.alreadyExists) as e:
entityActionFailedWarning(kvList, str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> show csekeypairs
# [showpem] [showkaclsdata] [formatjson]
# gam <UserTypeEntity> print csekeypairs [todrive <ToDriveAttribute>*]
# [showpem] [showkaclsdata] [formatjson [quotechar <Character>]]
def printShowCSEKeyPairs(users):
_printShowCSEItems(users, Ent.CSE_KEYPAIR, 'keyPairId', CSE_KEYPAIR_TIME_OBJECTS)
# gam <UserTypeEntity> signature|sig
# <SignatureContent>
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
# [name <String>]
# [primary]
def setSignature(users):
tagReplacements = _initTagReplacements()
signature, _, html = getStringOrFile('sig')
body = {}
primary = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'primary':
primary = True
elif myarg == 'html':
html = getBoolean()
else:
getSendAsAttributes(myarg, body, tagReplacements)
if not tagReplacements['subs']:
body['signature'] = _processSignature(tagReplacements, signature, html)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if tagReplacements['subs']:
_getTagReplacementFieldValues(user, i, count, tagReplacements)
body['signature'] = _processSignature(tagReplacements, signature, html)
if primary:
try:
result = callGAPI(gmail.users().settings().sendAs(), 'list',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
for sendas in result['sendAs']:
if sendas.get('isPrimary', False):
emailAddress = sendas['sendAsEmail']
_processSendAs(user, i, count, Ent.SIGNATURE, emailAddress, i, count, gmail, 'patch', False, body=body, sendAsEmail=emailAddress, fields='')
break
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
else:
_processSendAs(user, i, count, Ent.SIGNATURE, user, i, count, gmail, 'patch', False, body=body, sendAsEmail=user, fields='')
VACATION_START_STARTED = 'Started'
VACATION_END_NOT_SPECIFIED = 'NotSpecified'
def _showVacation(user, i, count, result, showDisabled, sigReplyFormat):
enabled = result['enableAutoReply']
if not enabled and not showDisabled:
return
printEntity([Ent.USER, user, Ent.VACATION, None], i, count)
Ind.Increment()
printKeyValueList(['Enabled', enabled])
printKeyValueList(['Contacts Only', result['restrictToContacts']])
printKeyValueList(['Domain Only', result['restrictToDomain']])
if 'startTime' in result:
printKeyValueList(['Start Date', formatLocalDatestamp(result['startTime'])])
elif enabled:
printKeyValueList(['Start Date', VACATION_START_STARTED])
if 'endTime' in result:
printKeyValueList(['End Date', formatLocalDatestamp(result['endTime'])])
elif enabled:
printKeyValueList(['End Date', VACATION_END_NOT_SPECIFIED])
printKeyValueList(['Subject', result.get('responseSubject', 'None')])
if sigReplyFormat == SIG_REPLY_HTML:
printKeyValueList(['Message', None])
Ind.Increment()
if result.get('responseBodyPlainText'):
printKeyValueList([Ind.MultiLineText(result['responseBodyPlainText'])])
elif result.get('responseBodyHtml'):
printKeyValueList([Ind.MultiLineText(result['responseBodyHtml'])])
else:
printKeyValueList(['None'])
Ind.Decrement()
elif sigReplyFormat == SIG_REPLY_FORMAT:
printKeyValueList(['Message', None])
Ind.Increment()
if result.get('responseBodyPlainText'):
printKeyValueList([Ind.MultiLineText(result['responseBodyPlainText'])])
elif result.get('responseBodyHtml'):
printKeyValueList([Ind.MultiLineText(dehtml(result['responseBodyHtml']))])
else:
printKeyValueList(['None'])
Ind.Decrement()
else: # SIG_REPLY_COMPACT
if result.get('responseBodyPlainText'):
printKeyValueList(['Message', escapeCRsNLs(result['responseBodyPlainText'])])
elif result.get('responseBodyHtml'):
printKeyValueList(['Message', escapeCRsNLs(result['responseBodyHtml'])])
else:
printKeyValueList(['Message', 'None'])
Ind.Decrement()
# gam <UserTypeEntity> vacation [<Boolean>] [subject <String>]
# [<VacationMessageContent>
# (replace <Tag> <UserReplacement>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
# [html [<Boolean>]] [contactsonly [<Boolean>]] [domainonly [<Boolean>]]
# [start|startdate <Date>|Started] [end|enddate <Date>|NotSpecified]
def setVacation(users):
body = {}
if Cmd.PeekArgumentPresent(TRUE_VALUES) or Cmd.PeekArgumentPresent(FALSE_VALUES):
body['enableAutoReply'] = getBoolean(None)
responseBodyType = 'responseBodyPlainText'
message = subject = None
tagReplacements = _initTagReplacements()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'subject':
subject = getString(Cmd.OB_STRING, minLen=0)
elif myarg in SORF_MSG_FILE_ARGUMENTS:
message, _, html = getStringOrFile(myarg)
if html:
responseBodyType = 'responseBodyHtml'
elif _getTagReplacement(myarg, tagReplacements, True):
pass
elif myarg == 'html':
if getBoolean():
responseBodyType = 'responseBodyHtml'
elif myarg == 'contactsonly':
body['restrictToContacts'] = getBoolean()
elif myarg == 'domainonly':
body['restrictToDomain'] = getBoolean()
elif myarg in {'start', 'startdate'}:
body['startTime'] = getYYYYMMDD(returnTimeStamp=True, alternateValue=VACATION_START_STARTED)
elif myarg in {'end', 'enddate'}:
body['endTime'] = getYYYYMMDD(returnTimeStamp=True, alternateValue=VACATION_END_NOT_SPECIFIED)
else:
unknownArgumentExit()
if message:
if responseBodyType == 'responseBodyHtml':
message = message.replace('\r', '').replace('\\n', '<br/>')
else:
message = message.replace('\r', '').replace('\\n', '\n')
if tagReplacements['tags'] and not tagReplacements['subs']:
message = _processTagReplacements(tagReplacements, message)
body[responseBodyType] = message
if subject:
if tagReplacements['tags'] and not tagReplacements['subs']:
subject = _processTagReplacements(tagReplacements, subject)
body['responseSubject'] = subject
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if (message or subject) and tagReplacements['subs']:
_getTagReplacementFieldValues(user, i, count, tagReplacements)
if message:
body[responseBodyType] = _processTagReplacements(tagReplacements, message)
if subject:
body['responseSubject'] = _processTagReplacements(tagReplacements, subject)
try:
oldBody = callGAPI(gmail.users().settings(), 'getVacation',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
if body.get(responseBodyType):
oldBody.pop('responseBodyPlainText', None)
oldBody.pop('responseBodyHtml', None)
oldBody.update(body)
result = callGAPI(gmail.users().settings(), 'updateVacation',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_ARGUMENT,
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
userId='me', body=oldBody)
printEntity([Ent.USER, user, Ent.VACATION_ENABLED, result['enableAutoReply']], i, count)
except (GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
entityActionFailedWarning([Ent.USER, user, Ent.VACATION_ENABLED, oldBody['enableAutoReply']], str(e), i, count)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
# gam <UserTypeEntity> print vacation [compact] [enabledonly] [todrive <ToDriveAttribute>*]
# gam <UserTypeEntity> show vacation [compact|format|html] [enabledonly]
def printShowVacation(users):
def _printVacation(user, result, showDisabled):
enabled = result['enableAutoReply']
if not enabled and not showDisabled:
return
row = {'User': user, 'enabled': enabled}
row['contactsonly'] = result['restrictToContacts']
row['domainonly'] = result['restrictToDomain']
if 'startTime' in result:
row['startdate'] = formatLocalDatestamp(result['startTime'])
elif enabled:
row['startdate'] = VACATION_START_STARTED
if 'endTime' in result:
row['enddate'] = formatLocalDatestamp(result['endTime'])
elif enabled:
row['enddate'] = VACATION_END_NOT_SPECIFIED
row['subject'] = result.get('responseSubject', 'None')
if result.get('responseBodyPlainText'):
row['html'] = False
row['message'] = escapeCRsNLs(result['responseBodyPlainText'])
elif result.get('responseBodyHtml'):
row['html'] = True
if sigReplyFormat == SIG_REPLY_HTML:
row['message'] = escapeCRsNLs(result['responseBodyHtml'])
else:
row['message'] = result['responseBodyHtml'].replace('\r', '').replace('\n', '')
else:
row['html'] = False
row['message'] = 'None'
csvPF.WriteRow(row)
csvPF = CSVPrintFile(['User', 'enabled', 'contactsonly', 'domainonly',
'startdate', 'enddate', 'subject', 'html', 'message']) if Act.csvFormat() else None
showDisabled = True
sigReplyFormat = SIG_REPLY_HTML
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif (not csvPF and myarg in SIG_REPLY_OPTIONS_MAP) or (csvPF and myarg == 'compact'):
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
elif myarg == 'enabledonly':
showDisabled = False
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.VACATION, user, i, count)
try:
result = callGAPI(gmail.users().settings(), 'getVacation',
throwReasons=GAPI.GMAIL_THROW_REASONS,
userId='me')
if not csvPF:
_showVacation(user, i, count, result, showDisabled, sigReplyFormat)
else:
_printVacation(user, result, showDisabled)
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Vacation')
def normalizeNoteName(noteName):
if noteName.startswith('notes/'):
return noteName
return f'notes/{noteName}'
def _assignNoteOwner(note, user):
for permission in note.get('permissions', []):
if permission['role'] == 'OWNER':
noteOwner = permission.get('user', {}).get('email', '').lower()
note['owner'] = noteOwner
note['ownedByMe'] = noteOwner == user
break
def _checkNoteUserRole(note, user, role):
for permission in note['permissions']:
if permission['role'] == role and permission.get('user', {}).get('email', '').lower() == user:
return True
return False
def _showNoteListItems(listItems):
printKeyValueList(['list', ''])
Ind.Increment()
kcount = len(listItems)
k = 0
for listItem in listItems:
k += 1
printKeyValueListWithCount(['item', ''], k, kcount)
Ind.Increment()
if 'text' in listItem and 'text' in listItem['text']:
printKeyValueList(['text', listItem['text']['text']])
if 'checked' in listItem:
printKeyValueList(['checked', listItem['checked']])
if 'childListItems' in listItem:
_showNoteListItems(listItem['childListItems'])
Ind.Decrement()
Ind.Decrement()
def _showNotePermissions(permissions):
printKeyValueList(['permissions', ''])
Ind.Increment()
kcount = len(permissions)
k = 0
for permission in permissions:
k += 1
printKeyValueListWithCount(['name', permission['name']], k, kcount)
Ind.Increment()
for field in ['role', 'deleted']:
if field in permission:
printKeyValueList([field, permission[field]])
for field in ['user', 'group']:
if field in permission:
printKeyValueList([field, permission[field]['email']])
break
else:
if 'email' in permission:
printKeyValueList(['email', permission['email']])
if 'family' in permission:
family = permission['family']
if 'text' in family:
printKeyValueList(['family', family['text']['text']])
elif 'list' in family:
_showNoteListItems(family['list']['listItems'])
Ind.Decrement()
Ind.Decrement()
def _showNoteAttachments(attachments):
printKeyValueList(['attachments', ''])
Ind.Increment()
kcount = len(attachments)
k = 0
for attachment in attachments:
k += 1
printKeyValueListWithCount(['name', attachment['name']], k, kcount)
if 'mimeType' in attachment:
Ind.Increment()
printKeyValueList(['mimeType', ','.join(attachment['mimeType'])])
Ind.Decrement()
Ind.Decrement()
NOTES_TIME_OBJECTS = {'createTime', 'updateTime', 'trashTime'}
def _showNote(note, j=0, jcount=0, FJQC=None, compact=False):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(note, timeObjects=NOTES_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.NOTE, note['name']], j, jcount)
Ind.Increment()
printKeyValueList(['title', note.get('title', None)])
for field in NOTES_TIME_OBJECTS:
if field in note:
printKeyValueList([field, formatLocalTime(note[field])])
if 'trashed' in note:
printKeyValueList(['trashed', note['trashed']])
for field in ['owner', 'ownedByMe']:
if field in note:
printKeyValueList([field, note[field]])
if 'permissions' in note:
_showNotePermissions(note['permissions'])
if 'attachments' in note:
_showNoteAttachments(note['attachments'])
body = note.get('body', {})
if 'text' in body and 'text' in body['text']:
if not compact:
printKeyValueList(['text', None])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(body['text']['text'])])
Ind.Decrement()
else:
printKeyValueList(['text', escapeCRsNLs(body['text']['text'])])
elif 'list' in body and 'listItems' in body['list']:
_showNoteListItems(body['list']['listItems'])
Ind.Decrement()
# gam <UserTypeEntity> create note [title <String>]
# <NoteContent>
# [missingtextvalue <String>]
# [copyacls [copyowneraswriter]
# [compact|formatjson|nodetails]
def createNote(users):
def fixTextItem(item):
if 'text' in item:
if item['text']:
item['text'] = unescapeCRsNLs(item['text'])
return
if missingTextValue:
item['text'] = missingTextValue
def fixListItem(item):
if 'listItems' in item:
if item['listItems']:
for listItem in item['listItems']:
fixTextItem(listItem['text'])
return
if missingTextValue:
item['listItems'] = [{'checked': False, 'text': {'text': missingTextValue}}]
FJQC = FormatJSONQuoteChar()
compact = False
showDetails = True
missingTextValue = ''
copyACLs = copyOwnerAsWriter = False
body = {'title': '', 'body': {}}
copyUsers = []
copyGroups = []
noteOwner = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'title':
body['title'] = getString(Cmd.OB_STRING, minLen=1, maxLen=1000)
elif myarg in SORF_TEXT_ARGUMENTS:
msgText, _, _ = getStringOrFile(myarg, minLen=1, unescapeCRLF=True)
body['body']['text'] = {'text': msgText}
elif myarg == 'missingtextvalue':
missingTextValue = getString(Cmd.OB_STRING, minLen=1)
elif myarg == 'json':
jsonData = getJSON([])
if not body['title']:
body['title'] = jsonData.get('title', missingTextValue)
body['body'] = jsonData.get('body', {})
if 'text' in body['body']:
fixTextItem(body['body']['text'])
elif 'list' in body['body']:
fixListItem(body['body']['list'])
for permission in jsonData.get('permissions', []):
if permission['role'] == 'WRITER':
if 'user' in permission:
copyUsers.append(permission['user']['email'].lower())
elif 'group' in permission:
copyGroups.append(permission['group']['email'].lower())
elif permission['role'] == 'OWNER':
if 'user' in permission:
noteOwner = permission['user']['email'].lower()
elif myarg == 'copyacls':
copyACLs = True
elif myarg == 'copyowneraswriter':
copyOwnerAsWriter = True
elif myarg == 'compact':
compact = True
elif myarg == 'nodetails':
showDetails = False
else:
FJQC.GetFormatJSON(myarg)
if not body['body']:
choices = list(SORF_TEXT_ARGUMENTS)
choices.append('json')
missingArgumentExit('|'.join(choices))
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, keep = buildGAPIServiceObject(API.KEEP, user, i, count)
if not keep:
continue
if copyACLs:
rbody = {'requests': []}
for addr in copyUsers:
if addr != user:
rbody['requests'].append({'parent': None,
'permission': {'role': 'WRITER', 'user': {'email': addr}}})
for addr in copyGroups:
rbody['requests'].append({'parent': None,
'permission': {'role': 'WRITER', 'group': {'email': addr}}})
if copyOwnerAsWriter and noteOwner and noteOwner != user:
rbody['requests'].append({'parent': None,
'permission': {'role': 'WRITER', 'user': {'email': noteOwner}}})
kcount = len(rbody['requests'])
try:
note = callGAPI(keep.notes(), 'create',
throwReasons=GAPI.KEEP_THROW_REASONS,
body=body)
name = note['name']
entityKVList = [Ent.USER, user, Ent.NOTE, name]
_assignNoteOwner(note, user)
if copyACLs and kcount > 0:
for request in rbody['requests']:
request['parent'] = name
try:
callGAPI(keep.notes().permissions(), 'batchCreate',
throwReasons=GAPI.KEEP_THROW_REASONS,
parent=name, body=rbody)
note = callGAPI(keep.notes(), 'get',
throwReasons=GAPI.KEEP_THROW_REASONS,
name=name)
except (GAPI.badRequest, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound):
pass
except GAPI.serviceNotAvailable:
pass
if showDetails:
_showNote(note, FJQC=FJQC, compact=compact)
else:
entityActionPerformed(entityKVList, i, count)
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, body['title']], str(e), i, count)
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
NOTES_FIELDS_CHOICE_MAP = {
'attachments': 'attachments',
'body': 'body',
'createtime': 'createTime',
'name': 'name',
'permissions': 'permissions',
'title': 'title',
'trashed': 'trashed',
'trashtime': 'trashTime',
'updatetime': 'updateTime',
}
# gam <UserTypeEntity> info note <NotesNameEntity>
# [fields <NotesFieldList>]
# [compact|formatjson]
# gam <UserTypeEntity> delete note <NotesNameEntity>
def deleteInfoNotes(users):
function = 'delete' if Act.Get() == Act.DELETE else 'get'
fieldsList = []
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTE)
showPermissions = True
if function == 'get':
FJQC = FormatJSONQuoteChar()
compact = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if getFieldsList(myarg, NOTES_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
pass
elif myarg == 'compact':
compact = True
else:
FJQC.GetFormatJSON(myarg)
fields = getFieldsFromFieldsList(fieldsList)
else:
FJQC = None
checkForExtraneousArguments()
if fieldsList and 'permissions' not in fieldsList:
showPermissions = False
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity,
api=API.KEEP, showAction=FJQC is None or not FJQC.formatJSON)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in noteNames:
j += 1
name = normalizeNoteName(name)
try:
if function == 'get':
note = callGAPI(keep.notes(), function,
throwReasons=GAPI.KEEP_THROW_REASONS,
name=name, fields=fields)
if showPermissions:
_assignNoteOwner(note, user)
_showNote(note, j, jcount, FJQC, compact)
else:
callGAPI(keep.notes(), function,
throwReasons=GAPI.KEEP_THROW_REASONS,
name=name)
entityActionPerformed([Ent.USER, user, Ent.NOTE, name], j, jcount)
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, name], str(e), j, jcount)
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
NOTES_ROLE_CHOICE_MAP = {
'owner': 'OWNER',
'writer': 'WRITER',
}
NOTES_COUNTS_MAP = {
'OWNER': 'noteOwner',
'WRITER': 'noteWriter',
}
# gam <UserTypeEntity> show notes
# [fields <NotesFieldList>] [filter <String>]
# [role owner|writer]
# [countsonly]
# [compact] [formatjson]
# gam <UserTypeEntity> print notes [todrive <ToDriveAttribute>*]
# [fields <NotesFieldList>] [filter <String>]
# [role owner|writer]
# [countsonly]
# [formatjson [quotechar <Character>]]
def printShowNotes(users):
csvPF = CSVPrintFile(['User', 'name', 'title', 'owner', 'ownedByMe']) if Act.csvFormat() else None
if csvPF:
csvPF.SetNoEscapeChar(True)
FJQC = FormatJSONQuoteChar(csvPF)
compact = countsOnly = False
fieldsList = []
noteFilter = None
role = None
showPermissions = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif getFieldsList(myarg, NOTES_FIELDS_CHOICE_MAP, fieldsList, initialField=['name', 'title']):
pass
elif myarg == 'filter':
noteFilter = getString(Cmd.OB_STRING)
elif myarg == 'role':
role = getChoice(NOTES_ROLE_CHOICE_MAP, mapChoice=True)
elif not csvPF and myarg == 'compact':
compact = True
elif myarg == 'countsonly':
countsOnly = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if FJQC.formatJSON:
GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] = False
if fieldsList and 'permissions' not in fieldsList:
showPermissions = False
fieldsList.append('permissions')
if csvPF:
if not countsOnly:
if not FJQC.formatJSON:
csvPF.RemoveTitles(['owner', 'ownedByMe'])
else:
csvPF.RemoveJSONTitles(['owner', 'ownedByMe'])
if countsOnly and csvPF:
if not FJQC.formatJSON:
csvPF.SetTitles(['User', 'noteOwner', 'noteWriter', 'totalNotes'])
else:
csvPF.SetJSONTitles(['User', 'JSON'])
fields = getItemFieldsFromFieldsList('notes', fieldsList, returnItemIfNoneList=False)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, keep = buildGAPIServiceObject(API.KEEP, user, i, count)
if not keep:
continue
if csvPF:
printGettingEntityItemForWhom(Ent.NOTE, user, i, count)
pageMessage = getPageMessageForWhom()
else:
pageMessage = None
try:
notes = callGAPIpages(keep.notes(), 'list', 'notes',
pageMessage=pageMessage,
throwReasons=GAPI.KEEP_THROW_REASONS,
filter=noteFilter, fields=fields)
if countsOnly:
noteCounts = {'User': user, 'noteOwner': 0, 'noteWriter': 0, 'totalNotes': 0}
for note in notes:
noteCounts['totalNotes'] += 1
for permission in note['permissions']:
if permission.get('user', {}).get('email', '').lower() == user:
noteCounts[NOTES_COUNTS_MAP[permission['role']]] += 1
break
if not csvPF:
if not FJQC.formatJSON:
printEntityKVList([Ent.USER, user], ['noteOwner', noteCounts['noteOwner'],
'noteWriter', noteCounts['noteWriter'],
'totalNotes', noteCounts['totalNotes']], i, count)
else:
printLine(json.dumps(cleanJSON(noteCounts), ensure_ascii=False, sort_keys=True))
else:
row = {'User': user, 'noteOwner': noteCounts['noteOwner'], 'noteWriter': noteCounts['noteWriter'],
'totalNotes': noteCounts['totalNotes']}
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': noteCounts.pop('User')}
row['JSON'] = json.dumps(cleanJSON(noteCounts), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
elif not csvPF:
jcount = len(notes)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.NOTE, i, count)
Ind.Increment()
j = 0
for note in notes:
j += 1
if showPermissions:
_assignNoteOwner(note, user)
if role is None or _checkNoteUserRole(note, user, role):
if not showPermissions:
note.pop('permissions', None)
_showNote(note, j, jcount, FJQC, compact)
Ind.Decrement()
else:
for note in notes:
if showPermissions:
_assignNoteOwner(note, user)
if role is None or _checkNoteUserRole(note, user, role):
if not showPermissions:
note.pop('permissions', None)
row = flattenJSON(note, flattened={'User': user}, timeObjects=NOTES_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user, 'name': note['name'], 'title': note.get('title', '')}
if showPermissions:
row.update({'owner': note['owner'], 'ownedByMe': note['ownedByMe']})
row['JSON'] = json.dumps(cleanJSON(note, timeObjects=NOTES_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, None], str(e), i, count)
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
if csvPF:
csvPF.writeCSVfile('Notes')
GET_NOTE_HTTP_ERROR_PATTERN = re.compile(r'^.*\'description\': \'(.*)\'')
# gam <UserTypeEntity> get noteattachments <NotesNameEntity>
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
# [<DriveFileParentAttribute>]
def getNoteAttachments(users):
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTE)
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
targetNamePattern = None
overwrite = False
body = {}
parentParms = initDriveFileAttributes()
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'targetfolder':
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
elif myarg == 'targetname':
targetNamePattern = getString(Cmd.OB_FILE_NAME)
elif myarg == 'overwrite':
overwrite = getBoolean()
elif getDriveFileParentAttribute(myarg, parentParms):
pass
else:
unknownArgumentExit()
parentSpecified = _driveFileParentSpecified(parentParms)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity,
api=API.KEEP, showAction=True)
if jcount == 0:
continue
if parentSpecified:
_ , drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
if not drive:
continue
if not _getDriveFileParentInfo(drive, user, i, count, body, parentParms):
continue
_, userName, _ = splitEmailAddressOrUID(user)
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
Ind.Increment()
j = 0
for name in noteNames:
j += 1
name = normalizeNoteName(name)
try:
result = callGAPI(keep.notes(), 'get',
throwReasons=GAPI.KEEP_THROW_REASONS,
name=name, fields='title,attachments')
title = result.get('title', 'attachment')
kcount = len(result['attachments'])
entityPerformActionNumItems([Ent.NOTE, name], kcount, Ent.ATTACHMENT, j, jcount)
Ind.Increment()
k = 0
for attachment in result['attachments']:
k += 1
attachmentName = attachment['name'][attachment['name'].find('attachments'):]
entityValueList = [Ent.ATTACHMENT, attachmentName]
mimeTypes = attachment.get('mimeType', [])
if not mimeTypes:
entityActionNotPerformedWarning(entityValueList, Msg.MIMETYPE_NOT_PRESENT_IN_ATTACHMENT, k, kcount)
continue
mimeType = mimeTypes[0]
localFilename, filename = uniqueFilename(targetFolder, f"{targetName or cleanFilename(title)}-{k}{MIMETYPE_EXTENSION_MAP.get(mimeType, '')}", overwrite)
request = keep.media().download(name=attachment['name'], mimeType=mimeType)
f = openFile(localFilename, 'wb', continueOnError=True)
if f is None:
continue
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
downloadOK = False
try:
while not done:
status, done = downloader.next_chunk()
if status.progress() < 1.0:
entityActionPerformedMessage(entityValueList, f'{status.progress():>7.2%}', k, kcount)
entityModifierNewValueActionPerformed(entityValueList, Act.MODIFIER_TO, localFilename, k, kcount)
downloadOK = True
except (IOError, httplib2.HttpLib2Error) as e:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, localFilename, str(e), k, kcount)
except googleapiclient.http.HttpError as e:
mg = GET_NOTE_HTTP_ERROR_PATTERN.match(str(e))
if mg:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, localFilename, mg.group(1), k, kcount)
else:
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, localFilename, str(e), k, kcount)
closeFile(f, True)
if downloadOK and parentSpecified:
body['name'] = filename
body['mimeType'] = mimeType
media_body = googleapiclient.http.MediaFileUpload(filename, mimetype=mimeType, resumable=True)
Act.Set(Act.CREATE)
try:
result = callGAPI(drive.files(), 'create',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS,
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
media_body=media_body, body=body, fields='id,name', supportsAllDrives=True)
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, f'{result["name"]}({result["id"]})'],
Act.MODIFIER_WITH_CONTENT_FROM, localFilename, k, kcount)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, body['name']], str(e), k, kcount)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userDriveServiceNotEnabledWarning(user, str(e), i, count)
Act.Set(Act.DOWNLOAD)
Ind.Decrement()
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
entityActionFailedWarning([Ent.NOTE, name], str(e), j, jcount)
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> create noteacl <NotesNameEntity>
# (user|group <EmailAddress>)+
# (json [charset <Charset>] <JSONData>)|(json file <FileName> [charset <Charset>])
# [nodetails]
def createNotesACLs(users):
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTES_ACLS)
body = {'requests': []}
showDetails = True
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'user', 'group'}:
body['requests'].append({'parent': None,
'permission': {'role': 'WRITER', myarg: {'email': getEmailAddress(noUid=True)}}})
elif myarg == 'json':
jsonData = getJSON([])
for permission in jsonData.get('permissions', []):
if permission['role'] == 'WRITER':
if 'user' in permission:
body['requests'].append({'parent': None,
'permission': {'role': 'WRITER', 'user': {'email': permission['user']['email']}}})
elif 'group' in permission:
body['requests'].append({'parent': None,
'permission': {'role': 'WRITER', 'group': {'email': permission['group']['email']}}})
elif myarg == 'nodetails':
showDetails = False
else:
unknownArgumentExit()
kcount = len(body['requests'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity, api=API.KEEP)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in noteNames:
j += 1
name = normalizeNoteName(name)
entityKVList = [Ent.USER, user, Ent.NOTE, name]
rbody = body.copy()
for request in rbody['requests']:
request['parent'] = name
try:
permissions = callGAPI(keep.notes().permissions(), 'batchCreate',
throwReasons=GAPI.KEEP_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
parent=name, body=rbody)
entityNumItemsActionPerformed(entityKVList, kcount, Ent.NOTE_ACL, j, jcount)
if showDetails:
Ind.Increment()
_showNotePermissions(permissions['permissions'])
Ind.Decrement()
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound, GAPI.failedPrecondition) as e:
entityActionFailedWarning(entityKVList, str(e), i, count)
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
break
# gam <UserTypeEntity> delete noteacl <NotesNameEntity>
# (user|group <EmailAddress>)+
# (json [charset <Charset>] <JSONData>)|(json file <FileName> [charset <Charset>])
def deleteNotesACLs(users):
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTES_ACLS)
emails = {'user': set(), 'group': set()}
body = {'names': []}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'user', 'group'}:
emails[myarg].add(getEmailAddress(noUid=True).lower())
elif myarg == 'json':
jsonData = getJSON([])
for permission in jsonData.get('permissions', []):
if permission['role'] == 'WRITER':
loc = permission['name'].find('/permissions')
body['names'].append(permission['name'][loc+1:])
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity, api=API.KEEP)
if jcount == 0:
continue
Ind.Increment()
j = 0
for name in noteNames:
j += 1
name = normalizeNoteName(name)
rbody = body.copy()
if emails['user'] or emails['group']:
try:
note = callGAPI(keep.notes(), 'get',
throwReasons=GAPI.KEEP_THROW_REASONS,
name=name, fields='permissions')
for permission in note['permissions']:
if permission['role'] == 'WRITER':
if (('user' in permission and permission['user']['email'].lower() in emails['user']) or
('group' in permission and permission['group']['email'].lower() in emails['group'])):
rbody['names'].append(permission['name'])
except (GAPI.badRequest, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, name], str(e), i, count)
break
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
break
for k, perm in enumerate(rbody['names']):
if perm.startswith('notes/'):
pass
elif perm.startswith('permissions'):
rbody['names'][k] = f'{name}/{perm}'
else:
rbody['names'][k] = f'{name}/permissions/{perm}'
kcount = len(rbody['names'])
entityKVList = [Ent.USER, user, Ent.NOTE, name]
try:
callGAPI(keep.notes().permissions(), 'batchDelete',
throwReasons=GAPI.KEEP_THROW_REASONS,
parent=name, body=rbody)
entityNumItemsActionPerformed(entityKVList, kcount, Ent.NOTE_ACL, j, jcount)
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
entityActionFailedWarning(entityKVList, str(e), i, count)
except GAPI.authError:
userKeepServiceNotEnabledWarning(user, i, count)
break
def verifyTasksServiceEnabled(svc, user, i, count):
try:
callGAPIpages(svc.tasklists(), 'list', 'items',
throwReasons=GAPI.TASKLIST_THROW_REASONS,
maxItems=1)
return True
except (GAPI.notFound, GAPI.badRequest, GAPI.invalid):
userTasksServiceNotEnabledWarning(user, i, count)
return False
def getTaskLists(svc, user, i, count):
try:
results = callGAPIpages(svc.tasklists(), 'list', 'items',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.TASKLIST_THROW_REASONS,
maxResults=100)
except (GAPI.badRequest, GAPI.invalid) as e:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, None], str(e), i, count)
results = None
except GAPI.notFound:
userTasksServiceNotEnabledWarning(user, i, count)
results = None
return results
def getTaskListIDfromTitle(svc, userTasklists, title, user, i, count):
if userTasklists is None:
printGettingEntityItemForWhom(Ent.TASKLIST, user, i, count)
userTasklists = getTaskLists(svc, user, i, count)
if userTasklists is None:
return None, None
for userTasklist in userTasklists:
if userTasklist['title'] == title:
return userTasklists, userTasklist['id']
return userTasklists, None
TASK_SKIP_OBJECTS = ['selfLink']
TASK_TIME_OBJECTS = ['completed', 'updated']
def _showTask(tasklist, task, j=0, jcount=0, FJQC=None, compact=False):
task['tasklistId'] = tasklist
task['taskId'] = f"{tasklist}/{task['id']}"
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(task, skipObjects=TASK_SKIP_OBJECTS, timeObjects=TASK_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.TASK, task['taskId']], j, jcount)
Ind.Increment()
showJSON(None, task, skipObjects=TASK_SKIP_OBJECTS+['notes'], timeObjects=TASK_TIME_OBJECTS)
field = 'notes'
if field in task:
if not compact:
printKeyValueList([field, None])
Ind.Increment()
printKeyValueList([Ind.MultiLineText(task[field])])
Ind.Decrement()
else:
printKeyValueList(['notes', escapeCRsNLs(task[field])])
Ind.Decrement()
TASK_STATUS_MAP = {
'completed': 'completed',
'needsaction': 'needsAction',
}
def getTaskAttribute(myarg, body):
if myarg == 'title':
body[myarg] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'notes':
body[myarg] = getStringWithCRsNLs()
elif myarg == 'status':
body[myarg] = getChoice(TASK_STATUS_MAP, mapChoice=True)
elif myarg == 'due':
body[myarg] = getTimeOrDeltaFromNow()
else:
return False
return True
def getTaskMoveAttribute(myarg, kwargs):
if myarg == 'parent':
kwargs[myarg] = getString(Cmd.OB_TASK_ID)
elif myarg == 'previous':
kwargs[myarg] = getString(Cmd.OB_TASK_ID)
else:
return False
return True
# gam <UserTypeEntity> create task <TasklistEntity>
# <TaskAttribute>* [parent <TaskID>] [previous <TaskID>]
# [compact|formatjson|returnidonly]
# gam <UserTypeEntity> update task <TasklistIDTaskIDEntity>
# <TaskAttribute>*
# [compact|formatjson]
# gam <UserTypeEntity> info task <TasklistIDTaskIDEntity>
# [compact|formatjson]
# gam <UserTypeEntity> delete task <TasklistIDTaskIDEntity>
# gam <UserTypeEntity> move task <TasklistIDTaskIDEntity>
# [parent <TaskID>] [previous <TaskID>]
# [compact|formatjson]
def processTasks(users):
action = Act.Get()
if action != Act.CREATE:
tasklistTaskEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_TASK_ID_ENTITY, Ent.TASK, shlexSplit=True)
else:
tasklistTaskEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_ENTITY, Ent.TASK, shlexSplit=True)
if action in {Act.DELETE, Act.CLEAR}:
FJQC = None
checkForExtraneousArguments()
else:
FJQC = FormatJSONQuoteChar()
body = {}
kwargs = {}
compact = returnIdOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if action in {Act.CREATE, Act.UPDATE} and getTaskAttribute(myarg, body):
pass
elif action in {Act.CREATE, Act.MOVE} and getTaskMoveAttribute(myarg, kwargs):
pass
elif action == Act.CREATE and myarg == 'returnidonly':
returnIdOnly = True
elif myarg == 'compact':
compact = True
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, svc, tasklistTasks, jcount = _validateUserGetObjectList(user, i, count, tasklistTaskEntity,
api=API.TASKS, showAction=FJQC is None or not FJQC.formatJSON)
if jcount == 0 or not verifyTasksServiceEnabled(svc, user, i, count):
continue
userTasklists = None
Ind.Increment()
j = 0
for tasklistTask in tasklistTasks:
j += 1
if action != Act.CREATE:
if '/' not in tasklistTask:
continue
tasklist, task = tasklistTask.split('/', 1)
else:
tasklist = tasklistTask
task = body.get('title', '')
if tasklist.startswith('tltitle:'):
tasklistTitle = tasklist[8:]
userTasklists, tasklist = getTaskListIDfromTitle(svc, userTasklists, tasklistTitle, user, i, count)
if tasklist is None:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklistTitle, Ent.TASK, task], Msg.TASKLIST_TITLE_NOT_FOUND, j, jcount)
continue
try:
if action == Act.DELETE:
callGAPI(svc.tasks(), 'delete',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, task=task)
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, task], j, jcount)
elif action == Act.INFO:
result = callGAPI(svc.tasks(), 'get',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, task=task)
_showTask(tasklist, result, j, jcount, FJQC, compact)
else:
if action == Act.CREATE:
result = callGAPI(svc.tasks(), 'insert',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, body=body, **kwargs)
if returnIdOnly:
writeStdout(f"{result['id']}\n")
continue
elif action == Act.UPDATE:
result = callGAPI(svc.tasks(), 'patch',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, task=task, body=body)
else: #elif action == Act.MOVE
result = callGAPI(svc.tasks(), 'move',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, task=task, **kwargs)
if not FJQC.formatJSON:
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, result['id']], j, jcount)
Ind.Increment()
_showTask(tasklist, result, j, jcount, FJQC, compact)
Ind.Decrement()
except (GAPI.badRequest, GAPI.permissionDenied, GAPI.invalid, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, task], str(e), j, jcount)
except GAPI.serviceNotAvailable:
Ind.Decrement()
userTasksServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
TASK_ORDERBY_CHOICE_MAP = {
'completed': ('completed', 'No date'),
'due': ('due', 'No date'),
'updated': ('updated', 'No date'),
}
TASK_QUERY_TIME_MAP = {
'completedmin': 'completedMin',
'completedmax': 'completedMax',
'duemin': 'dueMin',
'duemax': 'dueMax',
'updatedmin': 'updatedMin',
}
TASK_QUERY_STATE_MAP = {
'showcompleted': 'showCompleted',
'showdeleted': 'showDeleted',
'showhidden': 'showHidden',
}
# gam <UserTypeEntity> show tasks [tasklists <TasklistEntity>]
# [completedmin <Time>] [completedmax <Time>]
# [duemin <Time>] [duemax <Time>]
# [updatedmin <Time>]
# [showcompleted [<Boolean>]] [showdeleted [<Boolean>]] [showhidden [<Boolean>]] [showall]
# [orderby completed|due|updated]
# [countsonly|compact|formatjson]
# gam <UserTypeEntity> print tasks [tasklists <TasklistEntity>] [todrive <ToDriveAttribute>*]
# [completedmin <Time>] [completedmax <Time>]
# [duemin <Time>] [duemax <Time>]
# [updatedmin <Time>]
# [showcompleted [<Boolean>]] [showdeleted [<Boolean>]] [showhidden [<Boolean>]] [showall]
# [orderby completed|due|updated]
# [countsonly | (formatjson [quotechar <Character>])]
def printShowTasks(users):
def _showTaskAndChildren(tasklist, taskId, k, compact):
if taskId in taskParentsProcessed:
return k
taskParentsProcessed.add(taskId)
if taskId in taskData:
k += 1
_showTask(tasklist, taskData[taskId], k, kcount, FJQC, compact)
Ind.Increment()
for task in taskParents.get(taskId, []):
k = _showTaskAndChildren(tasklist, task['taskId'], k, compact)
if taskId in taskData:
Ind.Decrement()
return k
def _printTaskAndChildren(tasklist, taskId):
if taskId in taskParentsProcessed:
return
taskParentsProcessed.add(taskId)
if taskId in taskData:
task = taskData[taskId]
task['tasklistId'] = tasklist
task['taskId'] = f"{tasklist}/{task['id']}"
row = flattenJSON(task, flattened={'User': user}, skipObjects=TASK_SKIP_OBJECTS, timeObjects=TASK_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user, 'id': task['id'], 'tasklistId': tasklist, 'taskId': task['taskId'], 'title': task.get('title', '')}
row['JSON'] = json.dumps(cleanJSON(task, skipObjects=TASK_SKIP_OBJECTS, timeObjects=TASK_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
for task in taskParents.get(taskId, []):
_printTaskAndChildren(tasklist, task['taskId'])
csvPF = CSVPrintFile(['User', 'tasklistId', 'id', 'taskId', 'title', 'status', 'due', 'updated', 'completed'], 'sortall') if Act.csvFormat() else None
if csvPF:
csvPF.SetNoEscapeChar(True)
CSVTitle = 'Tasks'
FJQC = FormatJSONQuoteChar(csvPF)
tasklistEntity = None
kwargs = {'maxResults': 100}
compact = countsOnly = False
orderBy = orderByNoDataValue = None
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg in {'tasklist', 'tasklists'}:
tasklistEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_ENTITY, Ent.TASKLIST, shlexSplit=True)
elif myarg in TASK_QUERY_TIME_MAP:
kwargs[TASK_QUERY_TIME_MAP[myarg]] = getTimeOrDeltaFromNow()
elif myarg in TASK_QUERY_STATE_MAP:
kwargs[TASK_QUERY_STATE_MAP[myarg]] = getBoolean()
elif myarg == 'showall':
for field in TASK_QUERY_STATE_MAP.values():
kwargs[field] = True
elif not csvPF and myarg == 'compact':
compact = True
elif myarg == 'orderby':
orderBy, orderByNoDataValue = getChoice(TASK_ORDERBY_CHOICE_MAP, mapChoice=True)
elif myarg == 'countsonly':
countsOnly = True
if csvPF:
csvPF.SetTitles(['User', CSVTitle])
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
if csvPF and FJQC.formatJSON:
csvPF.SetJSONTitles(['User', 'tasklistId', 'id', 'taskId', 'title', 'JSON'])
i, count, users = getEntityArgument(users)
for user in users:
i += 1
if tasklistEntity is None:
user, svc = buildGAPIServiceObject(API.TASKS, user, i, count)
if not svc:
continue
printGettingEntityItemForWhom(Ent.TASKLIST, user, i, count)
results = getTaskLists(svc, user, i, count)
if results is None:
continue
tasklists = [tasklist['id'] for tasklist in results]
jcount = len(tasklists)
else:
userTasklists = None
user, svc, tasklists, jcount = _validateUserGetObjectList(user, i, count, tasklistEntity, api=API.TASKS,
showAction=FJQC is None or not FJQC.formatJSON)
if jcount == 0:
continue
taskCount = 0
# if not csvPF and not FJQC.formatJSON:
# entityPerformActionNumItems([Ent.USER, user], jcount, Ent.TASKLIST, i, count)
Ind.Increment()
j = 0
for tasklist in tasklists:
j += 1
if tasklist.startswith('tltitle:'):
tasklistTitle = tasklist[8:]
userTasklists, tasklist = getTaskListIDfromTitle(svc, userTasklists, tasklistTitle, user, i, count)
if tasklist is None:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklistTitle], Msg.TASKLIST_TITLE_NOT_FOUND, j, jcount)
continue
printGettingEntityItemForWhom(Ent.TASK, tasklist, j, jcount)
try:
tasks = callGAPIpages(svc.tasks(), 'list', 'items',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, **kwargs)
kcount = len(tasks)
if countsOnly:
taskCount += kcount
continue
taskParents = {None: []}
taskData = {}
taskParentsProcessed = set()
if orderBy is None:
for task in tasks:
taskData[task['id']] = task
parent = task.get('parent', None)
taskInfo = {'taskId': task['id'], 'parent': parent, 'position': task['position']}
taskParents.setdefault(parent, [])
taskParents[parent].append(taskInfo)
for parent in taskParents:
taskParents[parent].sort(key=lambda k: k['position'])
else:
for task in tasks:
taskData[task['id']] = task
if orderBy not in task:
task[orderBy] = orderByNoDataValue
taskInfo = {'taskId': task['id'], orderBy: task[orderBy],
'parent': task.get('parent', ' '), 'position': task['position']}
taskParents[None].append(taskInfo)
taskParents[None].sort(key=lambda k: (k[orderBy], k['parent'], k['position']))
if not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.TASKLIST, tasklist], kcount, Ent.TASK, j, jcount)
Ind.Increment()
k = 0
for parent in taskParents.values():
for task in parent:
k = _showTaskAndChildren(tasklist, task['taskId'], k, compact)
Ind.Decrement()
else:
for parent in taskParents.values():
for task in parent:
_printTaskAndChildren(tasklist, task['taskId'])
except (GAPI.badRequest, GAPI.invalid, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, None], str(e), i, count)
except GAPI.serviceNotAvailable:
userTasksServiceNotEnabledWarning(user, i, count)
Ind.Decrement()
if countsOnly:
if csvPF:
csvPF.WriteRowTitles({'User': user, CSVTitle: taskCount})
else:
printEntityKVList([Ent.USER, user], [CSVTitle, taskCount], i, count)
if csvPF:
csvPF.writeCSVfile(CSVTitle)
TASKLIST_SKIP_OBJECTS = ['selfLink']
TASKLIST_TIME_OBJECTS = ['updated']
def _showTasklist(tasklist, j=0, jcount=0, FJQC=None):
if FJQC is not None and FJQC.formatJSON:
printLine(json.dumps(cleanJSON(tasklist, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.TASKLIST, tasklist['id']], j, jcount)
Ind.Increment()
showJSON(None, tasklist, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS)
Ind.Decrement()
# gam <UserTypeEntity> create tasklist
# [title <String>]
# [returnidonly] [formatjson]
# gam <UserTypeEntity> update tasklist <TasklistEntity>
# [title <String>]
# [formatjson]
# gam <UserTypeEntity> info tasklist <TasklistEntity>
# [formatjson]
# gam <UserTypeEntity> delete tasklist <TasklistEntity>
# gam <UserTypeEntity> clear tasklist <TasklistEntity>
def processTasklists(users):
action = Act.Get()
if action != Act.CREATE:
tasklistEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_ENTITY, Ent.TASKLIST, shlexSplit=True)
else:
tasklistEntity = {'item': Ent.TASKLIST, 'list': [None], 'dict': None}
if action in {Act.DELETE, Act.CLEAR}:
FJQC = None
checkForExtraneousArguments()
else:
FJQC = FormatJSONQuoteChar()
body = {}
returnIdOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if action in {Act.CREATE, Act.UPDATE} and myarg == 'title':
body['title'] = getString(Cmd.OB_STRING, minLen=0)
elif action == Act.CREATE and myarg == 'returnidonly':
returnIdOnly = True
else:
FJQC.GetFormatJSON(myarg)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
userTasklists = None
user, svc, tasklists, jcount = _validateUserGetObjectList(user, i, count, tasklistEntity,
api=API.TASKS,
showAction=action != Act.CREATE and (FJQC is None or not FJQC.formatJSON))
if jcount == 0 or not verifyTasksServiceEnabled(svc, user, i, count):
continue
Ind.Increment()
j = 0
for tasklist in tasklists:
j += 1
if action != Act.CREATE:
if tasklist.startswith('tltitle:'):
tasklistTitle = tasklist[8:]
userTasklists, tasklist = getTaskListIDfromTitle(svc, userTasklists, tasklistTitle, user, i, count)
if userTasklists is None:
continue
if tasklist is None:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklistTitle], Msg.TASKLIST_TITLE_NOT_FOUND, j, jcount)
continue
try:
if action == Act.DELETE:
callGAPI(svc.tasklists(), 'delete',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist)
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist], j, jcount)
elif action == Act.CLEAR:
callGAPI(svc.tasks(), 'clear',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist)
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist], j, jcount)
elif action == Act.INFO:
result = callGAPI(svc.tasklists(), 'get',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist)
_showTasklist(result, j, jcount, FJQC)
else:
if action == Act.CREATE:
result = callGAPI(svc.tasklists(), 'insert',
throwReasons=GAPI.TASK_THROW_REASONS,
body=body)
if returnIdOnly:
writeStdout(f"{result['id']}\n")
continue
else: # Act.UPDATE
result = callGAPI(svc.tasklists(), 'patch',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklist, body=body)
if not FJQC.formatJSON:
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, result['id']], i, count)
Ind.Increment()
_showTasklist(result, j, jcount, FJQC)
Ind.Decrement()
except (GAPI.badRequest, GAPI.invalid, GAPI.notFound) as e:
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklist], str(e), j, jcount)
except GAPI.serviceNotAvailable:
Ind.Decrement()
userTasksServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
# gam <UserTypeEntity> show tasklists
# [countsonly|formatjson]
# gam <UserTypeEntity> print tasklists [todrive <ToDriveAttribute>*]
# [countsonly | (formatjson [quotechar <Character>])]
def printShowTasklists(users):
csvPF = CSVPrintFile(['User', 'id', 'title']) if Act.csvFormat() else None
if csvPF:
csvPF.SetNoEscapeChar(True)
CSVTitle = 'TaskLists'
FJQC = FormatJSONQuoteChar(csvPF)
countsOnly = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'countsonly':
countsOnly = True
if csvPF:
csvPF.SetTitles(['User', CSVTitle])
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, svc = buildGAPIServiceObject(API.TASKS, user, i, count)
if not svc:
continue
printGettingAllEntityItemsForWhom(Ent.TASKLIST, user, i, count)
tasklists = getTaskLists(svc, user, i, count)
if tasklists is None:
continue
jcount = len(tasklists)
if countsOnly:
if csvPF:
csvPF.WriteRowTitles({'User': user, CSVTitle: jcount})
else:
printEntityKVList([Ent.USER, user], [CSVTitle, jcount], i, count)
elif not csvPF:
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.TASKLIST, i, count)
Ind.Increment()
j = 0
for tasklist in tasklists:
j += 1
_showTasklist(tasklist, j, jcount, FJQC)
Ind.Decrement()
else:
for tasklist in tasklists:
row = flattenJSON(tasklist, flattened={'User': user}, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user, 'id': tasklist['id'], 'title': tasklist.get('title', '')}
row['JSON'] = json.dumps(cleanJSON(tasklist, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
if csvPF:
csvPF.writeCSVfile(CSVTitle)
# gam <UserTypeEntity> import tasklist <Filename> [charset <Charset>]))
def importTasklist(users):
filename = getString(Cmd.OB_FILE_NAME)
encoding = getCharSet()
try:
jsonData = json.loads(readFile(filename, encoding=encoding))
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
Cmd.Backup()
usageErrorExit(Msg.JSON_ERROR.format(str(e), filename))
if jsonData.get('kind', '') != 'tasks#taskLists':
Cmd.Backup()
usageErrorExit(f'{"Not a Tasks takeout JSON file"}: {filename}')
parentIdMap = {}
cleanData = {'items': []}
for tasklist in jsonData.get('items', []):
cleanTasklist = {'title': tasklist['title'], 'items': []}
for task in tasklist.get('items', []):
cleanTask = {}
for field in ['id', 'parent', 'title', 'notes', 'status', 'due', 'completed', 'deleted']:
if field in task:
cleanTask[field] = task[field]
parentIdMap[task['id']] = None
cleanTasklist['items'].append(cleanTask.copy())
cleanData['items'].append(cleanTasklist.copy())
if not cleanData['items']:
writeStdout('No tasks to import\n')
return
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, svc = buildGAPIServiceObject(API.TASKS, user, i, count)
if not svc:
continue
if not verifyTasksServiceEnabled(svc, user, i, count):
continue
for tasklist in cleanData['items']:
body = {'title': tasklist['title']}
result = callGAPI(svc.tasklists(), 'insert',
throwReasons=GAPI.TASK_THROW_REASONS,
body=body)
tasklistId = result['id']
for task in tasklist['items']:
taskId = task.pop('id')
if 'parent' in task:
parent = parentIdMap[task.pop('parent')]
else:
parent = None
result = callGAPI(svc.tasks(), 'insert',
throwReasons=GAPI.TASK_THROW_REASONS,
tasklist=tasklistId, parent=parent, body=task)
parentIdMap[taskId] = result['id']
def getCRMOrgId():
setTrueCustomerId()
_, crm = buildGAPIServiceObject(API.CLOUDRESOURCEMANAGER, None)
results = callGAPI(crm.organizations(), 'search',
query=f'directorycustomerid:{GC.Values[GC.CUSTOMER_ID]}',
pageSize=1, fields='organizations/name')
orgs = results.get('organizations')
if not orgs:
# return nothing and let calling API deal with it
# since caller knows what GCP role would serve best
return None
return orgs[0].get('name')
def CAARoleErrorExit(caa):
sa_email = caa._http.credentials.signer_email
systemErrorExit(NO_SA_ACCESS_CONTEXT_MANAGER_EDITOR_ROLE_RC,
f'Please grant service account {sa_email} the Access Context Manager Editor role in your GCP organization.')
def normalizeCAALevelName(caa, name):
if name.startswith('accessPolicies/'):
return name
ap_name = getAccessPolicy(caa)
return f'{ap_name}/accessLevels/{name}'
def buildCAAServiceObject():
_, caa = buildGAPIServiceObject(API.ACCESSCONTEXTMANAGER, None)
return caa
def getAccessPolicy(caa=None):
if not caa:
caa = buildCAAServiceObject()
parent = getCRMOrgId()
if not parent:
CAARoleErrorExit(caa)
try:
aps = callGAPIpages(caa.accessPolicies(), 'list', 'accessPolicies',
throwReasons=[GAPI.PERMISSION_DENIED],
parent=parent, fields='nextPageToken,accessPolicies(name,title)')
except GAPI.permissionDenied:
CAARoleErrorExit(caa)
if not aps:
systemErrorExit(ACCESS_POLICY_ERROR_RC, 'You don\'t seem to have any access policies. That is odd.')
elif len(aps) == 1:
return aps[0]['name']
for ap in aps:
if ap.get('title') == 'Access policy created in Cloud Identity Console':
return ap['name']
systemErrorExit(ACCESS_POLICY_ERROR_RC, ' Could not find a org level access policy. That is odd.')
CAA_OS_TYPE_MAP = {
'desktopmac': 'DESKTOP_MAC',
'desktopwindows': 'DESKTOP_WINDOWS',
'desktoplinux': 'DESKTOP_LINUX',
'desktopchromeos': 'DESKTOP_CHROME_OS',
'verifieddesktopchromeos': 'VERIFIED_DESKTOP_CHROME_OS',
'android': 'ANDROID',
'ios': 'IOS',
}
def CAABuildOsConstraints():
consts_obj = []
for constraint in getString(Cmd.OB_STRING).split(','):
new_const = {}
if ':' in constraint:
osType, new_const['minimumVersion'] = constraint.split(':')
else:
osType = constraint
osType = osType.lower().replace('_', '')
if osType not in CAA_OS_TYPE_MAP:
invalidChoiceExit(osType, CAA_OS_TYPE_MAP, True)
if osType != 'verifieddesktopchromeos':
new_const['osType'] = CAA_OS_TYPE_MAP[osType]
else:
new_const['osType'] = 'DESKTOP_CHROME_OS'
new_const['requireVerifiedChromeOs'] = True
consts_obj.append(new_const)
return consts_obj
CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP = {
'basic': 'BASIC',
'advanced': 'COMPLETE',
'complete': 'COMPLETE',
'none': 'NONE',
}
CAA_ALLOWED_ENCRYPTIION_STATUS_MAP = {
'encryptionunsupported': 'ENCRYPTION_UNSUPPORTED',
'encrypted': 'ENCRYPTED',
'unencrypted': 'UNENCRYPTED',
}
def CAABuildDevicePolicy():
device_policy = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'requirescreenlock':
device_policy['requireScreenLock'] = getBoolean()
elif myarg == 'allowedencryptionstatuses':
device_policy['allowedEncryptionStatuses'] = []
for status in getString(Cmd.OB_STRING).lower().split(','):
if status not in CAA_ALLOWED_ENCRYPTIION_STATUS_MAP:
invalidChoiceExit(status, CAA_ALLOWED_ENCRYPTIION_STATUS_MAP, True)
device_policy['allowedEncryptionStatuses'].append(CAA_ALLOWED_ENCRYPTIION_STATUS_MAP[status])
elif myarg == 'osconstraints':
device_policy['osConstraints'] = CAABuildOsConstraints()
elif myarg == 'alloweddevicemanagementlevels':
device_policy['allowedDeviceManagementLevels'] = []
for level in getString(Cmd.OB_STRING).lower().split(','):
if level not in CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP:
invalidChoiceExit(level, CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP, True)
device_policy['allowedDeviceManagementLevels'].append(CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP[level])
elif myarg == 'requireadminapproval':
device_policy['requireAdminApproval'] = getBoolean()
elif myarg == 'requirecorpowned':
device_policy['requireCorpOwned'] = getBoolean()
elif myarg == 'enddevicepolicy':
break
else:
unknownArgumentExit()
return device_policy
ISO3166_1_ALPHA_2_CODES = {
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
"BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ",
"CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ",
"DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR",
"GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY",
"HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP",
"KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY",
"MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ",
"NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ",
"OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
"SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ",
"TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ",
"UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW",
}
def validateISO3166_1_alpha2_code(region):
if region not in ISO3166_1_ALPHA_2_CODES:
Cmd.Backup()
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID_CHOICE][1].format(region), Msg.INVALID_REGION)
def CAABuildCondition():
condition = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'ipsubnetworks':
condition['ipSubnetworks'] = getString(Cmd.OB_STRING_LIST).split(',')
elif myarg == 'devicepolicy':
condition['devicePolicy'] = CAABuildDevicePolicy()
elif myarg == 'requiredaccesslevels':
condition['requiredAccessLevels'] = getString(Cmd.OB_STRING_LIST).split(',')
elif myarg == 'negate':
condition['negate'] = getBoolean()
elif myarg == 'members':
condition['members'] = getString(Cmd.OB_STRING_LIST).split(',')
elif myarg == 'regions':
condition['regions'] = getString(Cmd.OB_STRING_LIST).upper().split(',')
for region in condition['regions']:
validateISO3166_1_alpha2_code(region)
elif myarg == 'endcondition':
break
else:
unknownArgumentExit()
return condition
CAA_COMBINING_FUNCTIONS_MAP = {
'and': 'AND',
'or': 'OR',
}
def CAABuildBasicLevel():
basic_level = {'conditions': []}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'combiningfunction':
basic_level['combiningFunction'] = getChoice(CAA_COMBINING_FUNCTIONS_MAP, mapChoice=True)
elif myarg == 'condition':
basic_level['conditions'].append(CAABuildCondition())
else:
unknownArgumentExit()
return basic_level
def CAABuildLevel(body):
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'basic':
body['basic'] = CAABuildBasicLevel()
elif myarg == 'custom':
body['custom'] = {'expr': {'expression': getString(Cmd.OB_STRING), 'title': 'expr'}}
elif myarg == 'description':
body['description'] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'json':
body.update(getJSON(['name']))
else:
unknownArgumentExit()
# gam create caalevel <String> [description <String>] (basic <CAABasicAttribute>+)|(custom <String>)|<JSONData>
def doCreateCAALevel():
caa = buildCAAServiceObject()
ap_name = getAccessPolicy(caa)
title = getString(Cmd.OB_STRING).replace(' ', '_')
allowed_title_chars = string.ascii_letters + string.digits + '_'
name = ''.join([c for c in title if c in allowed_title_chars])[:50]
name = f'{ap_name}/accessLevels/{name}'
body = {'name': name, 'title': title}
CAABuildLevel(body)
try:
callGAPI(caa.accessPolicies().accessLevels(), 'create',
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
parent=ap_name, body=body)
entityActionPerformed([Ent.CAA_LEVEL, name])
except (GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.CAA_LEVEL, name], str(e))
except GAPI.permissionDenied:
CAARoleErrorExit(caa)
# gam update caalevel <CAALevelName> [description <String>] (basic <CAABasicAttribute>+)|(custom <String>)|<JSONData>
def doUpdateCAALevel():
caa = buildCAAServiceObject()
name = normalizeCAALevelName(caa, getString(Cmd.OB_ACCESS_LEVEL_NAME))
body = {}
CAABuildLevel(body)
updateMask = ','.join(body.keys())
try:
callGAPI(caa.accessPolicies().accessLevels(), 'patch',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name, updateMask=updateMask, body=body)
entityActionPerformed([Ent.CAA_LEVEL, name])
except (GAPI.notFound, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.CAA_LEVEL, name], str(e))
except GAPI.permissionDenied:
CAARoleErrorExit(caa)
# gam delete caalevel <CAALevelName>
def doDeleteCAALevel():
caa = buildCAAServiceObject()
name = normalizeCAALevelName(caa, getString(Cmd.OB_ACCESS_LEVEL_NAME))
checkForExtraneousArguments()
try:
callGAPI(caa.accessPolicies().accessLevels(), 'delete',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
name=name)
entityActionPerformed([Ent.CAA_LEVEL, name])
except (GAPI.notFound, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.CAA_LEVEL, name], str(e))
except GAPI.permissionDenied:
CAARoleErrorExit(caa)
# gam print caalevels [todrive <ToDriveAttribute>*]
# [formatjson [quotechar <Character>]]
# gam show caalevels
# [formatjson]
def doPrintShowCAALevels():
caa = buildCAAServiceObject()
ap_name = getAccessPolicy(caa)
csvPF = CSVPrintFile(['name', 'title']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
try:
levels = callGAPIpages(caa.accessPolicies().accessLevels(), 'list', 'accessLevels',
throwReasons=[GAPI.PERMISSION_DENIED],
parent=ap_name, accessLevelFormat='CEL', fields='*')
except GAPI.permissionDenied:
CAARoleErrorExit(caa)
if not csvPF:
count = len(levels)
if not FJQC.formatJSON:
performActionNumItems(count, Ent.CAA_LEVEL)
i = 0
for level in levels:
i += 1
printEntity([Ent.CAA_LEVEL, level['name']], i, count)
Ind.Increment()
showJSON(None, level)
Ind.Decrement()
else:
for level in levels:
printLine(json.dumps(cleanJSON(level), ensure_ascii=False, sort_keys=True))
else:
for level in levels:
row = flattenJSON(level)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'name': level['name'], 'title': level['title']}
row['JSON'] = json.dumps(cleanJSON(level, timeObjects=NOTES_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
if csvPF:
csvPF.writeCSVfile('Context Aware Access Levels')
# Command line processing
CMD_ACTION = 0
CMD_FUNCTION = 1
# Batch commands
BATCH_CSV_COMMANDS = {
Cmd.BATCH_CMD: (Act.PERFORM, doBatch),
Cmd.CSV_CMD: (Act.PERFORM, doCSV),
Cmd.CSVTEST_CMD: (Act.PERFORM, doCSVTest),
Cmd.TBATCH_CMD: (Act.PERFORM, doThreadBatch),
}
# Main commands
MAIN_COMMANDS = {
'checkconn': (Act.CHECK, doCheckConnection),
'checkconnection': (Act.CHECK, doCheckConnection),
'comment': (Act.COMMENT, doComment),
'help': (Act.PERFORM, doUsage),
'list': (Act.LIST, doListType),
'report': (Act.REPORT, doReport),
'sendemail': (Act.SENDEMAIL, doSendEmail),
'version': (Act.PERFORM, doVersion),
'whatis': (Act.INFO, doWhatIs),
}
# Main commands with objects
MAIN_ADD_CREATE_FUNCTIONS = {
Cmd.ARG_ADMIN: doCreateAdmin,
Cmd.ARG_ADMINROLE: doCreateUpdateAdminRoles,
Cmd.ARG_ALERTFEEDBACK: doCreateAlertFeedback,
Cmd.ARG_ALIAS: doCreateUpdateAliases,
Cmd.ARG_BROWSERTOKEN: doCreateBrowserToken,
Cmd.ARG_BUILDING: doCreateBuilding,
Cmd.ARG_CAALEVEL: doCreateCAALevel,
Cmd.ARG_CHATMESSAGE: doCreateChatMessage,
Cmd.ARG_CHROMENETWORK: doCreateChromeNetwork,
Cmd.ARG_CHROMEPOLICYIMAGE: doCreateChromePolicyImage,
Cmd.ARG_CIGROUP: doCreateCIGroup,
Cmd.ARG_CONTACT: doCreateDomainContact,
Cmd.ARG_COURSE: doCreateCourse,
Cmd.ARG_DATATRANSFER: doCreateDataTransfer,
Cmd.ARG_DEVICE: doCreateCIDevice,
Cmd.ARG_DOMAIN: doCreateDomain,
Cmd.ARG_DOMAINALIAS: doCreateDomainAlias,
Cmd.ARG_DRIVEFILEACL: doCreateDriveFileACL,
Cmd.ARG_DRIVELABELPERMISSION: doCreateDriveLabelPermissions,
Cmd.ARG_FEATURE: doCreateFeature,
Cmd.ARG_GCPFOLDER: doCreateGCPFolder,
Cmd.ARG_GCPSERVICEACCOUNT: doCreateGCPServiceAccount,
Cmd.ARG_GROUP: doCreateGroup,
Cmd.ARG_GUARDIAN: doInviteGuardian,
Cmd.ARG_GUARDIANINVITATION: doInviteGuardian,
Cmd.ARG_INBOUNDSSOASSIGNMENT: doCreateInboundSSOAssignment,
Cmd.ARG_INBOUNDSSOCREDENTIAL: doCreateInboundSSOCredential,
Cmd.ARG_INBOUNDSSOPROFILE: doCreateInboundSSOProfile,
Cmd.ARG_ORG: doCreateOrg,
Cmd.ARG_PERMISSION: doCreatePermissions,
Cmd.ARG_PRINTER: doCreatePrinter,
Cmd.ARG_PROJECT: doCreateProject,
Cmd.ARG_RESOLDCUSTOMER: doCreateResoldCustomer,
Cmd.ARG_RESOLDSUBSCRIPTION: doCreateResoldSubscription,
Cmd.ARG_RESOURCE: doCreateResourceCalendar,
Cmd.ARG_SAKEY: doCreateSvcAcctKeys,
Cmd.ARG_SCHEMA: doCreateUpdateUserSchemas,
Cmd.ARG_SHAREDDRIVE: doCreateSharedDrive,
Cmd.ARG_SITE: deprecatedDomainSites,
Cmd.ARG_SITEACL: deprecatedDomainSites,
Cmd.ARG_SVCACCT: doCreateSvcAcct,
Cmd.ARG_USER: doCreateUser,
Cmd.ARG_VAULTEXPORT: doCreateVaultExport,
Cmd.ARG_VAULTHOLD: doCreateVaultHold,
Cmd.ARG_VAULTMATTER: doCreateVaultMatter,
Cmd.ARG_VERIFY: doCreateSiteVerification,
}
MAIN_COMMANDS_WITH_OBJECTS = {
'add':
(Act.ADD,
MAIN_ADD_CREATE_FUNCTIONS
),
'approve':
(Act.APPROVE,
{Cmd.ARG_DEVICEUSER: doApproveCIDeviceUser,
}
),
'block':
(Act.BLOCK,
{Cmd.ARG_DEVICEUSER: doBlockCIDeviceUser,
}
),
'cancel':
(Act.CANCEL,
{Cmd.ARG_GUARDIANINVITATION: doCancelGuardianInvitation,
Cmd.ARG_USERINVITATION: doCIUserInvitationsAction,
}
),
'cancelwipe':
(Act.CANCEL_WIPE,
{Cmd.ARG_DEVICE: doCancelWipeCIDevice,
Cmd.ARG_DEVICEUSER: doCancelWipeCIDeviceUser,
}
),
'check':
(Act.CHECK,
{Cmd.ARG_SVCACCT: doCheckUpdateSvcAcct,
Cmd.ARG_USERINVITATION: doCheckCIUserInvitations,
Cmd.ARG_ISINVITABLE: doCheckCIUserInvitations,
Cmd.ARG_ORG: doCheckOrgUnit,
}
),
'clear':
(Act.CLEAR,
{Cmd.ARG_CONTACT: doClearDomainContacts,
}
),
'close':
(Act.CLOSE,
{Cmd.ARG_VAULTMATTER: doCloseVaultMatter,
}
),
'copy':
(Act.COPY,
{Cmd.ARG_SHAREDDRIVEACLS: doCopySyncSharedDriveACLs,
Cmd.ARG_STORAGEBUCKET: doCopyCloudStorageBucket,
Cmd.ARG_VAULTEXPORT: doCopyVaultExport,
}
),
'create':
(Act.CREATE,
MAIN_ADD_CREATE_FUNCTIONS
),
'dedup':
(Act.DEDUP,
{Cmd.ARG_CONTACT: doDedupDomainContacts,
}
),
'delete':
(Act.DELETE,
{Cmd.ARG_ADMIN: doDeleteAdmin,
Cmd.ARG_ADMINROLE: doDeleteAdminRole,
Cmd.ARG_ALIAS: doDeleteAliases,
Cmd.ARG_ALERT: doDeleteOrUndeleteAlert,
Cmd.ARG_BROWSER: doDeleteBrowsers,
Cmd.ARG_BUILDING: doDeleteBuilding,
Cmd.ARG_CAALEVEL: doDeleteCAALevel,
Cmd.ARG_CHATMESSAGE: doDeleteChatMessage,
Cmd.ARG_CHROMENETWORK: doDeleteChromeNetwork,
Cmd.ARG_CHROMEPOLICY: doDeleteChromePolicy,
Cmd.ARG_CHROMEPROFILE: doDeleteChromeProfile,
Cmd.ARG_CIGROUP: doDeleteCIGroups,
Cmd.ARG_CLASSROOMINVITATION: doDeleteClassroomInvitations,
Cmd.ARG_CONTACT: doDeleteDomainContacts,
Cmd.ARG_CONTACTPHOTO: doDeleteDomainContactPhoto,
Cmd.ARG_COURSE: doDeleteCourse,
Cmd.ARG_COURSES: doDeleteCourses,
Cmd.ARG_DEVICE: doDeleteCIDevice,
Cmd.ARG_DEVICEUSER: doDeleteCIDeviceUser,
Cmd.ARG_DOMAIN: doDeleteDomain,
Cmd.ARG_DOMAINALIAS: doDeleteDomainAlias,
Cmd.ARG_DOMAINCONTACT: doDeleteDomainContacts,
Cmd.ARG_DRIVEFILEACL: doDeleteDriveFileACLs,
Cmd.ARG_DRIVELABELPERMISSION: doDeleteDriveLabelPermissions,
Cmd.ARG_FEATURE: doDeleteFeature,
Cmd.ARG_GROUP: doDeleteGroups,
Cmd.ARG_GUARDIAN: doDeleteGuardian,
Cmd.ARG_INBOUNDSSOASSIGNMENT: doDeleteInboundSSOAssignment,
Cmd.ARG_INBOUNDSSOCREDENTIAL: doDeleteInboundSSOCredential,
Cmd.ARG_INBOUNDSSOPROFILE: doDeleteInboundSSOProfile,
Cmd.ARG_MOBILE: doDeleteMobileDevices,
Cmd.ARG_ORG: doDeleteOrg,
Cmd.ARG_ORGS: doDeleteOrgs,
Cmd.ARG_PERMISSION: doDeletePermissions,
Cmd.ARG_PRINTER: doDeletePrinter,
Cmd.ARG_PROJECT: doDeleteProject,
Cmd.ARG_RESOLDSUBSCRIPTION: doDeleteResoldSubscription,
Cmd.ARG_RESOURCE: doDeleteResourceCalendar,
Cmd.ARG_RESOURCES: doDeleteResourceCalendars,
Cmd.ARG_SAKEY: doDeleteSvcAcctKeys,
Cmd.ARG_SCHEMA: doDeleteUserSchemas,
Cmd.ARG_SHAREDDRIVE: doDeleteSharedDrive,
Cmd.ARG_SITEACL: deprecatedDomainSites,
Cmd.ARG_SVCACCT: doDeleteSvcAcct,
Cmd.ARG_USER: doDeleteUser,
Cmd.ARG_USERS: doDeleteUsers,
Cmd.ARG_VAULTEXPORT: doDeleteVaultExport,
Cmd.ARG_VAULTHOLD: doDeleteVaultHold,
Cmd.ARG_VAULTMATTER: doDeleteVaultMatter,
}
),
'download':
(Act.DOWNLOAD,
{Cmd.ARG_STORAGEBUCKET: doDownloadCloudStorageBucket,
Cmd.ARG_STORAGEFILE: doDownloadCloudStorageFile,
Cmd.ARG_VAULTEXPORT: doDownloadVaultExport,
}
),
'enable':
(Act.ENABLE,
{Cmd.ARG_API: doEnableAPIs,
}
),
'get':
(Act.DOWNLOAD,
{Cmd.ARG_CONTACTPHOTO: doGetDomainContactPhoto,
Cmd.ARG_DEVICEFILE: doGetCrOSDeviceFiles,
}
),
'getcommand':
(Act.GET_COMMAND_RESULT,
{Cmd.ARG_CROS: doGetCommandResultCrOSDevices,
}
),
'hide':
(Act.HIDE,
{Cmd.ARG_SHAREDDRIVE: doHideUnhideSharedDrive,
}
),
'info':
(Act.INFO,
{Cmd.ARG_ADMINROLE: doInfoAdminRole,
Cmd.ARG_ALERT: doInfoAlert,
Cmd.ARG_ALIAS: doInfoAliases,
Cmd.ARG_BUILDING: doInfoBuilding,
Cmd.ARG_BROWSER: doInfoBrowsers,
Cmd.ARG_CHATEVENT: doInfoChatEvent,
Cmd.ARG_CHATMEMBER: doInfoChatMember,
Cmd.ARG_CHATMESSAGE: doInfoChatMessage,
Cmd.ARG_CHATSPACE: doInfoChatSpace,
Cmd.ARG_CHROMEAPP: doInfoChromeApp,
Cmd.ARG_CHROMEPROFILE: doInfoChromeProfile,
Cmd.ARG_CHROMESCHEMA: doInfoChromePolicySchemas,
Cmd.ARG_CIGROUP: doInfoCIGroups,
Cmd.ARG_CIGROUPMEMBERS: doInfoCIGroupMembers,
Cmd.ARG_CIPOLICY: doInfoCIPolicies,
Cmd.ARG_CONTACT: doInfoDomainContacts,
Cmd.ARG_COURSE: doInfoCourse,
Cmd.ARG_COURSES: doInfoCourses,
Cmd.ARG_CROS: doInfoCrOSDevices,
Cmd.ARG_CROSTELEMETRY: doInfoPrintShowCrOSTelemetry,
Cmd.ARG_CURRENTPROJECTID: doInfoCurrentProjectId,
Cmd.ARG_CUSTOMER: doInfoCustomer,
Cmd.ARG_DATATRANSFER: doInfoDataTransfer,
Cmd.ARG_DEVICE: doInfoCIDevice,
Cmd.ARG_DEVICEUSER: doInfoCIDeviceUser,
Cmd.ARG_DEVICEUSERSTATE: doInfoCIDeviceUserState,
Cmd.ARG_DOMAIN: doInfoDomain,
Cmd.ARG_DOMAINALIAS: doInfoDomainAlias,
Cmd.ARG_DOMAINCONTACT: doInfoDomainContacts,
Cmd.ARG_DRIVEFILEACL: doInfoDriveFileACLs,
Cmd.ARG_DRIVELABEL: doInfoDriveLabels,
Cmd.ARG_INSTANCE: doInfoInstance,
Cmd.ARG_GAL: doInfoGAL,
Cmd.ARG_GROUP: doInfoGroups,
Cmd.ARG_GROUPMEMBERS: doInfoGroupMembers,
Cmd.ARG_INBOUNDSSOASSIGNMENT: doInfoInboundSSOAssignment,
Cmd.ARG_INBOUNDSSOCREDENTIAL: doInfoInboundSSOCredential,
Cmd.ARG_INBOUNDSSOPROFILE: doInfoInboundSSOProfile,
Cmd.ARG_MOBILE: doInfoMobileDevices,
Cmd.ARG_ORG: doInfoOrg,
Cmd.ARG_ORGS: doInfoOrgs,
Cmd.ARG_PEOPLEPROFILE: doInfoDomainPeopleProfile,
Cmd.ARG_PEOPLECONTACT: doInfoDomainPeopleContacts,
Cmd.ARG_PRINTER: doInfoPrinter,
Cmd.ARG_RESOLDCUSTOMER: doInfoResoldCustomer,
Cmd.ARG_RESOLDSUBSCRIPTION: doInfoResoldSubscription,
Cmd.ARG_RESOURCE: doInfoResourceCalendar,
Cmd.ARG_RESOURCES: doInfoResourceCalendars,
Cmd.ARG_SCHEMA: doInfoUserSchemas,
Cmd.ARG_SHAREDDRIVE: doInfoSharedDrive,
Cmd.ARG_SITE: deprecatedDomainSites,
Cmd.ARG_SITEACL: deprecatedDomainSites,
Cmd.ARG_USER: doInfoUser,
Cmd.ARG_USERS: doInfoUsers,
Cmd.ARG_USERINVITATION: doInfoCIUserInvitations,
Cmd.ARG_VAULTEXPORT: doInfoVaultExport,
Cmd.ARG_VAULTHOLD: doInfoVaultHold,
Cmd.ARG_VAULTMATTER: doInfoVaultMatter,
Cmd.ARG_VAULTQUERY: doInfoVaultQuery,
Cmd.ARG_VERIFY: doInfoSiteVerification,
}
),
'issuecommand':
(Act.ISSUE_COMMAND,
{Cmd.ARG_CROS: doIssueCommandCrOSDevices,
}
),
'move':
(Act.MOVE,
{Cmd.ARG_BROWSER: doMoveBrowsers,
}
),
'print':
(Act.PRINT,
{Cmd.ARG_ADDRESSES: doPrintAddresses,
Cmd.ARG_ADMINROLE: doPrintShowAdminRoles,
Cmd.ARG_ADMIN: doPrintShowAdmins,
Cmd.ARG_ALERT: doPrintShowAlerts,
Cmd.ARG_ALERTFEEDBACK: doPrintShowAlertFeedback,
Cmd.ARG_ALIAS: doPrintAliases,
Cmd.ARG_BROWSER: doPrintShowBrowsers,
Cmd.ARG_BROWSERTOKEN: doPrintShowBrowserTokens,
Cmd.ARG_BUILDING: doPrintShowBuildings,
Cmd.ARG_CAALEVEL: doPrintShowCAALevels,
Cmd.ARG_CHANNELCUSTOMER: doPrintShowChannelCustomers,
Cmd.ARG_CHANNELCUSTOMERENTITLEMENT: doPrintShowChannelCustomerEntitlements,
Cmd.ARG_CHANNELOFFER: doPrintShowChannelOffers,
Cmd.ARG_CHANNELPRODUCT: doPrintShowChannelProducts,
Cmd.ARG_CHANNELSKU: doPrintShowChannelSKUs,
Cmd.ARG_CHATMEMBER: doPrintShowChatMembers,
Cmd.ARG_CHATSPACE: doPrintShowChatSpaces,
Cmd.ARG_CHROMEAPP: doPrintShowChromeApps,
Cmd.ARG_CHROMEAPPDEVICES: doPrintShowChromeAppDevices,
Cmd.ARG_CHROMEAUES: doPrintShowChromeAues,
Cmd.ARG_CHROMEHISTORY: doPrintShowChromeHistory,
Cmd.ARG_CHROMENEEDSATTN: doPrintShowChromeNeedsAttn,
Cmd.ARG_CHROMEPOLICY: doPrintShowChromePolicies,
Cmd.ARG_CHROMEPROFILE: doPrintShowChromeProfiles,
Cmd.ARG_CHROMESCHEMA: doPrintShowChromeSchemas,
Cmd.ARG_CHROMESNVALIDITY: doPrintChromeSnValidity,
Cmd.ARG_CHROMEVERSIONS: doPrintShowChromeVersions,
Cmd.ARG_CIGROUP: doPrintCIGroups,
Cmd.ARG_CIGROUPMEMBERS: doPrintCIGroupMembers,
Cmd.ARG_CIPOLICY: doPrintShowCIPolicies,
Cmd.ARG_CLASSROOMINVITATION: doPrintShowClassroomInvitations,
Cmd.ARG_CONTACT: doPrintShowDomainContacts,
Cmd.ARG_COURSE: doPrintCourses,
Cmd.ARG_COURSES: doPrintCourses,
Cmd.ARG_COURSEANNOUNCEMENTS: doPrintCourseAnnouncements,
Cmd.ARG_COURSEMATERIALS: doPrintCourseMaterials,
Cmd.ARG_COURSEPARTICIPANTS: doPrintCourseParticipants,
Cmd.ARG_COURSESUBMISSIONS: doPrintCourseSubmissions,
Cmd.ARG_COURSETOPICS: doPrintCourseTopics,
Cmd.ARG_COURSEWORK: doPrintCourseWork,
Cmd.ARG_CROS: doPrintCrOSDevices,
Cmd.ARG_CROSACTIVITY: doPrintCrOSActivity,
Cmd.ARG_CROSTELEMETRY: doInfoPrintShowCrOSTelemetry,
Cmd.ARG_DATATRANSFER: doPrintShowDataTransfers,
Cmd.ARG_DEVICE: doPrintCIDevices,
Cmd.ARG_DEVICEUSER: doPrintCIDeviceUsers,
Cmd.ARG_DOMAIN: doPrintShowDomains,
Cmd.ARG_DOMAINALIAS: doPrintShowDomainAliases,
Cmd.ARG_DOMAINCONTACT: doPrintShowDomainContacts,
Cmd.ARG_DRIVEFILEACL: doPrintShowDriveFileACLs,
Cmd.ARG_DRIVELABEL: doPrintShowDriveLabels,
Cmd.ARG_DRIVELABELPERMISSION: doPrintShowDriveLabelPermissions,
Cmd.ARG_FEATURE: doPrintShowFeatures,
Cmd.ARG_GAL: doPrintShowGAL,
Cmd.ARG_GROUP: doPrintGroups,
Cmd.ARG_GROUPMEMBERS: doPrintGroupMembers,
Cmd.ARG_GROUPTREE: doPrintShowGroupTree,
Cmd.ARG_GUARDIAN: doPrintShowGuardians,
Cmd.ARG_INBOUNDSSOASSIGNMENT: doPrintShowInboundSSOAssignments,
Cmd.ARG_INBOUNDSSOCREDENTIAL: doPrintShowInboundSSOCredentials,
Cmd.ARG_INBOUNDSSOPROFILE: doPrintShowInboundSSOProfiles,
Cmd.ARG_LICENSE: doPrintLicenses,
Cmd.ARG_MOBILE: doPrintMobileDevices,
Cmd.ARG_ORG: doPrintOrgs,
Cmd.ARG_ORGS: doPrintOrgs,
Cmd.ARG_ORGUNITSHAREDDRIVE: doPrintShowOrgunitSharedDrives,
Cmd.ARG_OWNERSHIP: doPrintShowOwnership,
Cmd.ARG_PEOPLECONTACT: doPrintShowDomainPeopleContacts,
Cmd.ARG_PEOPLEPROFILE: doPrintShowDomainPeopleProfiles,
Cmd.ARG_PRINTER: doPrintShowPrinters,
Cmd.ARG_PRINTERMODEL: doPrintShowPrinterModels,
Cmd.ARG_PRIVILEGES: doPrintShowPrivileges,
Cmd.ARG_PROJECT: doPrintShowProjects,
Cmd.ARG_RESOLDSUBSCRIPTION: doPrintShowResoldSubscriptions,
Cmd.ARG_RESOURCE: doPrintShowResourceCalendars,
Cmd.ARG_RESOURCES: doPrintShowResourceCalendars,
Cmd.ARG_SCHEMA: doPrintShowUserSchemas,
Cmd.ARG_SHAREDDRIVE: doPrintShowSharedDrives,
Cmd.ARG_SHAREDDRIVEACLS: doPrintShowSharedDriveACLs,
Cmd.ARG_SITE: deprecatedDomainSites,
Cmd.ARG_SITEACL: deprecatedDomainSites,
Cmd.ARG_SITEACTIVITY: deprecatedDomainSites,
Cmd.ARG_SVCACCT: doPrintShowSvcAccts,
Cmd.ARG_TOKEN: doPrintShowTokens,
Cmd.ARG_TRANSFERAPPS: doShowTransferApps,
Cmd.ARG_USER: doPrintUsers,
Cmd.ARG_USERS: doPrintUsers,
Cmd.ARG_USERCOUNTSBYORGUNIT: doPrintUserCountsByOrgUnit,
Cmd.ARG_USERINVITATION: doPrintShowCIUserInvitations,
Cmd.ARG_VAULTCOUNT: doPrintVaultCounts,
Cmd.ARG_VAULTEXPORT: doPrintShowVaultExports,
Cmd.ARG_VAULTHOLD: doPrintShowVaultHolds,
Cmd.ARG_VAULTMATTER: doPrintShowVaultMatters,
Cmd.ARG_VAULTQUERY: doPrintShowVaultQueries,
}
),
'remove':
(Act.REMOVE,
{Cmd.ARG_ALIAS: doRemoveAliases,
Cmd.ARG_DRIVELABELPERMISSION: doDeleteDriveLabelPermissions,
}
),
'reopen':
(Act.REOPEN,
{Cmd.ARG_VAULTMATTER: doReopenVaultMatter,
}
),
'replace':
(Act.UPDATE,
{Cmd.ARG_SAKEY: doReplaceSvcAcctKeys,
}
),
'rotate':
(Act.UPDATE,
{Cmd.ARG_SAKEY: doProcessSvcAcctKeys,
}
),
'revoke':
(Act.REVOKE,
{Cmd.ARG_BROWSERTOKEN: doRevokeBrowserToken,
}
),
'send':
(Act.SEND,
{Cmd.ARG_USERINVITATION: doCIUserInvitationsAction,
}
),
'setup':
(Act.SETUP,
{Cmd.ARG_CHAT: doSetupChat,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_ADMINROLE: doPrintShowAdminRoles,
Cmd.ARG_ADMIN: doPrintShowAdmins,
Cmd.ARG_ALERT: doPrintShowAlerts,
Cmd.ARG_ALERTFEEDBACK: doPrintShowAlertFeedback,
Cmd.ARG_BROWSER: doPrintShowBrowsers,
Cmd.ARG_BROWSERTOKEN: doPrintShowBrowserTokens,
Cmd.ARG_BUILDING: doPrintShowBuildings,
Cmd.ARG_CAALEVEL: doPrintShowCAALevels,
Cmd.ARG_CHANNELCUSTOMER: doPrintShowChannelCustomers,
Cmd.ARG_CHANNELCUSTOMERENTITLEMENT: doPrintShowChannelCustomerEntitlements,
Cmd.ARG_CHANNELOFFER: doPrintShowChannelOffers,
Cmd.ARG_CHANNELPRODUCT: doPrintShowChannelProducts,
Cmd.ARG_CHANNELSKU: doPrintShowChannelSKUs,
Cmd.ARG_CHATMEMBER: doPrintShowChatMembers,
Cmd.ARG_CHATSPACE: doPrintShowChatSpaces,
Cmd.ARG_CHROMEAPP: doPrintShowChromeApps,
Cmd.ARG_CHROMEAPPDEVICES: doPrintShowChromeAppDevices,
Cmd.ARG_CHROMEAUES: doPrintShowChromeAues,
Cmd.ARG_CHROMEHISTORY: doPrintShowChromeHistory,
Cmd.ARG_CHROMENEEDSATTN: doPrintShowChromeNeedsAttn,
Cmd.ARG_CHROMEPOLICY: doPrintShowChromePolicies,
Cmd.ARG_CHROMEPROFILE: doPrintShowChromeProfiles,
Cmd.ARG_CHROMESCHEMA: doPrintShowChromeSchemas,
Cmd.ARG_CHROMEVERSIONS: doPrintShowChromeVersions,
Cmd.ARG_CIGROUPMEMBERS: doShowCIGroupMembers,
Cmd.ARG_CIPOLICY: doPrintShowCIPolicies,
Cmd.ARG_CLASSROOMINVITATION: doPrintShowClassroomInvitations,
Cmd.ARG_CONTACT: doPrintShowDomainContacts,
Cmd.ARG_CROSTELEMETRY: doInfoPrintShowCrOSTelemetry,
Cmd.ARG_DATATRANSFER: doPrintShowDataTransfers,
Cmd.ARG_DOMAIN: doPrintShowDomains,
Cmd.ARG_DOMAINALIAS: doPrintShowDomainAliases,
Cmd.ARG_DOMAINCONTACT: doPrintShowDomainContacts,
Cmd.ARG_DRIVEFILEACL: doPrintShowDriveFileACLs,
Cmd.ARG_DRIVELABEL: doPrintShowDriveLabels,
Cmd.ARG_DRIVELABELPERMISSION: doPrintShowDriveLabelPermissions,
Cmd.ARG_FEATURE: doPrintShowFeatures,
Cmd.ARG_GAL: doPrintShowGAL,
Cmd.ARG_GROUPMEMBERS: doShowGroupMembers,
Cmd.ARG_GROUPTREE: doPrintShowGroupTree,
Cmd.ARG_GUARDIAN: doPrintShowGuardians,
Cmd.ARG_INBOUNDSSOASSIGNMENT: doPrintShowInboundSSOAssignments,
Cmd.ARG_INBOUNDSSOCREDENTIAL: doPrintShowInboundSSOCredentials,
Cmd.ARG_INBOUNDSSOPROFILE: doPrintShowInboundSSOProfiles,
Cmd.ARG_ORGUNITSHAREDDRIVE: doPrintShowOrgunitSharedDrives,
Cmd.ARG_LICENSE: doShowLicenses,
Cmd.ARG_ORGTREE: doShowOrgTree,
Cmd.ARG_OWNERSHIP: doPrintShowOwnership,
Cmd.ARG_PEOPLECONTACT: doPrintShowDomainPeopleContacts,
Cmd.ARG_PEOPLEPROFILE: doPrintShowDomainPeopleProfiles,
Cmd.ARG_PRINTER: doPrintShowPrinters,
Cmd.ARG_PRINTERMODEL: doPrintShowPrinterModels,
Cmd.ARG_PRIVILEGES: doPrintShowPrivileges,
Cmd.ARG_PROJECT: doPrintShowProjects,
Cmd.ARG_RESOLDSUBSCRIPTION: doPrintShowResoldSubscriptions,
Cmd.ARG_RESOURCE: doPrintShowResourceCalendars,
Cmd.ARG_RESOURCES: doPrintShowResourceCalendars,
Cmd.ARG_SAKEY: doShowSvcAcctKeys,
Cmd.ARG_SCHEMA: doPrintShowUserSchemas,
Cmd.ARG_SHAREDDRIVE: doPrintShowSharedDrives,
Cmd.ARG_SHAREDDRIVEACLS: doPrintShowSharedDriveACLs,
Cmd.ARG_SHAREDDRIVEINFO: doInfoSharedDrive,
Cmd.ARG_SHAREDDRIVETHEMES: doShowSharedDriveThemes,
Cmd.ARG_SITE: deprecatedDomainSites,
Cmd.ARG_SITEACL: deprecatedDomainSites,
Cmd.ARG_SVCACCT: doPrintShowSvcAccts,
Cmd.ARG_TOKEN: doPrintShowTokens,
Cmd.ARG_TRANSFERAPPS: doShowTransferApps,
Cmd.ARG_USERINVITATION: doPrintShowCIUserInvitations,
Cmd.ARG_VAULTEXPORT: doPrintShowVaultExports,
Cmd.ARG_VAULTHOLD: doPrintShowVaultHolds,
Cmd.ARG_VAULTMATTER: doPrintShowVaultMatters,
Cmd.ARG_VAULTQUERY: doPrintShowVaultQueries,
}
),
'suspend':
(Act.SUSPEND,
{Cmd.ARG_USER: doSuspendUnsuspendUser,
Cmd.ARG_USERS: doSuspendUnsuspendUsers,
}
),
'sync':
(Act.SYNC,
{Cmd.ARG_DEVICE: doSyncCIDevices,
Cmd.ARG_SHAREDDRIVEACLS: copySyncSharedDriveACLs,
}
),
'unhide':
(Act.UNHIDE,
{Cmd.ARG_SHAREDDRIVE: doHideUnhideSharedDrive,
}
),
'update':
(Act.UPDATE,
{Cmd.ARG_ADMINROLE: doCreateUpdateAdminRoles,
Cmd.ARG_ALIAS: doCreateUpdateAliases,
Cmd.ARG_BROWSER: doUpdateBrowsers,
Cmd.ARG_BUILDING: doUpdateBuilding,
Cmd.ARG_CAALEVEL: doUpdateCAALevel,
Cmd.ARG_CHATMESSAGE: doUpdateChatMessage,
Cmd.ARG_CHROMEPOLICY: doUpdateChromePolicy,
Cmd.ARG_CIGROUP: doUpdateCIGroups,
Cmd.ARG_CONTACT: doUpdateDomainContacts,
Cmd.ARG_CONTACTPHOTO: doUpdateDomainContactPhoto,
Cmd.ARG_COURSE: doUpdateCourse,
Cmd.ARG_COURSES: doUpdateCourses,
Cmd.ARG_CROS: doUpdateCrOSDevices,
Cmd.ARG_CUSTOMER: doUpdateCustomer,
Cmd.ARG_DEVICE: doUpdateCIDevice,
Cmd.ARG_DEVICEUSER: doUpdateCIDeviceUser,
Cmd.ARG_DEVICEUSERSTATE: doUpdateCIDeviceUserState,
Cmd.ARG_DOMAIN: doUpdateDomain,
Cmd.ARG_DRIVEFILEACL: doUpdateDriveFileACLs,
Cmd.ARG_FEATURE: doUpdateFeature,
Cmd.ARG_GROUP: doUpdateGroups,
Cmd.ARG_INBOUNDSSOASSIGNMENT: doUpdateInboundSSOAssignment,
Cmd.ARG_INBOUNDSSOPROFILE: doUpdateInboundSSOProfile,
Cmd.ARG_MOBILE: doUpdateMobileDevices,
Cmd.ARG_ORG: doUpdateOrg,
Cmd.ARG_ORGS: doUpdateOrgs,
Cmd.ARG_PRINTER: doUpdatePrinter,
Cmd.ARG_PROJECT: doUpdateProject,
Cmd.ARG_RESOLDCUSTOMER: doUpdateResoldCustomer,
Cmd.ARG_RESOLDSUBSCRIPTION: doUpdateResoldSubscription,
Cmd.ARG_RESOURCE: doUpdateResourceCalendar,
Cmd.ARG_RESOURCES: doUpdateResourceCalendars,
Cmd.ARG_SAKEY: doUpdateSvcAcctKeys,
Cmd.ARG_SCHEMA: doCreateUpdateUserSchemas,
Cmd.ARG_SHAREDDRIVE: doUpdateSharedDrive,
Cmd.ARG_SITE: deprecatedDomainSites,
Cmd.ARG_SITEACL: deprecatedDomainSites,
Cmd.ARG_SVCACCT: doCheckUpdateSvcAcct,
Cmd.ARG_USER: doUpdateUser,
Cmd.ARG_USERS: doUpdateUsers,
Cmd.ARG_VAULTHOLD: doUpdateVaultHold,
Cmd.ARG_VAULTMATTER: doUpdateVaultMatter,
Cmd.ARG_VERIFY: doUpdateSiteVerification,
}
),
'undelete':
(Act.UNDELETE,
{Cmd.ARG_ALERT: doDeleteOrUndeleteAlert,
Cmd.ARG_USER: doUndeleteUser,
Cmd.ARG_USERS: doUndeleteUsers,
Cmd.ARG_VAULTMATTER: doUndeleteVaultMatter,
}
),
'unsuspend':
(Act.UNSUSPEND,
{Cmd.ARG_USER: doSuspendUnsuspendUser,
Cmd.ARG_USERS: doSuspendUnsuspendUsers,
}
),
'upload':
(Act.USE,
{Cmd.ARG_SAKEY: doUploadSvcAcctKeys,
}
),
'use':
(Act.USE,
{Cmd.ARG_PROJECT: doUseProject,
}
),
'wipe':
(Act.WIPE,
{Cmd.ARG_DEVICE: doWipeCIDevice,
Cmd.ARG_DEVICEUSER: doWipeCIDeviceUser,
}
),
'yubikey':
(Act.RESET_YUBIKEY_PIV,
{Cmd.ARG_RESETPIV: doResetYubiKeyPIV,
}
),
}
MAIN_COMMANDS_OBJ_ALIASES = {
Cmd.ARG_ADMINS: Cmd.ARG_ADMIN,
Cmd.ARG_ADMINROLES: Cmd.ARG_ADMINROLE,
Cmd.ARG_ALERTFEEDBACKS: Cmd.ARG_ALERTFEEDBACK,
Cmd.ARG_ALERTS: Cmd.ARG_ALERT,
Cmd.ARG_ALERTSFEEDBACK: Cmd.ARG_ALERTFEEDBACK,
Cmd.ARG_ALIASDOMAIN: Cmd.ARG_DOMAINALIAS,
Cmd.ARG_ALIASDOMAINS: Cmd.ARG_DOMAINALIAS,
Cmd.ARG_ALIASES: Cmd.ARG_ALIAS,
Cmd.ARG_APIS: Cmd.ARG_API,
Cmd.ARG_APIPROJECT: Cmd.ARG_PROJECT,
Cmd.ARG_APPDETAILS: Cmd.ARG_CHROMEAPP,
Cmd.ARG_BROWSERS: Cmd.ARG_BROWSER,
Cmd.ARG_BROWSERTOKENS: Cmd.ARG_BROWSERTOKEN,
Cmd.ARG_BUCKET: Cmd.ARG_STORAGEBUCKET,
Cmd.ARG_BUCKETS: Cmd.ARG_STORAGEBUCKET,
Cmd.ARG_BUILDINGS: Cmd.ARG_BUILDING,
Cmd.ARG_CAALEVELS: Cmd.ARG_CAALEVEL,
Cmd.ARG_CHATMEMBERS: Cmd.ARG_CHATMEMBER,
Cmd.ARG_CHANNELCUSTOMERS: Cmd.ARG_CHANNELCUSTOMER,
Cmd.ARG_CHANNELCUSTOMERENTITLEMENTS: Cmd.ARG_CHANNELCUSTOMERENTITLEMENT,
Cmd.ARG_CHANNELOFFERS: Cmd.ARG_CHANNELOFFER,
Cmd.ARG_CHANNELPRODUCTS: Cmd.ARG_CHANNELPRODUCT,
Cmd.ARG_CHANNELSKUS: Cmd.ARG_CHANNELSKU,
Cmd.ARG_CHATSPACES: Cmd.ARG_CHATSPACE,
Cmd.ARG_CHROMEAPPS: Cmd.ARG_CHROMEAPP,
Cmd.ARG_CHROMENETWORKS: Cmd.ARG_CHROMENETWORK,
Cmd.ARG_CHROMEPOLICIES: Cmd.ARG_CHROMEPOLICY,
Cmd.ARG_CHROMEPROFILES: Cmd.ARG_CHROMEPROFILE,
Cmd.ARG_CHROMESCHEMAS: Cmd.ARG_CHROMESCHEMA,
Cmd.ARG_CIGROUPS: Cmd.ARG_CIGROUP,
Cmd.ARG_CIGROUPSMEMBERS: Cmd.ARG_CIGROUPMEMBERS,
Cmd.ARG_CIMEMBER: Cmd.ARG_CIGROUPMEMBERS,
Cmd.ARG_CIMEMBERS: Cmd.ARG_CIGROUPMEMBERS,
Cmd.ARG_CIPOLICIES: Cmd.ARG_CIPOLICY,
Cmd.ARG_CLASSIFICATIONLABEL: Cmd.ARG_DRIVELABEL,
Cmd.ARG_CLASSIFICATIONLABELS: Cmd.ARG_DRIVELABEL,
Cmd.ARG_CLASSIFICATIONLABELPERMISSION: Cmd.ARG_DRIVELABELPERMISSION,
Cmd.ARG_CLASSIFICATIONLABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
Cmd.ARG_CLASS: Cmd.ARG_COURSE,
Cmd.ARG_CLASSES: Cmd.ARG_COURSES,
Cmd.ARG_CLASSPARTICIPANTS: Cmd.ARG_COURSEPARTICIPANTS,
Cmd.ARG_CLASSROOMINVITATIONS: Cmd.ARG_CLASSROOMINVITATION,
Cmd.ARG_CONTACTS: Cmd.ARG_CONTACT,
Cmd.ARG_CONTACTPHOTOS: Cmd.ARG_CONTACTPHOTO,
Cmd.ARG_CROSES: Cmd.ARG_CROS,
Cmd.ARG_DATATRANSFERS: Cmd.ARG_DATATRANSFER,
Cmd.ARG_DEVICES: Cmd.ARG_DEVICE,
Cmd.ARG_DEVICEFILES: Cmd.ARG_DEVICEFILE,
Cmd.ARG_DEVICEUSERS: Cmd.ARG_DEVICEUSER,
Cmd.ARG_DOMAINS: Cmd.ARG_DOMAIN,
Cmd.ARG_DOMAINALIASES: Cmd.ARG_DOMAINALIAS,
Cmd.ARG_DOMAINCONTACT: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_DOMAINCONTACTS: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_DOMAINPROFILES: Cmd.ARG_PEOPLEPROFILE,
Cmd.ARG_DRIVEFILEACLS: Cmd.ARG_DRIVEFILEACL,
Cmd.ARG_DRIVELABELS: Cmd.ARG_DRIVELABEL,
Cmd.ARG_DRIVELABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
Cmd.ARG_EXPORT: Cmd.ARG_VAULTEXPORT,
Cmd.ARG_EXPORTS: Cmd.ARG_VAULTEXPORT,
Cmd.ARG_FEATURES: Cmd.ARG_FEATURE,
Cmd.ARG_FORMS: Cmd.ARG_FORM,
Cmd.ARG_GROUPS: Cmd.ARG_GROUP,
Cmd.ARG_GROUPSMEMBERS: Cmd.ARG_GROUPMEMBERS,
Cmd.ARG_GUARDIANINVITATIONS: Cmd.ARG_GUARDIANINVITATION,
Cmd.ARG_GUARDIANINVITE: Cmd.ARG_GUARDIANINVITATION,
Cmd.ARG_GUARDIANS: Cmd.ARG_GUARDIAN,
Cmd.ARG_HOLD: Cmd.ARG_VAULTHOLD,
Cmd.ARG_HOLDS: Cmd.ARG_VAULTHOLD,
Cmd.ARG_INBOUNDSSOASSIGNMENTS: Cmd.ARG_INBOUNDSSOASSIGNMENT,
Cmd.ARG_INBOUNDSSOCREDENTIALS: Cmd.ARG_INBOUNDSSOCREDENTIAL,
Cmd.ARG_INBOUNDSSOPROFILES: Cmd.ARG_INBOUNDSSOPROFILE,
Cmd.ARG_INVITEGUARDIAN: Cmd.ARG_GUARDIANINVITATION,
Cmd.ARG_LICENCE: Cmd.ARG_LICENSE,
Cmd.ARG_LICENCES: Cmd.ARG_LICENSE,
Cmd.ARG_LICENSES: Cmd.ARG_LICENSE,
Cmd.ARG_MATTER: Cmd.ARG_VAULTMATTER,
Cmd.ARG_MATTERS: Cmd.ARG_VAULTMATTER,
Cmd.ARG_MEMBER: Cmd.ARG_GROUPMEMBERS,
Cmd.ARG_MEMBERS: Cmd.ARG_GROUPMEMBERS,
Cmd.ARG_MOBILES: Cmd.ARG_MOBILE,
Cmd.ARG_NICKNAME: Cmd.ARG_ALIAS,
Cmd.ARG_NICKNAMES: Cmd.ARG_ALIAS,
Cmd.ARG_ORGUNIT: Cmd.ARG_ORG,
Cmd.ARG_ORGUNITS: Cmd.ARG_ORGS,
Cmd.ARG_ORGUNITSHAREDDRIVES: Cmd.ARG_ORGUNITSHAREDDRIVE,
Cmd.ARG_OU: Cmd.ARG_ORG,
Cmd.ARG_OUS: Cmd.ARG_ORGS,
Cmd.ARG_OUSHAREDDRIVE: Cmd.ARG_ORGUNITSHAREDDRIVE,
Cmd.ARG_OUSHAREDDRIVES: Cmd.ARG_ORGUNITSHAREDDRIVE,
Cmd.ARG_OUTREE: Cmd.ARG_ORGTREE,
Cmd.ARG_PARTICIPANTS: Cmd.ARG_COURSEPARTICIPANTS,
Cmd.ARG_PEOPLE: Cmd.ARG_PEOPLEPROFILE,
Cmd.ARG_PEOPLECONTACTS: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_PEOPLEPROFILES: Cmd.ARG_PEOPLEPROFILE,
Cmd.ARG_PERMISSIONS: Cmd.ARG_PERMISSION,
Cmd.ARG_PRINTERS: Cmd.ARG_PRINTER,
Cmd.ARG_PRINTERMODELS: Cmd.ARG_PRINTERMODEL,
Cmd.ARG_PROJECTS: Cmd.ARG_PROJECT,
Cmd.ARG_RESELLERCUSTOMERS: Cmd.ARG_RESOLDCUSTOMER,
Cmd.ARG_RESELLERSUBSCRIPTIONS: Cmd.ARG_RESOLDSUBSCRIPTION,
Cmd.ARG_RESOLDCUSTOMERS: Cmd.ARG_RESOLDCUSTOMER,
Cmd.ARG_RESOLDSUBSCRIPTIONS: Cmd.ARG_RESOLDSUBSCRIPTION,
Cmd.ARG_ROLE: Cmd.ARG_ADMINROLE,
Cmd.ARG_ROLES: Cmd.ARG_ADMINROLE,
Cmd.ARG_SAKEYS: Cmd.ARG_SAKEY,
Cmd.ARG_SCHEMAS: Cmd.ARG_SCHEMA,
Cmd.ARG_SHAREDDRIVES: Cmd.ARG_SHAREDDRIVE,
Cmd.ARG_SIGNJWTSERVICEACCOUNT: Cmd.ARG_GCPSERVICEACCOUNT,
Cmd.ARG_SITEACLS: Cmd.ARG_SITEACL,
Cmd.ARG_SITES: Cmd.ARG_SITE,
Cmd.ARG_STORAGEBUCKETS: Cmd.ARG_STORAGEBUCKET,
Cmd.ARG_STORAGEFILES: Cmd.ARG_STORAGEFILE,
Cmd.ARG_SVCACCTS: Cmd.ARG_SVCACCT,
Cmd.ARG_TEAMDRIVE: Cmd.ARG_SHAREDDRIVE,
Cmd.ARG_TEAMDRIVES: Cmd.ARG_SHAREDDRIVE,
Cmd.ARG_TEAMDRIVEACLS: Cmd.ARG_SHAREDDRIVEACLS,
Cmd.ARG_TEAMDRIVEINFO: Cmd.ARG_SHAREDDRIVEINFO,
Cmd.ARG_TEAMDRIVETHEMES: Cmd.ARG_SHAREDDRIVETHEMES,
Cmd.ARG_TOKENS: Cmd.ARG_TOKEN,
Cmd.ARG_TRANSFER: Cmd.ARG_DATATRANSFER,
Cmd.ARG_TRANSFERS: Cmd.ARG_DATATRANSFER,
Cmd.ARG_USERINVITATIONS: Cmd.ARG_USERINVITATION,
Cmd.ARG_VAULTCOUNTS: Cmd.ARG_VAULTCOUNT,
Cmd.ARG_VAULTEXPORTS: Cmd.ARG_VAULTEXPORT,
Cmd.ARG_VAULTHOLDS: Cmd.ARG_VAULTHOLD,
Cmd.ARG_VAULTQUERIES: Cmd.ARG_VAULTQUERY,
Cmd.ARG_VAULTMATTERS: Cmd.ARG_VAULTMATTER,
Cmd.ARG_VERIFICATION: Cmd.ARG_VERIFY,
}
# Audit command sub-commands with objects
AUDIT_SUBCOMMANDS_WITH_OBJECTS = {
'monitor':
{'create': (Act.CREATE, doCreateMonitor),
'delete': (Act.DELETE, doDeleteMonitor),
'list': (Act.LIST, doShowMonitors),
},
}
def processAuditCommands():
CL_subCommand = getChoice(list(AUDIT_SUBCOMMANDS_WITH_OBJECTS))
CL_objectName = getChoice(AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand])
Act.Set(AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CL_objectName][CMD_ACTION])
AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CL_objectName][CMD_FUNCTION]()
# Oauth command sub-commands
OAUTH2_SUBCOMMANDS = {
'create': (Act.CREATE, doOAuthCreate),
'delete': (Act.DELETE, doOAuthDelete),
'export': (Act.EXPORT, doOAuthExport),
'info': (Act.INFO, doOAuthInfo),
'refresh': (Act.REFRESH, doOAuthRefresh),
'update': (Act.UPDATE, doOAuthUpdate),
}
# Oauth sub-command aliases
OAUTH2_SUBCOMMAND_ALIASES = {
'request': 'create',
'revoke': 'delete',
'verify': 'info',
}
def processOauthCommands():
CL_subCommand = getChoice(OAUTH2_SUBCOMMANDS, choiceAliases=OAUTH2_SUBCOMMAND_ALIASES)
Act.Set(OAUTH2_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
if GC.Values[GC.ENABLE_DASA]:
systemErrorExit(USAGE_ERROR_RC, Msg.COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA.format('oauth', CL_subCommand))
OAUTH2_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION]()
# Calendar command sub-commands
CALENDAR_SUBCOMMANDS = {
'showacl': (Act.SHOW, doCalendarsPrintShowACLs),
'printacl': (Act.PRINT, doCalendarsPrintShowACLs),
'addevent': (Act.ADD, doCalendarsCreateEvent),
'deleteevent': (Act.DELETE, doCalendarsDeleteEventsOld),
'moveevent': (Act.MOVE, doCalendarsMoveEventsOld),
'updateevent': (Act.UPDATE, doCalendarsUpdateEventsOld),
'printevents': (Act.PRINT, doCalendarsPrintShowEvents),
'wipe': (Act.WIPE, doCalendarsWipeEvents),
'modify': (Act.MODIFY, doCalendarsModifySettings),
}
CALENDAR_OLDACL_SUBCOMMANDS = {
'add': (Act.ADD, doCalendarsCreateACL),
'create': (Act.CREATE, doCalendarsCreateACL),
'delete': (Act.DELETE, doCalendarsDeleteACL),
'update': (Act.UPDATE, doCalendarsUpdateACL),
}
# Calendar sub-command aliases
CALENDAR_OLDACL_SUBCOMMAND_ALIASES = {
'del': 'delete',
}
# Calendars command sub-commands with objects
CALENDARS_SUBCOMMANDS_WITH_OBJECTS = {
'add':
(Act.ADD,
{Cmd.ARG_CALENDARACL: doCalendarsCreateACLs,
Cmd.ARG_EVENT: doCalendarsCreateEvent,
}
),
'create':
(Act.CREATE,
{Cmd.ARG_CALENDARACL: doCalendarsCreateACLs,
Cmd.ARG_EVENT: doCalendarsCreateEvent,
}
),
'delete':
(Act.DELETE,
{Cmd.ARG_CALENDARACL: doCalendarsDeleteACLs,
Cmd.ARG_EVENT: doCalendarsDeleteEvents,
}
),
'empty':
(Act.EMPTY,
{Cmd.ARG_CALENDARTRASH: doCalendarsEmptyTrash,
}
),
'import':
(Act.IMPORT,
{Cmd.ARG_EVENT: doCalendarsImportEvent,
}
),
'info':
(Act.INFO,
{Cmd.ARG_CALENDARACL: doCalendarsInfoACLs,
Cmd.ARG_EVENT: doCalendarsInfoEvents,
}
),
'move':
(Act.MOVE,
{Cmd.ARG_EVENT: doCalendarsMoveEvents,
}
),
'print':
(Act.PRINT,
{Cmd.ARG_CALENDARACL: doCalendarsPrintShowACLs,
Cmd.ARG_EVENT: doCalendarsPrintShowEvents,
Cmd.ARG_SETTINGS: doCalendarsPrintShowSettings,
}
),
'purge':
(Act.PURGE,
{Cmd.ARG_EVENT: doCalendarsPurgeEvents,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_CALENDARACL: doCalendarsPrintShowACLs,
Cmd.ARG_EVENT: doCalendarsPrintShowEvents,
Cmd.ARG_SETTINGS: doCalendarsPrintShowSettings,
}
),
'update':
(Act.UPDATE,
{Cmd.ARG_CALENDARACL: doCalendarsUpdateACLs,
Cmd.ARG_EVENT: doCalendarsUpdateEvents,
}
),
'wipe':
(Act.WIPE,
{Cmd.ARG_EVENT: doCalendarsWipeEvents,
}
),
}
CALENDARS_SUBCOMMANDS_OBJECT_ALIASES = {
Cmd.ARG_ACL: Cmd.ARG_CALENDARACL,
Cmd.ARG_ACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_EVENTS: Cmd.ARG_EVENT,
}
def processCalendarsCommands():
calendarList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
CL_subCommand = getChoice(CALENDAR_SUBCOMMANDS, defaultChoice=None)
if CL_subCommand:
Act.Set(CALENDAR_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
CALENDAR_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](calendarList)
return
CL_subCommand = getChoice(CALENDAR_OLDACL_SUBCOMMANDS, choiceAliases=CALENDAR_OLDACL_SUBCOMMAND_ALIASES, defaultChoice=None)
if CL_subCommand:
Act.Set(CALENDAR_OLDACL_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
CL_objectName = getChoice([Cmd.ARG_CALENDARACL, Cmd.ARG_EVENT], choiceAliases=CALENDARS_SUBCOMMANDS_OBJECT_ALIASES, defaultChoice=None)
if not CL_objectName:
CALENDAR_OLDACL_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](calendarList)
else:
CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](calendarList)
return
CL_subCommand = getChoice(CALENDARS_SUBCOMMANDS_WITH_OBJECTS)
Act.Set(CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_ACTION])
CL_objectName = getChoice(CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION], choiceAliases=CALENDARS_SUBCOMMANDS_OBJECT_ALIASES)
CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](calendarList)
# Course command sub-commands
COURSE_SUBCOMMANDS = {
'add': (Act.ADD, doCourseAddItems),
'clear': (Act.REMOVE, doCourseClearParticipants),
'remove': (Act.REMOVE, doCourseRemoveItems),
'sync': (Act.SYNC, doCourseSyncParticipants),
}
# Course sub-command aliases
COURSE_SUBCOMMAND_ALIASES = {
'create': 'add',
'del': 'remove',
'delete': 'remove',
}
def executeCourseCommands(courseIdList, getEntityListArg):
CL_subCommand = getChoice(COURSE_SUBCOMMANDS, choiceAliases=COURSE_SUBCOMMAND_ALIASES)
Act.Set(COURSE_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
COURSE_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](courseIdList, getEntityListArg)
def processCourseCommands():
executeCourseCommands(getStringReturnInList(Cmd.OB_COURSE_ID), False)
def processCoursesCommands():
executeCourseCommands(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True), True)
# Resource command sub-commands
RESOURCE_SUBCOMMANDS_WITH_OBJECTS = {
'add':
(Act.ADD,
{Cmd.ARG_CALENDARACL: doResourceCreateCalendarACLs,
}
),
'create':
(Act.CREATE,
{Cmd.ARG_CALENDARACL: doResourceCreateCalendarACLs,
}
),
'update':
(Act.UPDATE,
{Cmd.ARG_CALENDARACL: doResourceUpdateCalendarACLs,
}
),
'delete':
(Act.DELETE,
{Cmd.ARG_CALENDARACL: doResourceDeleteCalendarACLs,
}
),
'info':
(Act.INFO,
{Cmd.ARG_CALENDARACL: doResourceInfoCalendarACLs,
}
),
'print':
(Act.PRINT,
{Cmd.ARG_CALENDARACL: doResourcePrintShowCalendarACLs,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_CALENDARACL: doResourcePrintShowCalendarACLs,
}
),
}
# Resource sub-command aliases
RESOURCE_SUBCOMMAND_ALIASES = {
'del': 'delete',
}
RESOURCE_SUBCOMMANDS_OBJECT_ALIASES = {
Cmd.ARG_ACL: Cmd.ARG_CALENDARACL,
Cmd.ARG_ACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
}
def executeResourceCommands(resourceEntity):
CL_subCommand = getChoice(RESOURCE_SUBCOMMANDS_WITH_OBJECTS, choiceAliases=RESOURCE_SUBCOMMAND_ALIASES)
Act.Set(RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_ACTION])
CL_objectName = getChoice(RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION], choiceAliases=RESOURCE_SUBCOMMANDS_OBJECT_ALIASES)
RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](resourceEntity)
def processResourceCommands():
executeResourceCommands(getStringReturnInList(Cmd.OB_RESOURCE_ID))
def processResourcesCommands():
executeResourceCommands(getEntityList(Cmd.OB_RESOURCE_ENTITY))
# Commands
COMMANDS_MAP = {
'oauth': processOauthCommands,
'audit': processAuditCommands,
'calendars': processCalendarsCommands,
'course': processCourseCommands,
'courses': processCoursesCommands,
'resource': processResourceCommands,
'resources': processResourcesCommands,
}
# Commands aliases
COMMANDS_ALIASES = {
'oauth2': 'oauth',
'calendar': 'calendars',
}
# <CrOSTypeEntity> commands
CROS_COMMANDS = {
'getcommand': (Act.GET_COMMAND_RESULT, getCommandResultCrOSDevices),
'info': (Act.INFO, infoCrOSDevices),
'issuecommand': (Act.ISSUE_COMMAND, issueCommandCrOSDevices),
'list': (Act.LIST, doListCrOS),
'print': (Act.PRINT, doPrintCrOSEntity),
'update': (Act.UPDATE, updateCrOSDevices),
}
CROS_COMMANDS_WITH_OBJECTS = {
'get':
(Act.DOWNLOAD,
{Cmd.ARG_DEVICEFILE: getCrOSDeviceFiles,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_COUNT: showCountCrOS,
}
),
}
CROS_COMMANDS_OBJ_ALIASES = {
Cmd.ARG_DEVICEFILES: Cmd.ARG_DEVICEFILE,
Cmd.ARG_COUNTS: Cmd.ARG_COUNT,
}
# <UserTypeEntity> commands
USER_COMMANDS = {
'delegate': (Act.ADD, delegateTo),
'deprovision': (Act.DEPROVISION, deprovisionUser),
'draftemail': (Act.DRAFT, draftMessage),
'filter': (Act.ADD, createFilter),
'forward': (Act.SET, setForward),
'imap': (Act.SET, setImap),
'importemail': (Act.IMPORT, importMessage),
'insertemail': (Act.INSERT, insertMessage),
'label': (Act.ADD, createLabel),
'list': (Act.LIST, doListUser),
'language': (Act.SET, setLanguage),
'pop': (Act.SET, setPop),
'profile': (Act.SET, setProfile),
'sendas': (Act.ADD, createUpdateSendAs),
'sendemail': (Act.SENDEMAIL, doSendEmail),
'signature': (Act.SET, setSignature),
'signout': (Act.SIGNOUT, signoutTurnoff2SVUsers),
'turnoff2sv': (Act.TURNOFF2SV, signoutTurnoff2SVUsers),
'vacation': (Act.SET, setVacation),
'waitformailbox': (Act.WAITFORMAILBOX, waitForMailbox),
}
# User commands with objects
#
USER_ADD_CREATE_FUNCTIONS = {
Cmd.ARG_CALENDAR: addCreateCalendars,
Cmd.ARG_GROUP: addUserToGroups,
Cmd.ARG_CALENDARACL: createCalendarACLs,
Cmd.ARG_CHATMEMBER: createChatMember,
Cmd.ARG_CHATMESSAGE: createChatMessage,
Cmd.ARG_CHATSPACE: createChatSpace,
Cmd.ARG_CLASSROOMINVITATION: createClassroomInvitations,
Cmd.ARG_CONTACTDELEGATE: processContactDelegates,
Cmd.ARG_CSEIDENTITY: createUpdateCSEIdentity,
Cmd.ARG_CSEKEYPAIR: createCSEKeyPair,
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,
Cmd.ARG_DELEGATE: processDelegates,
Cmd.ARG_DRIVEFILE: createDriveFile,
Cmd.ARG_DRIVEFILEACL: createDriveFileACL,
Cmd.ARG_DRIVEFILESHORTCUT: createDriveFileShortcut,
Cmd.ARG_DRIVEFOLDERPATH: createDriveFolderPath,
Cmd.ARG_DRIVELABELPERMISSION: createDriveLabelPermissions,
Cmd.ARG_EVENT: createCalendarEvent,
Cmd.ARG_FILTER: createFilter,
Cmd.ARG_FOCUSTIME: createFocusTime,
Cmd.ARG_FORM: createForm,
Cmd.ARG_FORWARDINGADDRESS: createForwardingAddresses,
Cmd.ARG_GUARDIAN: inviteGuardians,
Cmd.ARG_GUARDIANINVITATION: inviteGuardians,
Cmd.ARG_LABEL: createLabel,
Cmd.ARG_LABELLIST: createLabelList,
Cmd.ARG_LICENSE: createLicense,
Cmd.ARG_MEETSPACE: createMeetSpace,
Cmd.ARG_NOTE: createNote,
Cmd.ARG_NOTEACL: createNotesACLs,
Cmd.ARG_OUTOFOFFICE: createOutOfOffice,
Cmd.ARG_PEOPLECONTACT: createUserPeopleContact,
Cmd.ARG_PEOPLECONTACTGROUP: createUserPeopleContactGroup,
Cmd.ARG_PERMISSION: createDriveFilePermissions,
Cmd.ARG_SENDAS: createUpdateSendAs,
Cmd.ARG_SHAREDDRIVE: createSharedDrive,
Cmd.ARG_SHEET: createSheet,
Cmd.ARG_SITE: deprecatedUserSites,
Cmd.ARG_SITEACL: deprecatedUserSites,
Cmd.ARG_SMIME: createSmime,
Cmd.ARG_TASK: processTasks,
Cmd.ARG_TASKLIST: processTasklists,
Cmd.ARG_WORKINGLOCATION: createWorkingLocation,
}
USER_COMMANDS_WITH_OBJECTS = {
'accept':
(Act.ACCEPT,
{Cmd.ARG_CLASSROOMINVITATION: acceptClassroomInvitations,
}
),
'add':
(Act.ADD,
USER_ADD_CREATE_FUNCTIONS
),
'append':
(Act.APPEND,
{Cmd.ARG_SHEETRANGE: appendSheetRanges,
}
),
'archive':
(Act.ARCHIVE,
{Cmd.ARG_MESSAGE: archiveMessages,
}
),
'cancel':
(Act.CANCEL,
{Cmd.ARG_GUARDIANINVITATION: cancelGuardianInvitations,
}
),
'check':
(Act.CHECK,
{Cmd.ARG_DRIVEFILESHORTCUT: checkDriveFileShortcut,
Cmd.ARG_GROUP: checkUserInGroups,
Cmd.ARG_ISINVITABLE: checkCIUserIsInvitable,
Cmd.ARG_SERVICEACCOUNT: checkServiceAccount,
}
),
'claim':
(Act.CLAIM,
{Cmd.ARG_OWNERSHIP: claimOwnership,
}
),
'clear':
(Act.CLEAR,
{Cmd.ARG_GUARDIAN: clearGuardians,
Cmd.ARG_PEOPLECONTACT: clearUserPeopleContacts,
Cmd.ARG_SHEETRANGE: clearSheetRanges,
Cmd.ARG_TASKLIST: processTasklists,
}
),
'collect':
(Act.COLLECT,
{Cmd.ARG_ORPHANS: collectOrphans,
}
),
'copy':
(Act.COPY,
{Cmd.ARG_DRIVEFILE: copyDriveFile,
Cmd.ARG_OTHERCONTACT: copyUserPeopleOtherContacts,
Cmd.ARG_SHAREDDRIVEACLS: copySyncSharedDriveACLs,
}
),
'create':
(Act.CREATE,
USER_ADD_CREATE_FUNCTIONS
),
'dedup':
(Act.DEDUP,
{Cmd.ARG_PEOPLECONTACT: dedupReplaceDomainUserPeopleContacts,
}
),
'delete':
(Act.DELETE,
{Cmd.ARG_ALIAS: deleteUsersAliases,
Cmd.ARG_ASP: deleteASP,
Cmd.ARG_BACKUPCODE: deleteBackupCodes,
Cmd.ARG_CALENDAR: deleteCalendars,
Cmd.ARG_CALENDARACL: deleteCalendarACLs,
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_CHATMESSAGE: deleteChatMessage,
Cmd.ARG_CHATSPACE: deleteChatSpace,
Cmd.ARG_CLASSROOMINVITATION: deleteClassroomInvitations,
Cmd.ARG_CONTACTDELEGATE: processContactDelegates,
Cmd.ARG_CSEIDENTITY: processCSEIdentity,
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,
Cmd.ARG_DELEGATE: processDelegates,
Cmd.ARG_DRIVEFILE: deleteDriveFile,
Cmd.ARG_DRIVEFILEACL: deleteDriveFileACLs,
Cmd.ARG_DRIVELABELPERMISSION: deleteDriveLabelPermissions,
Cmd.ARG_EMPTYDRIVEFOLDERS: deleteEmptyDriveFolders,
Cmd.ARG_EVENT: deleteCalendarEvents,
Cmd.ARG_FILEREVISION: deleteFileRevisions,
Cmd.ARG_FILTER: deleteFilters,
Cmd.ARG_FOCUSTIME: deleteFocusTime,
Cmd.ARG_FORWARDINGADDRESS: deleteForwardingAddresses,
Cmd.ARG_GROUP: deleteUserFromGroups,
Cmd.ARG_GUARDIAN: deleteGuardians,
Cmd.ARG_LABEL: deleteLabel,
Cmd.ARG_LABELLIST: deleteLabelList,
Cmd.ARG_LABELID: deleteLabelId,
Cmd.ARG_LABELIDLIST: deleteLabelIdList,
Cmd.ARG_LICENSE: deleteLicense,
Cmd.ARG_MESSAGE: processMessages,
Cmd.ARG_NOTE: deleteInfoNotes,
Cmd.ARG_NOTEACL: deleteNotesACLs,
Cmd.ARG_OUTOFOFFICE: deleteOutOfOffice,
Cmd.ARG_OTHERCONTACT: processUserPeopleOtherContacts,
Cmd.ARG_PEOPLECONTACT: deleteUserPeopleContacts,
Cmd.ARG_PEOPLECONTACTGROUP: deleteUserPeopleContactGroups,
Cmd.ARG_PEOPLECONTACTPHOTO: deleteUserPeopleContactPhoto,
Cmd.ARG_PERMISSION: deletePermissions,
Cmd.ARG_PHOTO: deletePhoto,
Cmd.ARG_SENDAS: deleteInfoSendAs,
Cmd.ARG_SMIME: deleteSmime,
Cmd.ARG_SHAREDDRIVE: deleteSharedDrive,
Cmd.ARG_SITEACL: deprecatedUserSites,
Cmd.ARG_TASK: processTasks,
Cmd.ARG_TASKLIST: processTasklists,
Cmd.ARG_THREAD: processThreads,
Cmd.ARG_TOKEN: deleteTokens,
Cmd.ARG_USER: deleteUsers,
Cmd.ARG_WORKINGLOCATION: deleteWorkingLocation,
}
),
'disable':
(Act.DISABLE,
{Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
}
),
'draft':
(Act.DRAFT,
{Cmd.ARG_MESSAGE: draftMessage,
}
),
'empty':
(Act.EMPTY,
{Cmd.ARG_CALENDARTRASH: emptyCalendarTrash,
Cmd.ARG_DRIVETRASH: emptyDriveTrash,
}
),
'enable':
(Act.ENABLE,
{Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
}
),
'end':
(Act.END,
{Cmd.ARG_MEETCONFERENCE: endMeetConference,
}
),
'export':
(Act.EXPORT,
{Cmd.ARG_MESSAGE: exportMessages,
Cmd.ARG_THREAD: exportThreads,
}
),
'get':
(Act.DOWNLOAD,
{Cmd.ARG_DOCUMENT: getGoogleDocument,
Cmd.ARG_DRIVEFILE: getDriveFile,
Cmd.ARG_NOTEATTACHMENT: getNoteAttachments,
Cmd.ARG_PEOPLECONTACTPHOTO: getUserPeopleContactPhoto,
Cmd.ARG_PHOTO: getUserPhoto,
Cmd.ARG_PROFILE_PHOTO: getProfilePhoto,
}
),
'hide':
(Act.HIDE,
{Cmd.ARG_SHAREDDRIVE: hideUnhideSharedDrive,
}
),
'import':
(Act.IMPORT,
{Cmd.ARG_EVENT: importCalendarEvent,
Cmd.ARG_MESSAGE: importMessage,
Cmd.ARG_TASKLIST: importTasklist,
}
),
'info':
(Act.INFO,
{Cmd.ARG_CALENDAR: infoCalendars,
Cmd.ARG_CALENDARACL: infoCalendarACLs,
Cmd.ARG_CHATEVENT: infoChatEvent,
Cmd.ARG_CHATMEMBER: infoChatMember,
Cmd.ARG_CHATMESSAGE: infoChatMessage,
Cmd.ARG_CHATSPACE: infoChatSpace,
Cmd.ARG_CHATSPACEDM: infoChatSpaceDM,
Cmd.ARG_CIGROUPMEMBERS: infoCIGroupMembers,
Cmd.ARG_CSEIDENTITY: processCSEIdentity,
Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
Cmd.ARG_DRIVEFILE: showFileInfo,
Cmd.ARG_DRIVEFILEACL: infoDriveFileACLs,
Cmd.ARG_DRIVELABEL: infoDriveLabels,
Cmd.ARG_EVENT: infoCalendarEvents,
Cmd.ARG_FILTER: infoFilters,
Cmd.ARG_FORWARDINGADDRESS: infoForwardingAddresses,
Cmd.ARG_GROUPMEMBERS: infoGroupMembers,
Cmd.ARG_MEETSPACE: infoMeetSpace,
Cmd.ARG_NOTE: deleteInfoNotes,
Cmd.ARG_PEOPLECONTACT: infoUserPeopleContacts,
Cmd.ARG_PEOPLECONTACTGROUP: infoUserPeopleContactGroups,
Cmd.ARG_SENDAS: deleteInfoSendAs,
Cmd.ARG_SHAREDDRIVE: infoSharedDrive,
Cmd.ARG_SHEET: infoPrintShowSheets,
Cmd.ARG_SITE: deprecatedUserSites,
Cmd.ARG_SITEACL: deprecatedUserSites,
Cmd.ARG_TASK: processTasks,
Cmd.ARG_TASKLIST: processTasklists,
Cmd.ARG_USER: infoUsers,
}
),
'insert':
(Act.INSERT,
{Cmd.ARG_MESSAGE: insertMessage,
}
),
'modify':
(Act.MODIFY,
{Cmd.ARG_CALENDAR: modifyCalendars,
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_MESSAGE: processMessages,
Cmd.ARG_THREAD: processThreads,
}
),
'move':
(Act.MOVE,
{Cmd.ARG_DRIVEFILE: moveDriveFile,
Cmd.ARG_EVENT: moveCalendarEvents,
Cmd.ARG_OTHERCONTACT: processUserPeopleOtherContacts,
Cmd.ARG_TASK: processTasks,
}
),
'obliterate':
(Act.OBLITERATE,
{Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
}
),
'purge':
(Act.PURGE,
{Cmd.ARG_DRIVEFILE: purgeDriveFile,
Cmd.ARG_EVENT: purgeCalendarEvents,
}
),
'print':
(Act.PRINT,
{Cmd.ARG_ANALYTICACCOUNT: printShowAnalyticAccounts,
Cmd.ARG_ANALYTICACCOUNTSUMMARY: printShowAnalyticAccountSummaries,
Cmd.ARG_ANALYTICDATASTREAM: printShowAnalyticDatastreams,
Cmd.ARG_ANALYTICPROPERTY: printShowAnalyticProperties,
Cmd.ARG_ANALYTICUAPROPERTY: printShowAnalyticUAProperties,
Cmd.ARG_ASP: printShowASPs,
Cmd.ARG_BACKUPCODE: printShowBackupCodes,
Cmd.ARG_CALENDAR: printShowCalendars,
Cmd.ARG_CALENDARACL: printShowCalendarACLs,
Cmd.ARG_CALSETTINGS: printShowCalSettings,
Cmd.ARG_CHATEVENT: printShowChatEvents,
Cmd.ARG_CHATMEMBER: printShowChatMembers,
Cmd.ARG_CHATMESSAGE: printShowChatMessages,
Cmd.ARG_CHATSPACE: printShowChatSpaces,
Cmd.ARG_CLASSROOMINVITATION: printShowClassroomInvitations,
Cmd.ARG_CLASSROOMPROFILE: printShowClassroomProfile,
Cmd.ARG_CONTACTDELEGATE: printShowContactDelegates,
Cmd.ARG_CSEIDENTITY: printShowCSEIdentities,
Cmd.ARG_CSEKEYPAIR: printShowCSEKeyPairs,
Cmd.ARG_LOOKERSTUDIOASSET: printShowLookerStudioAssets,
Cmd.ARG_LOOKERSTUDIOPERMISSION: printShowLookerStudioPermissions,
Cmd.ARG_DELEGATE: printShowDelegates,
Cmd.ARG_DISKUSAGE: printDiskUsage,
Cmd.ARG_DRIVEACTIVITY: printDriveActivity,
Cmd.ARG_DRIVEFILEACL: printShowDriveFileACLs,
Cmd.ARG_DRIVELABEL: printShowDriveLabels,
Cmd.ARG_DRIVELABELPERMISSION: printShowDriveLabelPermissions,
Cmd.ARG_DRIVELASTMODIFICATION: printShowDrivelastModifications,
Cmd.ARG_DRIVESETTINGS: printShowDriveSettings,
Cmd.ARG_EMPTYDRIVEFOLDERS: printEmptyDriveFolders,
Cmd.ARG_EVENT: printShowCalendarEvents,
Cmd.ARG_FILECOMMENT: printShowFileComments,
Cmd.ARG_FILECOUNT: printShowFileCounts,
Cmd.ARG_FILEINFO: showFileInfo,
Cmd.ARG_FILELIST: printFileList,
Cmd.ARG_FILEPATH: printShowFilePaths,
Cmd.ARG_FILEREVISION: printShowFileRevisions,
Cmd.ARG_FILESHARECOUNT: printShowFileShareCounts,
Cmd.ARG_FILETREE: printShowFileTree,
Cmd.ARG_FILTER: printShowFilters,
Cmd.ARG_FOCUSTIME: printShowFocusTime,
Cmd.ARG_FORM: printShowForms,
Cmd.ARG_FORMRESPONSE: printShowFormResponses,
Cmd.ARG_FORWARD: printShowForward,
Cmd.ARG_FORWARDINGADDRESS: printShowForwardingAddresses,
Cmd.ARG_GMAILPROFILE: printShowGmailProfile,
Cmd.ARG_GROUP: printShowUserGroups,
Cmd.ARG_GROUPSLIST: printUserGroupsList,
Cmd.ARG_GROUPTREE: printShowGroupTree,
Cmd.ARG_GUARDIAN: printShowGuardians,
Cmd.ARG_IMAP: printShowImap,
Cmd.ARG_LABEL: printShowLabels,
Cmd.ARG_LANGUAGE: printShowLanguage,
Cmd.ARG_MEETCONFERENCE: printShowMeetConferences,
Cmd.ARG_MEETPARTICIPANT: printShowMeetParticipants,
Cmd.ARG_MEETRECORDING: printShowMeetRecordings,
Cmd.ARG_MEETTRANSCRIPT: printShowMeetTranscripts,
Cmd.ARG_MESSAGE: printShowMessages,
Cmd.ARG_NOTE: printShowNotes,
Cmd.ARG_OTHERCONTACT: printShowUserPeopleOtherContacts,
Cmd.ARG_OUTOFOFFICE: printShowOutOfOffice,
Cmd.ARG_FILEPARENTTREE: printFileParentTree,
Cmd.ARG_PEOPLECONTACT: printShowUserPeopleContacts,
Cmd.ARG_PEOPLECONTACTGROUP: printShowUserPeopleContactGroups,
Cmd.ARG_PEOPLEPROFILE: printShowUserPeopleProfiles,
Cmd.ARG_POP: printShowPop,
Cmd.ARG_SENDAS: printShowSendAs,
Cmd.ARG_SHAREDDRIVE: printShowSharedDrives,
Cmd.ARG_SHAREDDRIVEACLS: printShowSharedDriveACLs,
Cmd.ARG_SHEET: infoPrintShowSheets,
Cmd.ARG_SHEETRANGE: printShowSheetRanges,
Cmd.ARG_SIGNATURE: printShowSignature,
Cmd.ARG_SMIME: printShowSmimes,
Cmd.ARG_SITE: deprecatedUserSites,
Cmd.ARG_SITEACL: deprecatedUserSites,
Cmd.ARG_SITEACTIVITY: deprecatedUserSites,
Cmd.ARG_TASK: printShowTasks,
Cmd.ARG_TASKLIST: printShowTasklists,
Cmd.ARG_THREAD: printShowThreads,
Cmd.ARG_TOKEN: printShowTokens,
Cmd.ARG_USER: doPrintUserEntity,
Cmd.ARG_USERLIST: doPrintUserList,
Cmd.ARG_VACATION: printShowVacation,
Cmd.ARG_VAULTHOLD: printShowUserVaultHolds,
Cmd.ARG_WORKINGLOCATION: printShowWorkingLocation,
Cmd.ARG_YOUTUBECHANNEL: printShowYouTubeChannel,
}
),
'process':
(Act.PROCESS,
{Cmd.ARG_FILEDRIVELABEL: processFileDriveLabels,
}
),
'remove':
(Act.REMOVE,
{Cmd.ARG_CALENDAR: removeCalendars,
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_DRIVELABELPERMISSION: deleteDriveLabelPermissions,
}
),
'replacedomain':
(Act.REPLACE_DOMAIN,
{Cmd.ARG_PEOPLECONTACT: dedupReplaceDomainUserPeopleContacts,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_ANALYTICACCOUNT: printShowAnalyticAccounts,
Cmd.ARG_ANALYTICACCOUNTSUMMARY: printShowAnalyticAccountSummaries,
Cmd.ARG_ANALYTICDATASTREAM: printShowAnalyticDatastreams,
Cmd.ARG_ANALYTICPROPERTY: printShowAnalyticProperties,
Cmd.ARG_ANALYTICUAPROPERTY: printShowAnalyticUAProperties,
Cmd.ARG_ASP: printShowASPs,
Cmd.ARG_BACKUPCODE: printShowBackupCodes,
Cmd.ARG_CALENDAR: printShowCalendars,
Cmd.ARG_CALENDARACL: printShowCalendarACLs,
Cmd.ARG_CALSETTINGS: printShowCalSettings,
Cmd.ARG_CHATEVENT: printShowChatEvents,
Cmd.ARG_CHATMEMBER: printShowChatMembers,
Cmd.ARG_CHATMESSAGE: printShowChatMessages,
Cmd.ARG_CHATSPACE: printShowChatSpaces,
Cmd.ARG_CLASSROOMINVITATION: printShowClassroomInvitations,
Cmd.ARG_CLASSROOMPROFILE: printShowClassroomProfile,
Cmd.ARG_CONTACTDELEGATE: printShowContactDelegates,
Cmd.ARG_COUNT: showCountUser,
Cmd.ARG_CSEIDENTITY: printShowCSEIdentities,
Cmd.ARG_CSEKEYPAIR: printShowCSEKeyPairs,
Cmd.ARG_LOOKERSTUDIOASSET: printShowLookerStudioAssets,
Cmd.ARG_LOOKERSTUDIOPERMISSION: printShowLookerStudioPermissions,
Cmd.ARG_DELEGATE: printShowDelegates,
Cmd.ARG_DISKUSAGE: printDiskUsage,
Cmd.ARG_DRIVEACTIVITY: printDriveActivity,
Cmd.ARG_DRIVEFILEACL: printShowDriveFileACLs,
Cmd.ARG_DRIVELABEL: printShowDriveLabels,
Cmd.ARG_DRIVELABELPERMISSION: printShowDriveLabelPermissions,
Cmd.ARG_DRIVELASTMODIFICATION: printShowDrivelastModifications,
Cmd.ARG_DRIVESETTINGS: printShowDriveSettings,
Cmd.ARG_EVENT: printShowCalendarEvents,
Cmd.ARG_FILECOMMENT: printShowFileComments,
Cmd.ARG_FILECOUNT: printShowFileCounts,
Cmd.ARG_FILEINFO: showFileInfo,
Cmd.ARG_FILELIST: printFileList,
Cmd.ARG_FILEPATH: printShowFilePaths,
Cmd.ARG_FILEREVISION: printShowFileRevisions,
Cmd.ARG_FILESHARECOUNT: printShowFileShareCounts,
Cmd.ARG_FILETREE: printShowFileTree,
Cmd.ARG_FILTER: printShowFilters,
Cmd.ARG_FOCUSTIME: printShowFocusTime,
Cmd.ARG_FORM: printShowForms,
Cmd.ARG_FORMRESPONSE: printShowFormResponses,
Cmd.ARG_FORWARD: printShowForward,
Cmd.ARG_FORWARDINGADDRESS: printShowForwardingAddresses,
Cmd.ARG_GMAILPROFILE: printShowGmailProfile,
Cmd.ARG_GROUP: printShowUserGroups,
Cmd.ARG_GROUPTREE: printShowGroupTree,
Cmd.ARG_GUARDIAN: printShowGuardians,
Cmd.ARG_IMAP: printShowImap,
Cmd.ARG_LABEL: printShowLabels,
Cmd.ARG_LANGUAGE: printShowLanguage,
Cmd.ARG_MEETCONFERENCE: printShowMeetConferences,
Cmd.ARG_MEETPARTICIPANT: printShowMeetParticipants,
Cmd.ARG_MEETRECORDING: printShowMeetRecordings,
Cmd.ARG_MEETTRANSCRIPT: printShowMeetTranscripts,
Cmd.ARG_MESSAGE: printShowMessages,
Cmd.ARG_NOTE: printShowNotes,
Cmd.ARG_OTHERCONTACT: printShowUserPeopleOtherContacts,
Cmd.ARG_OUTOFOFFICE: printShowOutOfOffice,
Cmd.ARG_PEOPLECONTACT: printShowUserPeopleContacts,
Cmd.ARG_PEOPLECONTACTGROUP: printShowUserPeopleContactGroups,
Cmd.ARG_PEOPLEPROFILE: printShowUserPeopleProfiles,
Cmd.ARG_POP: printShowPop,
Cmd.ARG_PROFILE: showProfile,
Cmd.ARG_SENDAS: printShowSendAs,
Cmd.ARG_SHAREDDRIVE: printShowSharedDrives,
Cmd.ARG_SHAREDDRIVEACLS: printShowSharedDriveACLs,
Cmd.ARG_SHAREDDRIVEINFO: infoSharedDrive,
Cmd.ARG_SHAREDDRIVETHEMES: showSharedDriveThemes,
Cmd.ARG_SHEET: infoPrintShowSheets,
Cmd.ARG_SHEETRANGE: printShowSheetRanges,
Cmd.ARG_SIGNATURE: printShowSignature,
Cmd.ARG_SITE: deprecatedUserSites,
Cmd.ARG_SITEACL: deprecatedUserSites,
Cmd.ARG_SMIME: printShowSmimes,
Cmd.ARG_TASK: printShowTasks,
Cmd.ARG_TASKLIST: printShowTasklists,
Cmd.ARG_THREAD: printShowThreads,
Cmd.ARG_TOKEN: printShowTokens,
Cmd.ARG_VAULTHOLD: printShowUserVaultHolds,
Cmd.ARG_VACATION: printShowVacation,
Cmd.ARG_WORKINGLOCATION: printShowWorkingLocation,
Cmd.ARG_YOUTUBECHANNEL: printShowYouTubeChannel,
}
),
'spam':
(Act.SPAM,
{Cmd.ARG_MESSAGE: processMessages,
Cmd.ARG_THREAD: processThreads,
}
),
'suspend':
(Act.SUSPEND,
{Cmd.ARG_USER: suspendUnsuspendUsers,
}
),
'sync':
(Act.SYNC,
{Cmd.ARG_CHATMEMBER: syncChatMembers,
Cmd.ARG_GROUP: syncUserWithGroups,
Cmd.ARG_GUARDIAN: syncGuardians,
Cmd.ARG_LICENSE: syncLicense,
Cmd.ARG_SHAREDDRIVEACLS: copySyncSharedDriveACLs,
}
),
'transfer':
(Act.TRANSFER,
{Cmd.ARG_DRIVE: transferDrive,
Cmd.ARG_CALENDAR: transferCalendars,
Cmd.ARG_OWNERSHIP: transferOwnership,
}
),
'trash':
(Act.TRASH,
{Cmd.ARG_DRIVEFILE: trashDriveFile,
Cmd.ARG_MESSAGE: processMessages,
Cmd.ARG_THREAD: processThreads,
}
),
'undelete':
(Act.UNDELETE,
{Cmd.ARG_USER: undeleteUsers,
}
),
'unhide':
(Act.UNHIDE,
{Cmd.ARG_SHAREDDRIVE: hideUnhideSharedDrive,
}
),
'unsuspend':
(Act.UNSUSPEND,
{Cmd.ARG_USER: suspendUnsuspendUsers,
}
),
'untrash':
(Act.UNTRASH,
{Cmd.ARG_DRIVEFILE: untrashDriveFile,
Cmd.ARG_MESSAGE: processMessages,
Cmd.ARG_THREAD: processThreads,
}
),
'update':
(Act.UPDATE,
{Cmd.ARG_BACKUPCODE: updateBackupCodes,
Cmd.ARG_CALATTENDEES: updateCalendarAttendees,
Cmd.ARG_CALENDAR: updateCalendars,
Cmd.ARG_CALENDARACL: updateCalendarACLs,
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
Cmd.ARG_CHATMESSAGE: updateChatMessage,
Cmd.ARG_CHATSPACE: updateChatSpace,
Cmd.ARG_CSEIDENTITY: createUpdateCSEIdentity,
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,
Cmd.ARG_DELEGATE: updateDelegates,
Cmd.ARG_DOCUMENT: updateGoogleDocument,
Cmd.ARG_DRIVEFILE: updateDriveFile,
Cmd.ARG_DRIVEFILEACL: updateDriveFileACLs,
Cmd.ARG_EVENT: updateCalendarEvents,
Cmd.ARG_FILEREVISION: updateFileRevisions,
Cmd.ARG_FORM: updateForm,
Cmd.ARG_GROUP: updateUserGroups,
Cmd.ARG_LABEL: updateLabels,
Cmd.ARG_LABELID: updateLabelSettingsById,
Cmd.ARG_LABELSETTINGS: updateLabelSettings,
Cmd.ARG_LICENSE: updateLicense,
Cmd.ARG_MEETSPACE: updateMeetSpace,
Cmd.ARG_OTHERCONTACT: processUserPeopleOtherContacts,
Cmd.ARG_PEOPLECONTACT: updateUserPeopleContacts,
Cmd.ARG_PEOPLECONTACTGROUP: updateUserPeopleContactGroup,
Cmd.ARG_PEOPLECONTACTPHOTO: updateUserPeopleContactPhoto,
Cmd.ARG_PHOTO: updatePhoto,
Cmd.ARG_SENDAS: createUpdateSendAs,
Cmd.ARG_SERVICEACCOUNT: checkServiceAccount,
Cmd.ARG_SHAREDDRIVE: updateSharedDrive,
Cmd.ARG_SHEET: updateSheets,
Cmd.ARG_SHEETRANGE: updateSheetRanges,
Cmd.ARG_SMIME: updateSmime,
Cmd.ARG_SITE: deprecatedUserSites,
Cmd.ARG_SITEACL: deprecatedUserSites,
Cmd.ARG_TASK: processTasks,
Cmd.ARG_TASKLIST: processTasklists,
Cmd.ARG_USER: updateUsers,
}
),
'watch':
(Act.WATCH,
{Cmd.ARG_GMAIL: watchGmail,
}
),
'wipe':
(Act.WIPE,
{Cmd.ARG_EVENT: wipeCalendarEvents,
}
),
}
# User commands aliases
USER_COMMANDS_ALIASES = {
'del': 'delete',
'delegates': 'delegate',
'deprov': 'deprovision',
'imap4': 'imap',
'pop3': 'pop',
'sig': 'signature',
'utf': 'unicode',
'utf-8': 'unicode',
'utf8': 'unicode',
}
USER_COMMANDS_OBJ_ALIASES = {
Cmd.ARG_3LO: Cmd.ARG_TOKEN,
Cmd.ARG_ALIASES: Cmd.ARG_ALIAS,
Cmd.ARG_APPLICATIONSPECIFICPASSWORDS: Cmd.ARG_ASP,
Cmd.ARG_ANALYTICACCOUNTS: Cmd.ARG_ANALYTICACCOUNT,
Cmd.ARG_ANALYTICACCOUNTSUMMARIES: Cmd.ARG_ANALYTICACCOUNTSUMMARY,
Cmd.ARG_ANALYTICDATASTREAMS: Cmd.ARG_ANALYTICDATASTREAM,
Cmd.ARG_ANALYTICPROPERTIES: Cmd.ARG_ANALYTICPROPERTY,
Cmd.ARG_ANALYTICUAPROPERTIES: Cmd.ARG_ANALYTICUAPROPERTY,
Cmd.ARG_ASPS: Cmd.ARG_ASP,
Cmd.ARG_BACKUPCODES: Cmd.ARG_BACKUPCODE,
Cmd.ARG_CALENDARS: Cmd.ARG_CALENDAR,
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_CLASSIFICATIONLABEL: Cmd.ARG_DRIVELABEL,
Cmd.ARG_CLASSIFICATIONLABELS: Cmd.ARG_DRIVELABEL,
Cmd.ARG_CLASSIFICATIONLABELPERMISSION: Cmd.ARG_DRIVELABELPERMISSION,
Cmd.ARG_CLASSIFICATIONLABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
Cmd.ARG_CLASSROOMINVITATIONS: Cmd.ARG_CLASSROOMINVITATION,
Cmd.ARG_CHATEVENTS: Cmd.ARG_CHATEVENT,
Cmd.ARG_CHATMEMBERS: Cmd.ARG_CHATMEMBER,
Cmd.ARG_CHATMESSAGES: Cmd.ARG_CHATMESSAGE,
Cmd.ARG_CHATSPACES: Cmd.ARG_CHATSPACE,
Cmd.ARG_CIMEMBER: Cmd.ARG_CIGROUPMEMBERS,
Cmd.ARG_CIMEMBERS: Cmd.ARG_CIGROUPMEMBERS,
Cmd.ARG_CONTACT: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_CONTACTS: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_CONTACTDELEGATES: Cmd.ARG_CONTACTDELEGATE,
Cmd.ARG_CONTACTGROUP: Cmd.ARG_PEOPLECONTACTGROUP,
Cmd.ARG_CONTACTGROUPS: Cmd.ARG_PEOPLECONTACTGROUP,
Cmd.ARG_CONTACTPHOTO: Cmd.ARG_PEOPLECONTACTPHOTO,
Cmd.ARG_CONTACTPHOTOS: Cmd.ARG_PEOPLECONTACTPHOTO,
Cmd.ARG_CSEIDENTITIES: Cmd.ARG_CSEIDENTITY,
Cmd.ARG_CSEKEYPAIRS: Cmd.ARG_CSEKEYPAIR,
Cmd.ARG_COUNTS: Cmd.ARG_COUNT,
Cmd.ARG_DATASTUDIOASSET: Cmd.ARG_LOOKERSTUDIOASSET,
Cmd.ARG_DATASTUDIOPERMISSION: Cmd.ARG_LOOKERSTUDIOPERMISSION,
Cmd.ARG_DATASTUDIOASSETS: Cmd.ARG_LOOKERSTUDIOASSET,
Cmd.ARG_DATASTUDIOPERMISSIONS: Cmd.ARG_LOOKERSTUDIOPERMISSION,
Cmd.ARG_LOOKERSTUDIOASSETS: Cmd.ARG_LOOKERSTUDIOASSET,
Cmd.ARG_LOOKERSTUDIOPERMISSIONS: Cmd.ARG_LOOKERSTUDIOPERMISSION,
Cmd.ARG_DELEGATES: Cmd.ARG_DELEGATE,
Cmd.ARG_DOMAINCONTACT: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_DOMAINCONTACTS: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_DRIVEFILEACLS: Cmd.ARG_DRIVEFILEACL,
Cmd.ARG_DRIVEFILESHORTCUTS: Cmd.ARG_DRIVEFILESHORTCUT,
Cmd.ARG_DRIVELABELS: Cmd.ARG_DRIVELABEL,
Cmd.ARG_DRIVELABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
Cmd.ARG_DRIVELASTMODIFICATIONS: Cmd.ARG_DRIVELASTMODIFICATION,
Cmd.ARG_EVENTS: Cmd.ARG_EVENT,
Cmd.ARG_FILECOMMENTS: Cmd.ARG_FILECOMMENT,
Cmd.ARG_FILECOUNTS: Cmd.ARG_FILECOUNT,
Cmd.ARG_FILEDRIVELABELS: Cmd.ARG_FILEDRIVELABEL,
Cmd.ARG_FILEPATHS: Cmd.ARG_FILEPATH,
Cmd.ARG_FILEREVISIONS: Cmd.ARG_FILEREVISION,
Cmd.ARG_FILESHARECOUNTS: Cmd.ARG_FILESHARECOUNT,
Cmd.ARG_FILTERS: Cmd.ARG_FILTER,
Cmd.ARG_FOCUSTIMES: Cmd.ARG_FOCUSTIME,
Cmd.ARG_FORMS: Cmd.ARG_FORM,
Cmd.ARG_FORMRESPONSES: Cmd.ARG_FORMRESPONSE,
Cmd.ARG_FORWARDS: Cmd.ARG_FORWARD,
Cmd.ARG_FORWARDINGADDRESSES: Cmd.ARG_FORWARDINGADDRESS,
Cmd.ARG_GROUPS: Cmd.ARG_GROUP,
Cmd.ARG_GROUPLIST: Cmd.ARG_GROUPSLIST,
Cmd.ARG_GROUPSMEMBERS: Cmd.ARG_GROUPMEMBERS,
Cmd.ARG_GUARDIANINVITATIONS: Cmd.ARG_GUARDIANINVITATION,
Cmd.ARG_GUARDIANINVITE: Cmd.ARG_GUARDIANINVITATION,
Cmd.ARG_GUARDIANS: Cmd.ARG_GUARDIAN,
Cmd.ARG_HOLD: Cmd.ARG_VAULTHOLD,
Cmd.ARG_HOLDS: Cmd.ARG_VAULTHOLD,
Cmd.ARG_IMAP4: Cmd.ARG_IMAP,
Cmd.ARG_INVITEGUARDIAN: Cmd.ARG_GUARDIANINVITATION,
Cmd.ARG_LABELS: Cmd.ARG_LABEL,
Cmd.ARG_LICENCE: Cmd.ARG_LICENSE,
Cmd.ARG_LICENCES: Cmd.ARG_LICENSE,
Cmd.ARG_LICENSES: Cmd.ARG_LICENSE,
Cmd.ARG_MEETCONFERENCES: Cmd.ARG_MEETCONFERENCE,
Cmd.ARG_MEETPARTICIPANTS: Cmd.ARG_MEETPARTICIPANT,
Cmd.ARG_MEETRECORDINGS: Cmd.ARG_MEETRECORDING,
Cmd.ARG_MEETTRANSCRIPTS: Cmd.ARG_MEETTRANSCRIPT,
Cmd.ARG_MEETSPACES: Cmd.ARG_MEETSPACE,
Cmd.ARG_MEMBER: Cmd.ARG_GROUPMEMBERS,
Cmd.ARG_MEMBERS: Cmd.ARG_GROUPMEMBERS,
Cmd.ARG_MESSAGES: Cmd.ARG_MESSAGE,
Cmd.ARG_NOTES: Cmd.ARG_NOTE,
Cmd.ARG_NOTEACLS: Cmd.ARG_NOTEACL,
Cmd.ARG_NOTESACL: Cmd.ARG_NOTEACL,
Cmd.ARG_NOTESACLS: Cmd.ARG_NOTEACL,
Cmd.ARG_NOTEATTACHMENTS: Cmd.ARG_NOTEATTACHMENT,
Cmd.ARG_OAUTH: Cmd.ARG_TOKEN,
Cmd.ARG_OTHERCONTACTS: Cmd.ARG_OTHERCONTACT,
Cmd.ARG_OUTOFOFFICES: Cmd.ARG_OUTOFOFFICE,
Cmd.ARG_PEOPLECONTACTS: Cmd.ARG_PEOPLECONTACT,
Cmd.ARG_PEOPLECONTACTGROUPS: Cmd.ARG_PEOPLECONTACTGROUP,
Cmd.ARG_PEOPLECONTACTPHOTOS: Cmd.ARG_PEOPLECONTACTPHOTO,
Cmd.ARG_PEOPLE: Cmd.ARG_PEOPLEPROFILE,
Cmd.ARG_PEOPLEPROFILES: Cmd.ARG_PEOPLEPROFILE,
Cmd.ARG_PERMISSIONS: Cmd.ARG_PERMISSION,
Cmd.ARG_POP3: Cmd.ARG_POP,
Cmd.ARG_SECCALS: Cmd.ARG_CALENDAR,
Cmd.ARG_SHAREDDRIVES: Cmd.ARG_SHAREDDRIVE,
Cmd.ARG_SHEETS: Cmd.ARG_SHEET,
Cmd.ARG_SHEETRANGES: Cmd.ARG_SHEETRANGE,
Cmd.ARG_SIG: Cmd.ARG_SIGNATURE,
Cmd.ARG_SITES: Cmd.ARG_SITE,
Cmd.ARG_SITEACLS: Cmd.ARG_SITEACL,
Cmd.ARG_SMIMES: Cmd.ARG_SMIME,
Cmd.ARG_TASKS: Cmd.ARG_TASK,
Cmd.ARG_TASKLISTS: Cmd.ARG_TASKLIST,
Cmd.ARG_TEAMDRIVE: Cmd.ARG_SHAREDDRIVE,
Cmd.ARG_TEAMDRIVES: Cmd.ARG_SHAREDDRIVE,
Cmd.ARG_TEAMDRIVEACLS: Cmd.ARG_SHAREDDRIVEACLS,
Cmd.ARG_TEAMDRIVEINFO: Cmd.ARG_SHAREDDRIVEINFO,
Cmd.ARG_TEAMDRIVETHEMES: Cmd.ARG_SHAREDDRIVETHEMES,
Cmd.ARG_THREADS: Cmd.ARG_THREAD,
Cmd.ARG_TOKENS: Cmd.ARG_TOKEN,
Cmd.ARG_USERS: Cmd.ARG_USER,
Cmd.ARG_VAULTHOLDS: Cmd.ARG_VAULTHOLD,
Cmd.ARG_VERIFICATIONCODES: Cmd.ARG_BACKUPCODE,
Cmd.ARG_WORKINGLOCATIONS: Cmd.ARG_WORKINGLOCATION,
Cmd.ARG_YOUTUBECHANNELS: Cmd.ARG_YOUTUBECHANNEL,
}
def showAPICallsRetryData():
if GC.Values.get(GC.SHOW_API_CALLS_RETRY_DATA) and GM.Globals[GM.API_CALLS_RETRY_DATA]:
Ind.Reset()
writeStderr(Msg.API_CALLS_RETRY_DATA)
Ind.Increment()
for k, v in sorted(iter(GM.Globals[GM.API_CALLS_RETRY_DATA].items())):
m, s = divmod(int(v[1]), 60)
h, m = divmod(m, 60)
writeStderr(formatKeyValueList(Ind.Spaces(), [k, f'{v[0]}/{h}:{m:02d}:{s:02d}'], '\n'))
Ind.Decrement()
def adjustRedirectedSTDFilesIfNotMultiprocessing():
def adjustRedirectedSTDFile(stdtype):
rdFd = GM.Globals[stdtype].get(GM.REDIRECT_FD)
rdMultiFd = GM.Globals[stdtype].get(GM.REDIRECT_MULTI_FD)
if rdFd and rdMultiFd and rdFd != rdMultiFd:
try:
rdFd.write(rdMultiFd.getvalue())
rdMultiFd.close()
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = rdFd
if (stdtype == GM.STDOUT) and (GM.Globals.get(GM.SAVED_STDOUT) is not None):
sys.stdout = rdFd
except IOError as e:
systemErrorExit(FILE_ERROR_RC, e)
adjustRedirectedSTDFile(GM.STDOUT)
if GM.Globals[GM.STDERR].get(GM.REDIRECT_NAME) != 'stdout':
adjustRedirectedSTDFile(GM.STDERR)
else:
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
def closeSTDFilesIfNotMultiprocessing(closeSTD):
def closeSTDFile(stdtype, stdfile):
rdFd = GM.Globals[stdtype].get(GM.REDIRECT_FD)
rdMultiFd = GM.Globals[stdtype].get(GM.REDIRECT_MULTI_FD)
if rdFd and rdMultiFd and (rdFd == rdMultiFd) and (rdFd != stdfile):
try:
rdFd.flush()
if closeSTD:
rdFd.close()
except BrokenPipeError:
pass
closeSTDFile(GM.STDOUT, sys.stdout)
if GM.Globals[GM.STDERR].get(GM.REDIRECT_NAME) != 'stdout':
closeSTDFile(GM.STDERR, sys.stderr)
# Process GAM command
def ProcessGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=True):
setSysExitRC(0)
Cmd.InitializeArguments(args)
Ind.Reset()
try:
logCmd = Cmd.QuotedArgumentList(args)
if checkArgumentPresent(Cmd.LOOP_CMD):
if processGamCfg and (not SetGlobalVariables()):
sys.exit(GM.Globals[GM.SYSEXITRC])
doLoop(logCmd)
sys.exit(GM.Globals[GM.SYSEXITRC])
if processGamCfg and (not SetGlobalVariables()):
sys.exit(GM.Globals[GM.SYSEXITRC])
if checkArgumentPresent(Cmd.LOOP_CMD):
doLoop(logCmd)
sys.exit(GM.Globals[GM.SYSEXITRC])
if not Cmd.ArgumentsRemaining():
doUsage()
sys.exit(GM.Globals[GM.SYSEXITRC])
CL_command = getChoice(BATCH_CSV_COMMANDS, defaultChoice=None)
if CL_command:
Act.Set(BATCH_CSV_COMMANDS[CL_command][CMD_ACTION])
if GM.Globals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(GM.Globals, logCmd, '*')
BATCH_CSV_COMMANDS[CL_command][CMD_FUNCTION]()
sys.exit(GM.Globals[GM.SYSEXITRC])
CL_command = getChoice(MAIN_COMMANDS, defaultChoice=None)
if CL_command:
adjustRedirectedSTDFilesIfNotMultiprocessing()
Act.Set(MAIN_COMMANDS[CL_command][CMD_ACTION])
MAIN_COMMANDS[CL_command][CMD_FUNCTION]()
sys.exit(GM.Globals[GM.SYSEXITRC])
CL_command = getChoice(MAIN_COMMANDS_WITH_OBJECTS, defaultChoice=None)
if CL_command:
adjustRedirectedSTDFilesIfNotMultiprocessing()
Act.Set(MAIN_COMMANDS_WITH_OBJECTS[CL_command][CMD_ACTION])
CL_objectName = getChoice(MAIN_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION], choiceAliases=MAIN_COMMANDS_OBJ_ALIASES)
MAIN_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION][CL_objectName]()
sys.exit(GM.Globals[GM.SYSEXITRC])
CL_command = getChoice(COMMANDS_MAP, choiceAliases=COMMANDS_ALIASES, defaultChoice=None)
if CL_command:
adjustRedirectedSTDFilesIfNotMultiprocessing()
COMMANDS_MAP[CL_command]()
sys.exit(GM.Globals[GM.SYSEXITRC])
GM.Globals[GM.ENTITY_CL_START] = Cmd.Location()
entityType, entityList = getEntityToModify(crosAllowed=True, delayGet=True)
if entityType == Cmd.ENTITY_USERS:
CL_command = getChoice(list(USER_COMMANDS)+list(USER_COMMANDS_WITH_OBJECTS), choiceAliases=USER_COMMANDS_ALIASES)
if (CL_command != 'list') and (GC.Values[GC.AUTO_BATCH_MIN] > 0):
_, count, entityList = getEntityArgument(entityList)
if count > GC.Values[GC.AUTO_BATCH_MIN]:
if GM.Globals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(GM.Globals, logCmd, '*')
doAutoBatch(Cmd.ENTITY_USER, entityList, CL_command)
sys.exit(GM.Globals[GM.SYSEXITRC])
adjustRedirectedSTDFilesIfNotMultiprocessing()
if CL_command in USER_COMMANDS:
Act.Set(USER_COMMANDS[CL_command][CMD_ACTION])
USER_COMMANDS[CL_command][CMD_FUNCTION](entityList)
else:
Act.Set(USER_COMMANDS_WITH_OBJECTS[CL_command][CMD_ACTION])
CL_objectName = getChoice(USER_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION], choiceAliases=USER_COMMANDS_OBJ_ALIASES,
defaultChoice=[Cmd.ARG_USER, NO_DEFAULT][CL_command != 'print'])
USER_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION][CL_objectName](entityList)
else:
CL_command = getChoice(list(CROS_COMMANDS)+list(CROS_COMMANDS_WITH_OBJECTS))
if (CL_command != 'list') and (GC.Values[GC.AUTO_BATCH_MIN] > 0):
_, count, entityList = getEntityArgument(entityList)
if count > GC.Values[GC.AUTO_BATCH_MIN]:
if GM.Globals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(GM.Globals, logCmd, '*')
doAutoBatch(Cmd.ENTITY_CROS, entityList, CL_command)
sys.exit(GM.Globals[GM.SYSEXITRC])
adjustRedirectedSTDFilesIfNotMultiprocessing()
if CL_command in CROS_COMMANDS:
Act.Set(CROS_COMMANDS[CL_command][CMD_ACTION])
CROS_COMMANDS[CL_command][CMD_FUNCTION](entityList)
else:
Act.Set(CROS_COMMANDS_WITH_OBJECTS[CL_command][CMD_ACTION])
CL_objectName = getChoice(CROS_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION], choiceAliases=CROS_COMMANDS_OBJ_ALIASES,
defaultChoice=NO_DEFAULT)
CROS_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION][CL_objectName](entityList)
sys.exit(GM.Globals[GM.SYSEXITRC])
except KeyboardInterrupt:
batchWriteStderr('\nControl-C\n')
setSysExitRC(KEYBOARD_INTERRUPT_RC)
showAPICallsRetryData()
adjustRedirectedSTDFilesIfNotMultiprocessing()
except OSError as e:
printErrorMessage(SOCKET_ERROR_RC, str(e))
showAPICallsRetryData()
adjustRedirectedSTDFilesIfNotMultiprocessing()
except MemoryError:
printErrorMessage(MEMORY_ERROR_RC, Msg.GAM_OUT_OF_MEMORY)
showAPICallsRetryData()
adjustRedirectedSTDFilesIfNotMultiprocessing()
except (GAPI.permissionDenied, GAPI.accessNotConfigured):
SvcAcctAPIDisabledExit()
except SystemExit as e:
GM.Globals[GM.SYSEXITRC] = e.code
if GM.Globals[GM.SYSEXITRC] != STDOUT_STDERR_ERROR_RC and not inLoop:
showAPICallsRetryData()
try:
adjustRedirectedSTDFilesIfNotMultiprocessing()
except SystemExit:
pass
except Exception:
print_exc(file=sys.stderr)
setSysExitRC(UNKNOWN_ERROR_RC)
showAPICallsRetryData()
adjustRedirectedSTDFilesIfNotMultiprocessing()
if processGamCfg:
if not inLoop:
if GM.Globals.get(GM.SAVED_STDOUT) is not None:
sys.stdout = GM.Globals[GM.SAVED_STDOUT]
closeSTDFilesIfNotMultiprocessing(closeSTD)
if GM.Globals[GM.PID] == 0 and GM.Globals[GM.CMDLOG_LOGGER]:
writeGAMCommandLog(GM.Globals, logCmd, GM.Globals[GM.SYSEXITRC])
if not inLoop:
closeGAMCommandLog(GM.Globals)
return GM.Globals[GM.SYSEXITRC]
# Process GAM command
def CallGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=False):
return ProcessGAMCommand(args, processGamCfg=processGamCfg, inLoop=inLoop, closeSTD=closeSTD)