gam7 7.20.3__py3-none-any.whl → 7.28.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gam/__init__.py CHANGED
@@ -25,17 +25,15 @@ https://github.com/GAM-team/GAM/wiki
25
25
  """
26
26
 
27
27
  __author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
28
- __version__ = '7.20.03'
28
+ __version__ = '7.28.02'
29
29
  __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
30
30
 
31
31
  #pylint: disable=wrong-import-position
32
32
  import base64
33
- import calendar as calendarlib
34
33
  import codecs
35
34
  import collections
36
35
  import configparser
37
36
  import csv
38
- import datetime
39
37
  from email.charset import add_charset, QP
40
38
  from email.generator import Generator
41
39
  from email.header import decode_header, Header
@@ -51,7 +49,7 @@ from email.policy import SMTP as policySMTP
51
49
  import hashlib
52
50
  from html.entities import name2codepoint
53
51
  from html.parser import HTMLParser
54
- import http.client as http_client
52
+ import http.client
55
53
  import importlib
56
54
  from importlib.metadata import version as lib_version
57
55
  import io
@@ -109,7 +107,7 @@ from cryptography.x509.oid import NameOID
109
107
  if not getattr(sys, 'frozen', False):
110
108
  sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
111
109
 
112
- from dateutil.relativedelta import relativedelta
110
+ import arrow
113
111
 
114
112
  from pathvalidate import sanitize_filename, sanitize_filepath
115
113
 
@@ -120,6 +118,10 @@ from google.auth.jwt import Credentials as JWTCredentials
120
118
  import google.oauth2.service_account
121
119
  import google_auth_oauthlib.flow
122
120
  import google_auth_httplib2
121
+ import googleapiclient
122
+ import googleapiclient.discovery
123
+ import googleapiclient.errors
124
+ import googleapiclient.http
123
125
  import httplib2
124
126
 
125
127
  httplib2.RETRIES = 5
@@ -149,12 +151,6 @@ import gdata.apps.audit
149
151
  import gdata.apps.audit.service
150
152
  import gdata.apps.contacts
151
153
  import gdata.apps.contacts.service
152
- # Import local library, does not include discovery documents
153
- import googleapiclient
154
- import googleapiclient.discovery
155
- import googleapiclient.errors
156
- import googleapiclient.http
157
- from iso8601 import iso8601
158
154
 
159
155
  IS08601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%:z'
160
156
  RFC2822_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S %z'
@@ -163,7 +159,7 @@ def ISOformatTimeStamp(timestamp):
163
159
  return timestamp.isoformat('T', 'seconds')
164
160
 
165
161
  def currentISOformatTimeStamp(timespec='milliseconds'):
166
- return datetime.datetime.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec)
162
+ return arrow.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec)
167
163
 
168
164
  Act = glaction.GamAction()
169
165
  Cmd = glclargs.GamCLArgs()
@@ -213,6 +209,7 @@ ONE_GIGA_10_BYTES = 1000000000
213
209
  ONE_KILO_BYTES = 1024
214
210
  ONE_MEGA_BYTES = 1048576
215
211
  ONE_GIGA_BYTES = 1073741824
212
+ DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
216
213
  SECONDS_PER_MINUTE = 60
217
214
  SECONDS_PER_HOUR = 3600
218
215
  SECONDS_PER_DAY = 86400
@@ -378,6 +375,37 @@ YUBIKEY_VALUE_ERROR_RC = 85
378
375
  YUBIKEY_MULTIPLE_CONNECTED_RC = 86
379
376
  YUBIKEY_NOT_FOUND_RC = 87
380
377
 
378
+ DEBUG_REDACTION_PATTERNS = [
379
+ # Positional patterns that redact sensitive credentials based on their location
380
+ (r'(Bearer\s+)\S+', r'\1*****'), # access tokens and JWTs in auth header
381
+ (r'([?&]refresh_token=)[^&]*', r'\1*****'), # refresh token URL parameter
382
+ (r'([?&]client_secret=)[^&]*', r'\1*****'), # client secret URL parameter
383
+ (r'([?&]key=)[^&]*', r'\1*****'), # API key URL parameter
384
+ (r'([?&]code=)[^&]*', r'\1*****'), # auth code URL parameter
385
+
386
+ # Pattern match patterns that redact sensitive credentials based on known credential pattern
387
+ (r'ya29.[0-9A-Za-z-_]+', '*****'), # Access token
388
+ (r'1%2F%2F[0-9A-Za-z-_]{100}|1%2F%2F[0-9A-Za-z-_]{64}|1%2F%2F[0-9A-Za-z-_]{43}', '*****'), # Refresh token
389
+ (r'4/[0-9A-Za-z-_]+', '*****'), # Auth code
390
+ (r'GOCSPX-[0-9a-zA-Z-_]{28}', '*****'), # Client secret
391
+ (r'AIza[0-9A-Za-z-_]{35}', '*****'), # API key
392
+ (r'eyJ[a-zA-Z0-9\-_]+\.eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]*', '*****'), # JWT
393
+ ]
394
+
395
+ def redactable_debug_print(*args):
396
+ processed_args = []
397
+ for arg in args:
398
+ if arg.startswith('b\''):
399
+ sbytes = arg[2:-1]
400
+ sbytes = bytes(sbytes, 'utf-8')
401
+ arg = sbytes.decode()
402
+ arg = arg.replace('\\r\\n', "\n ")
403
+ if GC.Values[GC.DEBUG_REDACTION]:
404
+ for pattern, replace in DEBUG_REDACTION_PATTERNS:
405
+ arg = re.sub(pattern, replace, arg)
406
+ processed_args.append(arg)
407
+ print(*processed_args)
408
+
381
409
  # Multiprocessing lock
382
410
  mplock = None
383
411
 
@@ -972,14 +1000,14 @@ SUSPENDED_CHOICE_MAP = {'notsuspended': False, 'suspended': True}
972
1000
  def _getIsSuspended(myarg):
973
1001
  if myarg in SUSPENDED_CHOICE_MAP:
974
1002
  return SUSPENDED_CHOICE_MAP[myarg]
975
- return getBoolean()
1003
+ return getBoolean() #issuspended
976
1004
 
977
1005
  ARCHIVED_ARGUMENTS = {'notarchived', 'archived', 'isarchived'}
978
1006
  ARCHIVED_CHOICE_MAP = {'notarchived': False, 'archived': True}
979
1007
  def _getIsArchived(myarg):
980
1008
  if myarg in ARCHIVED_CHOICE_MAP:
981
1009
  return ARCHIVED_CHOICE_MAP[myarg]
982
- return getBoolean()
1010
+ return getBoolean() #isarchived
983
1011
 
984
1012
  def _getOptionalIsSuspendedIsArchived():
985
1013
  isSuspended = isArchived = None
@@ -1552,10 +1580,12 @@ def getOrderBySortOrder(choiceMap, defaultSortOrderChoice='ASCENDING', mapSortOr
1552
1580
  return (getChoice(choiceMap, mapChoice=True),
1553
1581
  getChoice(SORTORDER_CHOICE_MAP, defaultChoice=defaultSortOrderChoice, mapChoice=mapSortOrderChoice))
1554
1582
 
1555
- def orgUnitPathQuery(path, isSuspended):
1583
+ def orgUnitPathQuery(path, isSuspended, isArchived):
1556
1584
  query = "orgUnitPath='{0}'".format(path.replace("'", "\\'")) if path != '/' else ''
1557
1585
  if isSuspended is not None:
1558
1586
  query += f' isSuspended={isSuspended}'
1587
+ if isArchived is not None:
1588
+ query += f' isArchived={isArchived}'
1559
1589
  return query
1560
1590
 
1561
1591
  def makeOrgUnitPathAbsolute(path):
@@ -1843,13 +1873,13 @@ def getStringWithCRsNLsOrFile():
1843
1873
  return (unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=0)), UTF8, False)
1844
1874
 
1845
1875
  def todaysDate():
1846
- return datetime.datetime(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
1847
- tzinfo=GC.Values[GC.TIMEZONE])
1876
+ return arrow.Arrow(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
1877
+ tzinfo=GC.Values[GC.TIMEZONE])
1848
1878
 
1849
1879
  def todaysTime():
1850
- return datetime.datetime(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
1851
- GM.Globals[GM.DATETIME_NOW].hour, GM.Globals[GM.DATETIME_NOW].minute,
1852
- tzinfo=GC.Values[GC.TIMEZONE])
1880
+ return arrow.Arrow(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
1881
+ GM.Globals[GM.DATETIME_NOW].hour, GM.Globals[GM.DATETIME_NOW].minute,
1882
+ tzinfo=GC.Values[GC.TIMEZONE])
1853
1883
 
1854
1884
  def getDelta(argstr, pattern):
1855
1885
  if argstr == 'NOW':
@@ -1862,22 +1892,22 @@ def getDelta(argstr, pattern):
1862
1892
  sign = tg.group(1)
1863
1893
  delta = int(tg.group(2))
1864
1894
  unit = tg.group(3)
1865
- if unit == 'y':
1866
- deltaTime = datetime.timedelta(days=delta*365)
1867
- elif unit == 'w':
1868
- deltaTime = datetime.timedelta(weeks=delta)
1869
- elif unit == 'd':
1870
- deltaTime = datetime.timedelta(days=delta)
1871
- elif unit == 'h':
1872
- deltaTime = datetime.timedelta(hours=delta)
1873
- elif unit == 'm':
1874
- deltaTime = datetime.timedelta(minutes=delta)
1895
+ if sign == '-':
1896
+ delta = -delta
1875
1897
  baseTime = todaysDate()
1876
1898
  if unit in {'h', 'm'}:
1877
- baseTime = baseTime+datetime.timedelta(hours=GM.Globals[GM.DATETIME_NOW].hour, minutes=GM.Globals[GM.DATETIME_NOW].minute)
1878
- if sign == '-':
1879
- return baseTime-deltaTime
1880
- return baseTime+deltaTime
1899
+ baseTime = baseTime.shift(hours=GM.Globals[GM.DATETIME_NOW].hour, minutes=GM.Globals[GM.DATETIME_NOW].minute)
1900
+ if unit == 'y':
1901
+ return baseTime.shift(days=delta*365)
1902
+ if unit == 'w':
1903
+ return baseTime.shift(weeks=delta)
1904
+ if unit == 'd':
1905
+ return baseTime.shift(days=delta)
1906
+ if unit == 'h':
1907
+ return baseTime.shift(hours=delta)
1908
+ if unit == 'm':
1909
+ return baseTime.shift(minutes=delta)
1910
+ return baseTime
1881
1911
 
1882
1912
  DELTA_DATE_PATTERN = re.compile(r'^([+-])(\d+)([dwy])$')
1883
1913
  DELTA_DATE_FORMAT_REQUIRED = '(+|-)<Number>(d|w|y)'
@@ -1916,7 +1946,7 @@ def getYYYYMMDD(minLen=1, returnTimeStamp=False, returnDateTime=False, alternate
1916
1946
  elif argstr == 'NEVER':
1917
1947
  argstr = NEVER_DATE
1918
1948
  try:
1919
- dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
1949
+ dateTime = arrow.Arrow.strptime(argstr, YYYYMMDD_FORMAT)
1920
1950
  Cmd.Advance()
1921
1951
  if returnTimeStamp:
1922
1952
  return time.mktime(dateTime.timetuple())*1000
@@ -1938,7 +1968,7 @@ def getHHMM():
1938
1968
  argstr = Cmd.Current().strip().upper()
1939
1969
  if argstr:
1940
1970
  try:
1941
- datetime.datetime.strptime(argstr, HHMM_FORMAT)
1971
+ arrow.Arrow.strptime(argstr, HHMM_FORMAT)
1942
1972
  Cmd.Advance()
1943
1973
  return argstr
1944
1974
  except ValueError:
@@ -1958,7 +1988,7 @@ def getYYYYMMDD_HHMM():
1958
1988
  argstr = NEVER_DATETIME
1959
1989
  argstr = argstr.replace('T', ' ')
1960
1990
  try:
1961
- datetime.datetime.strptime(argstr, YYYYMMDD_HHMM_FORMAT)
1991
+ arrow.Arrow.strptime(argstr, YYYYMMDD_HHMM_FORMAT)
1962
1992
  Cmd.Advance()
1963
1993
  return argstr
1964
1994
  except ValueError:
@@ -1977,10 +2007,10 @@ def getDateOrDeltaFromNow(returnDateTime=False):
1977
2007
  argstr = 'TODAY'
1978
2008
  argDate = getDeltaDate(argstr)
1979
2009
  elif argstr == 'NEVER':
1980
- argDate = datetime.datetime.strptime(NEVER_DATE, YYYYMMDD_FORMAT)
2010
+ argDate = arrow.Arrow.strptime(NEVER_DATE, YYYYMMDD_FORMAT)
1981
2011
  elif YYYYMMDD_PATTERN.match(argstr):
1982
2012
  try:
1983
- argDate = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
2013
+ argDate = arrow.Arrow.strptime(argstr, YYYYMMDD_FORMAT)
1984
2014
  except ValueError:
1985
2015
  invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
1986
2016
  else:
@@ -1988,12 +2018,12 @@ def getDateOrDeltaFromNow(returnDateTime=False):
1988
2018
  Cmd.Advance()
1989
2019
  if not returnDateTime:
1990
2020
  return argDate.strftime(YYYYMMDD_FORMAT)
1991
- return (datetime.datetime(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE]),
2021
+ return (arrow.Arrow(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE]),
1992
2022
  GC.Values[GC.TIMEZONE], argDate.strftime(YYYYMMDD_FORMAT))
1993
2023
  missingArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
1994
2024
 
1995
2025
  YYYYMMDDTHHMMSS_FORMAT_REQUIRED = 'yyyy-mm-ddThh:mm:ss[.fff](Z|(+|-(hh:mm)))'
1996
- TIMEZONE_FORMAT_REQUIRED = 'Z|(+|-(hh:mm))'
2026
+ TIMEZONE_FORMAT_REQUIRED = 'utc|z|local|(+|-(hh:mm))|<ValidTimezoneName>'
1997
2027
 
1998
2028
  def getTimeOrDeltaFromNow(returnDateTime=False):
1999
2029
  if Cmd.ArgumentsRemaining():
@@ -2005,7 +2035,7 @@ def getTimeOrDeltaFromNow(returnDateTime=False):
2005
2035
  argstr = NEVER_TIME
2006
2036
  elif YYYYMMDD_PATTERN.match(argstr):
2007
2037
  try:
2008
- dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
2038
+ dateTime = arrow.Arrow.strptime(argstr, YYYYMMDD_FORMAT)
2009
2039
  except ValueError:
2010
2040
  invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
2011
2041
  try:
@@ -2013,12 +2043,12 @@ def getTimeOrDeltaFromNow(returnDateTime=False):
2013
2043
  except OverflowError:
2014
2044
  pass
2015
2045
  try:
2016
- fullDateTime, tz = iso8601.parse_date(argstr)
2046
+ fullDateTime = arrow.get(argstr)
2017
2047
  Cmd.Advance()
2018
2048
  if not returnDateTime:
2019
2049
  return argstr.replace(' ', 'T')
2020
- return (fullDateTime, tz, argstr.replace(' ', 'T'))
2021
- except (iso8601.ParseError, OverflowError):
2050
+ return (fullDateTime, fullDateTime.tzinfo, argstr.replace(' ', 'T'))
2051
+ except (arrow.parser.ParserError, OverflowError):
2022
2052
  pass
2023
2053
  invalidArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
2024
2054
  missingArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
@@ -2031,19 +2061,19 @@ def getRowFilterDateOrDeltaFromNow(argstr):
2031
2061
  deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
2032
2062
  if deltaDate is None:
2033
2063
  return (False, DELTA_DATE_FORMAT_REQUIRED)
2034
- argstr = ISOformatTimeStamp(deltaDate.replace(tzinfo=iso8601.UTC))
2064
+ argstr = ISOformatTimeStamp(deltaDate.replace(tzinfo='UTC'))
2035
2065
  elif argstr == 'NEVER' or YYYYMMDD_PATTERN.match(argstr):
2036
2066
  if argstr == 'NEVER':
2037
2067
  argstr = NEVER_DATE
2038
2068
  try:
2039
- dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
2069
+ dateTime = arrow.Arrow.strptime(argstr, YYYYMMDD_FORMAT)
2040
2070
  except ValueError:
2041
2071
  return (False, YYYYMMDD_FORMAT_REQUIRED)
2042
- argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=iso8601.UTC))
2072
+ argstr = ISOformatTimeStamp(dateTime.replace(tzinfo='UTC'))
2043
2073
  try:
2044
- iso8601.parse_date(argstr)
2074
+ arrow.get(argstr)
2045
2075
  return (True, argstr.replace(' ', 'T'))
2046
- except (iso8601.ParseError, OverflowError):
2076
+ except (arrow.parser.ParserError, OverflowError):
2047
2077
  return (False, YYYYMMDD_FORMAT_REQUIRED)
2048
2078
 
2049
2079
  def getRowFilterTimeOrDeltaFromNow(argstr):
@@ -2057,14 +2087,14 @@ def getRowFilterTimeOrDeltaFromNow(argstr):
2057
2087
  argstr = NEVER_TIME
2058
2088
  elif YYYYMMDD_PATTERN.match(argstr):
2059
2089
  try:
2060
- dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
2090
+ dateTime = arrow.Arrow.strptime(argstr, YYYYMMDD_FORMAT)
2061
2091
  except ValueError:
2062
2092
  return (False, YYYYMMDD_FORMAT_REQUIRED)
2063
2093
  argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=GC.Values[GC.TIMEZONE]))
2064
2094
  try:
2065
- iso8601.parse_date(argstr)
2095
+ arrow.get(argstr)
2066
2096
  return (True, argstr.replace(' ', 'T'))
2067
- except (iso8601.ParseError, OverflowError):
2097
+ except (arrow.parser.ParserError, OverflowError):
2068
2098
  return (False, YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
2069
2099
 
2070
2100
  def mapQueryRelativeTimes(query, keywords):
@@ -2097,9 +2127,9 @@ class StartEndTime():
2097
2127
  self.endDateTime, _, self.endTime = self._getValueOrDeltaFromNow(True)
2098
2128
  elif myarg == 'yesterday':
2099
2129
  currDate = todaysDate()
2100
- self.startDateTime = currDate+datetime.timedelta(days=-1)
2130
+ self.startDateTime = currDate.shift(days=-1)
2101
2131
  self.startTime = ISOformatTimeStamp(self.startDateTime)
2102
- self.endDateTime = currDate+datetime.timedelta(seconds=-1)
2132
+ self.endDateTime = currDate.shift(seconds=-1)
2103
2133
  self.endTime = ISOformatTimeStamp(self.endDateTime)
2104
2134
  elif myarg == 'today':
2105
2135
  currDate = todaysDate()
@@ -2114,12 +2144,12 @@ class StartEndTime():
2114
2144
  else:
2115
2145
  firstMonth = getInteger(minVal=1, maxVal=6)
2116
2146
  currDate = todaysDate()
2117
- self.startDateTime = currDate+relativedelta(months=-firstMonth, day=1, hour=0, minute=0, second=0, microsecond=0)
2147
+ self.startDateTime = currDate.replace(day=1, hour=0, minute=0, second=0, microsecond=0).shift(months=-firstMonth)
2118
2148
  self.startTime = ISOformatTimeStamp(self.startDateTime)
2119
2149
  if myarg == 'thismonth':
2120
2150
  self.endDateTime = todaysTime()
2121
2151
  else:
2122
- self.endDateTime = currDate+relativedelta(day=1, hour=23, minute=59, second=59, microsecond=0)+relativedelta(days=-1)
2152
+ self.endDateTime = currDate.replace(day=1, hour=23, minute=59, second=59, microsecond=0).shift(days=-1)
2123
2153
  self.endTime = ISOformatTimeStamp(self.endDateTime)
2124
2154
  if self.startDateTime and self.endDateTime and self.endDateTime < self.startDateTime:
2125
2155
  Cmd.Backup()
@@ -2335,7 +2365,7 @@ def formatLocalTime(dateTimeStr):
2335
2365
  if dateTimeStr in {NEVER_TIME, NEVER_TIME_NOMS}:
2336
2366
  return GC.Values[GC.NEVER_TIME]
2337
2367
  try:
2338
- timestamp, _ = iso8601.parse_date(dateTimeStr)
2368
+ timestamp = arrow.get(dateTimeStr)
2339
2369
  if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
2340
2370
  if GM.Globals[GM.CONVERT_TO_LOCAL_TIME]:
2341
2371
  return ISOformatTimeStamp(timestamp.astimezone(GC.Values[GC.TIMEZONE]))
@@ -2343,27 +2373,27 @@ def formatLocalTime(dateTimeStr):
2343
2373
  if GM.Globals[GM.CONVERT_TO_LOCAL_TIME]:
2344
2374
  return timestamp.astimezone(GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
2345
2375
  return timestamp.strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
2346
- except (iso8601.ParseError, OverflowError):
2376
+ except (arrow.parser.ParserError, OverflowError):
2347
2377
  return dateTimeStr
2348
2378
 
2349
2379
  def formatLocalSecondsTimestamp(timestamp):
2350
2380
  if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
2351
- return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]))
2352
- return datetime.datetime.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
2381
+ return ISOformatTimeStamp(arrow.Arrow.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]))
2382
+ return arrow.Arrow.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
2353
2383
 
2354
2384
  def formatLocalTimestamp(timestamp):
2355
2385
  if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
2356
- return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]))
2357
- return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
2386
+ return ISOformatTimeStamp(arrow.Arrow.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]))
2387
+ return arrow.Arrow.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
2358
2388
 
2359
2389
  def formatLocalTimestampUTC(timestamp):
2360
- return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp)//1000, iso8601.UTC))
2390
+ return ISOformatTimeStamp(arrow.Arrow.fromtimestamp(int(timestamp)//1000, 'UTC'))
2361
2391
 
2362
2392
  def formatLocalDatestamp(timestamp):
2363
2393
  try:
2364
2394
  if not GC.Values[GC.OUTPUT_DATEFORMAT]:
2365
- return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(YYYYMMDD_FORMAT)
2366
- return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_DATEFORMAT])
2395
+ return arrow.Arrow.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(YYYYMMDD_FORMAT)
2396
+ return arrow.Arrow.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_DATEFORMAT])
2367
2397
  except OverflowError:
2368
2398
  return NEVER_DATE
2369
2399
 
@@ -3050,9 +3080,13 @@ def getGDocData(gformat):
3050
3080
  mimeType = GDOC_FORMAT_MIME_TYPES[gformat]
3051
3081
  user = getEmailAddress()
3052
3082
  fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
3053
- user, drive, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity)
3083
+ if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]:
3084
+ _, drive = buildGAPIServiceObject(API.DRIVE3, user)
3085
+ else:
3086
+ drive = buildGAPIObject(API.DRIVE3)
3054
3087
  if not drive:
3055
3088
  sys.exit(GM.Globals[GM.SYSEXITRC])
3089
+ _, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive)
3056
3090
  if jcount == 0:
3057
3091
  getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
3058
3092
  if jcount > 1:
@@ -3106,14 +3140,21 @@ def getGSheetData():
3106
3140
  user = getEmailAddress()
3107
3141
  fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
3108
3142
  sheetEntity = getSheetEntity(False)
3109
- user, drive, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity)
3143
+ if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]:
3144
+ user, drive = buildGAPIServiceObject(API.DRIVE3, user)
3145
+ else:
3146
+ drive = buildGAPIObject(API.DRIVE3)
3110
3147
  if not drive:
3111
3148
  sys.exit(GM.Globals[GM.SYSEXITRC])
3149
+ _, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive)
3112
3150
  if jcount == 0:
3113
3151
  getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
3114
3152
  if jcount > 1:
3115
3153
  getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
3116
- _, sheet = buildGAPIServiceObject(API.SHEETS, user)
3154
+ if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]:
3155
+ _, sheet = buildGAPIServiceObject(API.SHEETS, user)
3156
+ else:
3157
+ sheet = buildGAPIObject(API.SHEETS)
3117
3158
  if not sheet:
3118
3159
  sys.exit(GM.Globals[GM.SYSEXITRC])
3119
3160
  fileId = fileIdEntity['list'][0]
@@ -3706,19 +3747,19 @@ def SetGlobalVariables():
3706
3747
  return stringlist
3707
3748
 
3708
3749
  def _getCfgTimezone(sectionName, itemName):
3709
- value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName).lower())
3710
- if value == 'utc':
3750
+ value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName))
3751
+ if value.lower() in {'utc', 'z'}:
3711
3752
  GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = False
3712
- return iso8601.UTC
3753
+ return arrow.now('utc').tzinfo
3713
3754
  GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = True
3714
- if value == 'local':
3715
- return iso8601.Local
3755
+ if value.lower() == 'local':
3756
+ return arrow.now(value).tzinfo
3716
3757
  try:
3717
- return iso8601.parse_timezone_str(value)
3718
- except (iso8601.ParseError, OverflowError):
3758
+ return arrow.now(value).tzinfo
3759
+ except (arrow.parser.ParserError, OverflowError):
3719
3760
  _printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {TIMEZONE_FORMAT_REQUIRED}')
3720
3761
  GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = False
3721
- return iso8601.UTC
3762
+ return arrow.now('utc').tzinfo
3722
3763
 
3723
3764
  def _getCfgDirectory(sectionName, itemName):
3724
3765
  dirPath = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)))
@@ -3736,10 +3777,7 @@ def SetGlobalVariables():
3736
3777
  if value and not os.path.isabs(value):
3737
3778
  value = os.path.expanduser(os.path.join(_getCfgDirectory(sectionName, GC.CONFIG_DIR), value))
3738
3779
  elif not value and itemName == GC.CACERTS_PEM:
3739
- if hasattr(sys, '_MEIPASS'):
3740
- value = os.path.join(sys._MEIPASS, GC.FN_CACERTS_PEM) #pylint: disable=no-member
3741
- else:
3742
- value = os.path.join(GM.Globals[GM.GAM_PATH], GC.FN_CACERTS_PEM)
3780
+ value = os.path.join(GM.Globals[GM.GAM_PATH], GC.FN_CACERTS_PEM)
3743
3781
  return value
3744
3782
 
3745
3783
  def _readGamCfgFile(config, fileName):
@@ -4047,7 +4085,7 @@ def SetGlobalVariables():
4047
4085
  GC.Values[itemName] = _getCfgDirectory(sectionName, itemName)
4048
4086
  elif varType == GC.TYPE_TIMEZONE:
4049
4087
  GC.Values[itemName] = _getCfgTimezone(sectionName, itemName)
4050
- GM.Globals[GM.DATETIME_NOW] = datetime.datetime.now(GC.Values[GC.TIMEZONE])
4088
+ GM.Globals[GM.DATETIME_NOW] = arrow.now(GC.Values[GC.TIMEZONE])
4051
4089
  # Everything else except row filters
4052
4090
  for itemName, itemEntry in sorted(GC.VAR_INFO.items()):
4053
4091
  varType = itemEntry[GC.VAR_TYPE]
@@ -4117,6 +4155,8 @@ def SetGlobalVariables():
4117
4155
  GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
4118
4156
  # Override httplib2 settings
4119
4157
  httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL]
4158
+ # Use our own print function for http.client so we can redact and cleanup
4159
+ http.client.print = redactable_debug_print
4120
4160
  # Reset global variables if required
4121
4161
  if prevExtraArgsTxt != GC.Values[GC.EXTRA_ARGS]:
4122
4162
  GM.Globals[GM.EXTRA_ARGS_LIST] = [('prettyPrint', GC.Values[GC.DEBUG_LEVEL] > 0)]
@@ -4393,12 +4433,11 @@ _DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
4393
4433
  class signjwtJWTCredentials(google.auth.jwt.Credentials):
4394
4434
  ''' Class used for DASA '''
4395
4435
  def _make_jwt(self):
4396
- now = datetime.datetime.utcnow()
4397
- lifetime = datetime.timedelta(seconds=self._token_lifetime)
4398
- expiry = now + lifetime
4436
+ now = arrow.utcnow()
4437
+ expiry = now.shift(seconds=self._token_lifetime)
4399
4438
  payload = {
4400
- "iat": google.auth._helpers.datetime_to_secs(now),
4401
- "exp": google.auth._helpers.datetime_to_secs(expiry),
4439
+ "iat": now.int_timestamp,
4440
+ "exp": expiry.int_timestamp,
4402
4441
  "iss": self._issuer,
4403
4442
  "sub": self._subject,
4404
4443
  }
@@ -4406,7 +4445,7 @@ class signjwtJWTCredentials(google.auth.jwt.Credentials):
4406
4445
  payload["aud"] = self._audience
4407
4446
  payload.update(self._additional_claims)
4408
4447
  jwt = self._signer.sign(payload)
4409
- return jwt, expiry
4448
+ return jwt, expiry.naive
4410
4449
 
4411
4450
  # Some Workforce Identity Federation endpoints such as GitHub Actions
4412
4451
  # only allow TLS 1.2 as of April 2023.
@@ -4419,15 +4458,14 @@ class signjwtCredentials(google.oauth2.service_account.Credentials):
4419
4458
  ''' Class used for DwD '''
4420
4459
 
4421
4460
  def _make_authorization_grant_assertion(self):
4422
- now = datetime.datetime.utcnow()
4423
- lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
4424
- expiry = now + lifetime
4461
+ now = arrow.utcnow()
4462
+ expiry = now.shift(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
4425
4463
  payload = {
4426
- "iat": google.auth._helpers.datetime_to_secs(now),
4427
- "exp": google.auth._helpers.datetime_to_secs(expiry),
4428
- "iss": self._service_account_email,
4429
- "aud": API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
4430
- "scope": google.auth._helpers.scopes_to_string(self._scopes or ()),
4464
+ "iat": now.int_timestamp,
4465
+ "exp": expiry.int_timestamp,
4466
+ "iss": self._service_account_email,
4467
+ "aud": API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
4468
+ "scope": google.auth._helpers.scopes_to_string(self._scopes or ()),
4431
4469
  }
4432
4470
  payload.update(self._additional_claims)
4433
4471
  # The subject can be a user email for domain-wide delegation.
@@ -4541,7 +4579,7 @@ def getOauth2TxtCredentials(exitOnError=True, api=None, noDASA=False, refreshOnl
4541
4579
  creds.token = jsonDict['access_token']
4542
4580
  creds._id_token = jsonDict['id_token_jwt']
4543
4581
  GM.Globals[GM.DECODED_ID_TOKEN] = jsonDict['id_token']
4544
- creds.expiry = datetime.datetime.strptime(token_expiry, YYYYMMDDTHHMMSSZ_FORMAT)
4582
+ creds.expiry = arrow.Arrow.strptime(token_expiry, YYYYMMDDTHHMMSSZ_FORMAT, tzinfo='UTC').naive
4545
4583
  return (not noScopes, creds)
4546
4584
  if jsonDict and exitOnError:
4547
4585
  invalidOauth2TxtExit(Msg.INVALID)
@@ -4770,7 +4808,7 @@ def getService(api, httpObj):
4770
4808
  waitOnFailure(n, triesLimit, INVALID_JSON_RC, str(e))
4771
4809
  continue
4772
4810
  systemErrorExit(INVALID_JSON_RC, str(e))
4773
- except (http_client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e:
4811
+ except (http.client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e:
4774
4812
  errMsg = f'Connection error: {str(e) or repr(e)}'
4775
4813
  if n != triesLimit:
4776
4814
  waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
@@ -4805,10 +4843,7 @@ def defaultSvcAcctScopes():
4805
4843
  saScopes[scope['api']].append(scope['scope'])
4806
4844
  else:
4807
4845
  saScopes[scope['api']].extend(scope['scope'])
4808
- saScopes[API.DRIVEACTIVITY].append(API.DRIVE_SCOPE)
4809
4846
  saScopes[API.DRIVE2] = saScopes[API.DRIVE3]
4810
- saScopes[API.DRIVETD] = saScopes[API.DRIVE3]
4811
- saScopes[API.SHEETSTD] = saScopes[API.SHEETS]
4812
4847
  return saScopes
4813
4848
 
4814
4849
  def _getSvcAcctData():
@@ -5086,7 +5121,7 @@ def callGData(service, function,
5086
5121
  e = e.args[0]
5087
5122
  handleOAuthTokenError(e, GDATA.SERVICE_NOT_APPLICABLE in throwErrors)
5088
5123
  raise GDATA.ERROR_CODE_EXCEPTION_MAP[GDATA.SERVICE_NOT_APPLICABLE](str(e))
5089
- except (http_client.ResponseNotReady, OSError) as e:
5124
+ except (http.client.ResponseNotReady, OSError) as e:
5090
5125
  errMsg = f'Connection error: {str(e) or repr(e)}'
5091
5126
  if n != triesLimit:
5092
5127
  waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
@@ -5396,7 +5431,7 @@ def callGAPI(service, function,
5396
5431
  e = e.args[0]
5397
5432
  handleOAuthTokenError(e, GAPI.SERVICE_NOT_AVAILABLE in throwReasons)
5398
5433
  raise GAPI.REASON_EXCEPTION_MAP[GAPI.SERVICE_NOT_AVAILABLE](str(e))
5399
- except (http_client.ResponseNotReady, OSError) as e:
5434
+ except (http.client.ResponseNotReady, OSError) as e:
5400
5435
  errMsg = f'Connection error: {str(e) or repr(e)}'
5401
5436
  if n != triesLimit:
5402
5437
  waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
@@ -5615,6 +5650,12 @@ def getSaUser(user):
5615
5650
  GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = currentClientAPIScopes
5616
5651
  return userEmail
5617
5652
 
5653
+ def chooseSaAPI(api1, api2):
5654
+ _getSvcAcctData()
5655
+ if api1 in GM.Globals[GM.SVCACCT_SCOPES]:
5656
+ return api1
5657
+ return api2
5658
+
5618
5659
  def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True):
5619
5660
  userEmail = getSaUser(user)
5620
5661
  httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR])
@@ -6255,63 +6296,77 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6255
6296
  _showInvalidEntity(Ent.USER, user)
6256
6297
  if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
6257
6298
  return entityList
6258
- elif entityType in {Cmd.ENTITY_ALL_USERS, Cmd.ENTITY_ALL_USERS_NS, Cmd.ENTITY_ALL_USERS_NS_SUSP, Cmd.ENTITY_ALL_USERS_SUSP}:
6299
+ elif entityType in Cmd.ALL_USER_ENTITY_TYPES:
6259
6300
  cd = buildGAPIObject(API.DIRECTORY)
6260
- if entityType == Cmd.ENTITY_ALL_USERS and isSuspended is not None:
6261
- query = f'isSuspended={isSuspended}'
6301
+ if entityType == Cmd.ENTITY_ALL_USERS and ((isSuspended is not None) or (isArchived is not None)):
6302
+ if isSuspended is not None:
6303
+ query = f'isSuspended={isSuspended}'
6304
+ if isArchived is not None:
6305
+ query += f' isArchived={isArchived}'
6306
+ else:
6307
+ query = f'isArchived={isArchived}'
6262
6308
  else:
6263
6309
  query = Cmd.ALL_USERS_QUERY_MAP[entityType]
6264
- printGettingAllAccountEntities(Ent.USER)
6310
+ printGettingAllAccountEntities(Ent.USER, query=query)
6265
6311
  try:
6266
6312
  result = callGAPIpages(cd.users(), 'list', 'users',
6267
6313
  pageMessage=getPageMessage(),
6268
6314
  throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
6269
6315
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
6270
- customer=GC.Values[GC.CUSTOMER_ID],
6271
- query=query, orderBy='email', fields='nextPageToken,users(primaryEmail,archived)',
6316
+ customer=GC.Values[GC.CUSTOMER_ID], query=query, orderBy='email',
6317
+ fields='nextPageToken,users(primaryEmail)',
6272
6318
  maxResults=GC.Values[GC.USER_MAX_RESULTS])
6273
6319
  except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
6274
6320
  accessErrorExit(cd)
6275
- entityList = [user['primaryEmail'] for user in result if isArchived is None or isArchived == user['archived']]
6276
- printGotAccountEntities(len(entityList))
6277
- elif entityType in {Cmd.ENTITY_DOMAINS, Cmd.ENTITY_DOMAINS_NS, Cmd.ENTITY_DOMAINS_SUSP}:
6278
- if entityType == Cmd.ENTITY_DOMAINS_NS:
6279
- query = 'isSuspended=False'
6280
- elif entityType == Cmd.ENTITY_DOMAINS_SUSP:
6281
- query = 'isSuspended=True'
6282
- elif isSuspended is not None:
6283
- query = f'isSuspended={isSuspended}'
6321
+ entityList = [user['primaryEmail'] for user in result]
6322
+ elif entityType == Cmd.ENTITY_ALL_USERS_ARCH_OR_SUSP:
6323
+ cd = buildGAPIObject(API.DIRECTORY)
6324
+ for query in ['isSuspended=True', 'isArchived=True']:
6325
+ printGettingAllAccountEntities(Ent.USER, query)
6326
+ try:
6327
+ result = callGAPIpages(cd.users(), 'list', 'users',
6328
+ pageMessage=getPageMessage(),
6329
+ throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
6330
+ retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
6331
+ customer=GC.Values[GC.CUSTOMER_ID], query=query, orderBy='email',
6332
+ fields='nextPageToken,users(primaryEmail)',
6333
+ maxResults=GC.Values[GC.USER_MAX_RESULTS])
6334
+ except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
6335
+ accessErrorExit(cd)
6336
+ entitySet |= {user['primaryEmail'] for user in result}
6337
+ entityList = sorted(list(entitySet))
6338
+ elif entityType in Cmd.DOMAIN_ENTITY_TYPES:
6339
+ if entityType == Cmd.ENTITY_DOMAINS and ((isSuspended is not None) or (isArchived is not None)):
6340
+ if isSuspended is not None:
6341
+ query = f'isSuspended={isSuspended}'
6342
+ if isArchived is not None:
6343
+ query += f' isArchived={isArchived}'
6344
+ else:
6345
+ query = f'isArchived={isArchived}'
6284
6346
  else:
6285
- query = None
6347
+ query = Cmd.DOMAINS_QUERY_MAP[entityType]
6286
6348
  cd = buildGAPIObject(API.DIRECTORY)
6287
6349
  domains = convertEntityToList(entity)
6288
6350
  for domain in domains:
6289
- printGettingAllEntityItemsForWhom(Ent.USER, domain, entityType=Ent.DOMAIN)
6351
+ printGettingAllEntityItemsForWhom(Ent.USER, domain, query=query, entityType=Ent.DOMAIN)
6290
6352
  try:
6291
6353
  result = callGAPIpages(cd.users(), 'list', 'users',
6292
6354
  pageMessage=getPageMessageForWhom(),
6293
6355
  throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN],
6294
6356
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
6295
- domain=domain,
6296
- query=query, orderBy='email', fields='nextPageToken,users(primaryEmail,archived)',
6357
+ domain=domain, query=query, orderBy='email',
6358
+ fields='nextPageToken,users(primaryEmail)',
6297
6359
  maxResults=GC.Values[GC.USER_MAX_RESULTS])
6298
6360
  except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden):
6299
6361
  checkEntityDNEorAccessErrorExit(cd, Ent.DOMAIN, domain)
6300
6362
  _incrEntityDoesNotExist(Ent.DOMAIN)
6301
6363
  continue
6302
- entityList = [user['primaryEmail'] for user in result if isArchived is None or isArchived == user['archived']]
6303
- printGotAccountEntities(len(entityList))
6304
- elif entityType in {Cmd.ENTITY_GROUP, Cmd.ENTITY_GROUPS,
6305
- Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUPS_NS,
6306
- Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUPS_SUSP,
6307
- Cmd.ENTITY_GROUP_INDE, Cmd.ENTITY_GROUPS_INDE}:
6308
- if entityType in {Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUPS_NS}:
6309
- isSuspended = False
6310
- elif entityType in {Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUPS_SUSP}:
6311
- isSuspended = True
6364
+ entityList.extend([user['primaryEmail'] for user in result])
6365
+ elif entityType in Cmd.GROUP_ENTITY_TYPES or entityType in Cmd.GROUPS_ENTITY_TYPES:
6366
+ isArchived, isSuspended = Cmd.GROUPS_QUERY_MAP.get(entityType, (isArchived, isSuspended))
6312
6367
  includeDerivedMembership = entityType in {Cmd.ENTITY_GROUP_INDE, Cmd.ENTITY_GROUPS_INDE}
6313
6368
  cd = buildGAPIObject(API.DIRECTORY)
6314
- groups = convertEntityToList(entity, nonListEntityType=entityType in {Cmd.ENTITY_GROUP, Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUP_INDE})
6369
+ groups = convertEntityToList(entity, nonListEntityType=entityType in Cmd.GROUP_ENTITY_TYPES)
6315
6370
  for group in groups:
6316
6371
  if validateEmailAddressOrUID(group, checkPeople=False):
6317
6372
  group = normalizeEmailAddressOrUID(group)
@@ -6337,11 +6392,8 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6337
6392
  entityList.append(email)
6338
6393
  else:
6339
6394
  _showInvalidEntity(Ent.GROUP, group)
6340
- elif entityType in {Cmd.ENTITY_GROUP_USERS, Cmd.ENTITY_GROUP_USERS_NS, Cmd.ENTITY_GROUP_USERS_SUSP, Cmd.ENTITY_GROUP_USERS_SELECT}:
6341
- if entityType == Cmd.ENTITY_GROUP_USERS_NS:
6342
- isSuspended = False
6343
- elif entityType == Cmd.ENTITY_GROUP_USERS_SUSP:
6344
- isSuspended = True
6395
+ elif entityType in Cmd.GROUP_USERS_ENTITY_TYPES:
6396
+ isArchived, isSuspended = Cmd.GROUP_USERS_QUERY_MAP.get(entityType, (isArchived, isSuspended))
6345
6397
  cd = buildGAPIObject(API.DIRECTORY)
6346
6398
  groups = convertEntityToList(entity)
6347
6399
  includeDerivedMembership = False
@@ -6432,21 +6484,13 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6432
6484
  _addCIGroupUsersToUsers(name, groupEmail, recursive)
6433
6485
  else:
6434
6486
  _showInvalidEntity(Ent.GROUP, group)
6435
- elif entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS, Cmd.ENTITY_OU_AND_CHILDREN, Cmd.ENTITY_OUS_AND_CHILDREN,
6436
- Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
6437
- Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP}:
6438
- if entityType in {Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS}:
6439
- isSuspended = False
6440
- elif entityType in {Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP}:
6441
- isSuspended = True
6487
+ elif entityType in Cmd.OU_ENTITY_TYPES or entityType in Cmd.OUS_ENTITY_TYPES:
6488
+ isArchived, isSuspended = Cmd.OU_QUERY_MAP.get(entityType, (isArchived, isSuspended))
6442
6489
  cd = buildGAPIObject(API.DIRECTORY)
6443
- ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN,
6444
- Cmd.ENTITY_OU_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS,
6445
- Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP})
6446
- 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}
6490
+ ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType in Cmd.OU_ENTITY_TYPES)
6491
+ directlyInOU = entityType in Cmd.OU_DIRECT_ENTITY_TYPES
6447
6492
  qualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if directlyInOU else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
6448
- fields = 'nextPageToken,users(primaryEmail,orgUnitPath,archived)' if directlyInOU else 'nextPageToken,users(primaryEmail,archived)'
6449
- prevLen = 0
6493
+ fields = 'nextPageToken,users(primaryEmail,orgUnitPath)' if directlyInOU else 'nextPageToken,users(primaryEmail)'
6450
6494
  for ou in ous:
6451
6495
  ou = makeOrgUnitPathAbsolute(ou)
6452
6496
  if ou.startswith('id:'):
@@ -6471,22 +6515,17 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6471
6515
  throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
6472
6516
  GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
6473
6517
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
6474
- customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(ou, isSuspended), orderBy='email',
6518
+ customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(ou, isSuspended, isArchived), orderBy='email',
6475
6519
  fields=fields, maxResults=GC.Values[GC.USER_MAX_RESULTS])
6476
6520
  for users in feed:
6477
6521
  if directlyInOU:
6478
6522
  for user in users:
6479
- if ouLower == user.get('orgUnitPath', '').lower() and (isArchived is None or isArchived == user['archived']):
6523
+ if ouLower == user.get('orgUnitPath', '').lower():
6480
6524
  usersInOU += 1
6481
6525
  entityList.append(user['primaryEmail'])
6482
- elif isArchived is None:
6526
+ else:
6483
6527
  entityList.extend([user['primaryEmail'] for user in users])
6484
6528
  usersInOU += len(users)
6485
- else:
6486
- for user in users:
6487
- if isArchived == user['archived']:
6488
- usersInOU += 1
6489
- entityList.append(user['primaryEmail'])
6490
6529
  setGettingAllEntityItemsForWhom(Ent.USER, ou, qualifier=qualifier)
6491
6530
  printGotEntityItemsForWhom(usersInOU)
6492
6531
  except (GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest,
@@ -6496,7 +6535,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6496
6535
  elif entityType in {Cmd.ENTITY_QUERY, Cmd.ENTITY_QUERIES}:
6497
6536
  cd = buildGAPIObject(API.DIRECTORY)
6498
6537
  queries = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType == Cmd.ENTITY_QUERY)
6499
- prevLen = 0
6500
6538
  for query in queries:
6501
6539
  printGettingAllAccountEntities(Ent.USER, query)
6502
6540
  try:
@@ -6520,9 +6558,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6520
6558
  email not in entitySet):
6521
6559
  entitySet.add(email)
6522
6560
  entityList.append(email)
6523
- totalLen = len(entityList)
6524
- printGotAccountEntities(totalLen-prevLen)
6525
- prevLen = totalLen
6526
6561
  elif entityType == Cmd.ENTITY_LICENSES:
6527
6562
  skusList = []
6528
6563
  for item in entity.split(','):
@@ -6581,8 +6616,7 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6581
6616
  ClientAPIAccessDeniedExit(str(e))
6582
6617
  elif entityType == Cmd.ENTITY_CROS:
6583
6618
  buildGAPIObject(API.DIRECTORY)
6584
- result = convertEntityToList(entity)
6585
- for deviceId in result:
6619
+ for deviceId in convertEntityToList(entity):
6586
6620
  if deviceId not in entitySet:
6587
6621
  entitySet.add(deviceId)
6588
6622
  entityList.append(deviceId)
@@ -6606,7 +6640,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6606
6640
  nonListEntityType=entityType == Cmd.ENTITY_CROS_QUERY)
6607
6641
  if entityType == Cmd.ENTITY_CROS_SN:
6608
6642
  queries = [f'id:{query}' for query in queries]
6609
- prevLen = 0
6610
6643
  for query in queries:
6611
6644
  printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
6612
6645
  try:
@@ -6627,25 +6660,15 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6627
6660
  if deviceId not in entitySet:
6628
6661
  entitySet.add(deviceId)
6629
6662
  entityList.append(deviceId)
6630
- totalLen = len(entityList)
6631
- printGotAccountEntities(totalLen-prevLen)
6632
- prevLen = totalLen
6633
- elif entityType in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
6634
- Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
6635
- Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
6663
+ elif entityType in Cmd.CROS_OU_ENTITY_TYPES or entityType in Cmd.CROS_OUS_ENTITY_TYPES:
6636
6664
  cd = buildGAPIObject(API.DIRECTORY)
6637
- ous = convertEntityToList(entity, shlexSplit=True,
6638
- nonListEntityType=entityType in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN,
6639
- Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY,
6640
- Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES})
6665
+ ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType in Cmd.CROS_OU_ENTITY_TYPES)
6641
6666
  numOus = len(ous)
6642
- includeChildOrgunits = entityType in {Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
6643
- Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
6644
- Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}
6667
+ includeChildOrgunits = entityType in Cmd.CROS_OU_CHILDREN_ENTITY_TYPES
6645
6668
  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))
6646
- 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}:
6669
+ if entityType in Cmd.CROS_OU_QUERY_ENTITY_TYPES:
6647
6670
  queries = getQueries('query')
6648
- 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}:
6671
+ elif entityType in Cmd.CROS_OU_QUERIES_ENTITY_TYPES:
6649
6672
  queries = getQueries('queries')
6650
6673
  else:
6651
6674
  queries = [None]
@@ -6718,7 +6741,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6718
6741
  customerId = _getCustomerIdNoC()
6719
6742
  queries = convertEntityToList(entity, shlexSplit=entityType == Cmd.ENTITY_BROWSER_QUERIES,
6720
6743
  nonListEntityType=entityType == Cmd.ENTITY_BROWSER_QUERY)
6721
- prevLen = 0
6722
6744
  for query in queries:
6723
6745
  printGettingAllAccountEntities(Ent.CHROME_BROWSER, query)
6724
6746
  try:
@@ -6738,9 +6760,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA
6738
6760
  if deviceId not in entitySet:
6739
6761
  entitySet.add(deviceId)
6740
6762
  entityList.append(deviceId)
6741
- totalLen = len(entityList)
6742
- printGotAccountEntities(totalLen-prevLen)
6743
- prevLen = totalLen
6744
6763
  else:
6745
6764
  systemErrorExit(UNKNOWN_ERROR_RC, 'getItemsToModify coding error')
6746
6765
  for errorType in [ENTITY_ERROR_DNE, ENTITY_ERROR_INVALID]:
@@ -7053,10 +7072,7 @@ def getEntityToModify(defaultEntityType=None, browserAllowed=False, crosAllowed=
7053
7072
  choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
7054
7073
  entityType = mapEntityType(getChoice(choices), typeMap)
7055
7074
  return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
7056
- getItemsToModify(entityType, getEntitiesFromFile(shlexSplit=entityType in [Cmd.ENTITY_OUS, Cmd.ENTITY_OUS_AND_CHILDREN,
7057
- Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
7058
- Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP,
7059
- Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN])))
7075
+ getItemsToModify(entityType, getEntitiesFromFile(shlexSplit=entityType in Cmd.OUS_ENTITY_TYPES | {Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN})))
7060
7076
  if entitySelector == Cmd.ENTITY_SELECTOR_CSVDATAFILE:
7061
7077
  if userAllowed:
7062
7078
  choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
@@ -7064,10 +7080,7 @@ def getEntityToModify(defaultEntityType=None, browserAllowed=False, crosAllowed=
7064
7080
  choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
7065
7081
  entityType = mapEntityType(getChoice(choices), typeMap)
7066
7082
  return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
7067
- getItemsToModify(entityType, getEntitiesFromCSVFile(shlexSplit=entityType in [Cmd.ENTITY_OUS, Cmd.ENTITY_OUS_AND_CHILDREN,
7068
- Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
7069
- Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP,
7070
- Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN])))
7083
+ getItemsToModify(entityType, getEntitiesFromCSVFile(shlexSplit=entityType in Cmd.OUS_ENTITY_TYPES | {Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN})))
7071
7084
  if entitySelector == Cmd.ENTITY_SELECTOR_CSVKMD:
7072
7085
  if userAllowed:
7073
7086
  choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
@@ -7110,10 +7123,7 @@ def getEntityToModify(defaultEntityType=None, browserAllowed=False, crosAllowed=
7110
7123
  if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
7111
7124
  buildGAPIObject(API.DIRECTORY)
7112
7125
  if entityClass == Cmd.ENTITY_USERS:
7113
- if entityType in [Cmd.ENTITY_GROUP_USERS,
7114
- Cmd.ENTITY_GROUP_USERS_NS, Cmd.ENTITY_GROUP_USERS_SUSP,
7115
- Cmd.ENTITY_GROUP_USERS_SELECT,
7116
- Cmd.ENTITY_CIGROUP_USERS]:
7126
+ if entityType in Cmd.GROUP_USERS_ENTITY_TYPES | {Cmd.ENTITY_CIGROUP_USERS}:
7117
7127
  # Skip over sub-arguments
7118
7128
  while Cmd.ArgumentsRemaining():
7119
7129
  myarg = getArgument()
@@ -7431,15 +7441,15 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter,
7431
7441
  def stripTimeFromDateTime(rowDate):
7432
7442
  if YYYYMMDD_PATTERN.match(rowDate):
7433
7443
  try:
7434
- rowTime = datetime.datetime.strptime(rowDate, YYYYMMDD_FORMAT)
7444
+ rowTime = arrow.Arrow.strptime(rowDate, YYYYMMDD_FORMAT)
7435
7445
  except ValueError:
7436
7446
  return None
7437
7447
  else:
7438
7448
  try:
7439
- rowTime, _ = iso8601.parse_date(rowDate)
7440
- except (iso8601.ParseError, OverflowError):
7449
+ rowTime = arrow.get(rowDate)
7450
+ except (arrow.parser.ParserError, OverflowError):
7441
7451
  return None
7442
- return ISOformatTimeStamp(datetime.datetime(rowTime.year, rowTime.month, rowTime.day, tzinfo=iso8601.UTC))
7452
+ return ISOformatTimeStamp(arrow.Arrow(rowTime.year, rowTime.month, rowTime.day, tzinfo='UTC'))
7443
7453
 
7444
7454
  def rowDateTimeFilterMatch(dateMode, op, filterDate):
7445
7455
  def checkMatch(rowDate):
@@ -7501,8 +7511,8 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter,
7501
7511
  if YYYYMMDD_PATTERN.match(rowDate):
7502
7512
  return None
7503
7513
  try:
7504
- rowTime, _ = iso8601.parse_date(rowDate)
7505
- except (iso8601.ParseError, OverflowError):
7514
+ rowTime = arrow.get(rowDate)
7515
+ except (arrow.parser.ParserError, OverflowError):
7506
7516
  return None
7507
7517
  return f'{rowTime.hour:02d}:{rowTime.minute:02d}'
7508
7518
 
@@ -7905,6 +7915,13 @@ class CSVPrintFile():
7905
7915
  if title not in self.titlesSet:
7906
7916
  self.AddTitle(title)
7907
7917
 
7918
+ def InsertTitles(self, position, titles):
7919
+ for title in titles if isinstance(titles, list) else [titles]:
7920
+ if title not in self.titlesSet:
7921
+ self.titlesSet.add(title)
7922
+ self.titlesList.insert(position, title)
7923
+ position += 1
7924
+
7908
7925
  def SetTitles(self, titles):
7909
7926
  self.titlesSet = set()
7910
7927
  self.titlesList = []
@@ -8060,7 +8077,7 @@ class CSVPrintFile():
8060
8077
 
8061
8078
  def getDriveObject():
8062
8079
  if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
8063
- _, drive = buildGAPIServiceObject(API.DRIVETD, self.todrive['user'])
8080
+ _, drive = buildGAPIServiceObject(chooseSaAPI(API.DRIVETD, API.DRIVE3), self.todrive['user'])
8064
8081
  if not drive:
8065
8082
  invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
8066
8083
  else:
@@ -8213,7 +8230,7 @@ class CSVPrintFile():
8213
8230
  if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
8214
8231
  invalidTodriveFileIdExit([], f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}', tdfileidLocation)
8215
8232
  if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
8216
- _, sheet = buildGAPIServiceObject(API.SHEETSTD, self.todrive['user'])
8233
+ _, sheet = buildGAPIServiceObject(chooseSaAPI(API.SHEETSTD, API.SHEETS), self.todrive['user'])
8217
8234
  if sheet is None:
8218
8235
  invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
8219
8236
  else:
@@ -8703,10 +8720,10 @@ class CSVPrintFile():
8703
8720
  sheetTitle = self.todrive['sheetEntity']['sheetTitle']
8704
8721
  else:
8705
8722
  sheetTitle = self.todrive['sheettitle']
8706
- tdbasetime = tdtime = datetime.datetime.now(GC.Values[GC.TIMEZONE])
8723
+ tdbasetime = tdtime = arrow.now(GC.Values[GC.TIMEZONE])
8707
8724
  if self.todrive['daysoffset'] is not None or self.todrive['hoursoffset'] is not None:
8708
- tdtime = tdbasetime+relativedelta(days=-self.todrive['daysoffset'] if self.todrive['daysoffset'] is not None else 0,
8709
- hours=-self.todrive['hoursoffset'] if self.todrive['hoursoffset'] is not None else 0)
8725
+ tdtime = tdbasetime.shift(days=-self.todrive['daysoffset'] if self.todrive['daysoffset'] is not None else 0,
8726
+ hours=-self.todrive['hoursoffset'] if self.todrive['hoursoffset'] is not None else 0)
8710
8727
  if self.todrive['timestamp']:
8711
8728
  if title:
8712
8729
  title += ' - '
@@ -8716,8 +8733,8 @@ class CSVPrintFile():
8716
8733
  title += tdtime.strftime(self.todrive['timeformat'])
8717
8734
  if self.todrive['sheettimestamp']:
8718
8735
  if self.todrive['sheetdaysoffset'] is not None or self.todrive['sheethoursoffset'] is not None:
8719
- tdtime = tdbasetime+relativedelta(days=-self.todrive['sheetdaysoffset'] if self.todrive['sheetdaysoffset'] is not None else 0,
8720
- hours=-self.todrive['sheethoursoffset'] if self.todrive['sheethoursoffset'] is not None else 0)
8736
+ tdtime = tdbasetime.shift(days=-self.todrive['sheetdaysoffset'] if self.todrive['sheetdaysoffset'] is not None else 0,
8737
+ hours=-self.todrive['sheethoursoffset'] if self.todrive['sheethoursoffset'] is not None else 0)
8721
8738
  if sheetTitle:
8722
8739
  sheetTitle += ' - '
8723
8740
  if not self.todrive['sheettimeformat']:
@@ -8726,7 +8743,7 @@ class CSVPrintFile():
8726
8743
  sheetTitle += tdtime.strftime(self.todrive['sheettimeformat'])
8727
8744
  action = Act.Get()
8728
8745
  if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
8729
- user, drive = buildGAPIServiceObject(API.DRIVETD, self.todrive['user'])
8746
+ user, drive = buildGAPIServiceObject(chooseSaAPI(API.DRIVETD, API.DRIVE3), self.todrive['user'])
8730
8747
  if not drive:
8731
8748
  closeFile(csvFile)
8732
8749
  return
@@ -8759,7 +8776,7 @@ class CSVPrintFile():
8759
8776
  if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
8760
8777
  todriveCSVErrorExit(entityValueList, f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}')
8761
8778
  if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
8762
- _, sheet = buildGAPIServiceObject(API.SHEETSTD, user)
8779
+ _, sheet = buildGAPIServiceObject(chooseSaAPI(API.SHEETSTD, API.SHEETS), user)
8763
8780
  if sheet is None:
8764
8781
  return
8765
8782
  else:
@@ -8907,7 +8924,7 @@ class CSVPrintFile():
8907
8924
  (self.todrive['sheetEntity'] or self.todrive['locale'] or self.todrive['timeZone'] or
8908
8925
  self.todrive['sheettitle'] or self.todrive['cellwrap'] or self.todrive['cellnumberformat'])):
8909
8926
  if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
8910
- _, sheet = buildGAPIServiceObject(API.SHEETSTD, user)
8927
+ _, sheet = buildGAPIServiceObject(chooseSaAPI(API.SHEETSTD, API.SHEETS), user)
8911
8928
  if sheet is None:
8912
8929
  return
8913
8930
  else:
@@ -9160,12 +9177,12 @@ def flattenJSON(topStructure, flattened=None,
9160
9177
  # Show a json object
9161
9178
  def showJSON(showName, showValue, skipObjects=None, timeObjects=None,
9162
9179
  simpleLists=None, dictObjectsKey=None, sortDictKeys=True):
9163
- def _show(objectName, objectValue, subObjectKey, level, subSkipObjects):
9180
+ def _show(objectName, objectValue, subObjectKey, subObjectName, level, subSkipObjects):
9164
9181
  if objectName in subSkipObjects:
9165
9182
  return
9166
9183
  if objectName is not None:
9167
9184
  printJSONKey(objectName)
9168
- subObjectKey = dictObjectsKey.get(objectName)
9185
+ subObjectKey = dictObjectsKey.get(objectName)
9169
9186
  if isinstance(objectValue, list):
9170
9187
  if objectName in simpleLists:
9171
9188
  printJSONValue(' '.join(objectValue))
@@ -9183,10 +9200,12 @@ def showJSON(showName, showValue, skipObjects=None, timeObjects=None,
9183
9200
  if isinstance(subValue, (str, bool, float, int)):
9184
9201
  printKeyValueList([subValue])
9185
9202
  else:
9186
- _show(None, subValue, subObjectKey, level+1, DEFAULT_SKIP_OBJECTS)
9203
+ _show(None, subValue, subObjectKey, objectName, level+1, DEFAULT_SKIP_OBJECTS)
9187
9204
  if objectName is not None:
9188
9205
  Ind.Decrement()
9189
9206
  elif isinstance(objectValue, dict):
9207
+ if not subObjectKey:
9208
+ subObjectKey = dictObjectsKey.get(subObjectName)
9190
9209
  indentAfterFirst = unindentAfterLast = False
9191
9210
  if objectName is not None:
9192
9211
  printBlankLine()
@@ -9194,13 +9213,23 @@ def showJSON(showName, showValue, skipObjects=None, timeObjects=None,
9194
9213
  elif level > 0:
9195
9214
  indentAfterFirst = unindentAfterLast = True
9196
9215
  subObjects = sorted(objectValue) if sortDictKeys else objectValue.keys()
9197
- if subObjectKey and (subObjectKey in subObjects):
9198
- subObjects.remove(subObjectKey)
9199
- subObjects.insert(0, subObjectKey)
9200
- subObjectKey = None
9216
+ if subObjectKey:
9217
+ if subObjectKey in subObjects:
9218
+ subObjects.remove(subObjectKey)
9219
+ subObjects.insert(0, subObjectKey)
9220
+ subObjectKey = None
9221
+ elif subObjectName == 'permissions': # subObjectKey in displayName
9222
+ if 'id' in objectValue:
9223
+ if objectValue['id'] == 'anyone':
9224
+ objectValue[subObjectKey] = 'Anyone'
9225
+ elif objectValue['id'] == 'anyoneWithLink':
9226
+ objectValue[subObjectKey] = 'Anyone with Link'
9227
+ else:
9228
+ objectValue[subObjectKey] = objectValue['id']
9229
+ subObjects.insert(0, subObjectKey)
9201
9230
  for subObject in subObjects:
9202
9231
  if subObject not in subSkipObjects:
9203
- _show(subObject, objectValue[subObject], subObjectKey, level+1, DEFAULT_SKIP_OBJECTS)
9232
+ _show(subObject, objectValue[subObject], subObjectKey, None, level+1, DEFAULT_SKIP_OBJECTS)
9204
9233
  if indentAfterFirst:
9205
9234
  Ind.Increment()
9206
9235
  indentAfterFirst = False
@@ -9227,7 +9256,7 @@ def showJSON(showName, showValue, skipObjects=None, timeObjects=None,
9227
9256
  timeObjects = timeObjects or set()
9228
9257
  simpleLists = simpleLists or set()
9229
9258
  dictObjectsKey = dictObjectsKey or {}
9230
- _show(showName, showValue, None, 0, DEFAULT_SKIP_OBJECTS.union(skipObjects or set()))
9259
+ _show(showName, showValue, None, None, 0, DEFAULT_SKIP_OBJECTS.union(skipObjects or set()))
9231
9260
 
9232
9261
  class FormatJSONQuoteChar():
9233
9262
 
@@ -9306,7 +9335,7 @@ def getLocalGoogleTimeOffset(testLocation=GOOGLE_TIMECHECK_LOCATION):
9306
9335
  for prot in ['http', 'https']:
9307
9336
  try:
9308
9337
  headerData = httpObj.request(f'{prot}://'+testLocation, 'HEAD')
9309
- googleUTC = datetime.datetime.strptime(headerData[0]['date'], '%a, %d %b %Y %H:%M:%S %Z').replace(tzinfo=iso8601.UTC)
9338
+ googleUTC = arrow.Arrow.strptime(headerData[0]['date'], '%a, %d %b %Y %H:%M:%S %Z', tzinfo='UTC')
9310
9339
  except (httplib2.HttpLib2Error, RuntimeError) as e:
9311
9340
  handleServerError(e)
9312
9341
  except httplib2.socks.HTTPError as e:
@@ -9319,7 +9348,7 @@ def getLocalGoogleTimeOffset(testLocation=GOOGLE_TIMECHECK_LOCATION):
9319
9348
  if prot == 'http':
9320
9349
  continue
9321
9350
  systemErrorExit(NETWORK_ERROR_RC, Msg.INVALID_HTTP_HEADER.format(str(headerData)))
9322
- offset = remainder = int(abs((datetime.datetime.now(iso8601.UTC)-googleUTC).total_seconds()))
9351
+ offset = remainder = int(abs((arrow.utcnow()-googleUTC).total_seconds()))
9323
9352
  if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
9324
9353
  continue
9325
9354
  timeoff = []
@@ -9369,6 +9398,7 @@ MACOS_CODENAMES = {
9369
9398
  13: 'Ventura',
9370
9399
  14: 'Sonoma',
9371
9400
  15: 'Sequoia',
9401
+ 26: 'Tahoe',
9372
9402
  }
9373
9403
 
9374
9404
  def getOSPlatform():
@@ -9378,7 +9408,7 @@ def getOSPlatform():
9378
9408
  elif myos == 'Windows':
9379
9409
  pltfrm = ' '.join(platform.win32_ver())
9380
9410
  elif myos == 'Darwin':
9381
- myos = 'MacOS'
9411
+ myos = 'macOS'
9382
9412
  mac_ver = platform.mac_ver()[0]
9383
9413
  major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
9384
9414
  minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
@@ -9619,7 +9649,7 @@ def CSVFileQueueHandler(mpQueue, mpQueueStdout, mpQueueStderr, csvPF, datetimeNo
9619
9649
  clearRowFilters = False
9620
9650
  # if sys.platform.startswith('win'):
9621
9651
  # signal.signal(signal.SIGINT, signal.SIG_IGN)
9622
- if multiprocessing.get_start_method() == 'spawn':
9652
+ if multiprocessing.get_start_method() != 'fork':
9623
9653
  signal.signal(signal.SIGINT, signal.SIG_IGN)
9624
9654
  Cmd = glclargs.GamCLArgs()
9625
9655
  else:
@@ -9661,7 +9691,7 @@ def CSVFileQueueHandler(mpQueue, mpQueueStdout, mpQueueStderr, csvPF, datetimeNo
9661
9691
  Cmd.InitializeArguments(dataItem)
9662
9692
  elif dataType == GM.REDIRECT_QUEUE_GLOBALS:
9663
9693
  GM.Globals = dataItem
9664
- if multiprocessing.get_start_method() == 'spawn':
9694
+ if multiprocessing.get_start_method() != 'fork':
9665
9695
  reopenSTDFile(GM.STDOUT)
9666
9696
  reopenSTDFile(GM.STDERR)
9667
9697
  elif dataType == GM.REDIRECT_QUEUE_VALUES:
@@ -9707,7 +9737,7 @@ def initializeCSVFileQueueHandler(mpManager, mpQueueStdout, mpQueueStderr):
9707
9737
  def terminateCSVFileQueueHandler(mpQueue, mpQueueHandler):
9708
9738
  GM.Globals[GM.PARSER] = None
9709
9739
  GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None
9710
- if multiprocessing.get_start_method() == 'spawn':
9740
+ if multiprocessing.get_start_method() != 'fork':
9711
9741
  mpQueue.put((GM.REDIRECT_QUEUE_ARGS, Cmd.AllArguments()))
9712
9742
  savedValues = saveNonPickleableValues()
9713
9743
  mpQueue.put((GM.REDIRECT_QUEUE_GLOBALS, GM.Globals))
@@ -9737,13 +9767,13 @@ def StdQueueHandler(mpQueue, stdtype, gmGlobals, gcValues):
9737
9767
 
9738
9768
  # if sys.platform.startswith('win'):
9739
9769
  # signal.signal(signal.SIGINT, signal.SIG_IGN)
9740
- if multiprocessing.get_start_method() == 'spawn':
9770
+ if multiprocessing.get_start_method() != 'fork':
9741
9771
  signal.signal(signal.SIGINT, signal.SIG_IGN)
9742
9772
  GM.Globals = gmGlobals.copy()
9743
9773
  GC.Values = gcValues.copy()
9744
9774
  pid0DataItem = [KEYBOARD_INTERRUPT_RC, None]
9745
9775
  pidData = {}
9746
- if multiprocessing.get_start_method() == 'spawn':
9776
+ if multiprocessing.get_start_method() != 'fork':
9747
9777
  if GM.Globals[stdtype][GM.REDIRECT_NAME] == 'null':
9748
9778
  fd = open(os.devnull, GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=UTF8)
9749
9779
  elif GM.Globals[stdtype][GM.REDIRECT_NAME] == '-':
@@ -9831,7 +9861,7 @@ def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout,
9831
9861
  with mplock:
9832
9862
  initializeLogging()
9833
9863
  # if sys.platform.startswith('win'):
9834
- if multiprocessing.get_start_method() == 'spawn':
9864
+ if multiprocessing.get_start_method() != 'fork':
9835
9865
  signal.signal(signal.SIGINT, signal.SIG_IGN)
9836
9866
  GM.Globals[GM.API_CALLS_RETRY_DATA] = {}
9837
9867
  GM.Globals[GM.CMDLOG_LOGGER] = None
@@ -9972,7 +10002,7 @@ def MultiprocessGAMCommands(items, showCmds):
9972
10002
  mpManager = multiprocessing.Manager()
9973
10003
  l = mpManager.Lock()
9974
10004
  try:
9975
- if multiprocessing.get_start_method() == 'spawn':
10005
+ if multiprocessing.get_start_method() != 'fork':
9976
10006
  pool = mpManager.Pool(processes=numPoolProcesses, initializer=initGamWorker, initargs=(l,), maxtasksperchild=200)
9977
10007
  else:
9978
10008
  pool = multiprocessing.Pool(processes=numPoolProcesses, initializer=initGamWorker, initargs=(l,), maxtasksperchild=200)
@@ -9981,7 +10011,7 @@ def MultiprocessGAMCommands(items, showCmds):
9981
10011
  except AssertionError as e:
9982
10012
  Cmd.SetLocation(0)
9983
10013
  usageErrorExit(str(e))
9984
- if multiprocessing.get_start_method() == 'spawn':
10014
+ if multiprocessing.get_start_method() != 'fork':
9985
10015
  savedValues = saveNonPickleableValues()
9986
10016
  if GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS]:
9987
10017
  mpQueueStdout, mpQueueHandlerStdout = initializeStdQueueHandler(mpManager, GM.STDOUT, GM.Globals, GC.Values)
@@ -9996,7 +10026,7 @@ def MultiprocessGAMCommands(items, showCmds):
9996
10026
  mpQueueStderr = mpQueueStdout
9997
10027
  else:
9998
10028
  mpQueueStderr = None
9999
- if multiprocessing.get_start_method() == 'spawn':
10029
+ if multiprocessing.get_start_method() != 'fork':
10000
10030
  restoreNonPickleableValues(savedValues)
10001
10031
  if mpQueueStdout:
10002
10032
  mpQueueStdout.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()))
@@ -10156,8 +10186,8 @@ def threadBatchWorker(showCmds=False, numItems=0):
10156
10186
  batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Error,{str(e)},{logCmd}\n')
10157
10187
  GM.Globals[GM.TBATCH_QUEUE].task_done()
10158
10188
 
10159
- BATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD]
10160
- TBATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.EXECUTE_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD]
10189
+ BATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD, Cmd.DATETIME_CMD, Cmd.SET_CMD, Cmd.CLEAR_CMD]
10190
+ TBATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.EXECUTE_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD, Cmd.DATETIME_CMD, Cmd.SET_CMD, Cmd.CLEAR_CMD]
10161
10191
 
10162
10192
  def ThreadBatchGAMCommands(items, showCmds):
10163
10193
  if not items:
@@ -10269,6 +10299,14 @@ def doBatch(threadBatch=False):
10269
10299
  continue
10270
10300
  if argv:
10271
10301
  cmd = argv[0].strip().lower()
10302
+ if cmd == Cmd.DATETIME_CMD:
10303
+ if len(argv) == 2:
10304
+ kwValues['datetime'] = todaysTime().strftime(argv[1])
10305
+ else:
10306
+ writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
10307
+ writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.DATETIME_CMD} DateTimeFormat>)>\n')
10308
+ errors += 1
10309
+ continue
10272
10310
  if cmd == Cmd.SET_CMD:
10273
10311
  if len(argv) == 3:
10274
10312
  kwValues[argv[1]] = argv[2]
@@ -11033,7 +11071,7 @@ class Credentials(google.oauth2.credentials.Credentials):
11033
11071
  expiry = info.get('token_expiry')
11034
11072
  if expiry:
11035
11073
  # Convert the raw expiry to datetime
11036
- expiry = datetime.datetime.strptime(expiry, YYYYMMDDTHHMMSSZ_FORMAT)
11074
+ expiry = arrow.Arrow.strptime(expiry, YYYYMMDDTHHMMSSZ_FORMAT, tzinfo='UTC').naive
11037
11075
  id_token_data = info.get('decoded_id_token')
11038
11076
 
11039
11077
  # Provide backwards compatibility with field names when loading from JSON.
@@ -11152,7 +11190,7 @@ class Credentials(google.oauth2.credentials.Credentials):
11152
11190
 
11153
11191
  def doOAuthRequest(currentScopes, login_hint, verifyScopes=False):
11154
11192
  client_id, client_secret = getOAuthClientIDAndSecret()
11155
- scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS])
11193
+ scopesList = API.getClientScopesList(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
11156
11194
  if not currentScopes or verifyScopes:
11157
11195
  selectedScopes = getScopesFromUser(scopesList, True, currentScopes)
11158
11196
  if selectedScopes is None:
@@ -11198,7 +11236,7 @@ def doOAuthCreate():
11198
11236
  else:
11199
11237
  login_hint = None
11200
11238
  scopes = []
11201
- scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS])
11239
+ scopesList = API.getClientScopesList(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
11202
11240
  while Cmd.ArgumentsRemaining():
11203
11241
  myarg = getArgument()
11204
11242
  if myarg == 'admin':
@@ -11214,7 +11252,9 @@ def doOAuthCreate():
11214
11252
  scopes.append(uscope)
11215
11253
  break
11216
11254
  else:
11217
- invalidChoiceExit(uscope, API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS]), True)
11255
+ invalidChoiceExit(uscope,
11256
+ API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS]),
11257
+ True)
11218
11258
  else:
11219
11259
  unknownArgumentExit()
11220
11260
  if len(scopes) == 0:
@@ -11297,7 +11337,7 @@ def doOAuthInfo():
11297
11337
  if 'email' in token_info:
11298
11338
  printKeyValueList(['Google Workspace Admin', f'{token_info["email"]}'])
11299
11339
  if 'expires_in' in token_info:
11300
- printKeyValueList(['Expires', ISOformatTimeStamp((datetime.datetime.now()+datetime.timedelta(seconds=token_info['expires_in'])).replace(tzinfo=GC.Values[GC.TIMEZONE]))])
11340
+ printKeyValueList(['Expires', ISOformatTimeStamp(arrow.now(GC.Values[GC.TIMEZONE]).shift(seconds=token_info['expires_in']))])
11301
11341
  if showDetails:
11302
11342
  for k, v in sorted(token_info.items()):
11303
11343
  if k not in ['email', 'expires_in', 'issued_to', 'scope']:
@@ -11325,7 +11365,7 @@ def doOAuthUpdate():
11325
11365
  if 'scopes' in jsonDict:
11326
11366
  currentScopes = jsonDict['scopes']
11327
11367
  else:
11328
- currentScopes = API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS])
11368
+ currentScopes = API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
11329
11369
  else:
11330
11370
  currentScopes = []
11331
11371
  except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
@@ -12355,8 +12395,6 @@ def checkServiceAccount(users):
12355
12395
  saScopes[scope['api']].append(scope['roscope'])
12356
12396
  checkScopesSet.add(scope['roscope'])
12357
12397
  i += 1
12358
- if API.DRIVEACTIVITY in saScopes and API.DRIVE3 in saScopes:
12359
- saScopes[API.DRIVEACTIVITY].append(API.DRIVE_SCOPE)
12360
12398
  if API.DRIVE3 in saScopes:
12361
12399
  saScopes[API.DRIVE2] = saScopes[API.DRIVE3]
12362
12400
  GM.Globals[GM.OAUTH2SERVICE_JSON_DATA][API.OAUTH2SA_SCOPES] = saScopes
@@ -12413,7 +12451,7 @@ def checkServiceAccount(users):
12413
12451
  throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.NOT_FOUND,
12414
12452
  GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
12415
12453
  name=name, fields='validAfterTime')
12416
- key_created, _ = iso8601.parse_date(key['validAfterTime'])
12454
+ key_created = arrow.get(key['validAfterTime'])
12417
12455
  key_age = todaysTime()-key_created
12418
12456
  printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format(key_age.days), testWarn if key_age.days > 30 else testPass)
12419
12457
  except GAPI.permissionDenied:
@@ -12668,15 +12706,15 @@ def _generatePrivateKeyAndPublicCert(projectId, clientEmail, name, key_size, b64
12668
12706
  _validate=False)]))
12669
12707
  # Gooogle seems to enforce the not before date strictly. Set the not before
12670
12708
  # date to be UTC two minutes ago which should cover any clock skew.
12671
- now = datetime.datetime.utcnow()
12672
- builder = builder.not_valid_before(now - datetime.timedelta(minutes=2))
12709
+ now = arrow.utcnow()
12710
+ builder = builder.not_valid_before(now.shift(minutes=-2).naive)
12673
12711
  # Google defaults to 12/31/9999 date for end time if there's no
12674
12712
  # policy to restrict key age
12675
12713
  if validityHours:
12676
- expires = now + datetime.timedelta(hours=validityHours) - datetime.timedelta(minutes=2)
12714
+ expires = now.shift(hours=validityHours, minutes=-2).naive
12677
12715
  builder = builder.not_valid_after(expires)
12678
12716
  else:
12679
- builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
12717
+ builder = builder.not_valid_after(arrow.Arrow(9999, 12, 31, 23, 59).naive)
12680
12718
  builder = builder.serial_number(x509.random_serial_number())
12681
12719
  builder = builder.public_key(public_key)
12682
12720
  builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
@@ -13053,7 +13091,7 @@ def _showMailboxMonitorRequestStatus(request, i=0, count=0):
13053
13091
  def doCreateMonitor():
13054
13092
  auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=True)
13055
13093
  #end_date defaults to 30 days in the future...
13056
- end_date = (GM.Globals[GM.DATETIME_NOW]+datetime.timedelta(days=30)).strftime(YYYYMMDD_HHMM_FORMAT)
13094
+ end_date = GM.Globals[GM.DATETIME_NOW].shift(days=30).strftime(YYYYMMDD_HHMM_FORMAT)
13057
13095
  begin_date = None
13058
13096
  incoming_headers_only = outgoing_headers_only = drafts_headers_only = chats_headers_only = False
13059
13097
  drafts = chats = True
@@ -13226,7 +13264,7 @@ def _adjustTryDate(errMsg, numDateChanges, limitDateChanges, prevTryDate):
13226
13264
  else:
13227
13265
  match_date = re.match('End date greater than LastReportedDate.', errMsg)
13228
13266
  if match_date:
13229
- tryDateTime = datetime.datetime.strptime(prevTryDate, YYYYMMDD_FORMAT)-datetime.timedelta(days=1)
13267
+ tryDateTime = arrow.Arrow.strptime(prevTryDate, YYYYMMDD_FORMAT).shift(days=-1)
13230
13268
  tryDate = tryDateTime.strftime(YYYYMMDD_FORMAT)
13231
13269
  if (not match_date) or (numDateChanges > limitDateChanges >= 0):
13232
13270
  printWarningMessage(DATA_NOT_AVALIABLE_RC, errMsg)
@@ -13237,18 +13275,17 @@ def _checkDataRequiredServices(result, tryDate, dataRequiredServices, parameterS
13237
13275
  # -1: Data not available:
13238
13276
  # 0: Backup to earlier date
13239
13277
  # 1: Data available
13240
- oneDay = datetime.timedelta(days=1)
13241
13278
  dataWarnings = result.get('warnings', [])
13242
13279
  usageReports = result.get('usageReports', [])
13243
13280
  # move to day before if we don't have at least one usageReport with parameters
13244
13281
  if not usageReports or not usageReports[0].get('parameters', []):
13245
- tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
13282
+ tryDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT).shift(days=-1)
13246
13283
  return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
13247
13284
  for warning in dataWarnings:
13248
13285
  if warning['code'] == 'PARTIAL_DATA_AVAILABLE':
13249
13286
  for app in warning['data']:
13250
13287
  if app['key'] == 'application' and app['value'] != 'docs' and app['value'] in dataRequiredServices:
13251
- tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
13288
+ tryDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT).shift(days=-1)
13252
13289
  return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
13253
13290
  elif warning['code'] == 'DATA_NOT_AVAILABLE':
13254
13291
  for app in warning['data']:
@@ -13265,11 +13302,11 @@ def _checkDataRequiredServices(result, tryDate, dataRequiredServices, parameterS
13265
13302
  if not requiredServices:
13266
13303
  break
13267
13304
  else:
13268
- tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
13305
+ tryDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT).shift(days=-1)
13269
13306
  return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
13270
13307
  if checkUserEmail:
13271
13308
  if 'entity' not in usageReports[0] or 'userEmail' not in usageReports[0]['entity']:
13272
- tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
13309
+ tryDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT).shift(days=-1)
13273
13310
  return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
13274
13311
  return (1, tryDate, usageReports)
13275
13312
 
@@ -13361,7 +13398,7 @@ def getUserOrgUnits(cd, orgUnit, orgUnitId):
13361
13398
  throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
13362
13399
  GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
13363
13400
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
13364
- customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnit, None), orderBy='email',
13401
+ customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnit, None, None), orderBy='email',
13365
13402
  fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
13366
13403
  userOrgUnits = {}
13367
13404
  for user in result:
@@ -13383,13 +13420,17 @@ REPORTS_PARAMETERS_SIMPLE_TYPES = ['intValue', 'boolValue', 'datetimeValue', 'st
13383
13420
  # [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
13384
13421
  # [([start|startdate <Date>] [end|enddate <Date>])|(range <Date> <Date>)|
13385
13422
  # thismonth|(previousmonths <Integer>)]
13423
+ # [skipdates <Date>[:<Date>](,<Date>[:<Date>])*] [skipdaysofweek <DayOfWeek>(,<DayOfWeek>)*]
13386
13424
  # [fields|parameters <String>)]
13387
13425
  # [convertmbtogb]
13426
+ # (addcsvdata <FieldName> <String>)*
13388
13427
  # gam report usage customer [todrive <ToDriveAttribute>*]
13389
13428
  # [([start|startdate <Date>] [end|enddate <Date>])|(range <Date> <Date>)|
13390
13429
  # thismonth|(previousmonths <Integer>)]
13430
+ # [skipdates <Date>[:<Date>](,<Date>[:<Date>])*] [skipdaysofweek <DayOfWeek>(,<DayOfWeek>)*]
13391
13431
  # [fields|parameters <String>)]
13392
13432
  # [convertmbtogb]
13433
+ # (addcsvdata <FieldName> <String>)*
13393
13434
  def doReportUsage():
13394
13435
  def usageEntitySelectors():
13395
13436
  selectorChoices = Cmd.USER_ENTITY_SELECTORS+Cmd.USER_CSVDATA_ENTITY_SELECTORS
@@ -13409,8 +13450,8 @@ def doReportUsage():
13409
13450
  invalidArgumentExit(DELTA_DATE_FORMAT_REQUIRED)
13410
13451
  return deltaDate
13411
13452
  try:
13412
- argDate = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
13413
- return datetime.datetime(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE])
13453
+ argDate = arrow.Arrow.strptime(argstr, YYYYMMDD_FORMAT)
13454
+ return arrow.Arrow(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE])
13414
13455
  except ValueError:
13415
13456
  Cmd.Backup()
13416
13457
  invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
@@ -13441,7 +13482,7 @@ def doReportUsage():
13441
13482
  startEndTime = StartEndTime('startdate', 'enddate', 'date')
13442
13483
  skipDayNumbers = []
13443
13484
  skipDates = set()
13444
- oneDay = datetime.timedelta(days=1)
13485
+ addCSVData = {}
13445
13486
  while Cmd.ArgumentsRemaining():
13446
13487
  myarg = getArgument()
13447
13488
  if csvPF and myarg == 'todrive':
@@ -13479,10 +13520,10 @@ def doReportUsage():
13479
13520
  usageErrorExit(Msg.INVALID_DATE_TIME_RANGE.format(myarg, skipEnd, myarg, skipStart))
13480
13521
  while skipStartDate <= skipEndDate:
13481
13522
  skipDates.add(skipStartDate)
13482
- skipStartDate += oneDay
13523
+ skipStartDate = skipStartDate.shift(days=1)
13483
13524
  elif myarg == 'skipdaysofweek':
13484
- skipdaynames = getString(Cmd.OB_STRING).split(',')
13485
- dow = [d.lower() for d in calendarlib.day_abbr]
13525
+ skipdaynames = getString(Cmd.OB_STRING).lower().split(',')
13526
+ dow = [d.lower() for d in DAYS_OF_WEEK]
13486
13527
  skipDayNumbers = [dow.index(d) for d in skipdaynames if d in dow]
13487
13528
  elif userReports and myarg == 'user':
13488
13529
  userKey = getString(Cmd.OB_EMAIL_ADDRESS)
@@ -13496,12 +13537,15 @@ def doReportUsage():
13496
13537
  select = True
13497
13538
  elif myarg == 'convertmbtogb':
13498
13539
  convertMbToGb = True
13540
+ elif myarg == 'addcsvdata':
13541
+ k = getString(Cmd.OB_STRING)
13542
+ addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
13499
13543
  else:
13500
13544
  unknownArgumentExit()
13501
13545
  if startEndTime.endDateTime is None:
13502
13546
  startEndTime.endDateTime = todaysDate()
13503
13547
  if startEndTime.startDateTime is None:
13504
- startEndTime.startDateTime = startEndTime.endDateTime+datetime.timedelta(days=-30)
13548
+ startEndTime.startDateTime = startEndTime.endDateTime.shift(days=-30)
13505
13549
  startDateTime = startEndTime.startDateTime
13506
13550
  startDate = startDateTime.strftime(YYYYMMDD_FORMAT)
13507
13551
  endDateTime = startEndTime.endDateTime
@@ -13529,15 +13573,17 @@ def doReportUsage():
13529
13573
  titles.append('orgUnitPath')
13530
13574
  else:
13531
13575
  pageMessage = None
13576
+ if addCSVData:
13577
+ titles.extend(sorted(addCSVData.keys()))
13532
13578
  csvPF.SetTitles(titles)
13533
13579
  csvPF.SetSortAllTitles()
13534
13580
  parameters = ','.join(parameters) if parameters else None
13535
13581
  while startDateTime <= endDateTime:
13536
13582
  if startDateTime.weekday() in skipDayNumbers or startDateTime in skipDates:
13537
- startDateTime += oneDay
13583
+ startDateTime = startDateTime.shift(days=1)
13538
13584
  continue
13539
13585
  useDate = startDateTime.strftime(YYYYMMDD_FORMAT)
13540
- startDateTime += oneDay
13586
+ startDateTime = startDateTime.shift(days=1)
13541
13587
  try:
13542
13588
  for kwarg in kwargs:
13543
13589
  if userReports:
@@ -13563,6 +13609,8 @@ def doReportUsage():
13563
13609
  row['orgUnitPath'] = userOrgUnits.get(row['user'], UNKNOWN)
13564
13610
  else:
13565
13611
  row['user'] = UNKNOWN
13612
+ if addCSVData:
13613
+ row.update(addCSVData)
13566
13614
  for item in entity.get('parameters', []):
13567
13615
  if 'name' not in item:
13568
13616
  continue
@@ -13694,12 +13742,14 @@ REPORT_ACTIVITIES_TIME_OBJECTS = {'time'}
13694
13742
  # [aggregatebydate|aggregatebyuser [Boolean]]
13695
13743
  # [maxresults <Number>]
13696
13744
  # [convertmbtogb]
13745
+ # (addcsvdata <FieldName> <String>)*
13697
13746
  # gam report customers|customer|domain [todrive <ToDriveAttribute>*]
13698
13747
  # [(date <Date>)|(range <Date> <Date>)|
13699
13748
  # yesterday|today|thismonth|(previousmonths <Integer>)]
13700
13749
  # [nodatechange | (fulldatarequired all|<CustomerServiceNameList>)]
13701
13750
  # [(fields|parameters <String>)|(services <CustomerServiceNameList>)] [noauthorizedapps]
13702
13751
  # [convertmbtogb]
13752
+ # (addcsvdata <FieldName> <String>)*
13703
13753
  def doReport():
13704
13754
  def processUserUsage(usage, lastDate):
13705
13755
  if not usage:
@@ -13717,6 +13767,8 @@ def doReport():
13717
13767
  row['orgUnitPath'] = userOrgUnits.get(row['email'], UNKNOWN)
13718
13768
  else:
13719
13769
  row['email'] = UNKNOWN
13770
+ if addCSVData:
13771
+ row.update(addCSVData)
13720
13772
  for item in user_report.get('parameters', []):
13721
13773
  if 'name' not in item:
13722
13774
  continue
@@ -13728,7 +13780,7 @@ def doReport():
13728
13780
  mg = DISABLED_REASON_TIME_PATTERN.match(item['stringValue'])
13729
13781
  if mg:
13730
13782
  try:
13731
- disabledTime = formatLocalTime(datetime.datetime.strptime(mg.group(1), '%Y/%m/%d-%H:%M:%S').replace(tzinfo=iso8601.UTC).strftime(YYYYMMDDTHHMMSSZ_FORMAT))
13783
+ disabledTime = formatLocalTime(arrow.Arrow.strptime(mg.group(1), '%Y/%m/%d-%H:%M:%S').replace(tzinfo='UTC').strftime(YYYYMMDDTHHMMSSZ_FORMAT))
13732
13784
  row['accounts:disabled_time'] = disabledTime
13733
13785
  csvPF.AddTitles('accounts:disabled_time')
13734
13786
  except ValueError:
@@ -13808,6 +13860,8 @@ def doReport():
13808
13860
  return (False, lastDate)
13809
13861
  lastDate = usage[0]['date']
13810
13862
  row = {'date': lastDate}
13863
+ if addCSVData:
13864
+ row.update(addCSVData)
13811
13865
  for item in usage[0].get('parameters', []):
13812
13866
  if 'name' not in item:
13813
13867
  continue
@@ -13882,12 +13936,16 @@ def doReport():
13882
13936
  continue
13883
13937
  for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
13884
13938
  if ptype in item:
13939
+ row = {'date': lastDate}
13940
+ if addCSVData:
13941
+ row.update(addCSVData)
13885
13942
  if ptype != 'datetimeValue':
13886
13943
  if convertMbToGb and name.endswith('_in_mb'):
13887
13944
  name = convertReportMBtoGB(name, item)
13888
- csvPF.WriteRow({'date': lastDate, 'name': name, 'value': item[ptype]})
13945
+ row.update({'name': name, 'value': item[ptype]})
13889
13946
  else:
13890
- csvPF.WriteRow({'date': lastDate, 'name': name, 'value': formatLocalTime(item[ptype])})
13947
+ row.update({'name': name, 'value': formatLocalTime(item[ptype])})
13948
+ csvPF.WriteRow(row)
13891
13949
  break
13892
13950
  else:
13893
13951
  if 'msgValue' in item:
@@ -13896,6 +13954,8 @@ def doReport():
13896
13954
  continue
13897
13955
  for subitem in item['msgValue']:
13898
13956
  app = {'date': lastDate}
13957
+ if addCSVData:
13958
+ app.update(addCSVData)
13899
13959
  for an_item in subitem:
13900
13960
  if an_item == 'client_name':
13901
13961
  app['name'] = f'App: {escapeCRsNLs(subitem[an_item])}'
@@ -13908,7 +13968,11 @@ def doReport():
13908
13968
  values = []
13909
13969
  for subitem in item['msgValue']:
13910
13970
  values.append(f'{subitem["version_number"]}:{subitem["num_devices"]}')
13911
- csvPF.WriteRow({'date': lastDate, 'name': name, 'value': ' '.join(sorted(values, reverse=True))})
13971
+ row = {'date': lastDate}
13972
+ if addCSVData:
13973
+ row.update(addCSVData)
13974
+ row.update({'name': name, 'value': ' '.join(sorted(values, reverse=True))})
13975
+ csvPF.WriteRow(row)
13912
13976
  else:
13913
13977
  values = []
13914
13978
  for subitem in item['msgValue']:
@@ -13923,7 +13987,11 @@ def doReport():
13923
13987
  values.append(f'{myvalue}:{mycount}')
13924
13988
  else:
13925
13989
  continue
13926
- csvPF.WriteRow({'date': lastDate, 'name': name, 'value': ' '.join(sorted(values, reverse=True))})
13990
+ row = {'date': lastDate}
13991
+ if addCSVData:
13992
+ row.update(addCSVData)
13993
+ row.update({'name': name, 'value': ' '.join(sorted(values, reverse=True))})
13994
+ csvPF.WriteRow(row)
13927
13995
  csvPF.SortRowsTwoTitles('date', 'name', False)
13928
13996
  if authorizedApps:
13929
13997
  csvPF.AddTitle('client_id')
@@ -13963,7 +14031,6 @@ def doReport():
13963
14031
  eventCounts = {}
13964
14032
  eventNames = []
13965
14033
  startEndTime = StartEndTime('start', 'end')
13966
- oneDay = datetime.timedelta(days=1)
13967
14034
  filterTimes = {}
13968
14035
  maxActivities = maxEvents = 0
13969
14036
  maxResults = 1000
@@ -14074,7 +14141,7 @@ def doReport():
14074
14141
  eventRowFilter = True
14075
14142
  elif activityReports and myarg == 'groupidfilter':
14076
14143
  groupIdFilter = getString(Cmd.OB_STRING)
14077
- elif activityReports and myarg == 'addcsvdata':
14144
+ elif myarg == 'addcsvdata':
14078
14145
  k = getString(Cmd.OB_STRING)
14079
14146
  addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
14080
14147
  elif activityReports and myarg == 'shownoactivities':
@@ -14139,6 +14206,8 @@ def doReport():
14139
14206
  titles = ['email'] if not showOrgUnit else ['email', 'orgUnitPath']
14140
14207
  else:
14141
14208
  titles = ['email', 'date'] if not showOrgUnit else ['email', 'orgUnitPath', 'date']
14209
+ if addCSVData:
14210
+ titles.extend(sorted(addCSVData.keys()))
14142
14211
  csvPF.SetTitles(titles)
14143
14212
  csvPF.SetSortAllTitles()
14144
14213
  i = 0
@@ -14175,7 +14244,7 @@ def doReport():
14175
14244
  if fullData == 0:
14176
14245
  if numDateChanges > limitDateChanges >= 0:
14177
14246
  break
14178
- startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
14247
+ startDateTime = endDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT)
14179
14248
  continue
14180
14249
  if not select and userKey == 'all':
14181
14250
  pageMessage = getPageMessageForWhom(forWhom, showDate=tryDate)
@@ -14204,7 +14273,7 @@ def doReport():
14204
14273
  tryDate = _adjustTryDate(str(e), numDateChanges, limitDateChanges, tryDate)
14205
14274
  if not tryDate:
14206
14275
  break
14207
- startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
14276
+ startDateTime = endDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT)
14208
14277
  continue
14209
14278
  except GAPI.invalidInput as e:
14210
14279
  systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
@@ -14217,7 +14286,7 @@ def doReport():
14217
14286
  break
14218
14287
  except GAPI.forbidden as e:
14219
14288
  accessErrorExit(None, str(e))
14220
- startDateTime += oneDay
14289
+ startDateTime = startDateTime.shift(days=1)
14221
14290
  if exitUserLoop:
14222
14291
  break
14223
14292
  if user != 'all' and lastDate is None and GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
@@ -14225,6 +14294,8 @@ def doReport():
14225
14294
  if aggregateByDate:
14226
14295
  for usageDate, events in eventCounts.items():
14227
14296
  row = {'date': usageDate}
14297
+ if addCSVData:
14298
+ row.update(addCSVData)
14228
14299
  for event, count in events.items():
14229
14300
  if convertMbToGb and event.endswith('_in_gb'):
14230
14301
  count = f'{count/1024:.2f}'
@@ -14237,6 +14308,8 @@ def doReport():
14237
14308
  row = {'email': email}
14238
14309
  if showOrgUnit:
14239
14310
  row['orgUnitPath'] = userOrgUnits.get(email, UNKNOWN)
14311
+ if addCSVData:
14312
+ row.update(addCSVData)
14240
14313
  for event, count in events.items():
14241
14314
  if convertMbToGb and event.endswith('_in_gb'):
14242
14315
  count = f'{count/1024:.2f}'
@@ -14251,6 +14324,8 @@ def doReport():
14251
14324
  if startEndTime.startDateTime is None:
14252
14325
  startEndTime.startDateTime = startEndTime.endDateTime = todaysDate()
14253
14326
  csvPF.SetTitles('date')
14327
+ if addCSVData:
14328
+ csvPF.AddTitles(sorted(addCSVData.keys()))
14254
14329
  if not userCustomerRange or (startEndTime.startDateTime == startEndTime.endDateTime):
14255
14330
  csvPF.AddTitles(['name', 'value'])
14256
14331
  authorizedApps = []
@@ -14274,7 +14349,7 @@ def doReport():
14274
14349
  if fullData == 0:
14275
14350
  if numDateChanges > limitDateChanges >= 0:
14276
14351
  break
14277
- startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
14352
+ startDateTime = endDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT)
14278
14353
  continue
14279
14354
  usage = callGAPIpages(service, 'get', 'usageReports',
14280
14355
  throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN],
@@ -14292,13 +14367,13 @@ def doReport():
14292
14367
  tryDate = _adjustTryDate(str(e), numDateChanges, limitDateChanges, tryDate)
14293
14368
  if not tryDate:
14294
14369
  break
14295
- startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
14370
+ startDateTime = endDateTime = arrow.Arrow.strptime(tryDate, YYYYMMDD_FORMAT)
14296
14371
  continue
14297
14372
  except GAPI.invalidInput as e:
14298
14373
  systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
14299
14374
  except GAPI.forbidden as e:
14300
14375
  accessErrorExit(None, str(e))
14301
- startDateTime += oneDay
14376
+ startDateTime = startDateTime.shift(days=1)
14302
14377
  csvPF.writeCSVfile(f'Customer Report - {tryDate}')
14303
14378
  else: # activityReports
14304
14379
  csvPF.SetTitles('name')
@@ -14368,8 +14443,8 @@ def doReport():
14368
14443
  eventTime = activity.get('id', {}).get('time', UNKNOWN)
14369
14444
  if eventTime != UNKNOWN:
14370
14445
  try:
14371
- eventTime, _ = iso8601.parse_date(eventTime)
14372
- except (iso8601.ParseError, OverflowError):
14446
+ eventTime = arrow.get(eventTime)
14447
+ except (arrow.parser.ParserError, OverflowError):
14373
14448
  eventTime = UNKNOWN
14374
14449
  if eventTime != UNKNOWN:
14375
14450
  eventDate = eventTime.strftime(YYYYMMDD_FORMAT)
@@ -14393,7 +14468,7 @@ def doReport():
14393
14468
  if val is not None:
14394
14469
  val = int(val)
14395
14470
  if val >= 62135683200:
14396
- event[item['name']] = ISOformatTimeStamp(datetime.datetime.fromtimestamp(val-62135683200, GC.Values[GC.TIMEZONE]))
14471
+ event[item['name']] = ISOformatTimeStamp(arrow.Arrow.fromtimestamp(val-62135683200, GC.Values[GC.TIMEZONE]))
14397
14472
  else:
14398
14473
  event[item['name']] = val
14399
14474
  else:
@@ -16285,7 +16360,7 @@ def _showCustomerLicenseInfo(customerInfo, FJQC):
16285
16360
  while True:
16286
16361
  try:
16287
16362
  result = callGAPI(rep.customerUsageReports(), 'get',
16288
- throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
16363
+ throwReasons=[GAPI.INVALID, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
16289
16364
  date=tryDate, customerId=customerInfo['id'],
16290
16365
  fields='warnings,usageReports', parameters=parameters)
16291
16366
  usageReports = numUsersAvailable(result)
@@ -16298,7 +16373,7 @@ def _showCustomerLicenseInfo(customerInfo, FJQC):
16298
16373
  if fullData == 0:
16299
16374
  continue
16300
16375
  break
16301
- except GAPI.invalid as e:
16376
+ except (GAPI.invalid, GAPI.failedPrecondition) as e:
16302
16377
  tryDate = _adjustTryDate(str(e), 0, -1, tryDate)
16303
16378
  if not tryDate:
16304
16379
  return
@@ -17012,22 +17087,39 @@ def doDeleteAdmin():
17012
17087
  except (GAPI.forbidden, GAPI.permissionDenied) as e:
17013
17088
  ClientAPIAccessDeniedExit(str(e))
17014
17089
 
17015
- ASSIGNEE_EMAILTYPE_TOFIELD_MAP = {
17090
+ ADMIN_ASSIGNEE_TYPE_TO_ASSIGNEDTO_FIELD_MAP = {
17016
17091
  'user': 'assignedToUser',
17017
17092
  'group': 'assignedToGroup',
17018
17093
  'serviceaccount': 'assignedToServiceAccount',
17094
+ 'unknown': 'assignedToUnknown',
17019
17095
  }
17096
+ ALL_ASSIGNEE_TYPES = ['user', 'group', 'serviceaccount']
17097
+
17020
17098
  PRINT_ADMIN_FIELDS = ['roleAssignmentId', 'roleId', 'assignedTo', 'scopeType', 'orgUnitId']
17021
17099
  PRINT_ADMIN_TITLES = ['roleAssignmentId', 'roleId', 'role',
17022
17100
  'assignedTo', 'assignedToUser', 'assignedToGroup', 'assignedToServiceAccount', 'assignedToUnknown',
17023
17101
  'scopeType', 'orgUnitId', 'orgUnit']
17024
17102
 
17025
17103
  # gam print admins [todrive <ToDriveAttribute>*]
17026
- # [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>] [condition]
17027
- # [privileges] [oneitemperrow]
17104
+ # [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>]
17105
+ # [types <AdminAssigneeTypeList>]
17106
+ # [recursive] [condition] [privileges] [oneitemperrow]
17028
17107
  # gam show admins
17029
- # [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>] [condition] [privileges]
17108
+ # [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>]
17109
+ # [types <AdminAssigneeTypeList>]
17110
+ # [recursive] [condition] [privileges]
17030
17111
  def doPrintShowAdmins():
17112
+ def _getAssigneeTypes(myarg):
17113
+ if myarg in {'type', 'types'}:
17114
+ for gtype in getString(Cmd.OB_ADMIN_ASSIGNEE_TYPE_LIST).lower().replace(',', ' ').split():
17115
+ if gtype in ADMIN_ASSIGNEE_TYPE_TO_ASSIGNEDTO_FIELD_MAP:
17116
+ typesSet.add(ADMIN_ASSIGNEE_TYPE_TO_ASSIGNEDTO_FIELD_MAP[gtype])
17117
+ else:
17118
+ invalidChoiceExit(gtype, ADMIN_ASSIGNEE_TYPE_TO_ASSIGNEDTO_FIELD_MAP, True)
17119
+ else:
17120
+ return False
17121
+ return True
17122
+
17031
17123
  def _getPrivileges(admin):
17032
17124
  if showPrivileges:
17033
17125
  roleId = admin['roleId']
@@ -17055,17 +17147,14 @@ def doPrintShowAdmins():
17055
17147
  assignedTo = admin['assignedTo']
17056
17148
  admin['assignedToUnknown'] = False
17057
17149
  if assignedTo not in assignedToIdEmailMap:
17058
- assigneeType = admin.get('assigneeType')
17059
- assignedToField = ASSIGNEE_EMAILTYPE_TOFIELD_MAP.get(assigneeType, None)
17060
- assigneeEmail, assigneeType = convertUIDtoEmailAddressWithType(f'uid:{assignedTo}', cd, sal,
17061
- emailTypes=list(ASSIGNEE_EMAILTYPE_TOFIELD_MAP.keys()))
17062
- if not assignedToField and assigneeType in ASSIGNEE_EMAILTYPE_TOFIELD_MAP:
17063
- assignedToField = ASSIGNEE_EMAILTYPE_TOFIELD_MAP[assigneeType]
17064
- if assigneeType == 'unknown':
17065
- assignedToField = 'assignedToUnknown'
17150
+ emailTypes = ALL_ASSIGNEE_TYPES if admin.get('assigneeType', '') != 'group' else ['group']
17151
+ assigneeEmail, assigneeType = convertUIDtoEmailAddressWithType(f'uid:{assignedTo}', cd, sal, emailTypes=emailTypes)
17152
+ assignedToField = ADMIN_ASSIGNEE_TYPE_TO_ASSIGNEDTO_FIELD_MAP.get(assigneeType, 'assignedToUnknown')
17153
+ if assignedToField == 'assignedToUnknown':
17066
17154
  assigneeEmail = True
17067
17155
  assignedToIdEmailMap[assignedTo] = {'assignedToField': assignedToField, 'assigneeEmail': assigneeEmail}
17068
17156
  admin[assignedToIdEmailMap[assignedTo]['assignedToField']] = assignedToIdEmailMap[assignedTo]['assigneeEmail']
17157
+ admin['assignedToField'] = assignedToIdEmailMap[assignedTo]['assignedToField']
17069
17158
  if privileges is not None:
17070
17159
  admin.update(privileges)
17071
17160
  if 'orgUnitId' in admin:
@@ -17075,16 +17164,22 @@ def doPrintShowAdmins():
17075
17164
  admin['condition'] = 'securitygroup'
17076
17165
  elif admin['condition'] == NONSECURITY_GROUP_CONDITION:
17077
17166
  admin['condition'] = 'nonsecuritygroup'
17167
+ if debug:
17168
+ print('******', admin['assignedTo'], admin.get('assigneeType', 'no type'),
17169
+ admin['assignedToField'], not typesSet or admin['assignedToField'] in typesSet)
17170
+ return not typesSet or admin['assignedToField'] in typesSet
17078
17171
 
17079
17172
  cd = buildGAPIObject(API.DIRECTORY)
17080
17173
  sal = buildGAPIObject(API.SERVICEACCOUNTLOOKUP)
17081
17174
  csvPF = CSVPrintFile(PRINT_ADMIN_TITLES) if Act.csvFormat() else None
17082
17175
  roleId = None
17083
17176
  userKey = None
17084
- oneItemPerRow = showPrivileges = False
17177
+ debug = oneItemPerRow = recursive = showPrivileges = False
17178
+ typesSet = set()
17085
17179
  kwargs = {}
17086
17180
  rolePrivileges = {}
17087
- fieldsList = PRINT_ADMIN_FIELDS
17181
+ allGroupRoles = ','.join(sorted(ALL_GROUP_ROLES))
17182
+ fieldsList = PRINT_ADMIN_FIELDS+['assigneeType']
17088
17183
  assignedToIdEmailMap = {}
17089
17184
  while Cmd.ArgumentsRemaining():
17090
17185
  myarg = getArgument()
@@ -17094,6 +17189,16 @@ def doPrintShowAdmins():
17094
17189
  userKey = kwargs['userKey'] = getEmailAddress()
17095
17190
  elif myarg == 'role':
17096
17191
  _, roleId = getRoleId()
17192
+ elif _getAssigneeTypes(myarg):
17193
+ pass
17194
+ elif myarg == 'recursive':
17195
+ recursive = True
17196
+ memberOptions = initMemberOptions()
17197
+ memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
17198
+ memberOptions[MEMBEROPTION_DISPLAYMATCH] = False
17199
+ memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
17200
+ for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
17201
+ memberDisplayOptions[role]['show'] = True
17097
17202
  elif myarg == 'condition':
17098
17203
  fieldsList.append('condition')
17099
17204
  if csvPF:
@@ -17102,6 +17207,8 @@ def doPrintShowAdmins():
17102
17207
  showPrivileges = True
17103
17208
  elif myarg == 'oneitemperrow':
17104
17209
  oneItemPerRow = True
17210
+ elif myarg == 'debug':
17211
+ debug = True
17105
17212
  else:
17106
17213
  unknownArgumentExit()
17107
17214
  if roleId and not kwargs:
@@ -17113,7 +17220,7 @@ def doPrintShowAdmins():
17113
17220
  admins = callGAPIpages(cd.roleAssignments(), 'list', 'items',
17114
17221
  pageMessage=getPageMessage(),
17115
17222
  throwReasons=[GAPI.INVALID, GAPI.USER_NOT_FOUND,
17116
- GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE,
17223
+ GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE,
17117
17224
  GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND,
17118
17225
  GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
17119
17226
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
@@ -17121,39 +17228,75 @@ def doPrintShowAdmins():
17121
17228
  except (GAPI.invalid, GAPI.userNotFound):
17122
17229
  entityUnknownWarning(Ent.ADMINISTRATOR, userKey)
17123
17230
  return
17124
- except (GAPI.serviceNotAvailable) as e:
17125
- entityActionFailedExit([Ent.ADMINISTRATOR, userKey, Ent.ADMIN_ROLE, roleId], str(e))
17231
+ except GAPI.notFound as e:
17232
+ entityActionFailedExit([Ent.ADMIN_ROLE, kwargs['roleId']], str(e))
17233
+ except GAPI.serviceNotAvailable as e:
17234
+ entityActionFailedExit([Ent.ADMINISTRATOR, userKey], str(e))
17126
17235
  except (GAPI.badRequest, GAPI.customerNotFound):
17127
17236
  accessErrorExit(cd)
17128
17237
  except (GAPI.forbidden, GAPI.permissionDenied) as e:
17129
17238
  ClientAPIAccessDeniedExit(str(e))
17239
+ count = len(admins)
17240
+ groupMembers = {}
17241
+ expandedAdmins = []
17242
+ i = 0
17243
+ for admin in admins:
17244
+ i += 1
17245
+ if roleId and roleId != admin['roleId']:
17246
+ continue
17247
+ assignedTo = admin['assignedTo']
17248
+ if admin['assigneeType'] != 'group' or not recursive:
17249
+ if _setNamesFromIds(admin, _getPrivileges(admin)):
17250
+ expandedAdmins.append(admin)
17251
+ continue
17252
+ if assignedTo not in groupMembers:
17253
+ membersList = []
17254
+ membersSet = set()
17255
+ level = 0
17256
+ getGroupMembers(cd, assignedTo, allGroupRoles, membersList, membersSet, i, count,
17257
+ memberOptions, memberDisplayOptions, level, {Ent.TYPE_USER})
17258
+ groupMembers[assignedTo] = membersList[:]
17259
+ if not _setNamesFromIds(admin, _getPrivileges(admin)):
17260
+ continue
17261
+ if not groupMembers[assignedTo]:
17262
+ expandedAdmins.append(admin)
17263
+ continue
17264
+ admin['assigneeType'] = 'user'
17265
+ admin['assignedToGroup'] = assignedToIdEmailMap[assignedTo]['assigneeEmail']
17266
+ for member in groupMembers[assignedTo]:
17267
+ userAdmin = admin.copy()
17268
+ userAdmin['assignedTo'] = member['id']
17269
+ _setNamesFromIds(userAdmin, _getPrivileges(admin))
17270
+ expandedAdmins.append(userAdmin)
17271
+ admins = expandedAdmins
17272
+ count = len(expandedAdmins)
17130
17273
  if not csvPF:
17131
- count = len(admins)
17132
17274
  performActionNumItems(count, Ent.ADMIN_ROLE_ASSIGNMENT)
17133
17275
  Ind.Increment()
17134
17276
  i = 0
17135
- for admin in admins:
17277
+ for admin in expandedAdmins:
17136
17278
  i += 1
17137
- if roleId and roleId != admin['roleId']:
17138
- continue
17139
- _setNamesFromIds(admin, _getPrivileges(admin))
17140
17279
  printEntity([Ent.ADMIN_ROLE_ASSIGNMENT, admin['roleAssignmentId']], i, count)
17141
17280
  Ind.Increment()
17142
17281
  for field in PRINT_ADMIN_TITLES:
17143
17282
  if field in admin:
17144
- if field == 'roleAssignmentId':
17283
+ if (field == 'roleAssignmentId') or (field == 'assignedToUnknown' and not admin[field]):
17145
17284
  continue
17146
- if field != 'rolePrivileges':
17147
- printKeyValueList([field, admin[field]])
17148
- else:
17149
- showJSON(None, admin[field])
17285
+ printKeyValueList([field, admin[field]])
17286
+ if showPrivileges:
17287
+ rolePrivileges = admin.get('rolePrivileges', [])
17288
+ jcount = len(rolePrivileges)
17289
+ if jcount > 0:
17290
+ printKeyValueList(['rolePrivileges', jcount])
17291
+ Ind.Increment()
17292
+ showJSON(None, rolePrivileges)
17293
+ Ind.Decrement()
17150
17294
  Ind.Decrement()
17151
17295
  Ind.Decrement()
17152
17296
  else:
17153
- for admin in admins:
17154
- if roleId and roleId != admin['roleId']:
17155
- continue
17156
- _setNamesFromIds(admin, _getPrivileges(admin))
17297
+ for admin in expandedAdmins:
17298
+ admin.pop('assigneeType', None)
17299
+ admin.pop('assignedToField', None)
17157
17300
  if not oneItemPerRow or 'rolePrivileges' not in admin:
17158
17301
  csvPF.WriteRowTitles(flattenJSON(admin))
17159
17302
  else:
@@ -17856,10 +17999,29 @@ ORG_FIELD_INFO_ORDER = ['orgUnitId', 'name', 'description', 'parentOrgUnitPath',
17856
17999
  ORG_FIELDS_WITH_CRS_NLS = {'description'}
17857
18000
 
17858
18001
  def _doInfoOrgs(entityList):
18002
+ def _printUsers(entityType, orgUnitPath, isSuspended, isArchived):
18003
+ users = callGAPIpages(cd.users(), 'list', 'users',
18004
+ throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
18005
+ retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
18006
+ customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, isSuspended, isArchived), orderBy='email',
18007
+ fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
18008
+ printEntitiesCount(entityType, None)
18009
+ usersInOU = 0
18010
+ Ind.Increment()
18011
+ orgUnitPath = orgUnitPath.lower()
18012
+ for user in users:
18013
+ if orgUnitPath == user['orgUnitPath'].lower():
18014
+ printKeyValueList([user['primaryEmail']])
18015
+ usersInOU += 1
18016
+ elif showChildren:
18017
+ printKeyValueList([f'{user["primaryEmail"]} (child)'])
18018
+ usersInOU += 1
18019
+ Ind.Decrement()
18020
+ printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.ORGANIZATIONAL_UNIT)), usersInOU])
18021
+
17859
18022
  cd = buildGAPIObject(API.DIRECTORY)
17860
18023
  getUsers = True
17861
- isSuspended = None
17862
- entityType = Ent.USER
18024
+ isSuspended = isArchived = None
17863
18025
  showChildren = False
17864
18026
  while Cmd.ArgumentsRemaining():
17865
18027
  myarg = getArgument()
@@ -17867,7 +18029,8 @@ def _doInfoOrgs(entityList):
17867
18029
  getUsers = False
17868
18030
  elif myarg in SUSPENDED_ARGUMENTS:
17869
18031
  isSuspended = _getIsSuspended(myarg)
17870
- entityType = Ent.USER_SUSPENDED if isSuspended else Ent.USER_NOT_SUSPENDED
18032
+ elif myarg in ARCHIVED_ARGUMENTS:
18033
+ isArchived = _getIsArchived(myarg)
17871
18034
  elif myarg in {'children', 'child'}:
17872
18035
  showChildren = True
17873
18036
  else:
@@ -17898,38 +18061,29 @@ def _doInfoOrgs(entityList):
17898
18061
  printKeyValueWithCRsNLs(field, value)
17899
18062
  if getUsers:
17900
18063
  orgUnitPath = result['orgUnitPath']
17901
- users = callGAPIpages(cd.users(), 'list', 'users',
17902
- throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
17903
- retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
17904
- customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, isSuspended), orderBy='email',
17905
- fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
17906
- printEntitiesCount(entityType, None)
17907
- usersInOU = 0
17908
- Ind.Increment()
17909
- orgUnitPath = orgUnitPath.lower()
17910
- for user in users:
17911
- if orgUnitPath == user['orgUnitPath'].lower():
17912
- printKeyValueList([user['primaryEmail']])
17913
- usersInOU += 1
17914
- elif showChildren:
17915
- printKeyValueList([f'{user["primaryEmail"]} (child)'])
17916
- usersInOU += 1
17917
- Ind.Decrement()
17918
- printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.ORGANIZATIONAL_UNIT)), usersInOU])
18064
+ if isArchived is None and isSuspended is None:
18065
+ _printUsers(Ent.USER, orgUnitPath, None, None)
18066
+ else:
18067
+ if isArchived is not None:
18068
+ _printUsers(Ent.USER_ARCHIVED if isArchived else Ent.USER_NOT_ARCHIVED, orgUnitPath, None, isArchived)
18069
+ if isSuspended is not None:
18070
+ _printUsers(Ent.USER_SUSPENDED if isSuspended else Ent.USER_NOT_SUSPENDED, orgUnitPath, isSuspended, None)
17919
18071
  Ind.Decrement()
17920
18072
  except (GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
17921
18073
  entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
17922
18074
  except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
17923
18075
  checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
17924
18076
 
17925
- # gam info orgs|ous <OrgUnitEntity> [nousers|notsuspended|suspended] [children|child]
17926
- def doInfoOrgs():
17927
- _doInfoOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
17928
-
17929
- # gam info org|ou <OrgUnitItem> [nousers|notsuspended|suspended] [children|child]
18077
+ # gam info org|ou <OrgUnitItem>
18078
+ # [nousers | ([notarchived|archived] [notsuspended|suspended])] [children|child]
17930
18079
  def doInfoOrg():
17931
18080
  _doInfoOrgs([getOrgUnitItem()])
17932
18081
 
18082
+ # gam info orgs|ous <OrgUnitEntity>
18083
+ # [nousers | ([notarchived|archived] [notsuspended|suspended])] [children|child]
18084
+ def doInfoOrgs():
18085
+ _doInfoOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
18086
+
17933
18087
  ORG_ARGUMENT_TO_FIELD_MAP = {
17934
18088
  'blockinheritance': 'blockInheritance',
17935
18089
  'inheritanceblocked': 'blockInheritance',
@@ -18159,7 +18313,7 @@ def doPrintOrgs():
18159
18313
  return
18160
18314
  if showUserCounts:
18161
18315
  for orgUnit in orgUnits:
18162
- userCounts[orgUnit['orgUnitPath']] = [0, 0]
18316
+ userCounts[orgUnit['orgUnitPath']] = {'suspended': [0, 0], 'archived': [0, 0], 'total': 0}
18163
18317
  qualifier = Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
18164
18318
  printGettingAllEntityItemsForWhom(Ent.USER, orgUnitPath, qualifier=qualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
18165
18319
  pageMessage = getPageMessageForWhom()
@@ -18169,12 +18323,14 @@ def doPrintOrgs():
18169
18323
  throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
18170
18324
  GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
18171
18325
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
18172
- customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None), orderBy='email',
18173
- fields='nextPageToken,users(orgUnitPath,suspended)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
18326
+ customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None, None), orderBy='email',
18327
+ fields='nextPageToken,users(orgUnitPath,suspended,archived)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
18174
18328
  for users in feed:
18175
18329
  for user in users:
18176
18330
  if user['orgUnitPath'] in userCounts:
18177
- userCounts[user['orgUnitPath']][user['suspended']] += 1
18331
+ userCounts[user['orgUnitPath']]['suspended'][user['suspended']] += 1
18332
+ userCounts[user['orgUnitPath']]['archived'][user['archived']] += 1
18333
+ userCounts[user['orgUnitPath']]['total'] += 1
18178
18334
  except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput, GAPI.badRequest, GAPI.backendError,
18179
18335
  GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
18180
18336
  checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
@@ -18241,15 +18397,34 @@ def doPrintOrgs():
18241
18397
  (maxCrOSCounts != -1 and total > maxCrOSCounts)):
18242
18398
  continue
18243
18399
  if showUserCounts:
18244
- row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}NotSuspended'] = userCounts[orgUnitPath][0]
18245
- row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Suspended'] = userCounts[orgUnitPath][1]
18246
- row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Total'] = total = userCounts[orgUnitPath][0]+userCounts[orgUnitPath][1]
18400
+ row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}NotArchived'] = userCounts[orgUnitPath]['archived'][0]
18401
+ row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Archived'] = userCounts[orgUnitPath]['archived'][1]
18402
+ row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}NotSuspended'] = userCounts[orgUnitPath]['suspended'][0]
18403
+ row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Suspended'] = userCounts[orgUnitPath]['suspended'][1]
18404
+ row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Total'] = total = userCounts[orgUnitPath]['total']
18247
18405
  if ((minUserCounts != -1 and total < minUserCounts) or
18248
18406
  (maxUserCounts != -1 and total > maxUserCounts)):
18249
18407
  continue
18250
18408
  csvPF.WriteRowTitles(row)
18251
18409
  else:
18252
18410
  csvPF.WriteRow(row)
18411
+ if showCrOSCounts or showUserCounts:
18412
+ crosTitles = []
18413
+ userTitles = []
18414
+ allTitles = csvPF.titlesList[:]
18415
+ for title in allTitles:
18416
+ if showCrOSCounts and title.startswith(f'CrOS{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}'):
18417
+ crosTitles.append(title)
18418
+ csvPF.RemoveTitles(title)
18419
+ if showUserCounts and title.startswith(f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}'):
18420
+ userTitles.append(title)
18421
+ csvPF.RemoveTitles(title)
18422
+ if showCrOSCounts:
18423
+ csvPF.AddTitles(sorted(crosTitles))
18424
+ if showUserCounts:
18425
+ for title in ['NotArchived', 'Archived', 'NotSuspended', 'Suspended', 'Total']:
18426
+ csvPF.AddTitles(f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{title}')
18427
+ csvPF.SetSortTitles([])
18253
18428
  csvPF.writeCSVfile('Orgs')
18254
18429
 
18255
18430
  # gam show orgtree [fromparent <OrgUnitItem>] [batchsuborgs [Boolean>]]
@@ -18459,7 +18634,7 @@ def doCheckOrgUnit():
18459
18634
  throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
18460
18635
  GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
18461
18636
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
18462
- customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None),
18637
+ customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None, None),
18463
18638
  fields='nextPageToken,users(orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
18464
18639
  for users in feed:
18465
18640
  for user in users:
@@ -18880,7 +19055,7 @@ def makeUserGroupDomainQueryFilters(kwargsDict):
18880
19055
  kwargsQueries.append((kwargs, query))
18881
19056
  return kwargsQueries
18882
19057
 
18883
- def userFilters(kwargs, query, orgUnitPath, isSuspended):
19058
+ def userFilters(kwargs, query, orgUnitPath, isSuspended, isArchived):
18884
19059
  queryTitle = ''
18885
19060
  if kwargs.get('domain'):
18886
19061
  queryTitle += f'domain={kwargs["domain"]}, '
@@ -18899,6 +19074,12 @@ def userFilters(kwargs, query, orgUnitPath, isSuspended):
18899
19074
  else:
18900
19075
  query += ' '
18901
19076
  query += f'isSuspended={isSuspended}'
19077
+ if isArchived is not None:
19078
+ if query is None:
19079
+ query = ''
19080
+ else:
19081
+ query += ' '
19082
+ query += f'isArchived={isArchived}'
18902
19083
  if query is not None:
18903
19084
  queryTitle += f'query="{query}", '
18904
19085
  if queryTitle:
@@ -18910,7 +19091,7 @@ def userFilters(kwargs, query, orgUnitPath, isSuspended):
18910
19091
  # [limittoou <OrgUnitItem>])
18911
19092
  # [user|users <EmailAddressList>] [group|groups <EmailAddressList>]
18912
19093
  # [select <UserTypeEntity>]
18913
- # [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
19094
+ # [issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
18914
19095
  # [shownoneditable] [nogroups] [nousers]
18915
19096
  # [onerowpertarget] [delimiter <Character>]
18916
19097
  # [suppressnoaliasrows]
@@ -18956,7 +19137,7 @@ def doPrintAliases():
18956
19137
  groups = []
18957
19138
  users = []
18958
19139
  aliasMatchPattern = re.compile(r'^.*$')
18959
- isSuspended = orgUnitPath = None
19140
+ isArchived = isSuspended = orgUnitPath = None
18960
19141
  addCSVData = {}
18961
19142
  delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
18962
19143
  while Cmd.ArgumentsRemaining():
@@ -18982,6 +19163,8 @@ def doPrintAliases():
18982
19163
  _, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
18983
19164
  elif myarg == 'issuspended':
18984
19165
  isSuspended = getBoolean()
19166
+ elif myarg == 'isarchived':
19167
+ isArchived = getBoolean()
18985
19168
  elif myarg in {'user','users'}:
18986
19169
  users.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
18987
19170
  elif myarg in {'group', 'groups'}:
@@ -19016,7 +19199,7 @@ def doPrintAliases():
19016
19199
  for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
19017
19200
  kwargs = kwargsQuery[0]
19018
19201
  query = kwargsQuery[1]
19019
- query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended)
19202
+ query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended, isArchived)
19020
19203
  printGettingAllAccountEntities(Ent.USER, pquery)
19021
19204
  try:
19022
19205
  entityList = callGAPIpages(cd.users(), 'list', 'users',
@@ -22418,12 +22601,12 @@ def infoUserPeopleContacts(users):
22418
22601
 
22419
22602
  # gam <UserTypeEntity> print contacts [todrive <ToDriveAttribute>*] <PeoplePrintShowUserContactSelection>
22420
22603
  # [showgroups|showgroupnameslist] [orderby firstname|lastname|(lastmodified ascending)|(lastnodified descending)
22421
- # [countsonly|allfields|fields <PeopleFieldNameList>] [showmetadata]
22422
- # [formatjson [quotechar <Character>]]
22604
+ # [allfields|fields <PeopleFieldNameList>] [showmetadata]
22605
+ # [countsonly|(formatjson [quotechar <Character>])]
22423
22606
  # gam <UserTypeEntity> show contacts <PeoplePrintShowUserContactSelection>
22424
22607
  # [showgroups] [orderby firstname|lastname|(lastmodified ascending)|(lastnodified descending)
22425
- # [countsonly|allfields|(fields <PeopleFieldNameList>)] [showmetadata]
22426
- # [formatjson]
22608
+ # [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
22609
+ # [countsonly|formatjson]
22427
22610
  def printShowUserPeopleContacts(users):
22428
22611
  entityType = Ent.USER
22429
22612
  entityTypeName = Ent.Singular(entityType)
@@ -22462,6 +22645,8 @@ def printShowUserPeopleContacts(users):
22462
22645
  else:
22463
22646
  FJQC.GetFormatJSONQuoteChar(myarg, True)
22464
22647
  if countsOnly:
22648
+ if csvPF:
22649
+ csvPF.SetFormatJSON(False)
22465
22650
  fieldsList = ['emailAddresses']
22466
22651
  if contactQuery['mainContacts']:
22467
22652
  fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
@@ -22699,11 +22884,11 @@ def processUserPeopleOtherContacts(users):
22699
22884
  Ind.Decrement()
22700
22885
 
22701
22886
  # gam <UserTypeEntity> print othercontacts [todrive <ToDriveAttribute>*] <OtherContactSelection>
22702
- # [countsonly|allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
22703
- # [formatjson [quotechar <Character>]]
22887
+ # [allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
22888
+ # [countsonly|(formatjson [quotechar <Character>])]
22704
22889
  # gam <UserTypeEntity> show othercontacts <OtherContactSelection>
22705
- # [countsonly|allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
22706
- # [formatjson]
22890
+ # [allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
22891
+ # [countsonly|formatjson]
22707
22892
  def printShowUserPeopleOtherContacts(users):
22708
22893
  entityType = Ent.USER
22709
22894
  entityTypeName = Ent.Singular(entityType)
@@ -22733,6 +22918,8 @@ def printShowUserPeopleOtherContacts(users):
22733
22918
  else:
22734
22919
  FJQC.GetFormatJSONQuoteChar(myarg, True)
22735
22920
  if countsOnly:
22921
+ if csvPF:
22922
+ csvPF.SetFormatJSON(False)
22736
22923
  fieldsList = ['emailAddresses']
22737
22924
  fields = _getPersonFields(PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
22738
22925
  i, count, users = getEntityArgument(users)
@@ -22802,6 +22989,8 @@ def _printShowPeople(source):
22802
22989
  function = 'searchDirectoryPeople'
22803
22990
  else:
22804
22991
  FJQC.GetFormatJSONQuoteChar(myarg, True)
22992
+ if countsOnly and csvPF:
22993
+ csvPF.SetFormatJSON(False)
22805
22994
  fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
22806
22995
  printGettingAllEntityItemsForWhom(peopleEntityType, GC.Values[GC.DOMAIN], query=kwargs.get('query'))
22807
22996
  try:
@@ -22839,29 +23028,24 @@ def doInfoDomainPeopleContacts():
22839
23028
  # gam print people|peopleprofile [todrive <ToDriveAttribute>*]
22840
23029
  # [query <String>]
22841
23030
  # [mergesources <PeopleMergeSourceName>]
22842
- # [countsonly]
22843
23031
  # [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
22844
- # [formatjson [quotechar <Character>]]
23032
+ # [countsonly|(formatjson [quotechar <Character>])]
22845
23033
  # gam show people|peopleprofile
22846
23034
  # [query <String>]
22847
23035
  # [mergesources <PeopleMergeSourceName>]
22848
- # [countsonly]
22849
23036
  # [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
22850
- # [formatjson]
23037
+ # [countsonlyformatjson]
22851
23038
  # gam print domaincontacts|peoplecontacts [todrive <ToDriveAttribute>*]
22852
23039
  # [sources <PeopleSourceName>]
22853
23040
  # [query <String>]
22854
23041
  # [mergesources <PeopleMergeSourceName>]
22855
- # [countsonly]
22856
23042
  # [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
22857
- # [formatjson [quotechar <Character>]]
23043
+ # [countsonly|(formatjson [quotechar <Character>])]
22858
23044
  # gam show domaincontacts|peoplecontacts
22859
23045
  # [sources <PeopleSourceName>]
22860
23046
  # [query <String>]
22861
23047
  # [mergesources <PeopleMergeSourceName>]
22862
- # [countsonly]
22863
- # [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
22864
- # [formatjson]
23048
+ # [countsonlyformatjson]
22865
23049
  def doPrintShowDomainPeopleProfiles():
22866
23050
  _printShowPeople('profile')
22867
23051
 
@@ -23848,7 +24032,7 @@ def _filterActiveTimeRanges(cros, selected, listLimit, startDate, endDate, activ
23848
24032
  activeTimeRanges.reverse()
23849
24033
  i = 0
23850
24034
  for item in activeTimeRanges:
23851
- activityDate = datetime.datetime.strptime(item['date'], YYYYMMDD_FORMAT)
24035
+ activityDate = arrow.Arrow.strptime(item['date'], YYYYMMDD_FORMAT)
23852
24036
  if ((startDate is None) or (activityDate >= startDate)) and ((endDate is None) or (activityDate <= endDate)):
23853
24037
  item['duration'] = formatMilliSeconds(item['activeTime'])
23854
24038
  item['minutes'] = item['activeTime']//60000
@@ -23867,7 +24051,7 @@ def _filterDeviceFiles(cros, selected, listLimit, startTime, endTime):
23867
24051
  filteredItems = []
23868
24052
  i = 0
23869
24053
  for item in cros.get('deviceFiles', []):
23870
- timeValue, _ = iso8601.parse_date(item['createTime'])
24054
+ timeValue = arrow.get(item['createTime'])
23871
24055
  if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
23872
24056
  item['createTime'] = formatLocalTime(item['createTime'])
23873
24057
  filteredItems.append(item)
@@ -23884,7 +24068,7 @@ def _filterCPUStatusReports(cros, selected, listLimit, startTime, endTime):
23884
24068
  filteredItems = []
23885
24069
  i = 0
23886
24070
  for item in cros.get('cpuStatusReports', []):
23887
- timeValue, _ = iso8601.parse_date(item['reportTime'])
24071
+ timeValue = arrow.get(item['reportTime'])
23888
24072
  if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
23889
24073
  item['reportTime'] = formatLocalTime(item['reportTime'])
23890
24074
  for tempInfo in item.get('cpuTemperatureInfo', []):
@@ -23905,7 +24089,7 @@ def _filterSystemRamFreeReports(cros, selected, listLimit, startTime, endTime):
23905
24089
  filteredItems = []
23906
24090
  i = 0
23907
24091
  for item in cros.get('systemRamFreeReports', []):
23908
- timeValue, _ = iso8601.parse_date(item['reportTime'])
24092
+ timeValue = arrow.get(item['reportTime'])
23909
24093
  if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
23910
24094
  item['reportTime'] = formatLocalTime(item['reportTime'])
23911
24095
  item['systemRamFreeInfo'] = ','.join([str(x) for x in item['systemRamFreeInfo']])
@@ -23938,7 +24122,7 @@ def _filterScreenshotFiles(cros, selected, listLimit, startTime, endTime):
23938
24122
  filteredItems = []
23939
24123
  i = 0
23940
24124
  for item in cros.get('screenshotFiles', []):
23941
- timeValue, _ = iso8601.parse_date(item['createTime'])
24125
+ timeValue = arrow.get(item['createTime'])
23942
24126
  if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
23943
24127
  item['createTime'] = formatLocalTime(item['createTime'])
23944
24128
  filteredItems.append(item)
@@ -23975,7 +24159,7 @@ def _computeDVRstorageFreePercentage(cros):
23975
24159
 
23976
24160
  def _getFilterDateTime():
23977
24161
  filterDate = getYYYYMMDD(returnDateTime=True)
23978
- return (filterDate, filterDate.replace(tzinfo=iso8601.UTC))
24162
+ return (filterDate, filterDate.replace(tzinfo='UTC'))
23979
24163
 
23980
24164
  CROS_FIELDS_CHOICE_MAP = {
23981
24165
  'activetimeranges': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
@@ -24093,6 +24277,7 @@ CROS_LIST_FIELDS_CHOICE_MAP = {
24093
24277
 
24094
24278
  CROS_TIME_OBJECTS = {
24095
24279
  'createTime',
24280
+ 'extendedSupportStart',
24096
24281
  'firstEnrollmentTime',
24097
24282
  'lastDeprovisionTimestamp',
24098
24283
  'lastEnrollmentTime',
@@ -24386,9 +24571,9 @@ def getDeviceFilesEntity():
24386
24571
  else:
24387
24572
  for timeItem in myarg.split(','):
24388
24573
  try:
24389
- timestamp, _ = iso8601.parse_date(timeItem)
24574
+ timestamp = arrow.get(timeItem)
24390
24575
  deviceFilesEntity['list'].append(ISOformatTimeStamp(timestamp.astimezone(GC.Values[GC.TIMEZONE])))
24391
- except (iso8601.ParseError, OverflowError):
24576
+ except (arrow.parser.ParserError, OverflowError):
24392
24577
  Cmd.Backup()
24393
24578
  invalidArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
24394
24579
  return deviceFilesEntity
@@ -24415,14 +24600,14 @@ def _selectDeviceFiles(deviceId, deviceFiles, deviceFilesEntity):
24415
24600
  count = 0
24416
24601
  if deviceFilesEntity['time'][0] == 'before':
24417
24602
  for deviceFile in deviceFiles:
24418
- createTime, _ = iso8601.parse_date(deviceFile['createTime'])
24603
+ createTime = arrow.get(deviceFile['createTime'])
24419
24604
  if createTime >= dateTime:
24420
24605
  break
24421
24606
  count += 1
24422
24607
  return deviceFiles[:count]
24423
24608
  # if deviceFilesEntity['time'][0] == 'after':
24424
24609
  for deviceFile in deviceFiles:
24425
- createTime, _ = iso8601.parse_date(deviceFile['createTime'])
24610
+ createTime = arrow.get(deviceFile['createTime'])
24426
24611
  if createTime >= dateTime:
24427
24612
  break
24428
24613
  count += 1
@@ -24431,14 +24616,14 @@ def _selectDeviceFiles(deviceId, deviceFiles, deviceFilesEntity):
24431
24616
  dateTime = deviceFilesEntity['range'][1]
24432
24617
  spos = 0
24433
24618
  for deviceFile in deviceFiles:
24434
- createTime, _ = iso8601.parse_date(deviceFile['createTime'])
24619
+ createTime = arrow.get(deviceFile['createTime'])
24435
24620
  if createTime >= dateTime:
24436
24621
  break
24437
24622
  spos += 1
24438
24623
  dateTime = deviceFilesEntity['range'][2]
24439
24624
  epos = spos
24440
24625
  for deviceFile in deviceFiles[spos:]:
24441
- createTime, _ = iso8601.parse_date(deviceFile['createTime'])
24626
+ createTime = arrow.get(deviceFile['createTime'])
24442
24627
  if createTime >= dateTime:
24443
24628
  break
24444
24629
  epos += 1
@@ -25221,7 +25406,7 @@ def doInfoPrintShowCrOSTelemetry():
25221
25406
  i = 0
25222
25407
  for item in listItems:
25223
25408
  if 'reportTime' in item:
25224
- timeValue, _ = iso8601.parse_date(item['reportTime'])
25409
+ timeValue = arrow.get(item['reportTime'])
25225
25410
  else:
25226
25411
  timeValue = None
25227
25412
  if (timeValue is None) or (((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime))):
@@ -26372,14 +26557,18 @@ CHAT_TIME_OBJECTS = {'createTime', 'deleteTime', 'eventTime', 'lastActiveTime',
26372
26557
  def _showChatItem(citem, entityType, FJQC, i=0, count=0):
26373
26558
  if entityType == Ent.CHAT_SPACE:
26374
26559
  _cleanChatSpace(citem)
26560
+ dictObjectsKey = {None: 'displayName'}
26375
26561
  elif entityType == Ent.CHAT_MESSAGE:
26376
26562
  _cleanChatMessage(citem)
26563
+ dictObjectsKey = {None: 'text'}
26564
+ else:
26565
+ dictObjectsKey={}
26377
26566
  if FJQC.formatJSON:
26378
26567
  printLine(json.dumps(cleanJSON(citem, timeObjects=CHAT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
26379
26568
  return
26380
26569
  printEntity([entityType, citem['name']], i, count)
26381
26570
  Ind.Increment()
26382
- showJSON(None, citem, timeObjects=CHAT_TIME_OBJECTS)
26571
+ showJSON(None, citem, timeObjects=CHAT_TIME_OBJECTS, dictObjectsKey=dictObjectsKey)
26383
26572
  Ind.Decrement()
26384
26573
 
26385
26574
  def _printChatItem(user, citem, parent, entityType, csvPF, FJQC, addCSVData=None):
@@ -26568,7 +26757,7 @@ def printShowChatEmojis(users):
26568
26757
  pageMessage=_getChatPageMessage(Ent.CHAT_EMOJI, user, i, count, pfilter),
26569
26758
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
26570
26759
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
26571
- pageSize=CHAT_PAGE_SIZE, filter=pfilter)
26760
+ pageSize=GC.Values[GC.CHAT_MAX_RESULTS], filter=pfilter)
26572
26761
  except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
26573
26762
  exitIfChatNotConfigured(chat, kvList, str(e), i, count)
26574
26763
  continue
@@ -26638,7 +26827,14 @@ def getChatSpaceParameters(myarg, body, typeChoicesMap, updateMask):
26638
26827
 
26639
26828
  CHAT_MEMBER_ROLE_MAP = {
26640
26829
  'member': 'ROLE_MEMBER',
26641
- 'manager': 'ROLE_MANAGER'
26830
+ 'manager': 'ROLE_ASSISTANT_MANAGER',
26831
+ 'owner': 'ROLE_MANAGER',
26832
+ }
26833
+
26834
+ CHAT_ROLE_ENTITY_TYPE_MAP = {
26835
+ 'ROLE_MEMBER': Ent.CHAT_MEMBER_USER,
26836
+ 'ROLE_ASSISTANT_MANAGER': Ent.CHAT_MANAGER_USER,
26837
+ 'ROLE_MANAGER': Ent.CHAT_OWNER_USER,
26642
26838
  }
26643
26839
 
26644
26840
  CHAT_MEMBER_TYPE_MAP = {
@@ -26772,7 +26968,8 @@ CHAT_UPDATE_SPACE_TYPE_MAP = {
26772
26968
  }
26773
26969
 
26774
26970
  CHAT_SPACE_ROLE_PERMISSIONS_MAP = {
26775
- 'managers': 'managersAllowed',
26971
+ 'owners': 'managersAllowed',
26972
+ 'managers': 'assistantManagersAllowed',
26776
26973
  'members': 'membersAllowed',
26777
26974
  }
26778
26975
 
@@ -26792,13 +26989,13 @@ CHAT_UPDATE_SPACE_PERMISSIONS_MAP = {
26792
26989
  # [type space]
26793
26990
  # [description <String>] [guidelines|rules <String>]
26794
26991
  # [history <Boolean>])
26795
- # managemembersandgroups managers|members
26796
- # modifyspacedetails managers|members
26797
- # togglehistory managers|members
26798
- # useatmentionall managers|members
26799
- # manageapps managers|members
26800
- # managewebhooks managers|members
26801
- # replymessages managers|members
26992
+ # [managemembersandgroups owners|managers|members]
26993
+ # [modifyspacedetails owners|managers|members]
26994
+ # [togglehistory owners|managers|members]
26995
+ # [useatmentionall owners|managers|members]
26996
+ # [manageapps owners|managers|members]
26997
+ # [managewebhooks owners|managers|members]
26998
+ 2# [replymessages owners|managers|members]
26802
26999
  # [formatjson]
26803
27000
  def updateChatSpace(users):
26804
27001
  FJQC = FormatJSONQuoteChar()
@@ -26816,9 +27013,13 @@ def updateChatSpace(users):
26816
27013
  body.setdefault('permissionSettings', {})
26817
27014
  permissionSetting = CHAT_UPDATE_SPACE_PERMISSIONS_MAP[myarg]
26818
27015
  role = getChoice(CHAT_SPACE_ROLE_PERMISSIONS_MAP, mapChoice=True)
26819
- body['permissionSettings'][permissionSetting] = {'managersAllowed': True}
27016
+ body['permissionSettings'][permissionSetting] = {}
27017
+ body['permissionSettings'][permissionSetting][role] = True
26820
27018
  if role == 'membersAllowed':
26821
- body['permissionSettings'][permissionSetting].update({'membersAllowed': True})
27019
+ body['permissionSettings'][permissionSetting]['assistantManagersAllowed'] = True
27020
+ body['permissionSettings'][permissionSetting]['managersAllowed'] = True
27021
+ elif role == 'assistantManagersAllowed':
27022
+ body['permissionSettings'][permissionSetting]['managersAllowed'] = True
26822
27023
  updateMask.add(f'permissionSettings.{permissionSetting}')
26823
27024
  else:
26824
27025
  FJQC.GetFormatJSON(myarg)
@@ -26995,7 +27196,6 @@ def _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
26995
27196
  return False
26996
27197
  return True
26997
27198
 
26998
- CHAT_PAGE_SIZE = 1000
26999
27199
  CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP = {
27000
27200
  'createtime': 'createTime',
27001
27201
  'lastactivetime': 'lastActiveTime',
@@ -27075,7 +27275,7 @@ def printShowChatSpaces(users):
27075
27275
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
27076
27276
  GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
27077
27277
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
27078
- fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargsCS)
27278
+ fields=fields, pageSize=GC.Values[GC.CHAT_MAX_RESULTS], **kwargsCS)
27079
27279
  if showAccessSettings:
27080
27280
  for space in spaces:
27081
27281
  if space['spaceType'] == 'SPACE':
@@ -27141,7 +27341,7 @@ def _getChatSpaceMembers(cd, chatSpace, ciGroupName):
27141
27341
  pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, 0, 0, qfilter),
27142
27342
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
27143
27343
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
27144
- parent=chatSpace, fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargsUAA)
27344
+ parent=chatSpace, fields=fields, pageSize=GC.Values[GC.CHAT_MAX_RESULTS], **kwargsUAA)
27145
27345
  for member in members:
27146
27346
  _getChatMemberEmail(cd, member)
27147
27347
  gmember = {}
@@ -27193,12 +27393,12 @@ def getGroupMemberID(cd, group, groupList):
27193
27393
  groupList.append(convertEmailAddressToUID(group, cd, emailType='group'))
27194
27394
 
27195
27395
  # gam <UserTypeEntity> create chatmember <ChatSpace>
27196
- # [type human|bot] [role member|manager]
27396
+ # [type human|bot] [role member|manager|owner]
27197
27397
  # (user <UserItem>)* (members <UserTypeEntity>)*
27198
27398
  # (group <GroupItem>)* (groups <GroupEntity>)*
27199
27399
  # [formatjson|returnidonly]
27200
27400
  # gam <UserItem> create chatmember asadmin <ChatSpace>
27201
- # [type human|bot] [role member|manager]
27401
+ # [type human|bot] [role member|manager|owner]
27202
27402
  # (user <UserItem>)* (members <UserTypeEntity>)*
27203
27403
  # (group <GroupItem>)* (groups <GroupEntity>)*
27204
27404
  # [formatjson|returnidonly]
@@ -27220,7 +27420,7 @@ def createChatMember(users):
27220
27420
  throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
27221
27421
  GAPI.INTERNAL_ERROR, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
27222
27422
  parent=parent, body=body, **kwargsUAA)
27223
- if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
27423
+ if role != 'ROLE_MEMBER' and entityType in (Ent.CHAT_MANAGER_USER, Ent.CHAT_OWNER_USER):
27224
27424
  member = callGAPI(chat.spaces().members(), 'patch',
27225
27425
  bailOnInternalError=True,
27226
27426
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
@@ -27278,7 +27478,7 @@ def createChatMember(users):
27278
27478
  missingArgumentExit('space')
27279
27479
  if not userList and not groupList:
27280
27480
  missingArgumentExit('user|members|group|groups')
27281
- userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
27481
+ userEntityType = CHAT_ROLE_ENTITY_TYPE_MAP[role]
27282
27482
  userMembers = []
27283
27483
  for user in userList:
27284
27484
  userMembers.append({'member': {'name': f'users/{user}', 'type': mtype}})
@@ -27327,16 +27527,16 @@ def _deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA):
27327
27527
  # gam <UserItem> remove chatmember asadmin
27328
27528
  # members <ChatMemberList>
27329
27529
  # gam <UserTypeEntity> update chatmember <ChatSpace>
27330
- # role member|manager
27530
+ # role member|manager|owner
27331
27531
  # ((user <UserItem>)|(members <UserTypeEntity>))+
27332
27532
  # gam <UserTypeEntity> modify chatmember
27333
- # role member|manager
27533
+ # role member|manager|owner
27334
27534
  # members <ChatMemberList>
27335
27535
  # gam <UserItem> update chatmember asadmin<ChatSpace>
27336
- # role member|manager
27536
+ # role member|manager|owner
27337
27537
  # ((user <UserItem>)|(members <UserTypeEntity>))+
27338
27538
  # gam <UserItem> modify chatmember asadmin
27339
- # role member|manager
27539
+ # role member|manager|owner
27340
27540
  # members <ChatMemberList>
27341
27541
  def deleteUpdateChatMember(users):
27342
27542
  cd = buildGAPIObject(API.DIRECTORY)
@@ -27430,7 +27630,7 @@ def deleteUpdateChatMember(users):
27430
27630
  CHAT_SYNC_PREVIEW_TITLES = ['space', 'member', 'role', 'action', 'message']
27431
27631
 
27432
27632
  # gam <UserTypeEntity> sync chatmembers [asadmin] <ChatSpace>
27433
- # [role member|manager] [type human|bot]
27633
+ # [role member|manager|owner] [type human|bot]
27434
27634
  # [addonly|removeonly]
27435
27635
  # [preview [actioncsv]]
27436
27636
  # (users <UserTypeEntity>)* (groups <GroupEntity>)*
@@ -27535,7 +27735,7 @@ def syncChatMembers(users):
27535
27735
  unknownArgumentExit()
27536
27736
  if not parent:
27537
27737
  missingArgumentExit('space')
27538
- userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
27738
+ userEntityType = CHAT_ROLE_ENTITY_TYPE_MAP[role]
27539
27739
  userMembers = {}
27540
27740
  syncUsersSet = set()
27541
27741
  for user in userList:
@@ -27565,7 +27765,7 @@ def syncChatMembers(users):
27565
27765
  pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, i, count, qfilter),
27566
27766
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
27567
27767
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
27568
- parent=parent, showGroups=groupsSpecified, pageSize=CHAT_PAGE_SIZE, **kwargs, **kwargsUAA)
27768
+ parent=parent, showGroups=groupsSpecified, pageSize=GC.Values[GC.CHAT_MAX_RESULTS], **kwargs, **kwargsUAA)
27569
27769
  for member in members:
27570
27770
  if 'member' in member:
27571
27771
  if member['member']['type'] == mtype and member['role'] == role:
@@ -27766,7 +27966,7 @@ def printShowChatMembers(users):
27766
27966
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
27767
27967
  GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
27768
27968
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
27769
- fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=CHAT_PAGE_SIZE,
27969
+ fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=GC.Values[GC.CHAT_MAX_RESULTS],
27770
27970
  **kwargsCS)
27771
27971
  for space in sorted(spaces, key=lambda k: k[sortName]):
27772
27972
  if space['spaceType'] == 'SPACE' and 'membershipCount' in space:
@@ -27783,7 +27983,7 @@ def printShowChatMembers(users):
27783
27983
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
27784
27984
  GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
27785
27985
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
27786
- fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=CHAT_PAGE_SIZE,
27986
+ fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=GC.Values[GC.CHAT_MAX_RESULTS],
27787
27987
  **kwargsCS)
27788
27988
  for space in sorted(spaces, key=lambda k: k[sortName]):
27789
27989
  # if 'membershipCount' in space:
@@ -27814,7 +28014,7 @@ def printShowChatMembers(users):
27814
28014
  pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, j, jcount, qfilter),
27815
28015
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
27816
28016
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
27817
- parent=parentName, fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargs, **kwargsUAA)
28017
+ parent=parentName, fields=fields, pageSize=GC.Values[GC.CHAT_MAX_RESULTS], **kwargs, **kwargsUAA)
27818
28018
  for member in members:
27819
28019
  _getChatMemberEmail(cd, member)
27820
28020
  except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
@@ -27846,10 +28046,11 @@ def _getChatSenderEmail(cd, sender):
27846
28046
  sender['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{senderUid}', cd, None, emailTypes=['user'])
27847
28047
 
27848
28048
  def trimChatMessageIfRequired(body):
27849
- msgLen = len(body['text'])
27850
- if msgLen > 4096:
27851
- stderrWarningMsg(Msg.TRIMMED_MESSAGE_FROM_LENGTH_TO_MAXIMUM.format(msgLen, 4096))
27852
- body['text'] = body['text'][:4095]
28049
+ if 'text' in body:
28050
+ msgLen = len(body['text'])
28051
+ if msgLen > 4096:
28052
+ stderrWarningMsg(Msg.TRIMMED_MESSAGE_FROM_LENGTH_TO_MAXIMUM.format(msgLen, 4096))
28053
+ body['text'] = body['text'][:4095]
27853
28054
 
27854
28055
  CHAT_MESSAGE_REPLY_OPTION_MAP = {
27855
28056
  'fail': 'REPLY_MESSAGE_OR_FAIL',
@@ -27926,22 +28127,29 @@ def doCreateChatMessage():
27926
28127
  createChatMessage([None])
27927
28128
 
27928
28129
  # gam [<UserTypeMessage>] update chatmessage name <ChatMessage>
27929
- # <ChatContent>
28130
+ # [<ChatContent>] [clearattachments <String>]
27930
28131
  def updateChatMessage(users):
27931
28132
  name = None
27932
28133
  body = {}
28134
+ updateMask = []
28135
+ clearMsg = ''
27933
28136
  while Cmd.ArgumentsRemaining():
27934
28137
  myarg = getArgument()
27935
28138
  if myarg == 'name':
27936
28139
  name = getString(Cmd.OB_CHAT_MESSAGE)
27937
28140
  elif myarg in SORF_TEXT_ARGUMENTS:
27938
28141
  body['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
28142
+ updateMask.append('text')
28143
+ elif myarg == 'clearattachments':
28144
+ clearMsg = getString(Cmd.OB_STRING, minLen=0)
28145
+ body['attachment'] = []
28146
+ updateMask.append('attachment')
27939
28147
  else:
27940
28148
  unknownArgumentExit()
27941
28149
  if not name:
27942
28150
  missingArgumentExit('name')
27943
- if 'text' not in body:
27944
- missingArgumentExit('text or textfile')
28151
+ if not updateMask:
28152
+ missingArgumentExit('text|textfile|clearattachments')
27945
28153
  trimChatMessageIfRequired(body)
27946
28154
  i, count, users = getEntityArgument(users)
27947
28155
  for user in users:
@@ -27950,9 +28158,19 @@ def updateChatMessage(users):
27950
28158
  if not chat:
27951
28159
  continue
27952
28160
  try:
28161
+ if 'attachment' in updateMask and 'text' not in updateMask:
28162
+ resp = callGAPI(chat.spaces().messages(), 'get',
28163
+ throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
28164
+ name=name, fields='text')
28165
+ body['text'] = resp.get('text', '')
28166
+ if clearMsg:
28167
+ body['text'] += clearMsg
28168
+ elif not body['text']:
28169
+ body['text'] = 'Attachments cleared'
28170
+ updateMask.append('text')
27953
28171
  resp = callGAPI(chat.spaces().messages(), 'patch',
27954
28172
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
27955
- name=name, updateMask='text', body=body)
28173
+ name=name, updateMask=','.join(updateMask), body=body)
27956
28174
  kvList.extend([Ent.CHAT_THREAD, resp['thread']['name']])
27957
28175
  entityActionPerformed(kvList, i, count)
27958
28176
  except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
@@ -28128,7 +28346,7 @@ def printShowChatMessages(users):
28128
28346
  pageMessage=_getChatPageMessage(Ent.CHAT_MESSAGE, user, i, count, qfilter),
28129
28347
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
28130
28348
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
28131
- pageSize=CHAT_PAGE_SIZE, parent=parentName, filter=pfilter, showDeleted=showDeleted,
28349
+ pageSize=GC.Values[GC.CHAT_MAX_RESULTS], parent=parentName, filter=pfilter, showDeleted=showDeleted,
28132
28350
  fields=fields)
28133
28351
  for message in messages:
28134
28352
  if 'sender' in message:
@@ -28246,7 +28464,7 @@ def printShowChatEvents(users):
28246
28464
  pageMessage=_getChatPageMessage(Ent.CHAT_EVENT, user, i, count, qfilter),
28247
28465
  throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
28248
28466
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
28249
- pageSize=CHAT_PAGE_SIZE, parent=parentName, filter=pfilter)
28467
+ pageSize=GC.Values[GC.CHAT_MAX_RESULTS], parent=parentName, filter=pfilter)
28250
28468
  except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
28251
28469
  exitIfChatNotConfigured(chat, kvList, str(e), i, count)
28252
28470
  continue
@@ -31701,6 +31919,65 @@ def doPrintShowChromeNeedsAttn():
31701
31919
  if csvPF:
31702
31920
  csvPF.writeCSVfile('Chrome Devices Needing Attention')
31703
31921
 
31922
+ CHROME_DEVICE_COUNTS_MODE_CHOICES = ['active', 'perboottype', 'perreleasechannel']
31923
+ CHROME_DEVICE_COUNTS_MODE_FUNCTIONS = {
31924
+ 'active': 'countActiveDevices',
31925
+ 'perboottype': 'countDevicesPerBootType',
31926
+ 'perreleasechannel': 'countDevicesPerReleaseChannel'
31927
+ }
31928
+ CHROME_DEVICE_COUNTS_MODE_CSV_TITLE = {
31929
+ 'active': 'Chrome Active Devices',
31930
+ 'perboottype': 'Chrome Devices per Boot Type',
31931
+ 'perreleasechannel': 'Chrome Devices per Release Channel'
31932
+ }
31933
+
31934
+ # gam print chromedevicecounts [todrive <ToDriveAttribute>*]
31935
+ # [mode active|perboottype|perreleasechannel] [date <Date>]
31936
+ # [formatjson [quotechar <Character>]]
31937
+ # gam show chromedevicecounts
31938
+ # [mode active|perboottype|perreleasechannel] [date <Date>]
31939
+ # [formatjson]
31940
+ def doPrintShowChromeDeviceCounts():
31941
+ cm = buildGAPIObject(API.CHROMEMANAGEMENT)
31942
+ customerId = _getCustomersCustomerIdWithC()
31943
+ csvPF = CSVPrintFile() if Act.csvFormat() else None
31944
+ FJQC = FormatJSONQuoteChar(csvPF)
31945
+ pdate = todaysDate()
31946
+ mode = 'active'
31947
+ while Cmd.ArgumentsRemaining():
31948
+ myarg = getArgument()
31949
+ if csvPF and myarg == 'todrive':
31950
+ csvPF.GetTodriveParameters()
31951
+ elif myarg == 'mode':
31952
+ mode = getChoice(CHROME_DEVICE_COUNTS_MODE_CHOICES)
31953
+ elif myarg == 'date':
31954
+ pdate = getYYYYMMDD(returnDateTime=True)
31955
+ else:
31956
+ FJQC.GetFormatJSONQuoteChar(myarg, True)
31957
+ kwargs = {'date_day': pdate.day, 'date_month': pdate.month, 'date_year': pdate.year}
31958
+ try:
31959
+ counts = callGAPI(cm.customers().reports(), CHROME_DEVICE_COUNTS_MODE_FUNCTIONS[mode],
31960
+ throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
31961
+ retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
31962
+ customer=customerId, **kwargs)
31963
+ except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
31964
+ entityActionFailedWarning([Ent.CHROME_DEVICE_COUNT, None], str(e))
31965
+ return
31966
+ for k, v in counts.items():
31967
+ counts[k] = int(v)
31968
+ if not csvPF:
31969
+ if not FJQC.formatJSON:
31970
+ showJSON(CHROME_DEVICE_COUNTS_MODE_CSV_TITLE[mode], counts)
31971
+ else:
31972
+ printLine(json.dumps(cleanJSON(counts), ensure_ascii=False, sort_keys=True))
31973
+ else:
31974
+ row = flattenJSON(counts)
31975
+ if not FJQC.formatJSON:
31976
+ csvPF.WriteRowTitles(row)
31977
+ elif csvPF.CheckRowTitles(row):
31978
+ csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(counts), ensure_ascii=False, sort_keys=True)})
31979
+ csvPF.writeCSVfile(CHROME_DEVICE_COUNTS_MODE_CSV_TITLE[mode])
31980
+
31704
31981
  CHROME_VERSIONS_TITLES = ['channel', 'system', 'deviceOsVersion']
31705
31982
 
31706
31983
  # gam print chromeversions [todrive <ToDriveAttribute>*]
@@ -32861,6 +33138,8 @@ def doCreateGroup(ciGroupsAPI=False):
32861
33138
  initialGroupConfig = 'WITH_INITIAL_OWNER'
32862
33139
  elif ciGroupsAPI and myarg in {'security', 'makesecuritygroup'}:
32863
33140
  body['labels'][CIGROUP_SECURITY_LABEL] = ''
33141
+ elif ciGroupsAPI and myarg in ['locked']:
33142
+ body['labels'][CIGROUP_LOCKED_LABEL] = ''
32864
33143
  elif myarg == 'verifynotinvitable':
32865
33144
  verifyNotInvitable = True
32866
33145
  else:
@@ -35752,7 +36031,7 @@ def doPrintShowGroupTree():
35752
36031
  # gam create cigroup <EmailAddress>
35753
36032
  # [copyfrom <GroupItem>] <GroupAttribute>
35754
36033
  # [makeowner] [alias|aliases <CIGroupAliasList>]
35755
- # [security|makesecuritygroup]
36034
+ # [security|makesecuritygroup] [locked]
35756
36035
  # [dynamic <QueryDynamicGroup>]
35757
36036
  def doCreateCIGroup():
35758
36037
  doCreateGroup(ciGroupsAPI=True)
@@ -35936,7 +36215,6 @@ def doUpdateCIGroups():
35936
36215
 
35937
36216
  cd = buildGAPIObject(API.DIRECTORY)
35938
36217
  ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
35939
- cib = None
35940
36218
  entityType = Ent.CLOUD_IDENTITY_GROUP
35941
36219
  csvPF = None
35942
36220
  getBeforeUpdate = preview = False
@@ -36045,7 +36323,6 @@ def doUpdateCIGroups():
36045
36323
  _, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
36046
36324
  if not name:
36047
36325
  continue
36048
- cipl = ci
36049
36326
  twoUpdates = False
36050
36327
  if 'labels' in ci_body or lockGroup is not None:
36051
36328
  try:
@@ -36074,20 +36351,16 @@ def doUpdateCIGroups():
36074
36351
  else:
36075
36352
  if CIGROUP_LOCKED_LABEL in ci_body['labels']:
36076
36353
  ci_body['labels'].pop(CIGROUP_LOCKED_LABEL)
36077
- if CIGROUP_LOCKED_LABEL in ci_body['labels']:
36078
- if cib is None:
36079
- cib = buildGAPIObject(API.CLOUDIDENTITY_GROUPS_BETA)
36080
- cipl = cib
36081
36354
  if ci_body:
36082
36355
  try:
36083
36356
  if twoUpdates:
36084
36357
  ci_body['labels'].pop(CIGROUP_LOCKED_LABEL)
36085
- callGAPI(cipl.groups(), 'patch',
36358
+ callGAPI(ci.groups(), 'patch',
36086
36359
  throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS,
36087
36360
  retryReasons=GAPI.CIGROUP_RETRY_REASONS,
36088
36361
  name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
36089
36362
  ci_body['labels'][CIGROUP_LOCKED_LABEL] = ''
36090
- callGAPI(cipl.groups(), 'patch',
36363
+ callGAPI(ci.groups(), 'patch',
36091
36364
  throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS,
36092
36365
  retryReasons=GAPI.CIGROUP_RETRY_REASONS,
36093
36366
  name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
@@ -37071,20 +37344,17 @@ def doPrintCIGroups():
37071
37344
  else:
37072
37345
  getFullFieldsList = list(CIGROUP_FULL_FIELDS)
37073
37346
  getFullFields = ','.join(getFullFieldsList)#
37074
- cipl = ci
37075
37347
  if query:
37076
37348
  method = 'search'
37077
37349
  if 'parent' not in query:
37078
37350
  query += f" && parent == '{parent}'"
37079
37351
  kwargs = {'query': query}
37080
- if CIGROUP_LOCKED_LABEL in query:
37081
- cipl = buildGAPIObject(API.CLOUDIDENTITY_GROUPS_BETA)
37082
37352
  else:
37083
37353
  method = 'list'
37084
37354
  kwargs = {'parent': parent}
37085
37355
  printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, query)
37086
37356
  try:
37087
- entityList = callGAPIpages(cipl.groups(), method, 'groups',
37357
+ entityList = callGAPIpages(ci.groups(), method, 'groups',
37088
37358
  pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
37089
37359
  throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
37090
37360
  view='FULL', fields=fieldsnp, pageSize=pageSize, **kwargs)
@@ -40062,15 +40332,15 @@ def _getEventDaysOfWeek(event):
40062
40332
  if attr in event:
40063
40333
  if 'date' in event[attr]:
40064
40334
  try:
40065
- dateTime = datetime.datetime.strptime(event[attr]['date'], YYYYMMDD_FORMAT)
40066
- event[attr]['dayOfWeek'] = calendarlib.day_abbr[dateTime.weekday()]
40335
+ dateTime = arrow.Arrow.strptime(event[attr]['date'], YYYYMMDD_FORMAT)
40336
+ event[attr]['dayOfWeek'] = DAYS_OF_WEEK[dateTime.weekday()]
40067
40337
  except ValueError:
40068
40338
  pass
40069
40339
  elif 'dateTime' in event[attr]:
40070
40340
  try:
40071
- dateTime, _ = iso8601.parse_date(event[attr]['dateTime'])
40072
- event[attr]['dayOfWeek'] = calendarlib.day_abbr[dateTime.weekday()]
40073
- except (iso8601.ParseError, OverflowError):
40341
+ dateTime = arrow.get(event[attr]['dateTime'])
40342
+ event[attr]['dayOfWeek'] = DAYS_OF_WEEK[dateTime.weekday()]
40343
+ except (arrow.parser.ParserError, OverflowError):
40074
40344
  pass
40075
40345
 
40076
40346
  def _createCalendarEvents(user, origCal, function, calIds, count, body, parameters):
@@ -40108,7 +40378,7 @@ def _createCalendarEvents(user, origCal, function, calIds, count, body, paramete
40108
40378
  else:
40109
40379
  if parameters['showDayOfWeek']:
40110
40380
  _getEventDaysOfWeek(event)
40111
- _printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'])
40381
+ _printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'], {})
40112
40382
  except (GAPI.invalid, GAPI.required, GAPI.timeRangeEmpty, GAPI.eventDurationExceedsLimit,
40113
40383
  GAPI.requiredAccessLevel, GAPI.participantIsNeitherOrganizerNorAttendee,
40114
40384
  GAPI.malformedWorkingLocationEvent, GAPI.badRequest) as e:
@@ -40212,7 +40482,7 @@ def _updateCalendarEvents(origUser, user, origCal, calIds, count, calendarEventE
40212
40482
  else:
40213
40483
  if parameters['showDayOfWeek']:
40214
40484
  _getEventDaysOfWeek(event)
40215
- _printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'])
40485
+ _printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'], {})
40216
40486
  except (GAPI.notFound, GAPI.deleted) as e:
40217
40487
  if not checkCalendarExists(cal, calId, j, jcount):
40218
40488
  entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
@@ -40509,10 +40779,12 @@ def _showCalendarEvent(primaryEmail, calId, eventEntityType, event, k, kcount, F
40509
40779
  showJSON(None, event, skipObjects)
40510
40780
  Ind.Decrement()
40511
40781
 
40512
- def _printCalendarEvent(user, calId, event, csvPF, FJQC):
40782
+ def _printCalendarEvent(user, calId, event, csvPF, FJQC, addCSVData):
40513
40783
  row = {'calendarId': calId, 'id': event['id']}
40514
40784
  if user:
40515
40785
  row['primaryEmail'] = user
40786
+ if addCSVData:
40787
+ row.update(addCSVData)
40516
40788
  flattenJSON(event, flattened=row, timeObjects=EVENT_TIME_OBJECTS)
40517
40789
  if not FJQC.formatJSON:
40518
40790
  csvPF.WriteRowTitles(row)
@@ -40525,7 +40797,7 @@ def _printCalendarEvent(user, calId, event, csvPF, FJQC):
40525
40797
  csvPF.WriteRowNoFilter(row)
40526
40798
 
40527
40799
  def _printShowCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity,
40528
- csvPF, FJQC, fieldsList):
40800
+ csvPF, FJQC, fieldsList, addCSVData):
40529
40801
  i = 0
40530
40802
  for calId in calIds:
40531
40803
  i += 1
@@ -40551,7 +40823,7 @@ def _printShowCalendarEvents(origUser, user, origCal, calIds, count, calendarEve
40551
40823
  for event in events:
40552
40824
  if calendarEventEntity['showDayOfWeek']:
40553
40825
  _getEventDaysOfWeek(event)
40554
- _printCalendarEvent(user, calId, event, csvPF, FJQC)
40826
+ _printCalendarEvent(user, calId, event, csvPF, FJQC, addCSVData)
40555
40827
  elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT] and user:
40556
40828
  csvPF.WriteRowNoFilter({'calendarId': calId, 'primaryEmail': user, 'id': ''})
40557
40829
  else:
@@ -40569,6 +40841,8 @@ def _printShowCalendarEvents(origUser, user, origCal, calIds, count, calendarEve
40569
40841
  row = {'calendarId': calId}
40570
40842
  if user:
40571
40843
  row['primaryEmail'] = user
40844
+ if addCSVData:
40845
+ row.update(addCSVData)
40572
40846
  row['events'] = jcount
40573
40847
  if not calendarEventEntity['eventRowFilter']:
40574
40848
  csvPF.WriteRow(row)
@@ -40799,6 +41073,8 @@ def _getCalendarPrintShowEventOptions(calendarEventEntity, entityType):
40799
41073
  csvPF = CSVPrintFile(['primaryEmail', 'calendarId', 'id'] if entityType == Ent.USER else ['calendarId', 'id'], 'sortall', indexedTitles=EVENT_INDEXED_TITLES) if Act.csvFormat() else None
40800
41074
  FJQC = FormatJSONQuoteChar(csvPF)
40801
41075
  fieldsList = []
41076
+ addCSVData = {}
41077
+ addCSVDataLoc = 2 if entityType == Ent.USER else 1
40802
41078
  while Cmd.ArgumentsRemaining():
40803
41079
  myarg = getArgument()
40804
41080
  if csvPF and myarg == 'todrive':
@@ -40807,6 +41083,9 @@ def _getCalendarPrintShowEventOptions(calendarEventEntity, entityType):
40807
41083
  pass
40808
41084
  elif myarg == 'fields':
40809
41085
  _getEventFields(fieldsList)
41086
+ elif csvPF and myarg == 'addcsvdata':
41087
+ k = getString(Cmd.OB_STRING)
41088
+ addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
40810
41089
  elif myarg == 'countsonly':
40811
41090
  calendarEventEntity['countsOnly'] = True
40812
41091
  elif myarg == 'showdayofweek':
@@ -40819,27 +41098,35 @@ def _getCalendarPrintShowEventOptions(calendarEventEntity, entityType):
40819
41098
  fieldsList = ['id']
40820
41099
  if csvPF:
40821
41100
  if calendarEventEntity['countsOnly']:
41101
+ csvPF.SetFormatJSON(False)
40822
41102
  csvPF.RemoveTitles(['id'])
41103
+ if addCSVData:
41104
+ csvPF.InsertTitles(addCSVDataLoc, sorted(addCSVData.keys()))
40823
41105
  csvPF.AddTitles(['events'])
41106
+ csvPF.SetSortAllTitles()
40824
41107
  calendarEventEntity['countsOnlyTitles'] = csvPF.titlesList[:]
40825
- elif not FJQC.formatJSON and not fieldsList:
40826
- csvPF.AddSortTitles(EVENT_PRINT_ORDER)
41108
+ else:
41109
+ if addCSVData:
41110
+ csvPF.InsertTitles(addCSVDataLoc, sorted(addCSVData.keys()))
41111
+ if not FJQC.formatJSON and not fieldsList:
41112
+ csvPF.AddTitles(EVENT_PRINT_ORDER)
41113
+ csvPF.SetSortAllTitles()
40827
41114
  _addEventEntitySelectFields(calendarEventEntity, fieldsList)
40828
- return (csvPF, FJQC, fieldsList)
41115
+ return (csvPF, FJQC, fieldsList, addCSVData)
40829
41116
 
40830
41117
  # gam calendars <CalendarEntity> print events <EventEntity> <EventDisplayProperties>*
40831
41118
  # [fields <EventFieldNameList>] [showdayofweek]
40832
- # [countsonly [eventrowfilter]]
40833
- # [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
41119
+ # (addcsvdata <FieldName> <String>)*
41120
+ # [eventrowfilter]
41121
+ # [countsonly|(formatjson [quotechar <Character>])] [todrive <ToDriveAttribute>*]
40834
41122
  # gam calendars <CalendarEntity> show events <EventEntity> <EventDisplayProperties>*
40835
41123
  # [fields <EventFieldNameList>] [showdayofweek]
40836
- # [countsonly]
40837
- # [formatjson]
41124
+ # [countsonly|formatjson]
40838
41125
  def doCalendarsPrintShowEvents(calIds):
40839
41126
  calendarEventEntity = getCalendarEventEntity()
40840
- csvPF, FJQC, fieldsList = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.CALENDAR)
41127
+ csvPF, FJQC, fieldsList, addCSVData = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.CALENDAR)
40841
41128
  _printShowCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity,
40842
- csvPF, FJQC, fieldsList)
41129
+ csvPF, FJQC, fieldsList, addCSVData)
40843
41130
  if csvPF:
40844
41131
  if calendarEventEntity['countsOnly'] and calendarEventEntity['eventRowFilter']:
40845
41132
  csvPF.SetTitles(calendarEventEntity['countsOnlyTitles'])
@@ -41643,7 +41930,7 @@ def convertQueryNameToID(v, nameOrId, matterId, matterNameId):
41643
41930
  query = callGAPI(v.matters().savedQueries(), 'get',
41644
41931
  throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
41645
41932
  matterId=matterId, savedQueryId=cg.group(1))
41646
- return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']))
41933
+ return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']), query['query'])
41647
41934
  except (GAPI.notFound, GAPI.badRequest):
41648
41935
  entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, nameOrId])
41649
41936
  except (GAPI.forbidden, GAPI.invalidArgument) as e:
@@ -41652,12 +41939,12 @@ def convertQueryNameToID(v, nameOrId, matterId, matterNameId):
41652
41939
  try:
41653
41940
  queries = callGAPIpages(v.matters().savedQueries(), 'list', 'savedQueries',
41654
41941
  throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
41655
- matterId=matterId, fields='savedQueries(savedQueryId,displayName),nextPageToken')
41942
+ matterId=matterId, fields='savedQueries(savedQueryId,displayName,query),nextPageToken')
41656
41943
  except (GAPI.forbidden, GAPI.invalidArgument) as e:
41657
41944
  ClientAPIAccessDeniedExit(str(e))
41658
41945
  for query in queries:
41659
41946
  if query['displayName'].lower() == nameOrIdlower:
41660
- return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']))
41947
+ return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']), query['query'])
41661
41948
  entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, nameOrId])
41662
41949
 
41663
41950
  def getMatterItem(v, state=None):
@@ -41705,6 +41992,7 @@ VAULT_SEARCH_METHODS_MAP = {
41705
41992
  'accounts': 'ACCOUNT',
41706
41993
  'chatspace': 'ROOM',
41707
41994
  'chatspaces': 'ROOM',
41995
+ 'documentids': 'DRIVE_DOCUMENT',
41708
41996
  'entireorg': 'ENTIRE_ORG',
41709
41997
  'everyone': 'ENTIRE_ORG',
41710
41998
  'org': 'ORG_UNIT',
@@ -41802,11 +42090,13 @@ VAULT_QUERY_ARGS = [
41802
42090
  # drive
41803
42091
  'driveclientsideencryption', 'driveversiondate', 'includeshareddrives', 'includeteamdrives', 'shareddrivesoption',
41804
42092
  # hangoutsChat
41805
- 'includerooms',
42093
+ 'includerooms',
41806
42094
  # mail
41807
42095
  'mailclientsideencryption', 'excludedrafts',
41808
42096
  # voice
41809
42097
  'covereddata',
42098
+ # all
42099
+ 'json',
41810
42100
  ] + list(VAULT_SEARCH_METHODS_MAP.keys())
41811
42101
 
41812
42102
  def _buildVaultQuery(myarg, query, corpusArgumentMap):
@@ -41820,12 +42110,14 @@ def _buildVaultQuery(myarg, query, corpusArgumentMap):
41820
42110
  query['dataScope'] = 'ALL_DATA'
41821
42111
  if myarg == 'corpus':
41822
42112
  query['corpus'] = getChoice(corpusArgumentMap, mapChoice=True)
42113
+ elif myarg == 'scope':
42114
+ query['dataScope'] = getChoice(VAULT_EXPORT_DATASCOPE_MAP, mapChoice=True)
41823
42115
  elif myarg in VAULT_SEARCH_METHODS_MAP:
41824
- if query.get('searchMethod'):
42116
+ if query.get('method'):
41825
42117
  Cmd.Backup()
41826
42118
  usageErrorExit(Msg.MULTIPLE_SEARCH_METHODS_SPECIFIED.format(formatChoiceList(VAULT_SEARCH_METHODS_MAP)))
41827
42119
  searchMethod = VAULT_SEARCH_METHODS_MAP[myarg]
41828
- query['searchMethod'] = searchMethod
42120
+ query['method'] = searchMethod
41829
42121
  if searchMethod == 'ACCOUNT':
41830
42122
  query['accountInfo'] = {'emails': getNormalizedEmailAddressEntity()}
41831
42123
  elif searchMethod == 'ORG_UNIT':
@@ -41840,8 +42132,8 @@ def _buildVaultQuery(myarg, query, corpusArgumentMap):
41840
42132
  query['hangoutsChatInfo'] = {'roomId': roomIds}
41841
42133
  elif searchMethod == 'SITES_URL':
41842
42134
  query['sitesUrlInfo'] = {'urls': _getQueryList(Cmd.OB_URL_LIST)}
41843
- elif myarg == 'scope':
41844
- query['dataScope'] = getChoice(VAULT_EXPORT_DATASCOPE_MAP, mapChoice=True)
42135
+ elif searchMethod == 'DRIVE_DOCUMENT':
42136
+ query['driveDocumentInfo'] = {'documentIds': {'ids': _getQueryList(Cmd.OB_DRIVE_FILE_ID_LIST)}}
41845
42137
  elif myarg == 'terms':
41846
42138
  query['terms'] = getString(Cmd.OB_STRING)
41847
42139
  elif myarg in {'start', 'starttime'}:
@@ -41880,50 +42172,73 @@ def _buildVaultQuery(myarg, query, corpusArgumentMap):
41880
42172
  query['hangoutsChatOptions'] = {'includeRooms': getBoolean()}
41881
42173
  # mail
41882
42174
  elif myarg == 'excludedrafts':
41883
- query['mailOptions'] = {'excludeDrafts': getBoolean()}
42175
+ query.setdefault('mailOptions', {})['excludeDrafts'] = getBoolean()
41884
42176
  elif myarg == 'mailclientsideencryption':
41885
42177
  query.setdefault('mailOptions', {})['clientSideEncryptedOption'] = getChoice(VAULT_CSE_OPTION_MAP, mapChoice=True)
41886
42178
  # voice
41887
42179
  elif myarg == 'covereddata':
41888
- query['voiceOptions'] = {'coveredData': getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)}
42180
+ query.setdefault('voiceOptions', {'coveredData': []})
42181
+ query['voiceOptions']['coveredData'].append(getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True))
42182
+ # all
42183
+ elif myarg == 'json':
42184
+ jsonData = getJSON([])
42185
+ if 'query' in jsonData:
42186
+ query.update(jsonData['query'])
42187
+ else:
42188
+ query.update(jsonData)
42189
+
41889
42190
 
41890
42191
  def _validateVaultQuery(body, corpusArgumentMap):
41891
42192
  if 'corpus' not in body['query']:
41892
42193
  missingArgumentExit(f'corpus {formatChoiceList(corpusArgumentMap)}')
41893
- if 'searchMethod' not in body['query']:
42194
+ if 'method' not in body['query']:
41894
42195
  missingArgumentExit(formatChoiceList(VAULT_SEARCH_METHODS_MAP))
41895
42196
  if 'exportOptions' in body:
41896
42197
  for corpus, options in VAULT_CORPUS_OPTIONS_MAP.items():
41897
42198
  if body['query']['corpus'] != corpus:
41898
42199
  body['exportOptions'].pop(options, None)
41899
42200
 
41900
- # gam create vaultexport|export matter <MatterItem> [name <String>] corpus calendar|drive|gemini|groups|hangouts_chat|mail|voice
41901
- # (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone
42201
+ # gam create vaultexport|export matter <MatterItem> [name <String>]
42202
+ # vaultquery <QueryItem>
42203
+ # [driveclientsideencryption any|encrypted|unencrypted]
42204
+ # [includeaccessinfo <Boolean>]
42205
+ # [excludedrafts <Boolean>] [mailclientsideencryption any|encrypted|unencrypted]
42206
+ # [showconfidentialmodecontent <Boolean>] [usenewexport <Boolean>] [exportlinkeddrivefiles <Boolean>]
42207
+ # [format ics|mbox|pst|xml]
42208
+ # [region any|europe|us] [showdetails|returnidonly]
42209
+ # gam create vaultexport|export matter <MatterItem> [name <String>]
42210
+ # corpus calendar|drive|gemini|groups|hangouts_chat|mail|voice
42211
+ # [scope all_data|held_data|unprocessed_data]
42212
+ # (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone|entireorg
42213
+ # (documentids (<DriveFileIDList>|(select <FileSelector>|<CSVFileSelector>))) |
41902
42214
  # (shareddrives|teamdrives (<SharedDriveIDList>|(select <FileSelector>|<CSVFileSelector>))) |
41903
- # (rooms (<ChatSpaceList>|(select <FileSelector>|<CSVFileSelector>))) |
41904
- # (sitesurl (<URLList>||(select <FileSelector>|<CSVFileSelector>)))
41905
- # [scope <all_data|held_data|unprocessed_data>]
42215
+ # [(includeshareddrives <Boolean>)|(shareddrivesoption included|included_if_account_is_not_a_member|not_included)]
42216
+ # (sitesurl (<URLList>||(select <FileSelector>|<CSVFileSelector>)))
42217
+ # [driveversiondate <Date>|<Time>]
42218
+ # [includerooms <Boolean>]
42219
+ # (rooms (<ChatSpaceList>|(select <FileSelector>|<CSVFileSelector>))) |
41906
42220
  # [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
41907
42221
  # [locationquery <StringList>] [peoplequery <StringList>] [minuswords <StringList>]
41908
42222
  # [responsestatuses <AttendeeStatus>(,<AttendeeStatus>)*] [calendarversiondate <Date>|<Time>]
41909
- # [(includeshareddrives <Boolean>)|(shareddrivesoption included|included_if_account_is_not_a_member|not_included)]
41910
- # [driveversiondate <Date>|<Time>] [includeaccessinfo <Boolean>]
42223
+ # (covereddata calllogs|textmessages|voicemails)*
42224
+ # [<JSONData>]
41911
42225
  # [driveclientsideencryption any|encrypted|unencrypted]
41912
- # [includerooms <Boolean>]
42226
+ # [includeaccessinfo <Boolean>]
41913
42227
  # [excludedrafts <Boolean>] [mailclientsideencryption any|encrypted|unencrypted]
41914
42228
  # [showconfidentialmodecontent <Boolean>] [usenewexport <Boolean>] [exportlinkeddrivefiles <Boolean>]
41915
- # [covereddata calllogs|textmessages|voicemails]
41916
42229
  # [format ics|mbox|pst|xml]
41917
42230
  # [region any|europe|us] [showdetails|returnidonly]
41918
42231
  def doCreateVaultExport():
41919
42232
  v = buildGAPIObject(API.VAULT)
41920
42233
  matterId = None
41921
42234
  body = {'query': {'dataScope': 'ALL_DATA'}, 'exportOptions': {}}
42235
+ includeAccessInfo = None
41922
42236
  exportFormat = None
42237
+ formatLocation = None
42238
+ useNewExport = None
41923
42239
  showConfidentialModeContent = None
41924
42240
  exportLinkedDriveFiles = None
41925
42241
  returnIdOnly = showDetails = False
41926
- useNewExport = None
41927
42242
  while Cmd.ArgumentsRemaining():
41928
42243
  myarg = getArgument()
41929
42244
  if myarg == 'matter':
@@ -41931,22 +42246,23 @@ def doCreateVaultExport():
41931
42246
  body['matterId'] = matterId
41932
42247
  elif myarg == 'name':
41933
42248
  body['name'] = getString(Cmd.OB_STRING)
42249
+ elif matterId is not None and myarg == 'vaultquery':
42250
+ _, _, _, body['query'] = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
41934
42251
  elif myarg in VAULT_QUERY_ARGS:
41935
42252
  _buildVaultQuery(myarg, body['query'], VAULT_CORPUS_ARGUMENT_MAP)
41936
- elif myarg == 'usenewexport':
41937
- useNewExport = getBoolean()
42253
+ elif myarg == 'includeaccessinfo':
42254
+ includeAccessInfo = getBoolean()
41938
42255
  elif myarg == 'format':
42256
+ formatLocation = Cmd.Location()
41939
42257
  exportFormat = getChoice(VAULT_EXPORT_FORMAT_MAP, mapChoice=True)
42258
+ elif myarg == 'usenewexport':
42259
+ useNewExport = getBoolean()
41940
42260
  elif myarg == 'showconfidentialmodecontent':
41941
42261
  showConfidentialModeContent = getBoolean()
41942
42262
  elif myarg == 'exportlinkeddrivefiles':
41943
42263
  exportLinkedDriveFiles = getBoolean()
41944
42264
  elif myarg == 'region':
41945
42265
  body['exportOptions']['region'] = getChoice(VAULT_EXPORT_REGION_MAP, mapChoice=True)
41946
- elif myarg == 'includeaccessinfo':
41947
- body['exportOptions'].setdefault('driveOptions', {})['includeAccessInfo'] = getBoolean()
41948
- elif myarg == 'covereddata':
41949
- body['exportOptions'].setdefault('voiceOptions', {})['coveredData'] = getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)
41950
42266
  elif myarg == 'showdetails':
41951
42267
  showDetails = True
41952
42268
  returnIdOnly = False
@@ -41958,15 +42274,19 @@ def doCreateVaultExport():
41958
42274
  if not matterId:
41959
42275
  missingArgumentExit('matter')
41960
42276
  _validateVaultQuery(body, VAULT_CORPUS_ARGUMENT_MAP)
41961
- if exportFormat is not None:
41962
- if not exportFormat in VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']]:
41963
- invalidChoiceExit(exportFormat, VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']], False)
41964
- elif body['query']['corpus'] != 'DRIVE':
41965
- exportFormat = VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']][0]
41966
42277
  if 'name' not in body:
41967
42278
  body['name'] = f'GAM {body["query"]["corpus"]} Export - {ISOformatTimeStamp(todaysTime())}'
41968
42279
  optionsField = VAULT_CORPUS_OPTIONS_MAP[body['query']['corpus']]
41969
- if body['query']['corpus'] != 'DRIVE':
42280
+ if body['query']['corpus'] == 'DRIVE':
42281
+ if includeAccessInfo is not None:
42282
+ body['exportOptions'][optionsField] = {'includeAccessInfo': includeAccessInfo}
42283
+ else:
42284
+ if exportFormat is not None:
42285
+ if not exportFormat in VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']]:
42286
+ Cmd.SetLocation(formatLocation)
42287
+ invalidChoiceExit(exportFormat, VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']], False)
42288
+ else:
42289
+ exportFormat = VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']][0]
41970
42290
  body['exportOptions'][optionsField] = {'exportFormat': exportFormat}
41971
42291
  if body['query']['corpus'] == 'MAIL':
41972
42292
  if showConfidentialModeContent is not None:
@@ -42448,6 +42768,34 @@ def _showVaultHold(matterNameId, hold, cd, FJQC, k=0, kcount=0):
42448
42768
  showJSON(None, hold, timeObjects=VAULT_HOLD_TIME_OBJECTS)
42449
42769
  Ind.Decrement()
42450
42770
 
42771
+ def _useVaultQueryForHold(v, matterId, matterNameId, body):
42772
+ _, _, _, query = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
42773
+ body['corpus'] = query['corpus']
42774
+ method = query.get('method')
42775
+ if method == 'ACCOUNT':
42776
+ body['accounts'] = []
42777
+ for email in query['accountInfo']['emails']:
42778
+ body['accounts'].append({'email': email})
42779
+ elif method == 'ORG_UNIT':
42780
+ body['orgUnit'] = {'orgUnitId': query['orgUnitInfo']['orgUnitId']}
42781
+ queryType = VAULT_CORPUS_QUERY_MAP[query['corpus']]
42782
+ if queryType is None:
42783
+ return
42784
+ body['query'] = {queryType: {}}
42785
+ if query['corpus'] == 'DRIVE':
42786
+ body['query'][queryType]['includeSharedDriveFiles'] = query['driveOptions'].get('includeSharedDrives', False)
42787
+ elif query['corpus'] in {'GROUPS', 'MAIL'}:
42788
+ if query.get('terms'):
42789
+ body['query'][queryType]['terms'] = query['terms']
42790
+ if query.get('startTime'):
42791
+ body['query'][queryType]['startTime'] = query['startTime']
42792
+ if query.get('endTime'):
42793
+ body['query'][queryType]['endTime'] = query['endTime']
42794
+ elif query['corpus'] == 'HANGOUTS_CHAT':
42795
+ body['query'][queryType]['includeRooms'] = query['hangoutsChatOptions'].get('includeRooms', False)
42796
+ elif query['corpus'] == 'VOICE':
42797
+ body['query'][queryType]['coveredData'] = query['voiceOptions']['coveredData']
42798
+
42451
42799
  def _getHoldQueryParameters(myarg, queryParameters):
42452
42800
  if myarg == 'query':
42453
42801
  queryParameters['queryLocation'] = Cmd.Location()
@@ -42463,7 +42811,8 @@ def _getHoldQueryParameters(myarg, queryParameters):
42463
42811
  elif myarg in {'includeshareddrives', 'includeteamdrives'}:
42464
42812
  queryParameters['includeSharedDriveFiles'] = getBoolean()
42465
42813
  elif myarg == 'covereddata':
42466
- queryParameters['coveredData'] = getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)
42814
+ queryParameters.setdefault('coveredData', [])
42815
+ queryParameters['coveredData'].append(getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True))
42467
42816
  else:
42468
42817
  return False
42469
42818
  return True
@@ -42498,7 +42847,11 @@ def _setHoldQuery(body, queryParameters):
42498
42847
  if queryParameters.get('coveredData'):
42499
42848
  body['query'][queryType]['coveredData'] = queryParameters['coveredData']
42500
42849
 
42501
- # gam create vaulthold|hold matter <MatterItem> [name <String>] corpus calendar|drive|mail|groups|hangouts_chat|voice
42850
+ # gam create vaulthold|hold matter <MatterItem> [name <String>]
42851
+ # vaultquery <QueryItem>
42852
+ # [showdetails|returnidonly]
42853
+ # gam create vaulthold|hold matter <MatterItem> [name <String>]
42854
+ # corpus calendar|drive|mail|groups|hangouts_chat|voice
42502
42855
  # [(accounts|groups|users <EmailItemList>) | (orgunit|org|ou <OrgUnit>)]
42503
42856
  # [query <QueryVaultCorpus>]
42504
42857
  # [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
@@ -42512,13 +42865,16 @@ def doCreateVaultHold():
42512
42865
  matterId = None
42513
42866
  accounts = []
42514
42867
  queryParameters = {}
42515
- returnIdOnly = showDetails = False
42868
+ returnIdOnly = showDetails = usedVaultQuery = False
42516
42869
  while Cmd.ArgumentsRemaining():
42517
42870
  myarg = getArgument()
42518
42871
  if myarg == 'matter':
42519
42872
  matterId, matterNameId = getMatterItem(v)
42520
42873
  elif myarg == 'name':
42521
42874
  body['name'] = getString(Cmd.OB_STRING)
42875
+ elif matterId is not None and myarg == 'vaultquery':
42876
+ _useVaultQueryForHold(v, matterId, matterNameId, body)
42877
+ usedVaultQuery = True
42522
42878
  elif myarg == 'corpus':
42523
42879
  body['corpus'] = getChoice(VAULT_CORPUS_ARGUMENT_MAP, mapChoice=True)
42524
42880
  elif myarg in {'accounts', 'users', 'groups'}:
@@ -42542,7 +42898,8 @@ def doCreateVaultHold():
42542
42898
  missingArgumentExit(f'corpus {"|".join(VAULT_CORPUS_ARGUMENT_MAP)}')
42543
42899
  if 'name' not in body:
42544
42900
  body['name'] = f'GAM {body["corpus"]} Hold - {ISOformatTimeStamp(todaysTime())}'
42545
- _setHoldQuery(body, queryParameters)
42901
+ if not usedVaultQuery:
42902
+ _setHoldQuery(body, queryParameters)
42546
42903
  if accounts:
42547
42904
  body['accounts'] = []
42548
42905
  cd = buildGAPIObject(API.DIRECTORY)
@@ -42975,6 +43332,116 @@ def _showVaultQuery(matterNameId, query, cd, drive, FJQC, k=0, kcount=0):
42975
43332
  showJSON(None, query, timeObjects=VAULT_QUERY_TIME_OBJECTS)
42976
43333
  Ind.Decrement()
42977
43334
 
43335
+ def doCreateCopyVaultQuery(copyCmd):
43336
+ v = buildGAPIObject(API.VAULT)
43337
+ body = {'query': {'dataScope': 'ALL_DATA'}, 'displayName': ''}
43338
+ matterId, matterNameId = getMatterItem(v)
43339
+ if copyCmd:
43340
+ _, queryName, _, body['query'] = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
43341
+ targetId = None
43342
+ cd = drive = None
43343
+ FJQC = FormatJSONQuoteChar()
43344
+ returnIdOnly = showDetails = False
43345
+ while Cmd.ArgumentsRemaining():
43346
+ myarg = getArgument()
43347
+ if myarg == 'name':
43348
+ body['displayName'] = getString(Cmd.OB_STRING)
43349
+ elif copyCmd and myarg == 'targetmatter':
43350
+ targetId, targetNameId = getMatterItem(v)
43351
+ elif not copyCmd and myarg in VAULT_QUERY_ARGS:
43352
+ _buildVaultQuery(myarg, body['query'], VAULT_CORPUS_ARGUMENT_MAP)
43353
+ elif myarg == 'shownames':
43354
+ cd = buildGAPIObject(API.DIRECTORY)
43355
+ _, drive = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
43356
+ if drive is None:
43357
+ return
43358
+ elif myarg == 'showdetails':
43359
+ showDetails = True
43360
+ returnIdOnly = False
43361
+ elif myarg == 'returnidonly':
43362
+ returnIdOnly = True
43363
+ showDetails = False
43364
+ else:
43365
+ FJQC.GetFormatJSON(myarg)
43366
+ if copyCmd:
43367
+ if targetId is None:
43368
+ targetId = matterId
43369
+ targetNameId = matterNameId
43370
+ if not body['displayName']:
43371
+ body['displayName'] = f'Copy of {queryName}' if matterId == targetId else queryName
43372
+ resultId = targetId
43373
+ resultNameId = targetNameId
43374
+ else:
43375
+ _validateVaultQuery(body, VAULT_CORPUS_ARGUMENT_MAP)
43376
+ if not body['displayName']:
43377
+ body['displayName'] = 'GAM {body["query"]["corpus"]} Query - {ISOformatTimeStamp(todaysTime())}'
43378
+ resultId = matterId
43379
+ resultNameId = matterNameId
43380
+ try:
43381
+ result = callGAPI(v.matters().savedQueries(), 'create',
43382
+ throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT, GAPI.ALREADY_EXISTS],
43383
+ matterId=resultId, body=body)
43384
+ if not returnIdOnly:
43385
+ if not FJQC.formatJSON:
43386
+ entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, formatVaultNameId(result['displayName'], result['savedQueryId'])])
43387
+ if showDetails or FJQC.formatJSON:
43388
+ _showVaultQuery(resultNameId, result, cd, drive, FJQC)
43389
+ else:
43390
+ writeStdout(f'{result["savedQueryId"]}\n')
43391
+ except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument, GAPI.alreadyExists) as e:
43392
+ entityActionFailedWarning([Ent.VAULT_MATTER, resultNameId, Ent.VAULT_QUERY, body['displayName']], str(e))
43393
+
43394
+ # gam create vaultquery <MatterItem> [name <String>]
43395
+ # corpus calendar|drive|gemini|groups|hangouts_chat|mail|voice
43396
+ # [scope all_data|held_data|unprocessed_data]
43397
+ # (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone|entireorg
43398
+ # (documentids (<DriveFileIDList>|(select <FileSelector>|<CSVFileSelector>))) |
43399
+ # (shareddrives|teamdrives (<SharedDriveIDList>|(select <FileSelector>|<CSVFileSelector>))) |
43400
+ # [(includeshareddrives <Boolean>)|(shareddrivesoption included|included_if_account_is_not_a_member|not_included)]
43401
+ # (sitesurl (<URLList>||(select <FileSelector>|<CSVFileSelector>)))
43402
+ # [driveversiondate <Date>|<Time>]
43403
+ # [includerooms <Boolean>]
43404
+ # (rooms (<ChatSpaceList>|(select <FileSelector>|<CSVFileSelector>))) |
43405
+ # [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
43406
+ # [locationquery <StringList>] [peoplequery <StringList>] [minuswords <StringList>]
43407
+ # [responsestatuses <AttendeeStatus>(,<AttendeeStatus>)*] [calendarversiondate <Date>|<Time>]
43408
+ # (covereddata calllogs|textmessages|voicemails)*
43409
+ # [<JSONData>]
43410
+ # [shownames]
43411
+ # [showdetails|returnidonly|formatjson]
43412
+ def doCreateVaultQuery():
43413
+ doCreateCopyVaultQuery(False)
43414
+
43415
+ # gam copy vaultquery <MatterItem> <QueryItem> [targetmatter <MatterItem>] [name <String>]
43416
+ # [shownames]
43417
+ # [showdetails|returnidonly|formatjson]
43418
+ def doCopyVaultQuery():
43419
+ doCreateCopyVaultQuery(True)
43420
+
43421
+ # gam delete vaultquery <QueryItem> matter <MatterItem>
43422
+ # gam delete vaultquery <MatterItem> <QueryItem>
43423
+ def doDeleteVaultQuery():
43424
+ v = buildGAPIObject(API.VAULT)
43425
+ if not Cmd.ArgumentIsAhead('matter'):
43426
+ matterId, matterNameId = getMatterItem(v)
43427
+ queryId, queryName, queryNameId, _ = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
43428
+ else:
43429
+ queryName = getString(Cmd.OB_QUERY_ITEM)
43430
+ while Cmd.ArgumentsRemaining():
43431
+ myarg = getArgument()
43432
+ if myarg == 'matter':
43433
+ matterId, matterNameId = getMatterItem(v)
43434
+ queryId, queryName, queryNameId, _ = convertQueryNameToID(v, queryName, matterId, matterNameId)
43435
+ else:
43436
+ unknownArgumentExit()
43437
+ try:
43438
+ callGAPI(v.matters().savedQueries(), 'delete',
43439
+ throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
43440
+ matterId=matterId, savedQueryId=queryId)
43441
+ entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, queryNameId])
43442
+ except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
43443
+ entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, queryNameId], str(e))
43444
+
42978
43445
  VAULT_QUERY_FIELDS_CHOICE_MAP = {
42979
43446
  'createtime': 'createTime',
42980
43447
  'displayname': 'displayName',
@@ -42995,7 +43462,7 @@ def doInfoVaultQuery():
42995
43462
  v = buildGAPIObject(API.VAULT)
42996
43463
  if not Cmd.ArgumentIsAhead('matter'):
42997
43464
  matterId, matterNameId = getMatterItem(v)
42998
- queryId, queryName, queryNameId = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
43465
+ queryId, queryName, queryNameId, _ = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
42999
43466
  else:
43000
43467
  queryName = getString(Cmd.OB_QUERY_ITEM)
43001
43468
  cd = drive = None
@@ -43005,7 +43472,7 @@ def doInfoVaultQuery():
43005
43472
  myarg = getArgument()
43006
43473
  if myarg == 'matter':
43007
43474
  matterId, matterNameId = getMatterItem(v)
43008
- queryId, queryName, queryNameId = convertQueryNameToID(v, queryName, matterId, matterNameId)
43475
+ queryId, queryName, queryNameId, _ = convertQueryNameToID(v, queryName, matterId, matterNameId)
43009
43476
  elif myarg == 'shownames':
43010
43477
  cd = buildGAPIObject(API.DIRECTORY)
43011
43478
  _, drive = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
@@ -43460,14 +43927,16 @@ def doPrintShowVaultMatters():
43460
43927
  PRINT_VAULT_COUNTS_TITLES = ['account', 'count', 'error']
43461
43928
 
43462
43929
  # gam print vaultcounts [todrive <ToDriveAttributes>*]
43463
- # matter <MatterItem> corpus mail|groups
43464
- # (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone
43465
- # (shareddrives|teamdrives (<SharedDriveIDList>|(select <FileSelector>|<CSVFileSelector>))) |
43466
- # (rooms (<ChatSpaceList>|(select <FileSelector>|<CSVFileSelector>))) |
43467
- # (sitesurl (<URLList>||(select <FileSelector>|<CSVFileSelector>)))
43468
- # [scope <all_data|held_data|unprocessed_data>]
43930
+ # matter <MatterItem> <QueryItem>
43931
+ # [wait <Integer>]
43932
+ # gam print vaultcounts [todrive <ToDriveAttributes>*]
43933
+ # matter <MatterItem>
43934
+ # corpus mail|groups
43935
+ # [scope all_data|held_data|unprocessed_data]
43936
+ # (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone|entireorg
43469
43937
  # [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
43470
43938
  # [excludedrafts <Boolean>]
43939
+ # [<JSONData>]
43471
43940
  # [wait <Integer>]
43472
43941
  # gam print vaultcounts [todrive <ToDriveAttributes>*]
43473
43942
  # matter <MatterItem> operation <String> [wait <Integer>]
@@ -43476,18 +43945,19 @@ def doPrintVaultCounts():
43476
43945
  csvPF = CSVPrintFile(PRINT_VAULT_COUNTS_TITLES, 'sortall')
43477
43946
  matterId = name = None
43478
43947
  operationWait = 15
43479
- body = {'view': 'ALL'}
43480
- query = {}
43948
+ body = {'view': 'ALL', 'query': {}}
43481
43949
  while Cmd.ArgumentsRemaining():
43482
43950
  myarg = getArgument()
43483
43951
  if csvPF and myarg == 'todrive':
43484
43952
  csvPF.GetTodriveParameters()
43485
43953
  elif myarg == 'matter':
43486
- matterId, _ = getMatterItem(v)
43954
+ matterId, matterNameId = getMatterItem(v)
43955
+ elif matterId is not None and myarg == 'vaultquery':
43956
+ _, _, _, body['query'] = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
43487
43957
  elif myarg == 'operation':
43488
43958
  name = getString(Cmd.OB_STRING)
43489
43959
  elif myarg in VAULT_QUERY_ARGS:
43490
- _buildVaultQuery(myarg, query, VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
43960
+ _buildVaultQuery(myarg, body['query'], VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
43491
43961
  elif myarg == 'wait':
43492
43962
  operationWait = getInteger(minVal=1)
43493
43963
  else:
@@ -43498,7 +43968,6 @@ def doPrintVaultCounts():
43498
43968
  operation = {'name': name}
43499
43969
  doWait = False
43500
43970
  else:
43501
- body['query'] = query
43502
43971
  _validateVaultQuery(body, VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
43503
43972
  try:
43504
43973
  operation = callGAPI(v.matters(), 'count',
@@ -43522,7 +43991,7 @@ def doPrintVaultCounts():
43522
43991
  doWait = True
43523
43992
  response = operation.get('response', {})
43524
43993
  query = operation['metadata']['query']
43525
- search_method = query.get('searchMethod')
43994
+ search_method = query.get('method')
43526
43995
  # ARGH count results don't include accounts with zero items.
43527
43996
  # so we keep track of which accounts we searched and can report
43528
43997
  # zero data for them.
@@ -43868,6 +44337,28 @@ USER_JSON_SKIP_FIELDS = ['agreedToTerms', 'aliases', 'creationTime', 'customerId
43868
44337
 
43869
44338
  ALLOW_EMPTY_CUSTOM_TYPE = 'allowEmptyCustomType'
43870
44339
 
44340
+ def getNotifyArguments(myarg, notify, userNotification):
44341
+ if myarg == 'notify':
44342
+ if userNotification:
44343
+ notify['recipients'].extend(getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True))
44344
+ else: #delegateNotificatiomn
44345
+ notify['notify'] = getBoolean()
44346
+ elif myarg == 'subject':
44347
+ notify['subject'] = getString(Cmd.OB_STRING)
44348
+ elif myarg in SORF_MSG_FILE_ARGUMENTS:
44349
+ notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
44350
+ elif myarg == 'html':
44351
+ notify['html'] = getBoolean()
44352
+ elif myarg == 'from':
44353
+ notify['from'] = getString(Cmd.OB_EMAIL_ADDRESS)
44354
+ elif myarg == 'mailbox':
44355
+ notify['mailbox'] = getString(Cmd.OB_EMAIL_ADDRESS)
44356
+ elif myarg == 'replyto':
44357
+ notify['replyto'] = getString(Cmd.OB_EMAIL_ADDRESS)
44358
+ else:
44359
+ return False
44360
+ return True
44361
+
43871
44362
  def getUserAttributes(cd, updateCmd, noUid=False):
43872
44363
  def getKeywordAttribute(keywords, attrdict, **opts):
43873
44364
  if Cmd.ArgumentsRemaining():
@@ -43955,6 +44446,7 @@ def getUserAttributes(cd, updateCmd, noUid=False):
43955
44446
  invalidArgumentExit(Cmd.OB_SCHEMA_NAME_FIELD_NAME)
43956
44447
 
43957
44448
  parameters = {
44449
+ 'notifyRecoveryEmail': False,
43958
44450
  'verifyNotInvitable': False,
43959
44451
  'createIfNotFound': False,
43960
44452
  'noActionIfAlias': False,
@@ -43971,7 +44463,7 @@ def getUserAttributes(cd, updateCmd, noUid=False):
43971
44463
  body = {'name': {'givenName': UNKNOWN, 'familyName': UNKNOWN}}
43972
44464
  body['primaryEmail'] = getEmailAddress(noUid=noUid)
43973
44465
  notFoundBody = {}
43974
- notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
44466
+ notify = {'recipients': [], 'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
43975
44467
  primary = {}
43976
44468
  updatePrimaryEmail = {}
43977
44469
  groupOrgUnitMap = None
@@ -43982,20 +44474,10 @@ def getUserAttributes(cd, updateCmd, noUid=False):
43982
44474
  resolveConflictAccount = True
43983
44475
  while Cmd.ArgumentsRemaining():
43984
44476
  myarg = getArgument()
43985
- if myarg == 'notify':
43986
- notify['recipients'] = getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
43987
- elif myarg == 'subject':
43988
- notify['subject'] = getString(Cmd.OB_STRING)
43989
- elif myarg in SORF_MSG_FILE_ARGUMENTS:
43990
- notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
43991
- elif myarg == 'html':
43992
- notify['html'] = getBoolean()
43993
- elif myarg == 'from':
43994
- notify['from'] = getString(Cmd.OB_EMAIL_ADDRESS)
43995
- elif myarg == 'replyto':
43996
- notify['replyto'] = getString(Cmd.OB_EMAIL_ADDRESS)
43997
- elif myarg == 'mailbox':
43998
- notify['mailbox'] = getString(Cmd.OB_EMAIL_ADDRESS)
44477
+ if getNotifyArguments(myarg, notify, True):
44478
+ pass
44479
+ elif myarg == 'notifyrecoveryemail':
44480
+ parameters['notifyRecoveryEmail'] = True
43999
44481
  elif PwdOpts.ProcessArgument(myarg, notify, notFoundBody):
44000
44482
  pass
44001
44483
  elif _getTagReplacement(myarg, tagReplacements, True):
@@ -44399,14 +44881,14 @@ def createUserAddAliases(cd, user, aliasList, i, count):
44399
44881
  # (groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
44400
44882
  # [alias|aliases <EmailAddressList>]
44401
44883
  # [license <SKUID> [product|productid <ProductID>]]
44402
- # [notify <EmailAddressList>
44884
+ # [[notify <EmailAddressList>] [notifyrecoveryemail]
44403
44885
  # [subject <String>]
44404
- # [notifypassword <String>]
44405
- # [from <EmailAaddress>]
44886
+ # [from <EmailAaddress>] [mailbox <EmailAddress>]
44406
44887
  # [replyto <EmailAaddress>]
44407
- # [<NotifyMessageContent>]
44408
- # (replace <Tag> <UserReplacement>)*
44409
- # (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
44888
+ # [notifypassword <String>]
44889
+ # [<NotifyMessageContent>] [html [<Boolean>]]
44890
+ # (replace <Tag> <UserReplacement>)*
44891
+ # (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
44410
44892
  # [logpassword <FileName>] [ignorenullpassword]
44411
44893
  # [addnumericsuffixonduplicate <Number>]
44412
44894
  def doCreateUser():
@@ -44417,7 +44899,7 @@ def doCreateUser():
44417
44899
  suffix = 0
44418
44900
  originalEmail = body['primaryEmail']
44419
44901
  atLoc = originalEmail.find('@')
44420
- fields = '*' if tagReplacements['subs'] else 'primaryEmail,name'
44902
+ fields = '*' if tagReplacements['subs'] else 'primaryEmail,name,recoveryEmail'
44421
44903
  while True:
44422
44904
  user = body['primaryEmail']
44423
44905
  if parameters['verifyNotInvitable']:
@@ -44458,7 +44940,9 @@ def doCreateUser():
44458
44940
  createUserAddToGroups(cd, result['primaryEmail'], addGroups, 0, 0)
44459
44941
  if addAliases:
44460
44942
  createUserAddAliases(cd, result['primaryEmail'], addAliases, 0, 0)
44461
- if notify.get('recipients'):
44943
+ if (notify.get('recipients') or (parameters['notifyRecoveryEmail'] and result.get('recoveryEmail'))):
44944
+ if parameters['notifyRecoveryEmail'] and result.get('recoveryEmail'):
44945
+ notify['recipients'].append(result['recoveryEmail'])
44462
44946
  sendCreateUpdateUserNotification(result, notify, tagReplacements)
44463
44947
  for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
44464
44948
  productId = productSku[0]
@@ -44501,14 +44985,14 @@ def verifyUserPrimaryEmail(cd, user, createIfNotFound, i, count):
44501
44985
  # [createifnotfound] [notfoundpassword (random [<Integer>])|blocklogin|<Password>]
44502
44986
  # (groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
44503
44987
  # [alias|aliases <EmailAddressList>]
44504
- # [notify <EmailAddressList>
44988
+ # [[notify <EmailAddressList>] [notifyrecoveryemail]
44505
44989
  # [subject <String>]
44506
- # [notifypassword <String>]
44507
- # [from <EmailAaddress>]
44990
+ # [from <EmailAaddress>] [mailbox <EmailAddress>]
44508
44991
  # [replyto <EmailAaddress>]
44509
- # [<NotifyMessageContent>
44992
+ # [<NotifyMessageContent> [html [<Boolean>]]
44510
44993
  # (replace <Tag> <UserReplacement>)*
44511
44994
  # (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
44995
+ # [notifypassword <String>]]
44512
44996
  # [notifyonupdate [<Boolean>]]
44513
44997
  # [logpassword <FileName>] [ignorenullpassword]
44514
44998
  def updateUsers(entityList):
@@ -44530,7 +45014,7 @@ def updateUsers(entityList):
44530
45014
  else:
44531
45015
  checkImmutableOUs = False
44532
45016
  i, count, entityList = getEntityArgument(entityList)
44533
- fields = '*' if tagReplacements['subs'] else 'primaryEmail,name'
45017
+ fields = '*' if tagReplacements['subs'] else 'primaryEmail,name,recoveryEmail'
44534
45018
  for user in entityList:
44535
45019
  i += 1
44536
45020
  user = userKey = normalizeEmailAddressOrUID(user)
@@ -44606,7 +45090,10 @@ def updateUsers(entityList):
44606
45090
  entityActionPerformed([Ent.USER, user], i, count)
44607
45091
  if PwdOpts.filename and PwdOpts.password:
44608
45092
  writeFile(PwdOpts.filename, f'{userKey},{PwdOpts.password}\n', mode='a', continueOnError=True)
44609
- if parameters['notifyOnUpdate'] and notify.get('recipients') and notify['password']:
45093
+ if (parameters['notifyOnUpdate'] and notify['password'] and
45094
+ (notify.get('recipients') or (parameters['notifyRecoveryEmail'] and result.get('recoveryEmail')))):
45095
+ if parameters['notifyRecoveryEmail'] and result.get('recoveryEmail'):
45096
+ notify['recipients'].append(result['recoveryEmail'])
44610
45097
  sendCreateUpdateUserNotification(result, notify, tagReplacements, i, count, createMessage=False)
44611
45098
  break
44612
45099
  except GAPI.conditionNotMet as e:
@@ -44640,8 +45127,10 @@ def updateUsers(entityList):
44640
45127
  createUserAddToGroups(cd, result['primaryEmail'], addGroups, i, count)
44641
45128
  if addAliases:
44642
45129
  createUserAddAliases(cd, result['primaryEmail'], addAliases, i, count)
44643
- if notify.get('recipients'):
45130
+ if (notify.get('recipients') or (parameters['notifyRecoveryEmail'] and result.get('recoveryEmail'))):
44644
45131
  notify['password'] = notify['notFoundPassword']
45132
+ if parameters['notifyRecoveryEmail'] and result.get('recoveryEmail'):
45133
+ notify['recipients'].append(result['recoveryEmail'])
44645
45134
  sendCreateUpdateUserNotification(result, notify, tagReplacements, i, count)
44646
45135
  except GAPI.duplicate:
44647
45136
  duplicateAliasGroupUserWarning(cd, [Ent.USER, body['primaryEmail']], i, count)
@@ -45688,7 +46177,7 @@ USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails',
45688
46177
  # [userview] [basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
45689
46178
  # [delimiter <Character>] [sortheaders] [formatjson [quotechar <Character>]] [quoteplusphonenumbers]
45690
46179
  # [convertcrnl]
45691
- # [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
46180
+ # [issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
45692
46181
  # [showitemcountonly]
45693
46182
  # [showvalidcolumn] (addcsvdata <FieldName> <String>)*
45694
46183
  #
@@ -45701,7 +46190,7 @@ USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails',
45701
46190
  # [userview] [basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
45702
46191
  # [delimiter <Character>] [sortheaders] [formatjson [quotechar <Character>]] [quoteplusphonenumbers]
45703
46192
  # [convertcrnl]
45704
- # [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
46193
+ # [issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
45705
46194
  # [showitemcountonly]
45706
46195
  # [showvalidcolumn] (addcsvdata <FieldName> <String>)*
45707
46196
  #
@@ -45709,13 +46198,13 @@ USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails',
45709
46198
  # ([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)]
45710
46199
  # [limittoou <OrgUnitItem>] [deleted_only|only_deleted])|[select <UserTypeEntity>]
45711
46200
  # [formatjson [quotechar <Character>]] [countonly]
45712
- # [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
46201
+ # [issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
45713
46202
  # [showitemcountonly]
45714
46203
  # [showvalidcolumn] (addcsvdata <FieldName> <String>)*
45715
46204
  #
45716
46205
  # gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
45717
46206
  # [formatjson [quotechar <Character>]] [countonly]
45718
- # [issuspended <Boolean>]
46207
+ # [issuspended <Boolean>] [isarchived <Boolean>]
45719
46208
  # [showitemcountonly]
45720
46209
  def doPrintUsers(entityList=None):
45721
46210
  def _writeUserEntity(userEntity):
@@ -45733,71 +46222,81 @@ def doPrintUsers(entityList=None):
45733
46222
  csvPF.WriteRowNoFilter(row)
45734
46223
 
45735
46224
  def _printUser(userEntity, i, count):
45736
- if isSuspended is None or isSuspended == userEntity.get('suspended', isSuspended):
45737
- if showValidColumn:
45738
- userEntity[showValidColumn] = True
45739
- userEmail = userEntity['primaryEmail']
45740
- if printOptions['emailParts']:
45741
- if userEmail.find('@') != -1:
45742
- userEntity['primaryEmailLocal'], userEntity['primaryEmailDomain'] = splitEmailAddress(userEmail)
45743
- if 'languages' in userEntity and not FJQC.formatJSON:
45744
- userEntity['languages'] = _formatLanguagesList(userEntity.pop('languages'), delimiter)
45745
- for location in userEntity.get('locations', []):
45746
- location['buildingName'] = _getBuildingNameById(cd, location.get('buildingId', ''))
45747
- if quotePlusPhoneNumbers:
45748
- for phone in userEntity.get('phones', []):
45749
- phoneNumber = phone.get('value', '')
45750
- if phoneNumber.startswith('+'):
45751
- phone['value'] = "'"+phoneNumber
45752
- if schemaParms['selectedSchemaFields']:
45753
- _filterSchemaFields(userEntity, schemaParms)
45754
- if printOptions['getGroupFeed']:
45755
- printGettingAllEntityItemsForWhom(Ent.GROUP_MEMBERSHIP, userEmail, i, count)
45756
- try:
45757
- groups = callGAPIpages(cd.groups(), 'list', 'groups',
45758
- pageMessage=getPageMessageForWhom(),
45759
- throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
45760
- retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
45761
- userKey=userEmail, orderBy='email', fields='nextPageToken,groups(email)')
45762
- numGroups = len(groups)
45763
- if not printOptions['groupsInColumns']:
45764
- userEntity['GroupsCount'] = numGroups
45765
- userEntity['Groups'] = delimiter.join([groupname['email'] for groupname in groups])
45766
- else:
45767
- if numGroups > printOptions['maxGroups']:
45768
- printOptions['maxGroups'] = numGroups
45769
- userEntity['Groups'] = numGroups
45770
- for j, group in enumerate(groups):
45771
- userEntity[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}'] = group['email']
45772
- except (GAPI.invalidMember, GAPI.invalidInput):
45773
- badRequestWarning(Ent.GROUP, Ent.MEMBER, userEmail)
45774
- except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
45775
- accessErrorExit(cd)
45776
- if aliasMatchPattern and 'aliases' in userEntity:
45777
- userEntity['aliases'] = [alias for alias in userEntity['aliases'] if aliasMatchPattern.match(alias)]
45778
- if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
45779
- if printOptions['getLicenseFeed']:
45780
- u_licenses = licenses.get(userEmail.lower(), [])
46225
+ if (isSuspended is None and isArchived is None):
46226
+ showUser = True
46227
+ elif (isSuspended is not None and isArchived is None):
46228
+ showUser = isSuspended == userEntity.get('suspended', isSuspended)
46229
+ elif (isSuspended is None and isArchived is not None):
46230
+ showUser = isArchived == userEntity.get('archived', isArchived)
46231
+ else:
46232
+ showUser = ((isSuspended == userEntity.get('suspended', isSuspended)) or
46233
+ (isArchived == userEntity.get('archived', isArchived)))
46234
+ if not showUser:
46235
+ return
46236
+ if showValidColumn:
46237
+ userEntity[showValidColumn] = True
46238
+ userEmail = userEntity['primaryEmail']
46239
+ if printOptions['emailParts']:
46240
+ if userEmail.find('@') != -1:
46241
+ userEntity['primaryEmailLocal'], userEntity['primaryEmailDomain'] = splitEmailAddress(userEmail)
46242
+ if 'languages' in userEntity and not FJQC.formatJSON:
46243
+ userEntity['languages'] = _formatLanguagesList(userEntity.pop('languages'), delimiter)
46244
+ for location in userEntity.get('locations', []):
46245
+ location['buildingName'] = _getBuildingNameById(cd, location.get('buildingId', ''))
46246
+ if quotePlusPhoneNumbers:
46247
+ for phone in userEntity.get('phones', []):
46248
+ phoneNumber = phone.get('value', '')
46249
+ if phoneNumber.startswith('+'):
46250
+ phone['value'] = "'"+phoneNumber
46251
+ if schemaParms['selectedSchemaFields']:
46252
+ _filterSchemaFields(userEntity, schemaParms)
46253
+ if printOptions['getGroupFeed']:
46254
+ printGettingAllEntityItemsForWhom(Ent.GROUP_MEMBERSHIP, userEmail, i, count)
46255
+ try:
46256
+ groups = callGAPIpages(cd.groups(), 'list', 'groups',
46257
+ pageMessage=getPageMessageForWhom(),
46258
+ throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
46259
+ retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
46260
+ userKey=userEmail, orderBy='email', fields='nextPageToken,groups(email)')
46261
+ numGroups = len(groups)
46262
+ if not printOptions['groupsInColumns']:
46263
+ userEntity['GroupsCount'] = numGroups
46264
+ userEntity['Groups'] = delimiter.join([groupname['email'] for groupname in groups])
45781
46265
  else:
45782
- u_licenses = getUserLicenses(lic, userEntity, skus)
45783
- if not oneLicensePerRow:
45784
- userEntity['LicensesCount'] = len(u_licenses)
45785
- if u_licenses:
45786
- userEntity['Licenses'] = delimiter.join(u_licenses)
45787
- userEntity['LicensesDisplay'] = delimiter.join([SKU.skuIdToDisplayName(skuId) for skuId in u_licenses])
46266
+ if numGroups > printOptions['maxGroups']:
46267
+ printOptions['maxGroups'] = numGroups
46268
+ userEntity['Groups'] = numGroups
46269
+ for j, group in enumerate(groups):
46270
+ userEntity[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}'] = group['email']
46271
+ except (GAPI.invalidMember, GAPI.invalidInput):
46272
+ badRequestWarning(Ent.GROUP, Ent.MEMBER, userEmail)
46273
+ except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
46274
+ accessErrorExit(cd)
46275
+ if aliasMatchPattern and 'aliases' in userEntity:
46276
+ userEntity['aliases'] = [alias for alias in userEntity['aliases'] if aliasMatchPattern.match(alias)]
46277
+ if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
46278
+ if printOptions['getLicenseFeed']:
46279
+ u_licenses = licenses.get(userEmail.lower(), [])
45788
46280
  else:
45789
- u_licenses = []
46281
+ u_licenses = getUserLicenses(lic, userEntity, skus)
45790
46282
  if not oneLicensePerRow:
45791
- _writeUserEntity(userEntity)
45792
- else:
46283
+ userEntity['LicensesCount'] = len(u_licenses)
45793
46284
  if u_licenses:
45794
- for skuId in u_licenses:
45795
- userEntity['License'] = skuId
45796
- userEntity['LicenseDisplay'] = SKU.skuIdToDisplayName(skuId)
45797
- _writeUserEntity(userEntity)
45798
- else:
45799
- userEntity['License'] = userEntity['LicenseDisplay'] = ''
46285
+ userEntity['Licenses'] = delimiter.join(u_licenses)
46286
+ userEntity['LicensesDisplay'] = delimiter.join([SKU.skuIdToDisplayName(skuId) for skuId in u_licenses])
46287
+ else:
46288
+ u_licenses = []
46289
+ if not oneLicensePerRow:
46290
+ _writeUserEntity(userEntity)
46291
+ else:
46292
+ if u_licenses:
46293
+ for skuId in u_licenses:
46294
+ userEntity['License'] = skuId
46295
+ userEntity['LicenseDisplay'] = SKU.skuIdToDisplayName(skuId)
45800
46296
  _writeUserEntity(userEntity)
46297
+ else:
46298
+ userEntity['License'] = userEntity['LicenseDisplay'] = ''
46299
+ _writeUserEntity(userEntity)
45801
46300
 
45802
46301
  def _updateDomainCounts(emailAddress):
45803
46302
  nonlocal domainCounts
@@ -45871,7 +46370,7 @@ def doPrintUsers(entityList=None):
45871
46370
  schemaParms = _initSchemaParms('basic')
45872
46371
  projectionSet = False
45873
46372
  oneLicensePerRow = quotePlusPhoneNumbers = showDeleted = False
45874
- aliasMatchPattern = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None
46373
+ aliasMatchPattern = isArchived = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None
45875
46374
  viewType = 'admin_view'
45876
46375
  delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
45877
46376
  showValidColumn = ''
@@ -45892,6 +46391,8 @@ def doPrintUsers(entityList=None):
45892
46391
  _, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
45893
46392
  elif myarg == 'issuspended':
45894
46393
  isSuspended = getBoolean()
46394
+ elif myarg == 'isarchived':
46395
+ isArchived = getBoolean()
45895
46396
  elif myarg == 'orderby':
45896
46397
  orderBy, sortOrder = getOrderBySortOrder(USERS_ORDERBY_CHOICE_MAP)
45897
46398
  elif myarg == 'userview':
@@ -46004,7 +46505,7 @@ def doPrintUsers(entityList=None):
46004
46505
  for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
46005
46506
  kwargs = kwargsQuery[0]
46006
46507
  query = kwargsQuery[1]
46007
- query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended)
46508
+ query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended, isArchived)
46008
46509
  printGettingAllAccountEntities(Ent.USER, pquery)
46009
46510
  pageMessage = getPageMessage(showFirstLastItems=True)
46010
46511
  try:
@@ -46014,9 +46515,9 @@ def doPrintUsers(entityList=None):
46014
46515
  GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN,
46015
46516
  GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
46016
46517
  retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
46017
- query=query, fields=fields,
46018
46518
  showDeleted=showDeleted, orderBy=orderBy, sortOrder=sortOrder, viewType=viewType,
46019
46519
  projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
46520
+ query=query, fields=fields,
46020
46521
  maxResults=maxResults, **kwargs)
46021
46522
  for users in feed:
46022
46523
  if orgUnitPath is None:
@@ -46069,6 +46570,8 @@ def doPrintUsers(entityList=None):
46069
46570
  # If no individual fields were specified (allfields, basic, full) or individual fields other than primaryEmail were specified, look up each user
46070
46571
  if isSuspended is not None and fieldsList:
46071
46572
  fieldsList.append('suspended')
46573
+ if isArchived is not None and fieldsList:
46574
+ fieldsList.append('archived')
46072
46575
  if projectionSet or len(set(fieldsList)) > 1 or showValidColumn:
46073
46576
  jcount = len(entityList)
46074
46577
  fields = getFieldsFromFieldsList(fieldsList)
@@ -46864,7 +47367,7 @@ def doCreateInboundSSOCredential():
46864
47367
  count, Ent.Choose(Ent.INBOUND_SSO_CREDENTIALS, count)))
46865
47368
  if generateKey:
46866
47369
  privKey, pemData = _generatePrivateKeyAndPublicCert('', '', 'GAM', keySize, b64enc_pub=False)
46867
- timestamp = datetime.datetime.now(GC.Values[GC.TIMEZONE]).strftime('%Y%m%d-%I%M%S')
47370
+ timestamp = arrow.now(GC.Values[GC.TIMEZONE]).strftime('%Y%m%d-%I%M%S')
46868
47371
  priv_file = f'privatekey-{timestamp}.pem'
46869
47372
  writeFile(priv_file, privKey)
46870
47373
  writeStdout(Msg.WROTE_PRIVATE_KEY_DATA.format(priv_file))
@@ -47936,8 +48439,8 @@ class CourseAttributes():
47936
48439
  def checkDueDate(self, body):
47937
48440
  if 'dueDate' in body and 'dueTime' in body:
47938
48441
  try:
47939
- return self.currDateTime < datetime.datetime(body['dueDate']['year'], body['dueDate']['month'], body['dueDate']['day'],
47940
- body['dueTime'].get('hours', 0), body['dueTime'].get('minutes', 0), tzinfo=iso8601.UTC)
48442
+ return self.currDateTime < arrow.Arrow(body['dueDate']['year'], body['dueDate']['month'], body['dueDate']['day'],
48443
+ body['dueTime'].get('hours', 0), body['dueTime'].get('minutes', 0), tzinfo='UTC')
47941
48444
  except ValueError:
47942
48445
  pass
47943
48446
  return False
@@ -48119,7 +48622,7 @@ class CourseAttributes():
48119
48622
  entityPerformActionModifierItemValueList([Ent.COURSE, newCourse['id']], Act.MODIFIER_FROM, [Ent.COURSE, self.courseId], i, count)
48120
48623
  Ind.Increment()
48121
48624
  if not self.removeDueDate:
48122
- self.currDateTime = datetime.datetime.now(iso8601.UTC)
48625
+ self.currDateTime = arrow.utcnow()
48123
48626
  self.CopyAttributes(newCourse, i, count)
48124
48627
  if self.csvPF:
48125
48628
  self.csvPF.writeCSVfile('Course Drive File IDs')
@@ -48640,13 +49143,15 @@ def _doInfoCourses(courseIdList):
48640
49143
 
48641
49144
  # gam info courses <CourseEntity> [owneraccess]
48642
49145
  # [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
48643
- # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
49146
+ # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
49147
+ # [formatjson]
48644
49148
  def doInfoCourses():
48645
49149
  _doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
48646
49150
 
48647
49151
  # gam info course <CourseID> [owneraccess]
48648
49152
  # [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
48649
- # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
49153
+ # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
49154
+ # [formatjson]
48650
49155
  def doInfoCourse():
48651
49156
  _doInfoCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
48652
49157
 
@@ -48699,7 +49204,7 @@ def _courseItemPassesFilter(item, courseItemFilter):
48699
49204
  return False
48700
49205
  startTime = courseItemFilter['startTime']
48701
49206
  endTime = courseItemFilter['endTime']
48702
- timeValue, _ = iso8601.parse_date(timeStr)
49207
+ timeValue = arrow.get(timeStr)
48703
49208
  return ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime))
48704
49209
 
48705
49210
  def _gettingCoursesQuery(courseSelectionParameters):
@@ -48762,6 +49267,7 @@ def _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getO
48762
49267
  # [show none|all|students|teachers] [countsonly]
48763
49268
  # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
48764
49269
  # [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
49270
+ # (addcsvdata <FieldName> <String>)*
48765
49271
  # [showitemcountonly] [formatjson [quotechar <Character>]]
48766
49272
  def doPrintCourses():
48767
49273
  def _saveParticipants(course, participants, role, rtitles):
@@ -48805,6 +49311,7 @@ def doPrintCourses():
48805
49311
  delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
48806
49312
  showItemCountOnly = False
48807
49313
  useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
49314
+ addCSVData = {}
48808
49315
  while Cmd.ArgumentsRemaining():
48809
49316
  myarg = getArgument()
48810
49317
  if myarg == 'todrive':
@@ -48819,6 +49326,9 @@ def doPrintCourses():
48819
49326
  pass
48820
49327
  elif myarg == 'showitemcountonly':
48821
49328
  showItemCountOnly = True
49329
+ elif myarg == 'addcsvdata':
49330
+ k = getString(Cmd.OB_STRING)
49331
+ addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
48822
49332
  else:
48823
49333
  FJQC.GetFormatJSONQuoteChar(myarg, True)
48824
49334
  applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, None)
@@ -48830,6 +49340,11 @@ def doPrintCourses():
48830
49340
  if showItemCountOnly:
48831
49341
  writeStdout('0\n')
48832
49342
  return
49343
+ if addCSVData:
49344
+ csvPF.AddTitles(sorted(addCSVData.keys()))
49345
+ if FJQC.formatJSON:
49346
+ csvPF.AddJSONTitles(sorted(addCSVData.keys()))
49347
+ csvPF.MoveJSONTitlesToEnd(['JSON'])
48833
49348
  if courseShowProperties['aliases']:
48834
49349
  if FJQC.formatJSON:
48835
49350
  csvPF.AddJSONTitles('JSON-aliases')
@@ -48887,11 +49402,15 @@ def doPrintCourses():
48887
49402
  if courseShowProperties['members'] != 'teachers':
48888
49403
  _saveParticipants(course, students, 'students', stitles)
48889
49404
  row = flattenJSON(course, timeObjects=COURSE_TIME_OBJECTS, noLenObjects=COURSE_NOLEN_OBJECTS)
49405
+ if addCSVData:
49406
+ row.update(addCSVData)
48890
49407
  if not FJQC.formatJSON:
48891
49408
  csvPF.WriteRowTitles(row)
48892
49409
  elif csvPF.CheckRowTitles(row):
48893
49410
  row = {'id': courseId, 'JSON': json.dumps(cleanJSON(course, timeObjects=COURSE_TIME_OBJECTS),
48894
49411
  ensure_ascii=False, sort_keys=True)}
49412
+ if addCSVData:
49413
+ row.update(addCSVData)
48895
49414
  if courseShowProperties['aliases']:
48896
49415
  row['JSON-aliases'] = json.dumps(list(aliases))
48897
49416
  if courseShowProperties['members'] != 'none':
@@ -48964,7 +49483,7 @@ COURSE_ANNOUNCEMENTS_INDEXED_TITLES = ['materials']
48964
49483
  # (orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
48965
49484
  # [showcreatoremails|creatoremail] [fields <CourseAnnouncementFieldNameList>]
48966
49485
  # [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
48967
- # [countsonly] [formatjson [quotechar <Character>]]
49486
+ # [countsonly|(formatjson [quotechar <Character>])]
48968
49487
  def doPrintCourseAnnouncements():
48969
49488
  def _printCourseAnnouncement(course, courseAnnouncement, i, count):
48970
49489
  if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter):
@@ -49020,6 +49539,8 @@ def doPrintCourseAnnouncements():
49020
49539
  coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
49021
49540
  if coursesInfo is None:
49022
49541
  return
49542
+ if countsOnly:
49543
+ csvPF.SetFormatJSON(False)
49023
49544
  applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
49024
49545
  if showCreatorEmail and fieldsList:
49025
49546
  fieldsList.append('creatorUserId')
@@ -49076,7 +49597,7 @@ COURSE_TOPICS_SORT_TITLES = ['courseId', 'courseName', 'topicId', 'name', 'updat
49076
49597
  # (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
49077
49598
  # [topicids <CourseTopicIDEntity>]
49078
49599
  # [timefilter updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
49079
- # [countsonly] [formatjson [quotechar <Character>]]
49600
+ # [countsonly|(formatjson [quotechar <Character>])]
49080
49601
  def doPrintCourseTopics():
49081
49602
  def _printCourseTopic(course, courseTopic):
49082
49603
  if applyCourseItemFilter and not _courseItemPassesFilter(courseTopic, courseItemFilter):
@@ -49117,6 +49638,8 @@ def doPrintCourseTopics():
49117
49638
  coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
49118
49639
  if coursesInfo is None:
49119
49640
  return
49641
+ if countsOnly:
49642
+ csvPF.SetFormatJSON(False)
49120
49643
  applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
49121
49644
  courseTopicIdsLists = courseTopicIds if isinstance(courseTopicIds, dict) else None
49122
49645
  i = 0
@@ -49250,6 +49773,16 @@ def doPrintCourseWM(entityIDType, entityStateType):
49250
49773
  pass
49251
49774
  return topicNames
49252
49775
 
49776
+ def _printCourseWMrow(course, courseWM):
49777
+ row = flattenJSON(courseWM, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=TimeObjects,
49778
+ simpleLists=['studentIds'] if showStudentsAsList else None, delimiter=delimiter)
49779
+ if not FJQC.formatJSON:
49780
+ csvPF.WriteRowTitles(row)
49781
+ elif csvPF.CheckRowTitles(row):
49782
+ csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
49783
+ 'JSON': json.dumps(cleanJSON(courseWM, timeObjects=TimeObjects),
49784
+ ensure_ascii=False, sort_keys=True)})
49785
+
49253
49786
  def _printCourseWM(course, courseWM, i, count):
49254
49787
  if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter):
49255
49788
  return
@@ -49261,14 +49794,13 @@ def doPrintCourseWM(entityIDType, entityStateType):
49261
49794
  topicId = courseWM.get('topicId')
49262
49795
  if topicId:
49263
49796
  courseWM['topicName'] = topicNames.get(topicId, topicId)
49264
- row = flattenJSON(courseWM, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=TimeObjects,
49265
- simpleLists=['studentIds'] if showStudentsAsList else None, delimiter=delimiter)
49266
- if not FJQC.formatJSON:
49267
- csvPF.WriteRowTitles(row)
49268
- elif csvPF.CheckRowTitles(row):
49269
- csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
49270
- 'JSON': json.dumps(cleanJSON(courseWM, timeObjects=TimeObjects),
49271
- ensure_ascii=False, sort_keys=True)})
49797
+ if not oneItemPerRow or not courseWM.get('materials', []):
49798
+ _printCourseWMrow(course, courseWM)
49799
+ else:
49800
+ courseMaterials = courseWM.pop('materials')
49801
+ for courseMaterial in courseMaterials:
49802
+ courseWM['materials'] = courseMaterial
49803
+ _printCourseWMrow(course, courseWM)
49272
49804
 
49273
49805
  croom = buildGAPIObject(API.CLASSROOM)
49274
49806
  if entityIDType == Ent.COURSE_WORK_ID:
@@ -49306,7 +49838,7 @@ def doPrintCourseWM(entityIDType, entityStateType):
49306
49838
  courseShowProperties = _initCourseShowProperties(['name'])
49307
49839
  OBY = OrderBy(OrderbyChoiceMap)
49308
49840
  creatorEmails = {}
49309
- showCreatorEmail = showTopicNames = False
49841
+ oneItemPerRow = showCreatorEmail = showTopicNames = False
49310
49842
  delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
49311
49843
  countsOnly = showStudentsAsList = False
49312
49844
  while Cmd.ArgumentsRemaining():
@@ -49323,6 +49855,9 @@ def doPrintCourseWM(entityIDType, entityStateType):
49323
49855
  pass
49324
49856
  elif myarg == 'orderby':
49325
49857
  OBY.GetChoice()
49858
+ elif myarg == 'oneitemperrow':
49859
+ oneItemPerRow = True
49860
+ csvPF.RemoveIndexedTitles('materials')
49326
49861
  elif myarg in {'showcreatoremails', 'creatoremail'}:
49327
49862
  showCreatorEmail = True
49328
49863
  elif myarg == 'showtopicnames':
@@ -49349,6 +49884,8 @@ def doPrintCourseWM(entityIDType, entityStateType):
49349
49884
  coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
49350
49885
  if coursesInfo is None:
49351
49886
  return
49887
+ if countsOnly:
49888
+ csvPF.SetFormatJSON(False)
49352
49889
  applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
49353
49890
  courseWMIds = courseWMSelectionParameters['courseWMIds']
49354
49891
  courseWMIdsLists = courseWMIds if isinstance(courseWMIds, dict) else {}
@@ -49403,7 +49940,8 @@ def doPrintCourseWM(entityIDType, entityStateType):
49403
49940
  # (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*)
49404
49941
  # [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>]
49405
49942
  # [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
49406
- # [countsonly] [formatjson [quotechar <Character>]]
49943
+ # [oneitemperrow]
49944
+ # [countsonly|(formatjson [quotechar <Character>])]
49407
49945
  def doPrintCourseMaterials():
49408
49946
  doPrintCourseWM(Ent.COURSE_MATERIAL_ID, Ent.COURSE_MATERIAL_STATE)
49409
49947
 
@@ -49414,7 +49952,8 @@ def doPrintCourseMaterials():
49414
49952
  # [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>]
49415
49953
  # [showstudentsaslist [<Boolean>]] [delimiter <Character>]
49416
49954
  # [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
49417
- # [countsonly] [formatjson [quotechar <Character>]]
49955
+ # [oneitemperrow]
49956
+ # [countsonly|(formatjson [quotechar <Character>])]
49418
49957
  def doPrintCourseWork():
49419
49958
  doPrintCourseWM(Ent.COURSE_WORK_ID, Ent.COURSE_WORK_STATE)
49420
49959
 
@@ -49458,7 +49997,7 @@ def _gettingCourseSubmissionQuery(courseSubmissionStates, late, userId):
49458
49997
  # (submissionids <CourseSubmissionIDEntity>)|((submissionstates <CourseSubmissionStateList>)*) [late|notlate]
49459
49998
  # [fields <CourseSubmissionFieldNameList>] [showuserprofile]
49460
49999
  # [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
49461
- # [countsonly] [formatjson [quotechar <Character>]]
50000
+ # [countsonly|(formatjson [quotechar <Character>])]
49462
50001
  def doPrintCourseSubmissions():
49463
50002
  def _printCourseSubmission(course, courseSubmission):
49464
50003
  if applyCourseItemFilter and not _courseItemPassesFilter(courseSubmission, courseItemFilter):
@@ -49540,6 +50079,8 @@ def doPrintCourseSubmissions():
49540
50079
  coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getOwnerId=True)
49541
50080
  if coursesInfo is None:
49542
50081
  return
50082
+ if countsOnly:
50083
+ csvPF.SetFormatJSON(False)
49543
50084
  applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
49544
50085
  courseWorkIds = courseWMSelectionParameters['courseWMIds']
49545
50086
  courseWorkIdsLists = courseWorkIds if isinstance(courseWorkIds, dict) else {}
@@ -53286,16 +53827,16 @@ def infoCalendarEvents(users):
53286
53827
 
53287
53828
  # gam <UserTypeEntity> print events <UserCalendarEntity> <EventEntity> <EventDisplayProperties>*
53288
53829
  # [fields <EventFieldNameList>] [showdayofweek]
53289
- # [countsonly [eventrowfilter]]
53290
- # [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
53830
+ # (addcsvdata <FieldName> <String>)*
53831
+ # [eventrowfilter]
53832
+ # [countsonly|(formatjson [quotechar <Character>])] [todrive <ToDriveAttribute>*]
53291
53833
  # gam <UserTypeEntity> show events <UserCalendarEntity> <EventEntity> <EventDisplayProperties>*
53292
53834
  # [fields <EventFieldNameList>] [showdayofweek]
53293
- # [countsonly]]
53294
- # [formatjson]
53835
+ # ~[countsonly|formatjson]
53295
53836
  def printShowCalendarEvents(users):
53296
53837
  calendarEntity = getUserCalendarEntity()
53297
53838
  calendarEventEntity = getCalendarEventEntity()
53298
- csvPF, FJQC, fieldsList = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.USER)
53839
+ csvPF, FJQC, fieldsList, addCSVData = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.USER)
53299
53840
  i, count, users = getEntityArgument(users)
53300
53841
  for user in users:
53301
53842
  i += 1
@@ -53306,7 +53847,7 @@ def printShowCalendarEvents(users):
53306
53847
  continue
53307
53848
  Ind.Increment()
53308
53849
  _printShowCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity,
53309
- csvPF, FJQC, fieldsList)
53850
+ csvPF, FJQC, fieldsList, addCSVData)
53310
53851
  Ind.Decrement()
53311
53852
  if csvPF:
53312
53853
  if calendarEventEntity['countsOnly'] and calendarEventEntity['eventRowFilter']:
@@ -53330,22 +53871,21 @@ def getStatusEventDateTime(dateType, dateList):
53330
53871
  firstDate = getYYYYMMDD(minLen=1, returnDateTime=True).replace(tzinfo=GC.Values[GC.TIMEZONE])
53331
53872
  if dateType == 'range':
53332
53873
  lastDate = getYYYYMMDD(minLen=1, returnDateTime=True).replace(tzinfo=GC.Values[GC.TIMEZONE])
53333
- deltaDay = datetime.timedelta(days=1)
53334
- deltaWeek = datetime.timedelta(weeks=1)
53335
53874
  if dateType in {'date', 'allday'}:
53336
- dateList.append({'type': 'date', 'first': firstDate, 'last': firstDate+deltaDay,
53337
- 'ulast': firstDate+deltaDay, 'udelta': deltaDay})
53875
+ dateList.append({'type': 'date', 'first': firstDate, 'last': firstDate.shift(days=1),
53876
+ 'ulast': firstDate.shift(days=1), 'udelta': {'days': 1}})
53338
53877
  elif dateType == 'range':
53339
- dateList.append({'type': dateType, 'first': firstDate, 'last': lastDate+deltaDay,
53340
- 'ulast': lastDate, 'udelta': deltaDay})
53878
+ dateList.append({'type': dateType, 'first': firstDate, 'last': lastDate.shift(days=1),
53879
+ 'ulast': lastDate, 'udelta': {'days': 1}})
53341
53880
  elif dateType == 'daily':
53342
53881
  argRepeat = getInteger(minVal=1, maxVal=366)
53343
- dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate+datetime.timedelta(days=argRepeat),
53344
- 'ulast': firstDate+datetime.timedelta(days=argRepeat), 'udelta': deltaDay})
53882
+ dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate.shift(days=argRepeat),
53883
+ 'ulast': firstDate.shift(days=argRepeat), 'udelta': {'days': 1}})
53345
53884
  else: #weekly
53346
53885
  argRepeat = getInteger(minVal=1, maxVal=52)
53347
- dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate+deltaDay, 'pdelta': deltaWeek, 'repeats': argRepeat,
53348
- 'ulast': firstDate+datetime.timedelta(weeks=argRepeat), 'udelta': deltaWeek})
53886
+ dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate.shift(days=1),
53887
+ 'pdelta': {'weeks': 1}, 'repeats': argRepeat,
53888
+ 'ulast': firstDate.shift(weeks=argRepeat), 'udelta': {'weeks': 1}})
53349
53889
 
53350
53890
  def _showCalendarStatusEvent(primaryEmail, calId, eventEntityType, event, k, kcount, FJQC):
53351
53891
  if FJQC.formatJSON:
@@ -53533,7 +54073,7 @@ def createStatusEvent(users, eventType):
53533
54073
  if wlDate['type'] != 'timerange':
53534
54074
  body['start']['date'] = first.strftime(YYYYMMDD_FORMAT)
53535
54075
  kvList[5] = body['start']['date']
53536
- body['end']['date'] = (first+datetime.timedelta(days=1)).strftime(YYYYMMDD_FORMAT)
54076
+ body['end']['date'] = (first.shift(days=1)).strftime(YYYYMMDD_FORMAT)
53537
54077
  else:
53538
54078
  body['start']['dateTime'] = ISOformatTimeStamp(first)
53539
54079
  kvList[5] = body['start']['dateTime']
@@ -53552,7 +54092,7 @@ def createStatusEvent(users, eventType):
53552
54092
  entityActionPerformed(kvList, j, jcount)
53553
54093
  if wlDate['type'] == 'timerange':
53554
54094
  break
53555
- first += wlDate['udelta']
54095
+ first = first.shift(**wlDate['udelta'])
53556
54096
  except (GAPI.forbidden, GAPI.invalid) as e:
53557
54097
  entityActionFailedWarning([Ent.CALENDAR, user], str(e), i, count)
53558
54098
  break
@@ -53751,10 +54291,10 @@ def printShowStatusEvent(users, eventType):
53751
54291
  for event in events:
53752
54292
  if showDayOfWeek:
53753
54293
  _getEventDaysOfWeek(event)
53754
- _printCalendarEvent(user, calId, event, csvPF, FJQC)
54294
+ _printCalendarEvent(user, calId, event, csvPF, FJQC, {})
53755
54295
  if 'pdelta' in wlDate:
53756
- first += wlDate['pdelta']
53757
- last += wlDate['pdelta']
54296
+ first = first.shift(**wlDate['pdelta'])
54297
+ last = last.shift(**wlDate['pdelta'])
53758
54298
  if csvPF:
53759
54299
  csvPF.writeCSVfile(f'Calendar {Ent.Plural(entityType)}')
53760
54300
 
@@ -54969,7 +55509,7 @@ def getDriveFileAttribute(myarg, body, parameters, updateCmd):
54969
55509
  elif myarg == 'timestamp':
54970
55510
  parameters[DFA_TIMESTAMP] = getBoolean()
54971
55511
  elif myarg == 'timeformat':
54972
- parameters[DFA_TIMEFORMAT] = getString(Cmd.OB_STRING, minLen=0)
55512
+ parameters[DFA_TIMEFORMAT] = getString(Cmd.OB_DATETIME_FORMAT, minLen=0)
54973
55513
  elif getDriveFileCopyAttribute(myarg, body, parameters):
54974
55514
  pass
54975
55515
  else:
@@ -56258,7 +56798,7 @@ def _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revis
56258
56798
  count = 0
56259
56799
  if revisionsEntity['time'][0] == 'before':
56260
56800
  for revision in results:
56261
- modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
56801
+ modifiedDateTime = arrow.get(revision['modifiedTime'])
56262
56802
  if modifiedDateTime >= dateTime:
56263
56803
  break
56264
56804
  revisionIds.append(revision['id'])
@@ -56268,7 +56808,7 @@ def _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revis
56268
56808
  return revisionIds
56269
56809
  # time: after
56270
56810
  for revision in results:
56271
- modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
56811
+ modifiedDateTime = arrow.get(revision['modifiedTime'])
56272
56812
  if modifiedDateTime >= dateTime:
56273
56813
  revisionIds.append(revision['id'])
56274
56814
  count += 1
@@ -56280,7 +56820,7 @@ def _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revis
56280
56820
  endDateTime = revisionsEntity['range'][2]
56281
56821
  count = 0
56282
56822
  for revision in results:
56283
- modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
56823
+ modifiedDateTime = arrow.get(revision['modifiedTime'])
56284
56824
  if modifiedDateTime >= startDateTime:
56285
56825
  if modifiedDateTime >= endDateTime:
56286
56826
  break
@@ -56490,7 +57030,7 @@ def _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDe
56490
57030
  count = 0
56491
57031
  if revisionsEntity['time'][0] == 'before':
56492
57032
  for revision in results:
56493
- modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
57033
+ modifiedDateTime = arrow.get(revision['modifiedTime'])
56494
57034
  if modifiedDateTime >= dateTime:
56495
57035
  break
56496
57036
  count += 1
@@ -56501,7 +57041,7 @@ def _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDe
56501
57041
  return results[:count]
56502
57042
  # time: after
56503
57043
  for revision in results:
56504
- modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
57044
+ modifiedDateTime = arrow.get(revision['modifiedTime'])
56505
57045
  if modifiedDateTime >= dateTime:
56506
57046
  break
56507
57047
  count += 1
@@ -56518,7 +57058,7 @@ def _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDe
56518
57058
  count = 0
56519
57059
  selectedResults = []
56520
57060
  for revision in results:
56521
- modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
57061
+ modifiedDateTime = arrow.get(revision['modifiedTime'])
56522
57062
  if modifiedDateTime >= startDateTime:
56523
57063
  if modifiedDateTime >= endDateTime:
56524
57064
  break
@@ -57056,7 +57596,7 @@ class PermissionMatch():
57056
57596
  break
57057
57597
  elif field in {'expirationstart', 'expirationend'}:
57058
57598
  if 'expirationTime' in permission:
57059
- expirationDateTime, _ = iso8601.parse_date(permission['expirationTime'])
57599
+ expirationDateTime = arrow.get(permission['expirationTime'])
57060
57600
  if field == 'expirationstart':
57061
57601
  if expirationDateTime < value:
57062
57602
  break
@@ -59539,7 +60079,7 @@ def processFilenameReplacements(name, replacements):
59539
60079
  return name
59540
60080
 
59541
60081
  def addTimestampToFilename(parameters, body):
59542
- tdtime = datetime.datetime.now(GC.Values[GC.TIMEZONE])
60082
+ tdtime = arrow.now(GC.Values[GC.TIMEZONE])
59543
60083
  body['name'] += ' - '
59544
60084
  if not parameters[DFA_TIMEFORMAT]:
59545
60085
  body['name'] += ISOformatTimeStamp(tdtime)
@@ -59556,7 +60096,7 @@ createReturnItemMap = {
59556
60096
  # [(localfile <FileName>|-)|(url <URL>)]
59557
60097
  # [(drivefilename|newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
59558
60098
  # [stripnameprefix <String>]
59559
- # [timestamp <Boolean>]] [timeformat <String>]
60099
+ # [timestamp <Boolean>]] [timeformat <DateTimeFormat>]
59560
60100
  # <DriveFileCreateAttribute>* [noduplicate]
59561
60101
  # [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*)) |
59562
60102
  # (returnidonly|returnlinkonly|returneditlinkonly|showdetails)]
@@ -60083,7 +60623,7 @@ def checkDriveFileShortcut(users):
60083
60623
  # [(localfile <FileName>|-)|(url <URL>)]
60084
60624
  # [retainname | (newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
60085
60625
  # [stripnameprefix <String>]
60086
- # [timestamp <Boolean>]] [timeformat <String>]
60626
+ # [timestamp <Boolean>]] [timeformat <DateTimeFormat>]
60087
60627
  # <DriveFileUpdateAttribute>*
60088
60628
  # [(gsheet|csvsheet <SheetEntity> [clearfilter])|(addsheet <String>)]
60089
60629
  # [charset <String>] [columndelimiter <Character>]
@@ -64531,11 +65071,11 @@ def claimOwnership(users):
64531
65071
  elif myarg == 'onlyusers':
64532
65072
  _, userList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
64533
65073
  checkOnly = True
64534
- onlyOwners = set(userList)
65074
+ onlyOwners = {normalizeEmailAddressOrUID(user, noUid=True) for user in userList}
64535
65075
  elif myarg == 'skipusers':
64536
65076
  _, userList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
64537
65077
  checkSkip = len(userList) > 0
64538
- skipOwners = set(userList)
65078
+ skipOwners = {normalizeEmailAddressOrUID(user, noUid=True) for user in userList}
64539
65079
  elif myarg == 'subdomains':
64540
65080
  subdomains = getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)
64541
65081
  elif myarg == 'includetrashed':
@@ -71667,8 +72207,8 @@ def _mapMessageQueryDates(parameters):
71667
72207
  if not mg:
71668
72208
  break
71669
72209
  try:
71670
- dt = datetime.datetime(int(mg.groups()[1]), int(mg.groups()[2]), int(mg.groups()[3]), tzinfo=GC.Values[GC.TIMEZONE])
71671
- query = query[:mg.start(2)]+str(int(datetime.datetime.timestamp(dt)))+query[mg.end(4):]
72210
+ dt = arrow.Arrow(int(mg.groups()[1]), int(mg.groups()[2]), int(mg.groups()[3]), tzinfo=GC.Values[GC.TIMEZONE])
72211
+ query = query[:mg.start(2)]+str(dt.int_timestamp)+query[mg.end(4):]
71672
72212
  except ValueError:
71673
72213
  pass
71674
72214
  pos = mg.end()
@@ -72849,9 +73389,9 @@ def printShowMessagesThreads(users, entityType):
72849
73389
  if pLoc > 0:
72850
73390
  dateTimeValue = dateTimeValue[:pLoc]
72851
73391
  try:
72852
- dateTimeValue = datetime.datetime.strptime(dateTimeValue, RFC2822_TIME_FORMAT)
73392
+ dateTimeValue = arrow.Arrow.strptime(dateTimeValue, RFC2822_TIME_FORMAT)
72853
73393
  if dateHeaderConvertTimezone:
72854
- dateTimeValue = dateTimeValue.astimezone(GC.Values[GC.TIMEZONE])
73394
+ dateTimeValue = dateTimeValue.to(GC.Values[GC.TIMEZONE])
72855
73395
  return dateTimeValue.strftime(dateHeaderFormat)
72856
73396
  except ValueError:
72857
73397
  return headerValue
@@ -73508,14 +74048,58 @@ def printShowMessages(users):
73508
74048
  def printShowThreads(users):
73509
74049
  printShowMessagesThreads(users, Ent.THREAD)
73510
74050
 
74051
+ def sendCreateDelegateNotification(user, delegate, basenotify, i=0, count=0, msgFrom=None):
74052
+ # Substitute for #user#, #delegate#
74053
+ def _substituteForPattern(field, pattern, value):
74054
+ if field.find('#') == -1:
74055
+ return field
74056
+ return field.replace(pattern, value)
74057
+
74058
+ def _makeSubstitutions(field):
74059
+ notify[field] = _substituteForPattern(notify[field], '#user#', user)
74060
+ notify[field] = _substituteForPattern(notify[field], '#delegate#', delegate)
74061
+
74062
+ notify = basenotify.copy()
74063
+ if not notify['subject']:
74064
+ notify['subject'] = Msg.CREATE_DELEGATE_NOTIFY_SUBJECT
74065
+ _makeSubstitutions('subject')
74066
+ if not notify['message']:
74067
+ notify['message'] = Msg.CREATE_DELEGATE_NOTIFY_MESSAGE
74068
+ elif notify['html']:
74069
+ notify['message'] = notify['message'].replace('\r', '').replace('\\n', '<br/>')
74070
+ else:
74071
+ notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
74072
+ _makeSubstitutions('message')
74073
+ if 'from' in notify:
74074
+ msgFrom = notify['from']
74075
+ msgReplyTo = notify.get('replyto', None)
74076
+ mailBox = notify.get('mailbox', None)
74077
+ send_email(notify['subject'], notify['message'], delegate, i, count,
74078
+ msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'], mailBox=mailBox)
74079
+
73511
74080
  # gam <UserTypeEntity> create delegate|delegates [convertalias] <UserEntity>
74081
+ # [notify [<Boolean>]
74082
+ # [subject <String>]
74083
+ # [from <EmailAaddress>] [mailbox <EmailAddress>]
74084
+ # [replyto <EmailAaddress>]
74085
+ # [<NotifyMessageContent>] [html [<Boolean>]]
74086
+ # ]
73512
74087
  # gam <UserTypeEntity> delete delegate|delegates [convertalias] <UserEntity>
73513
74088
  def processDelegates(users):
73514
74089
  cd = buildGAPIObject(API.DIRECTORY)
73515
- function = 'delete' if Act.Get() == Act.DELETE else 'create'
74090
+ createCmd = Act.Get() != Act.DELETE
73516
74091
  aliasAllowed = not checkArgumentPresent(['convertalias'])
73517
74092
  delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
73518
- checkForExtraneousArguments()
74093
+ notify = {'notify': False, 'subject': '', 'message': '', 'html': False, 'charset': UTF8}
74094
+ if createCmd:
74095
+ while Cmd.ArgumentsRemaining():
74096
+ myarg = getArgument()
74097
+ if getNotifyArguments(myarg, notify, False):
74098
+ pass
74099
+ else:
74100
+ unknownArgumentExit()
74101
+ else:
74102
+ checkForExtraneousArguments()
73519
74103
  i, count, users = getEntityArgument(users)
73520
74104
  for user in users:
73521
74105
  i += 1
@@ -73527,25 +74111,37 @@ def processDelegates(users):
73527
74111
  for delegate in delegates:
73528
74112
  j += 1
73529
74113
  delegateEmail = convertUIDtoEmailAddress(delegate, cd=cd, emailTypes=['user', 'group'], aliasAllowed=aliasAllowed)
74114
+ kvList = [Ent.USER, user, Ent.DELEGATE, delegateEmail]
73530
74115
  try:
73531
- if function == 'create':
73532
- callGAPI(gmail.users().settings().delegates(), function,
74116
+ if createCmd:
74117
+ callGAPI(gmail.users().settings().delegates(), 'create',
73533
74118
  throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION, GAPI.INVALID,
73534
74119
  GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
73535
74120
  userId='me', body={'delegateEmail': delegateEmail})
74121
+ entityActionPerformed(kvList, j, jcount)
74122
+ if notify['notify']:
74123
+ Ind.Increment()
74124
+ sendCreateDelegateNotification(user, delegateEmail, notify, j, jcount)
74125
+ Ind.Decrement()
73536
74126
  else:
73537
- callGAPI(gmail.users().settings().delegates(), function,
74127
+ callGAPI(gmail.users().settings().delegates(), 'delete',
73538
74128
  throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_INPUT, GAPI.PERMISSION_DENIED],
73539
74129
  userId='me', delegateEmail=delegateEmail)
73540
- entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
74130
+ entityActionPerformed(kvList, j, jcount)
73541
74131
  except (GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.invalid,
73542
74132
  GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
73543
- entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, delegateEmail], str(e), j, jcount)
74133
+ entityActionFailedWarning(kvList, str(e), j, jcount)
73544
74134
  except GAPI.serviceNotAvailable:
73545
74135
  userGmailServiceNotEnabledWarning(user, i, count)
73546
74136
  Ind.Decrement()
73547
74137
 
73548
74138
  # gam <UserTypeEntity> delegate to [convertalias] <UserEntity>
74139
+ # [notify [<Boolean>]
74140
+ # [subject <String>]
74141
+ # [from <EmailAaddress>] [mailbox <EmailAddress>]
74142
+ # [replyto <EmailAaddress>]
74143
+ # [<NotifyMessageContent>] [html [<Boolean>]]
74144
+ # ]
73549
74145
  def delegateTo(users):
73550
74146
  checkArgumentPresent('to', required=True)
73551
74147
  processDelegates(users)
@@ -77002,7 +77598,7 @@ TASK_QUERY_STATE_MAP = {
77002
77598
  # [updatedmin <Time>]
77003
77599
  # [showcompleted [<Boolean>]] [showdeleted [<Boolean>]] [showhidden [<Boolean>]] [showall]
77004
77600
  # [orderby completed|due|updated]
77005
- # [countsonly | (formatjson [quotechar <Character>])]
77601
+ # [countsonly|(formatjson [quotechar <Character>])]
77006
77602
  def printShowTasks(users):
77007
77603
  def _showTaskAndChildren(tasklist, taskId, k, compact):
77008
77604
  if taskId in taskParentsProcessed:
@@ -77069,8 +77665,11 @@ def printShowTasks(users):
77069
77665
  csvPF.SetTitles(['User', CSVTitle])
77070
77666
  else:
77071
77667
  FJQC.GetFormatJSONQuoteChar(myarg, False)
77072
- if csvPF and FJQC.formatJSON:
77073
- csvPF.SetJSONTitles(['User', 'tasklistId', 'id', 'taskId', 'title', 'JSON'])
77668
+ if csvPF:
77669
+ if countsOnly:
77670
+ csvPF.SetFormatJSON(False)
77671
+ elif FJQC.formatJSON:
77672
+ csvPF.SetJSONTitles(['User', 'tasklistId', 'id', 'taskId', 'title', 'JSON'])
77074
77673
  i, count, users = getEntityArgument(users)
77075
77674
  for user in users:
77076
77675
  i += 1
@@ -77269,7 +77868,7 @@ def processTasklists(users):
77269
77868
  # gam <UserTypeEntity> show tasklists
77270
77869
  # [countsonly|formatjson]
77271
77870
  # gam <UserTypeEntity> print tasklists [todrive <ToDriveAttribute>*]
77272
- # [countsonly | (formatjson [quotechar <Character>])]
77871
+ # [countsonly|(formatjson [quotechar <Character>])]
77273
77872
  def printShowTasklists(users):
77274
77873
  csvPF = CSVPrintFile(['User', 'id', 'title']) if Act.csvFormat() else None
77275
77874
  if csvPF:
@@ -77287,6 +77886,8 @@ def printShowTasklists(users):
77287
77886
  csvPF.SetTitles(['User', CSVTitle])
77288
77887
  else:
77289
77888
  FJQC.GetFormatJSONQuoteChar(myarg, True)
77889
+ if countsOnly and csvPF:
77890
+ csvPF.SetFormatJSON(False)
77290
77891
  i, count, users = getEntityArgument(users)
77291
77892
  for user in users:
77292
77893
  i += 1
@@ -77890,6 +78491,7 @@ MAIN_ADD_CREATE_FUNCTIONS = {
77890
78491
  Cmd.ARG_VAULTEXPORT: doCreateVaultExport,
77891
78492
  Cmd.ARG_VAULTHOLD: doCreateVaultHold,
77892
78493
  Cmd.ARG_VAULTMATTER: doCreateVaultMatter,
78494
+ Cmd.ARG_VAULTQUERY: doCreateVaultQuery,
77893
78495
  Cmd.ARG_VERIFY: doCreateSiteVerification,
77894
78496
  }
77895
78497
 
@@ -77947,6 +78549,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
77947
78549
  {Cmd.ARG_SHAREDDRIVEACLS: doCopySyncSharedDriveACLs,
77948
78550
  Cmd.ARG_STORAGEBUCKET: doCopyCloudStorageBucket,
77949
78551
  Cmd.ARG_VAULTEXPORT: doCopyVaultExport,
78552
+ Cmd.ARG_VAULTQUERY: doCopyVaultQuery,
77950
78553
  }
77951
78554
  ),
77952
78555
  'create':
@@ -78011,6 +78614,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
78011
78614
  Cmd.ARG_VAULTEXPORT: doDeleteVaultExport,
78012
78615
  Cmd.ARG_VAULTHOLD: doDeleteVaultHold,
78013
78616
  Cmd.ARG_VAULTMATTER: doDeleteVaultMatter,
78617
+ Cmd.ARG_VAULTQUERY: doDeleteVaultQuery,
78014
78618
  }
78015
78619
  ),
78016
78620
  'download':
@@ -78138,6 +78742,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
78138
78742
  Cmd.ARG_CHROMEAPP: doPrintShowChromeApps,
78139
78743
  Cmd.ARG_CHROMEAPPDEVICES: doPrintShowChromeAppDevices,
78140
78744
  Cmd.ARG_CHROMEAUES: doPrintShowChromeAues,
78745
+ Cmd.ARG_CHROMEDEVICECOUNTS: doPrintShowChromeDeviceCounts,
78141
78746
  Cmd.ARG_CHROMEHISTORY: doPrintShowChromeHistory,
78142
78747
  Cmd.ARG_CHROMENEEDSATTN: doPrintShowChromeNeedsAttn,
78143
78748
  Cmd.ARG_CHROMEPOLICY: doPrintShowChromePolicies,
@@ -78275,6 +78880,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
78275
78880
  Cmd.ARG_CHROMEAPP: doPrintShowChromeApps,
78276
78881
  Cmd.ARG_CHROMEAPPDEVICES: doPrintShowChromeAppDevices,
78277
78882
  Cmd.ARG_CHROMEAUES: doPrintShowChromeAues,
78883
+ Cmd.ARG_CHROMEDEVICECOUNTS: doPrintShowChromeDeviceCounts,
78278
78884
  Cmd.ARG_CHROMEHISTORY: doPrintShowChromeHistory,
78279
78885
  Cmd.ARG_CHROMENEEDSATTN: doPrintShowChromeNeedsAttn,
78280
78886
  Cmd.ARG_CHROMEPOLICY: doPrintShowChromePolicies,