Initial commit of a new experimental modular GAM.

This commit is contained in:
Jay Lee
2026-07-03 08:33:14 -04:00
parent 2fbc3c5c35
commit 8a89a91414
129 changed files with 88262 additions and 82716 deletions

View File

@@ -0,0 +1,300 @@
"""User photo and profile management.
Part of the _userop_tmp sub-package."""
"""GAM user operations: Looker Studio, user groups, licenses, photos, profile, sheets, tokens, deprovision."""
import re
import sys
from gamlib import glaction
from gamlib import glapi as API
from gamlib import glcfg as GC
from gamlib import glclargs
from gamlib import glentity
from gamlib import glgapi as GAPI
from gamlib import glglobals as GM
from gamlib import glindent
from gamlib import glmsgs as Msg
Act = glaction.GamAction()
Ent = glentity.GamEntity()
Ind = glindent.GamIndent()
Cmd = glclargs.GamCLArgs()
def _getMain():
return sys.modules['gam']
def __getattr__(name):
"""Fall back to gam module for any undefined names."""
main = _getMain()
try:
return getattr(main, name)
except AttributeError:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
from tempfile import TemporaryFile
def updatePhoto(users):
cd = _getMain().buildGAPIObject(API.DIRECTORY)
baseFileIdEntity = drive = owner = None
sourceFolder = os.getcwd()
if Cmd.NumArgumentsRemaining() == 1:
filenamePattern = _getMain().getString(Cmd.OB_FILE_NAME_PATTERN)
else:
filenamePattern = '#email#.jpg'
while Cmd.ArgumentsRemaining():
myarg = _getMain().getArgument()
if myarg == 'drivedir':
sourceFolder = GC.Values[GC.DRIVE_DIR]
elif myarg == 'sourcefolder':
sourceFolder = _getMain().setFilePath(_getMain().getString(Cmd.OB_FILE_PATH), GC.INPUT_DIR)
if not os.path.isdir(sourceFolder):
_getMain().entityDoesNotExistExit(Ent.DIRECTORY, sourceFolder)
elif myarg == 'filename':
filenamePattern = _getMain().getString(Cmd.OB_FILE_NAME_PATTERN)
elif myarg == 'gphoto':
owner, drive = _getMain().buildGAPIServiceObject(API.DRIVE3, _getMain().getEmailAddress())
if not drive:
return
baseFileIdEntity = _getMain().getDriveFileEntity(queryShortcutsOK=False)
else:
_getMain().unknownArgumentExit()
p = re.compile('^(ht|f)tps?://.*$')
i, count, users = _getMain().getEntityArgument(users)
for user in users:
i += 1
user, userName, _ = _getMain().splitEmailAddressOrUID(user)
filename = _getMain()._substituteForUser(filenamePattern, user, userName)
if baseFileIdEntity is not None:
fileIdEntity = baseFileIdEntity.copy()
if fileIdEntity['query'] is not None:
fileIdEntity['query'] = _substituteForUser(fileIdEntity['query'], user, userName)
_, _, jcount = _getMain()._validateUserGetFileIDs(owner, 0, 0, fileIdEntity, drive=drive, entityType=None)
if jcount == 0:
_getMain().entityItemValueListActionNotPerformedWarning([Ent.USER, user], [Ent.OWNER, owner],
Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)), i, count)
continue
if jcount > 1:
_getMain().entityItemValueListActionNotPerformedWarning([Ent.USER, user], [Ent.OWNER, owner],
Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])), i, count)
continue
fb = TemporaryFile(mode='wb+')
filename = fileIdEntity['list'][0]
request = drive.files().get_media(fileId=filename)
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
done = False
while not done:
_, done = downloader.next_chunk()
fb.seek(0)
image_data = fb.read()
fb.close()
elif p.match(filename):
try:
status, image_data = _getMain().getHttpObj().request(filename, 'GET')
if status['status'] != '200':
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, i, count)
continue
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
continue
else:
filename = os.path.join(sourceFolder, filename)
try:
with open(filename, 'rb') as f:
image_data = f.read()
except (OSError, IOError) as e:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
continue
body = {'photoData': base64.urlsafe_b64encode(image_data).decode(_getMain().UTF8)}
try:
try:
_getMain().callGAPI(cd.users().photos(), 'delete',
bailOnInternalError=True,
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND, GAPI.INTERNAL_ERROR],
userKey=user)
except (GAPI.photoNotFound, GAPI.internalError):
pass
_getMain().callGAPI(cd.users().photos(), 'update',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT, GAPI.CONDITION_NOT_MET],
userKey=user, body=body, fields='')
_getMain().entityActionPerformed([Ent.USER, user, Ent.PHOTO, filename], i, count)
except (GAPI.invalidInput, GAPI.conditionNotMet) as e:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
except (GAPI.userNotFound, GAPI.forbidden):
_getMain().entityUnknownWarning(Ent.USER, user, i, count)
# gam <UserTypeEntity> delete photo
def deletePhoto(users):
cd = _getMain().buildGAPIObject(API.DIRECTORY)
_getMain().checkForExtraneousArguments()
i, count, users = _getMain().getEntityArgument(users)
for user in users:
i += 1
user = _getMain().normalizeEmailAddressOrUID(user)
try:
_getMain().callGAPI(cd.users().photos(), 'delete',
bailOnInternalError=True,
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND, GAPI.INTERNAL_ERROR],
userKey=user)
_getMain().entityActionPerformed([Ent.USER, user, Ent.PHOTO, ''], i, count)
except (GAPI.photoNotFound, GAPI.internalError) as e:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, ''], str(e), i, count)
except (GAPI.userNotFound, GAPI.forbidden):
_getMain().entityUnknownWarning(Ent.USER, user, i, count)
def getPhoto(users, profileMode):
cd = _getMain().buildGAPIObject(API.DIRECTORY)
targetFolder = os.getcwd()
filenamePattern = '#email#.#ext#'
noDefault = returnURLonly = False
writeFileData = showPhotoData = True
size = ''
while Cmd.ArgumentsRemaining():
myarg = _getMain().getArgument()
if myarg == 'drivedir':
targetFolder = GC.Values[GC.DRIVE_DIR]
elif myarg == 'targetfolder':
targetFolder = _getMain().setFilePath(_getMain().getString(Cmd.OB_FILE_PATH), GC.DRIVE_DIR)
if not os.path.isdir(targetFolder):
os.makedirs(targetFolder)
elif myarg == 'filename':
filenamePattern = _getMain().getString(Cmd.OB_FILE_NAME_PATTERN)
elif myarg == 'nofile':
writeFileData = False
elif myarg == 'noshow':
showPhotoData = False
elif profileMode and myarg == 'returnurlonly':
returnURLonly = True
elif myarg == 'nodefault':
noDefault = True
elif profileMode and myarg == 'size':
size = _getMain().getInteger(minVal=50)
else:
_getMain().unknownArgumentExit()
i, count, users = _getMain().getEntityArgument(users)
for user in users:
i += 1
if profileMode:
user, people = _getMain().buildGAPIServiceObject(API.PEOPLE, user, i, count)
if not people:
continue
else:
user = _getMain().normalizeEmailAddressOrUID(user)
_, userName, _ = _getMain().splitEmailAddressOrUID(user)
filename = os.path.join(targetFolder, _getMain()._substituteForUser(filenamePattern, user, userName))
try:
if not showPhotoData:
_getMain().entityPerformActionNumItems([Ent.USER, user], 1, Ent.PHOTO, i, count)
if not profileMode:
photo = _getMain().callGAPI(cd.users().photos(), 'get',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND],
userKey=user)
if showPhotoData:
_getMain().writeStdout(photo['photoData']+'\n')
photo_data = base64.urlsafe_b64decode(photo['photoData'])
else:
result = _getMain().callGAPI(people.people(), 'get',
throwReasons=[GAPI.NOT_FOUND],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
resourceName='people/me', personFields='photos')
default = False
url = None
for photo in result.get('photos', []):
if photo['metadata']['source']['type'] == 'PROFILE':
default = photo.get('default', False)
url = photo['url']
break
if not url:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], Msg.PROFILE_PHOTO_NOT_FOUND, i, count)
continue
if noDefault and default:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], Msg.PROFILE_PHOTO_IS_DEFAULT, i, count)
continue
if returnURLonly:
_getMain().writeStdout(f'{url}\n')
continue
if size:
url = re.sub(r"=s\d+$", f"=s{size}", url)
try:
status, photo_data = _getMain().getHttpObj().request(url, 'GET')
if status['status'] != '200':
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, i, count)
continue
if showPhotoData:
_getMain().writeStdout(base64.encodebytes(photo_data).decode(_getMain().UTF8))
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
continue
if writeFileData:
if photo_data[:3] == b'\xff\xd8\xff':
extension = 'jpg'
elif photo_data[:8] == b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a':
extension = 'png'
elif photo_data[:6] == b'\x47\x49\x46\x38\x37\x61' or photo_data[:6] == b'\x47\x49\x46\x38\x39\x61':
extension = 'gif'
elif photo_data[:2] == b'\x42\x4d':
extension= 'bmp'
elif photo_data[:4] == b'\x49\x49\x2A\x00' or photo_data[:4] == b'\x4D\x4D\x00\x2A':
extension= 'tif'
else:
extension = 'img'
filenameExt = filename.replace('#ext#', extension)
status, e = _getMain().writeFileReturnError(filenameExt, photo_data, mode='wb')
if status:
if not showPhotoData:
_getMain().entityActionPerformed([Ent.USER, user, Ent.PHOTO, filenameExt], i, count)
else:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filenameExt], str(e), i, count)
except (GAPI.notFound, GAPI.photoNotFound) as e:
_getMain().entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], str(e), i, count)
except (GAPI.userNotFound, GAPI.forbidden):
_getMain().entityUnknownWarning(Ent.USER, user, i, count)
# gam <UserTypeEntity> get photo [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]]
# [noshow] [nofile]
def getUserPhoto(users):
getPhoto(users, False)
# gam <UserTypeEntity> get profilephoto [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
# [noshow] [nofile] [returnurlonly] [nodefault] [size <Integer>]
def getProfilePhoto(users):
getPhoto(users, True)
PROFILE_SHARING_CHOICE_MAP = {
'share': True,
'shared': True,
'unshare': False,
'unshared': False,
}
def _setShowProfile(users, function, **kwargs):
cd = _getMain().buildGAPIObject(API.DIRECTORY)
_getMain().checkForExtraneousArguments()
i, count, users = _getMain().getEntityArgument(users)
for user in users:
i += 1
user = _getMain().normalizeEmailAddressOrUID(user)
try:
result = _getMain().callGAPI(cd.users(), function,
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN],
userKey=user, fields='includeInGlobalAddressList', **kwargs)
_getMain().printEntity([Ent.USER, user, Ent.PROFILE_SHARING_ENABLED, result.get('includeInGlobalAddressList', _getMain().UNKNOWN)], i, count)
except (GAPI.userNotFound, GAPI.forbidden):
_getMain().entityUnknownWarning(Ent.USER, user, i, count)
# gam <UserTypeEntity> profile share|shared|unshare|unshared
def setProfile(users):
body = {'includeInGlobalAddressList': _getMain().getChoice(PROFILE_SHARING_CHOICE_MAP, mapChoice=True)}
_setShowProfile(users, 'update', body=body)
# gam <UserTypeEntity> show profile
def showProfile(users):
_setShowProfile(users, 'get')
# gam <UserTypeEntity> create sheet
# ((json [charset <Charset>] <SpreadsheetJSONCreateRequest>) |
# (json file <FileName> [charset <Charset>]))
# [<DriveFileParentAttribute>]
# [formatjson] [returnidonly]