Phase 3 complete

This commit is contained in:
Jay Lee
2026-07-04 05:37:34 -04:00
parent 5b27b7b875
commit c41149bedb
58 changed files with 1716 additions and 1983 deletions

View File

@@ -167,19 +167,13 @@ from gam.util.email import ( # noqa: F401 # re-export
_addAttachmentsToMessage, _addEmbeddedImagesToMessage, send_email,
)
IS08601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%:z'
RFC2822_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S %z'
def ISOformatTimeStamp(timestamp):
return timestamp.isoformat('T', 'seconds')
from gam.constants import *
def currentISOformatTimeStamp(timespec='milliseconds'):
return arrow.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec)
from util.args import ISOformatTimeStamp, currentISOformatTimeStamp # noqa: E402,F401
from util.fileio import adjustRedirectedSTDFilesIfNotMultiprocessing, closeSTDFilesIfNotMultiprocessing # noqa: E402,F401
Act = glaction.GamAction()
Cmd = glclargs.GamCLArgs()
Ent = glentity.GamEntity()
Ind = glindent.GamIndent()
from gam.var import Act, Cmd, Ent, Ind # noqa: E402,F401
# Finding path method varies between Python source, PyInstaller and StaticX
if os.environ.get('STATICX_PROG_PATH', False):
@@ -195,35 +189,9 @@ else:
GM.Globals[GM.GAM_PATH] = os.path.dirname(os.path.realpath(__file__))
GM.Globals[GM.GAM_TYPE] = 'pythonsource'
GIT_USER = 'GAM-team'
GAM = 'GAM'
GAM_URL = f'https://github.com/{GIT_USER}/{GAM}'
GAM_USER_AGENT = (f'{GAM} {__version__} - {GAM_URL} / '
f'{__author__} / '
f'Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {sys.version_info[3]} / '
f'{platform.platform()} {platform.machine()} /'
)
GAM_RELEASES = f'https://github.com/{GIT_USER}/{GAM}/releases'
GAM_WIKI = f'https://github.com/{GIT_USER}/{GAM}/wiki'
GAM_LATEST_RELEASE = f'https://api.github.com/repos/{GIT_USER}/{GAM}/releases/latest'
GAM_PROJECT_CREATION = 'GAM Project Creation'
GAM_PROJECT_CREATION_CLIENT_ID = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com'
from gam.constants import *
from util.output import redactable_debug_print, showAPICallsRetryData # noqa: E402,F401
def redactable_debug_print(*args):
processed_args = []
for arg in args:
if arg.startswith('b\''):
sbytes = arg[2:-1]
sbytes = bytes(sbytes, 'utf-8')
arg = sbytes.decode()
arg = arg.replace('\\r\\n', "\n ")
if GC.Values[GC.DEBUG_REDACTION]:
for pattern, replace in DEBUG_REDACTION_PATTERNS:
arg = re.sub(pattern, replace, arg)
processed_args.append(arg)
print(*processed_args)
# Multiprocessing lock
mplock = None
@@ -1010,6 +978,7 @@ from gam.cmd.vault import (
doUpdateVaultMatter,
printShowUserVaultHolds,
)
from gam.cmd.yubikey import doResetYubiKeyPIV
# gam.cmd.alerts
@@ -1139,22 +1108,6 @@ class LazyLoader(types.ModuleType):
yubikey = LazyLoader('yubikey', globals(), 'gam.gamlib.yubikey')
# gam yubikey resetpvi [yubikey_serialnumber <String>]
def doResetYubiKeyPIV():
new_data = {}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'yubikeyserialnumber':
new_data['yubikey_serial_number'] = getInteger()
else:
unknownArgumentExit()
yk = yubikey.YubiKey(new_data)
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
BUILDING_ADDRESS_FIELD_MAP = {
'address': 'addressLines',
'addresslines': 'addressLines',
@@ -1796,7 +1749,7 @@ MAIN_COMMANDS_WITH_OBJECTS = {
),
'yubikey':
(Act.RESET_YUBIKEY_PIV,
{Cmd.ARG_RESETPIV: doResetYubiKeyPIV,
{Cmd.ARG_RESETPIV: doResetYubiKeyPIV, # from cmd/yubikey
}
),
}
@@ -1937,263 +1890,11 @@ MAIN_COMMANDS_OBJ_ALIASES = {
}
# Audit command sub-commands with objects
AUDIT_SUBCOMMANDS_WITH_OBJECTS = {
'monitor':
{'create': (Act.CREATE, doCreateMonitor),
'delete': (Act.DELETE, doDeleteMonitor),
'list': (Act.LIST, doShowMonitors),
},
}
def processAuditCommands():
CL_subCommand = getChoice(list(AUDIT_SUBCOMMANDS_WITH_OBJECTS))
CL_objectName = getChoice(AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand])
Act.Set(AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CL_objectName][CMD_ACTION])
AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CL_objectName][CMD_FUNCTION]()
# Oauth command sub-commands
OAUTH2_SUBCOMMANDS = {
'create': (Act.CREATE, doOAuthCreate),
'delete': (Act.DELETE, doOAuthDelete),
'export': (Act.EXPORT, doOAuthExport),
'info': (Act.INFO, doOAuthInfo),
'refresh': (Act.REFRESH, doOAuthRefresh),
'update': (Act.UPDATE, doOAuthUpdate),
}
# Oauth sub-command aliases
OAUTH2_SUBCOMMAND_ALIASES = {
'request': 'create',
'revoke': 'delete',
'verify': 'info',
}
def processOauthCommands():
CL_subCommand = getChoice(OAUTH2_SUBCOMMANDS, choiceAliases=OAUTH2_SUBCOMMAND_ALIASES)
Act.Set(OAUTH2_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
if GC.Values[GC.ENABLE_DASA]:
systemErrorExit(USAGE_ERROR_RC, Msg.COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA.format('oauth', CL_subCommand))
OAUTH2_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION]()
# Calendar command sub-commands
CALENDAR_SUBCOMMANDS = {
'showacl': (Act.SHOW, doCalendarsPrintShowACLs),
'printacl': (Act.PRINT, doCalendarsPrintShowACLs),
'addevent': (Act.ADD, doCalendarsCreateEvent),
'deleteevent': (Act.DELETE, doCalendarsDeleteEventsOld),
'moveevent': (Act.MOVE, doCalendarsMoveEventsOld),
'updateevent': (Act.UPDATE, doCalendarsUpdateEventsOld),
'printevents': (Act.PRINT, doCalendarsPrintShowEvents),
'wipe': (Act.WIPE, doCalendarsWipeEvents),
'modify': (Act.MODIFY, doCalendarsModifySettings),
}
CALENDAR_OLDACL_SUBCOMMANDS = {
'add': (Act.ADD, doCalendarsCreateACL),
'create': (Act.CREATE, doCalendarsCreateACL),
'delete': (Act.DELETE, doCalendarsDeleteACL),
'update': (Act.UPDATE, doCalendarsUpdateACL),
}
# Calendar sub-command aliases
CALENDAR_OLDACL_SUBCOMMAND_ALIASES = {
'del': 'delete',
}
# Calendars command sub-commands with objects
CALENDARS_SUBCOMMANDS_WITH_OBJECTS = {
'add':
(Act.ADD,
{Cmd.ARG_CALENDARACL: doCalendarsCreateACLs,
Cmd.ARG_EVENT: doCalendarsCreateEvent,
}
),
'create':
(Act.CREATE,
{Cmd.ARG_CALENDARACL: doCalendarsCreateACLs,
Cmd.ARG_EVENT: doCalendarsCreateEvent,
}
),
'delete':
(Act.DELETE,
{Cmd.ARG_CALENDARACL: doCalendarsDeleteACLs,
Cmd.ARG_EVENT: doCalendarsDeleteEvents,
}
),
'empty':
(Act.EMPTY,
{Cmd.ARG_CALENDARTRASH: doCalendarsEmptyTrash,
}
),
'import':
(Act.IMPORT,
{Cmd.ARG_EVENT: doCalendarsImportEvent,
}
),
'info':
(Act.INFO,
{Cmd.ARG_CALENDARACL: doCalendarsInfoACLs,
Cmd.ARG_EVENT: doCalendarsInfoEvents,
}
),
'move':
(Act.MOVE,
{Cmd.ARG_EVENT: doCalendarsMoveEvents,
}
),
'print':
(Act.PRINT,
{Cmd.ARG_CALENDARACL: doCalendarsPrintShowACLs,
Cmd.ARG_EVENT: doCalendarsPrintShowEvents,
Cmd.ARG_SETTINGS: doCalendarsPrintShowSettings,
}
),
'purge':
(Act.PURGE,
{Cmd.ARG_EVENT: doCalendarsPurgeEvents,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_CALENDARACL: doCalendarsPrintShowACLs,
Cmd.ARG_EVENT: doCalendarsPrintShowEvents,
Cmd.ARG_SETTINGS: doCalendarsPrintShowSettings,
}
),
'transfer':
(Act.TRANSFER,
{Cmd.ARG_OWNERSHIP: doCalendarsTransferOwnership,
}
),
'update':
(Act.UPDATE,
{Cmd.ARG_CALENDARACL: doCalendarsUpdateACLs,
Cmd.ARG_EVENT: doCalendarsUpdateEvents,
}
),
'wipe':
(Act.WIPE,
{Cmd.ARG_EVENT: doCalendarsWipeEvents,
}
),
}
CALENDARS_SUBCOMMANDS_OBJECT_ALIASES = {
Cmd.ARG_ACL: Cmd.ARG_CALENDARACL,
Cmd.ARG_ACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_EVENTS: Cmd.ARG_EVENT,
}
def processCalendarsCommands():
calendarList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
CL_subCommand = getChoice(CALENDAR_SUBCOMMANDS, defaultChoice=None)
if CL_subCommand:
Act.Set(CALENDAR_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
CALENDAR_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](calendarList)
return
CL_subCommand = getChoice(CALENDAR_OLDACL_SUBCOMMANDS, choiceAliases=CALENDAR_OLDACL_SUBCOMMAND_ALIASES, defaultChoice=None)
if CL_subCommand:
Act.Set(CALENDAR_OLDACL_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
CL_objectName = getChoice([Cmd.ARG_CALENDARACL, Cmd.ARG_EVENT], choiceAliases=CALENDARS_SUBCOMMANDS_OBJECT_ALIASES, defaultChoice=None)
if not CL_objectName:
CALENDAR_OLDACL_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](calendarList)
else:
CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](calendarList)
return
CL_subCommand = getChoice(CALENDARS_SUBCOMMANDS_WITH_OBJECTS)
Act.Set(CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_ACTION])
CL_objectName = getChoice(CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION], choiceAliases=CALENDARS_SUBCOMMANDS_OBJECT_ALIASES)
CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](calendarList)
# Course command sub-commands
COURSE_SUBCOMMANDS = {
'add': (Act.ADD, doCourseAddItems),
'clear': (Act.REMOVE, doCourseClearParticipants),
'remove': (Act.REMOVE, doCourseRemoveItems),
'update': (Act.UPDATE, doCourseUpdateItems),
'sync': (Act.SYNC, doCourseSyncParticipants),
}
# Course sub-command aliases
COURSE_SUBCOMMAND_ALIASES = {
'create': 'add',
'del': 'remove',
'delete': 'remove',
}
def executeCourseCommands(courseIdList, getEntityListArg):
CL_subCommand = getChoice(COURSE_SUBCOMMANDS, choiceAliases=COURSE_SUBCOMMAND_ALIASES)
Act.Set(COURSE_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
COURSE_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](courseIdList, getEntityListArg)
def processCourseCommands():
executeCourseCommands(getStringReturnInList(Cmd.OB_COURSE_ID), False)
def processCoursesCommands():
executeCourseCommands(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True), True)
# Resource command sub-commands
RESOURCE_SUBCOMMANDS_WITH_OBJECTS = {
'add':
(Act.ADD,
{Cmd.ARG_CALENDARACL: doResourceCreateCalendarACLs,
}
),
'create':
(Act.CREATE,
{Cmd.ARG_CALENDARACL: doResourceCreateCalendarACLs,
}
),
'update':
(Act.UPDATE,
{Cmd.ARG_CALENDARACL: doResourceUpdateCalendarACLs,
}
),
'delete':
(Act.DELETE,
{Cmd.ARG_CALENDARACL: doResourceDeleteCalendarACLs,
}
),
'info':
(Act.INFO,
{Cmd.ARG_CALENDARACL: doResourceInfoCalendarACLs,
}
),
'print':
(Act.PRINT,
{Cmd.ARG_CALENDARACL: doResourcePrintShowCalendarACLs,
}
),
'show':
(Act.SHOW,
{Cmd.ARG_CALENDARACL: doResourcePrintShowCalendarACLs,
}
),
}
# Resource sub-command aliases
RESOURCE_SUBCOMMAND_ALIASES = {
'del': 'delete',
}
RESOURCE_SUBCOMMANDS_OBJECT_ALIASES = {
Cmd.ARG_ACL: Cmd.ARG_CALENDARACL,
Cmd.ARG_ACLS: Cmd.ARG_CALENDARACL,
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
}
def executeResourceCommands(resourceEntity):
CL_subCommand = getChoice(RESOURCE_SUBCOMMANDS_WITH_OBJECTS, choiceAliases=RESOURCE_SUBCOMMAND_ALIASES)
Act.Set(RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_ACTION])
CL_objectName = getChoice(RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION], choiceAliases=RESOURCE_SUBCOMMANDS_OBJECT_ALIASES)
RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](resourceEntity)
def processResourceCommands():
executeResourceCommands(getStringReturnInList(Cmd.OB_RESOURCE_ID))
def processResourcesCommands():
executeResourceCommands(getEntityList(Cmd.OB_RESOURCE_ENTITY))
from gam.cmd.audit import processAuditCommands
from gam.cmd.oauth import processOauthCommands
from gam.cmd.calendar import processCalendarsCommands, processResourceCommands, processResourcesCommands
from gam.cmd.courses.participants import processCourseCommands, processCoursesCommands
# Commands
COMMANDS_MAP = {
@@ -3034,53 +2735,6 @@ USER_COMMANDS_OBJ_ALIASES = {
Cmd.ARG_YOUTUBECHANNELS: Cmd.ARG_YOUTUBECHANNEL,
}
def showAPICallsRetryData():
if GC.Values.get(GC.SHOW_API_CALLS_RETRY_DATA) and GM.Globals[GM.API_CALLS_RETRY_DATA]:
Ind.Reset()
writeStderr(Msg.API_CALLS_RETRY_DATA)
Ind.Increment()
for k, v in sorted(GM.Globals[GM.API_CALLS_RETRY_DATA].items()):
m, s = divmod(int(v[1]), 60)
h, m = divmod(m, 60)
writeStderr(formatKeyValueList(Ind.Spaces(), [k, f'{v[0]}/{h}:{m:02d}:{s:02d}'], '\n'))
Ind.Decrement()
def adjustRedirectedSTDFilesIfNotMultiprocessing():
def adjustRedirectedSTDFile(stdtype):
rdFd = GM.Globals[stdtype].get(GM.REDIRECT_FD)
rdMultiFd = GM.Globals[stdtype].get(GM.REDIRECT_MULTI_FD)
if rdFd and rdMultiFd and rdFd != rdMultiFd:
try:
rdFd.write(rdMultiFd.getvalue())
rdMultiFd.close()
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = rdFd
if (stdtype == GM.STDOUT) and (GM.Globals.get(GM.SAVED_STDOUT) is not None):
sys.stdout = rdFd
except IOError as e:
systemErrorExit(FILE_ERROR_RC, e)
adjustRedirectedSTDFile(GM.STDOUT)
if GM.Globals[GM.STDERR].get(GM.REDIRECT_NAME) != 'stdout':
adjustRedirectedSTDFile(GM.STDERR)
else:
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
def closeSTDFilesIfNotMultiprocessing(closeSTD):
def closeSTDFile(stdtype, stdfile):
rdFd = GM.Globals[stdtype].get(GM.REDIRECT_FD)
rdMultiFd = GM.Globals[stdtype].get(GM.REDIRECT_MULTI_FD)
if rdFd and rdMultiFd and (rdFd == rdMultiFd) and (rdFd != stdfile):
try:
rdFd.flush()
if closeSTD:
rdFd.close()
except BrokenPipeError:
pass
closeSTDFile(GM.STDOUT, sys.stdout)
if GM.Globals[GM.STDERR].get(GM.REDIRECT_NAME) != 'stdout':
closeSTDFile(GM.STDERR, sys.stderr)
# Process GAM command
def ProcessGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=True):
setSysExitRC(0)
@@ -3208,61 +2862,3 @@ def ProcessGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=True):
def CallGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=False):
return ProcessGAMCommand(args, processGamCfg=processGamCfg, inLoop=inLoop, closeSTD=closeSTD)
# Re-export util names for _gam().X access by util/ modules.
# This block runs AFTER all modules are fully loaded (no circular import risk).
# Only names actually referenced via _gam() are included.
from gamlib import glskus as SKU # noqa: E402,F811
from tempfile import TemporaryFile # noqa: E402,F401
from util.access import ( # noqa: E402,F401
APIAccessDeniedExit, ClientAPIAccessDeniedExit, SvcAcctAPIAccessDeniedExit, accessErrorExit,
accessErrorExitNonDirectory, checkEntityAFDNEorAccessErrorExit,
checkEntityDNEorAccessErrorExit, entityUnknownWarning,
)
from util.api import ( # noqa: E402,F401,F811
_getAdminEmail, _getSvcAcctData, buildGAPIObject, buildGAPIServiceObject,
callGAPI, callGAPIitems, callGAPIpages, chooseSaAPI, yieldGAPIpages,
)
from util.args import ( # noqa: E402,F401,F811
ARCHIVED_ARGUMENTS, ISOformatTimeStamp, LOCALE_CODES_MAP, NAME_EMAIL_ADDRESS_PATTERN,
SUSPENDED_ARGUMENTS, YYYYMMDDTHHMMSSZ_FORMAT, YYYYMMDD_FORMAT, YYYYMMDD_PATTERN,
_getIsArchived, _getIsSuspended, checkDataField, checkForExtraneousArguments,
checkMatchSkipFields, checkSubkeyField, encodeOrgUnitPath, escapeCRsNLs,
formatLocalTime, formatLocalTimestamp, getBoolean, getCharSet, getCharacter,
getDelimiter, getEmailAddress, getEmailAddressDomain, getLanguageCode,
getMatchSkipFields, getPhraseDNEorSNA, getREPattern, getString,
getSheetEntity, getSheetIdFromSheetEntity, makeOrgUnitPathAbsolute,
makeOrgUnitPathRelative, orgUnitPathQuery, protectedSheetId, removeCourseIdScope,
shlexSplitList, splitEmailAddress, todaysTime, validateEmailAddressOrUID,
)
from util.csv_pf import CheckInputRowFilterHeaders # noqa: E402,F401
from util.display import ( # noqa: E402,F401
FIRST_ITEM_MARKER, LAST_ITEM_MARKER, TOTAL_ITEMS_MARKER, actionPerformedNumItems,
entityActionFailedWarning, entityActionNotPerformedWarning, entityActionPerformed,
entityActionPerformedMessage, entityDoesNotExistWarning, entityPerformActionNumItems,
entityServiceNotApplicableWarning, getPageMessage, getPageMessageForWhom, printBlankLine,
printGettingAllAccountEntities, printGettingAllEntityItemsForWhom,
printGotEntityItemsForWhom, printJSONKey, printJSONValue, printKeyValueList,
setGettingAllEntityItemsForWhom, userDriveServiceNotEnabledWarning,
userServiceNotEnabledWarning,
)
from util.email import send_email # noqa: E402,F401,F811
from util.entity import checkUserExists, convertUIDtoEmailAddress # noqa: E402,F401,F811
from util.errors import ( # noqa: E402,F401,F811
csvDataAlreadySavedErrorExit, csvFieldErrorExit, entityActionFailedExit,
entityDoesNotExistExit, expiredRevokedOauth2TxtExit, formatChoiceList,
invalidArgumentExit, invalidChoiceExit, invalidDiscoveryJsonExit,
invalidOauth2TxtExit, invalidOauth2serviceJsonExit, missingArgumentExit,
)
from util.fileio import ( # noqa: E402,F401,F811
StringIOobject, checkAPICallsRate, closeFile, fdErrorMessage, fileErrorMessage,
getGDocSheetDataFailedExit, getGDocSheetDataRetryWarning, incrAPICallsRetryData,
openFile, readFile, writeFile,
)
from util.gdoc import ( # noqa: E402,F401
getGDocData, getStorageFileData, openCSVFileReader,
)
from util.output import ( # noqa: E402,F401,F811
currentCountNL, flushStderr, flushStdout,
printWarningMessage, readStdin, stderrWarningMsg, writeStdout,
)