mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-28 09:51:36 +00:00
* Refactor into a python module format -Updates import statements to be absolute vs implicitly relative -Uses import syntax that minimizes the need to update references in code and/or reformat affected lines (e.g. `import gapi.directory` becomes `from gam.gapi import directory as gapi_directory`) -Adds a `__main__.py` such that the module can be executed on its own using standard `python3 -m gam` syntax -Replaces __main__ import hack with module import -Updates the GAM path to be the module's parent dir * Add gam.py to /src for backwards compatibility A stub that calls gam.__main__.main() to be used by users who are not with the syntax of calling a module implementation. It should also provide immediate backwards-compatibility with existing scripts with references to this file. * Move build tools back to the main dir and out of the package * Fix pylint errors * Update build spec to use new package format Incorporates @jay0lee's patch from https://github.com/jay0lee/GAM/pull/1165#issuecomment-618430828
239 lines
8.9 KiB
Python
239 lines
8.9 KiB
Python
"""Methods related to display of information to the user."""
|
|
|
|
import csv
|
|
import datetime
|
|
import io
|
|
import sys
|
|
import webbrowser
|
|
|
|
import dateutil
|
|
import googleapiclient.http
|
|
|
|
#TODO: get rid of these hacks
|
|
import gam
|
|
from gam.var import *
|
|
from gam import controlflow
|
|
from gam import gapi
|
|
|
|
|
|
def current_count(i, count):
|
|
return f' ({i}/{count})' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else ''
|
|
|
|
def current_count_nl(i, count):
|
|
return f' ({i}/{count})\n' if (count > GC_Values[GC_SHOW_COUNTS_MIN]) else '\n'
|
|
|
|
def add_field_to_fields_list(fieldName, fieldsChoiceMap, fieldsList):
|
|
fields = fieldsChoiceMap[fieldName.lower()]
|
|
if isinstance(fields, list):
|
|
fieldsList.extend(fields)
|
|
else:
|
|
fieldsList.append(fields)
|
|
|
|
# Write a CSV file
|
|
def add_titles_to_csv_file(addTitles, titles):
|
|
for title in addTitles:
|
|
if title not in titles:
|
|
titles.append(title)
|
|
|
|
def add_row_titles_to_csv_file(row, csvRows, titles):
|
|
csvRows.append(row)
|
|
for title in row:
|
|
if title not in titles:
|
|
titles.append(title)
|
|
|
|
# fieldName is command line argument
|
|
# fieldNameMap maps fieldName to API field names; CSV file header will be API field name
|
|
#ARGUMENT_TO_PROPERTY_MAP = {
|
|
# u'admincreated': [u'adminCreated'],
|
|
# u'aliases': [u'aliases', u'nonEditableAliases'],
|
|
# }
|
|
# fieldsList is the list of API fields
|
|
# fieldsTitles maps the API field name to the CSV file header
|
|
def add_field_to_csv_file(fieldName, fieldNameMap, fieldsList, fieldsTitles, titles):
|
|
for ftList in fieldNameMap[fieldName]:
|
|
if ftList not in fieldsTitles:
|
|
fieldsList.append(ftList)
|
|
fieldsTitles[ftList] = ftList
|
|
add_titles_to_csv_file([ftList], titles)
|
|
|
|
# fieldName is command line argument
|
|
# fieldNameTitleMap maps fieldName to API field name and CSV file header
|
|
#ARGUMENT_TO_PROPERTY_TITLE_MAP = {
|
|
# u'admincreated': [u'adminCreated', u'Admin_Created'],
|
|
# u'aliases': [u'aliases', u'Aliases', u'nonEditableAliases', u'NonEditableAliases'],
|
|
# }
|
|
# fieldsList is the list of API fields
|
|
# fieldsTitles maps the API field name to the CSV file header
|
|
def add_field_title_to_csv_file(fieldName, fieldNameTitleMap, fieldsList, fieldsTitles, titles):
|
|
ftList = fieldNameTitleMap[fieldName]
|
|
for i in range(0, len(ftList), 2):
|
|
if ftList[i] not in fieldsTitles:
|
|
fieldsList.append(ftList[i])
|
|
fieldsTitles[ftList[i]] = ftList[i+1]
|
|
add_titles_to_csv_file([ftList[i+1]], titles)
|
|
|
|
def sort_csv_titles(firstTitle, titles):
|
|
restoreTitles = []
|
|
for title in firstTitle:
|
|
if title in titles:
|
|
titles.remove(title)
|
|
restoreTitles.append(title)
|
|
titles.sort()
|
|
for title in restoreTitles[::-1]:
|
|
titles.insert(0, title)
|
|
|
|
def QuotedArgumentList(items):
|
|
return ' '.join([item if item and (item.find(' ') == -1) and (item.find(',') == -1) else '"'+item+'"' for item in items])
|
|
|
|
def write_csv_file(csvRows, titles, list_type, todrive):
|
|
def rowDateTimeFilterMatch(dateMode, rowDate, op, filterDate):
|
|
if not rowDate or not isinstance(rowDate, str):
|
|
return False
|
|
try:
|
|
rowTime = dateutil.parser.parse(rowDate, ignoretz=True)
|
|
if dateMode:
|
|
rowDate = datetime.datetime(rowTime.year, rowTime.month, rowTime.day).isoformat()+'Z'
|
|
except ValueError:
|
|
rowDate = NEVER_TIME
|
|
if op == '<':
|
|
return rowDate < filterDate
|
|
if op == '<=':
|
|
return rowDate <= filterDate
|
|
if op == '>':
|
|
return rowDate > filterDate
|
|
if op == '>=':
|
|
return rowDate >= filterDate
|
|
if op == '!=':
|
|
return rowDate != filterDate
|
|
return rowDate == filterDate
|
|
|
|
def rowCountFilterMatch(rowCount, op, filterCount):
|
|
if isinstance(rowCount, str):
|
|
if not rowCount.isdigit():
|
|
return False
|
|
rowCount = int(rowCount)
|
|
elif not isinstance(rowCount, int):
|
|
return False
|
|
if op == '<':
|
|
return rowCount < filterCount
|
|
if op == '<=':
|
|
return rowCount <= filterCount
|
|
if op == '>':
|
|
return rowCount > filterCount
|
|
if op == '>=':
|
|
return rowCount >= filterCount
|
|
if op == '!=':
|
|
return rowCount != filterCount
|
|
return rowCount == filterCount
|
|
def rowBooleanFilterMatch(rowBoolean, filterBoolean):
|
|
if not isinstance(rowBoolean, bool):
|
|
return False
|
|
return rowBoolean == filterBoolean
|
|
|
|
def headerFilterMatch(filters, title):
|
|
for filterStr in filters:
|
|
if filterStr.match(title):
|
|
return True
|
|
return False
|
|
|
|
if GC_Values[GC_CSV_ROW_FILTER]:
|
|
for column, filterVal in iter(GC_Values[GC_CSV_ROW_FILTER].items()):
|
|
if column not in titles:
|
|
sys.stderr.write(f'WARNING: Row filter column "{column}" is not in output columns\n')
|
|
continue
|
|
if filterVal[0] == 'regex':
|
|
csvRows = [row for row in csvRows if filterVal[1].search(str(row.get(column, '')))]
|
|
elif filterVal[0] == 'notregex':
|
|
csvRows = [row for row in csvRows if not filterVal[1].search(str(row.get(column, '')))]
|
|
elif filterVal[0] in ['date', 'time']:
|
|
csvRows = [row for row in csvRows if rowDateTimeFilterMatch(filterVal[0] == 'date', row.get(column, ''), filterVal[1], filterVal[2])]
|
|
elif filterVal[0] == 'count':
|
|
csvRows = [row for row in csvRows if rowCountFilterMatch(row.get(column, 0), filterVal[1], filterVal[2])]
|
|
else: #boolean
|
|
csvRows = [row for row in csvRows if rowBooleanFilterMatch(row.get(column, False), filterVal[1])]
|
|
if GC_Values[GC_CSV_HEADER_FILTER] or GC_Values[GC_CSV_HEADER_DROP_FILTER]:
|
|
if GC_Values[GC_CSV_HEADER_DROP_FILTER]:
|
|
titles = [t for t in titles if not headerFilterMatch(GC_Values[GC_CSV_HEADER_DROP_FILTER], t)]
|
|
if GC_Values[GC_CSV_HEADER_FILTER]:
|
|
titles = [t for t in titles if headerFilterMatch(GC_Values[GC_CSV_HEADER_FILTER], t)]
|
|
if not titles:
|
|
controlflow.system_error_exit(3, 'No columns selected with GAM_CSV_HEADER_FILTER and GAM_CSV_HEADER_DROP_FILTER\n')
|
|
return
|
|
csv.register_dialect('nixstdout', lineterminator='\n')
|
|
if todrive:
|
|
write_to = io.StringIO()
|
|
else:
|
|
write_to = sys.stdout
|
|
writer = csv.DictWriter(write_to, fieldnames=titles, dialect='nixstdout', extrasaction='ignore', quoting=csv.QUOTE_MINIMAL)
|
|
try:
|
|
writer.writerow(dict((item, item) for item in writer.fieldnames))
|
|
writer.writerows(csvRows)
|
|
except IOError as e:
|
|
controlflow.system_error_exit(6, e)
|
|
if todrive:
|
|
admin_email = gam._getValueFromOAuth('email')
|
|
_, drive = gam.buildDrive3GAPIObject(admin_email)
|
|
if not drive:
|
|
print(f'''\nGAM is not authorized to create Drive files. Please run:
|
|
gam user {admin_email} check serviceaccount
|
|
and follow recommend steps to authorize GAM for Drive access.''')
|
|
sys.exit(5)
|
|
result = gapi.call(drive.about(), 'get', fields='maxImportSizes')
|
|
columns = len(titles)
|
|
rows = len(csvRows)
|
|
cell_count = rows * columns
|
|
data_size = len(write_to.getvalue())
|
|
max_sheet_bytes = int(result['maxImportSizes'][MIMETYPE_GA_SPREADSHEET])
|
|
if cell_count > MAX_GOOGLE_SHEET_CELLS or data_size > max_sheet_bytes:
|
|
print(f'{WARNING_PREFIX}{MESSAGE_RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET}')
|
|
mimeType = 'text/csv'
|
|
else:
|
|
mimeType = MIMETYPE_GA_SPREADSHEET
|
|
body = {'description': QuotedArgumentList(sys.argv),
|
|
'name': f'{GC_Values[GC_DOMAIN]} - {list_type}',
|
|
'mimeType': mimeType}
|
|
result = gapi.call(drive.files(), 'create', fields='webViewLink',
|
|
body=body,
|
|
media_body=googleapiclient.http.MediaInMemoryUpload(write_to.getvalue().encode(),
|
|
mimetype='text/csv'))
|
|
file_url = result['webViewLink']
|
|
if GC_Values[GC_NO_BROWSER]:
|
|
msg_txt = f'Drive file uploaded to:\n {file_url}'
|
|
msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}'
|
|
gam.send_email(msg_subj, msg_txt)
|
|
print(msg_txt)
|
|
else:
|
|
webbrowser.open(file_url)
|
|
|
|
def print_error(message):
|
|
"""Prints a one-line error message to stderr in a standard format."""
|
|
sys.stderr.write('\n{0}{1}\n'.format(ERROR_PREFIX, message))
|
|
|
|
|
|
def print_warning(message):
|
|
"""Prints a one-line warning message to stderr in a standard format."""
|
|
sys.stderr.write('\n{0}{1}\n'.format(WARNING_PREFIX, message))
|
|
|
|
def print_json(object_value, spacing=''):
|
|
"""Prints Dict or Array to screen in clean human-readable format.."""
|
|
if isinstance(object_value, list):
|
|
if len(object_value) == 1 and isinstance(object_value[0], (str, int, bool)):
|
|
sys.stdout.write(f'{object_value[0]}\n')
|
|
return
|
|
if spacing:
|
|
sys.stdout.write('\n')
|
|
for i, a_value in enumerate(object_value):
|
|
if isinstance(a_value, (str, int, bool)):
|
|
sys.stdout.write(f' {spacing}{i+1}) {a_value}\n')
|
|
else:
|
|
sys.stdout.write(f' {spacing}{i+1}) ')
|
|
print_json(a_value, f' {spacing}')
|
|
elif isinstance(object_value, dict):
|
|
for key in ['kind', 'etag', 'etags']:
|
|
object_value.pop(key, None)
|
|
for another_object, another_value in object_value.items():
|
|
sys.stdout.write(f' {spacing}{another_object}: ')
|
|
print_json(another_value, f' {spacing}')
|
|
else:
|
|
sys.stdout.write(f'{object_value}\n')
|