mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-05 05:11:35 +00:00
Initial commit of a new experimental modular GAM.
This commit is contained in:
0
src/gam/util/__init__.py
Normal file
0
src/gam/util/__init__.py
Normal file
188
src/gam/util/access.py
Normal file
188
src/gam/util/access.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""GAM access-error and entity-warning utilities.
|
||||
|
||||
Extracted from gam/__init__.py. Provides access-error diagnostics,
|
||||
API access denied handlers, and entity warning functions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gamlib import glaction
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
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
|
||||
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
|
||||
Act = glaction.GamAction()
|
||||
Ent = glentity.GamEntity()
|
||||
Ind = glindent.GamIndent()
|
||||
|
||||
|
||||
# Something's wrong with CustomerID??
|
||||
def accessErrorMessage(cd, errMsg=None):
|
||||
_m = _getMain()
|
||||
if cd is None:
|
||||
cd = _m.buildGAPIObject(API.DIRECTORY)
|
||||
try:
|
||||
_m.callGAPI(cd.customers(), 'get',
|
||||
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND,
|
||||
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
||||
customerKey=GC.Values[GC.CUSTOMER_ID], fields='id')
|
||||
except (GAPI.badRequest, GAPI.invalidInput):
|
||||
return _m.formatKeyValueList('',
|
||||
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
|
||||
Msg.INVALID],
|
||||
'')
|
||||
except GAPI.resourceNotFound:
|
||||
return _m.formatKeyValueList('',
|
||||
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
|
||||
Msg.DOES_NOT_EXIST],
|
||||
'')
|
||||
except (GAPI.forbidden, GAPI.permissionDenied):
|
||||
return _m.formatKeyValueList('',
|
||||
Ent.FormatEntityValueList([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID],
|
||||
Ent.DOMAIN, GC.Values[GC.DOMAIN],
|
||||
Ent.USER, GM.Globals[GM.ADMIN]])+[Msg.ACCESS_FORBIDDEN],
|
||||
'')
|
||||
if errMsg:
|
||||
return _m.formatKeyValueList('',
|
||||
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
|
||||
errMsg],
|
||||
'')
|
||||
return None
|
||||
|
||||
def accessErrorExit(cd, errMsg=None):
|
||||
_m = _getMain()
|
||||
_m.systemErrorExit(_m.INVALID_DOMAIN_RC, accessErrorMessage(cd or _m.buildGAPIObject(API.DIRECTORY), errMsg))
|
||||
|
||||
def accessErrorExitNonDirectory(api, errMsg):
|
||||
_m = _getMain()
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC,
|
||||
_m.formatKeyValueList('',
|
||||
Ent.FormatEntityValueList([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID],
|
||||
Ent.DOMAIN, GC.Values[GC.DOMAIN],
|
||||
Ent.API, api])+[errMsg],
|
||||
''))
|
||||
|
||||
def ClientAPIAccessDeniedExit(errMsg=None):
|
||||
_m = _getMain()
|
||||
if errMsg is None:
|
||||
_m.stderrErrorMsg(Msg.API_ACCESS_DENIED)
|
||||
missingScopes = API.getClientScopesSet(GM.Globals[GM.CURRENT_CLIENT_API])-GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
|
||||
if missingScopes:
|
||||
_m.writeStderr(Msg.API_CHECK_CLIENT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2_CLIENT_ID],
|
||||
','.join(sorted(missingScopes))))
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC, None)
|
||||
else:
|
||||
_m.stderrErrorMsg(errMsg)
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC, Msg.REAUTHENTICATION_IS_NEEDED)
|
||||
|
||||
def SvcAcctAPIAccessDenied():
|
||||
_m = _getMain()
|
||||
_m._getSvcAcctData()
|
||||
if (GM.Globals[GM.CURRENT_SVCACCT_API] == API.GMAIL and
|
||||
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] and
|
||||
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES][0] == API.GMAIL_SEND_SCOPE):
|
||||
_m.systemErrorExit(_m.OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED)
|
||||
_m.stderrErrorMsg(Msg.API_ACCESS_DENIED)
|
||||
apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]))
|
||||
_m.writeStderr(Msg.API_CHECK_SVCACCT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'],
|
||||
apiOrScopes,
|
||||
GM.Globals[GM.CURRENT_SVCACCT_USER] or _m._getAdminEmail()))
|
||||
def SvcAcctAPIAccessDeniedExit():
|
||||
_m = _getMain()
|
||||
SvcAcctAPIAccessDenied()
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC, None)
|
||||
|
||||
def SvcAcctAPIDisabledExit():
|
||||
_m = _getMain()
|
||||
if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]:
|
||||
ClientAPIAccessDeniedExit()
|
||||
if GM.Globals[GM.CURRENT_SVCACCT_API]:
|
||||
_m.stderrErrorMsg(Msg.SERVICE_ACCOUNT_API_DISABLED.format(API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API])))
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC, None)
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED)
|
||||
|
||||
def APIAccessDeniedExit():
|
||||
_m = _getMain()
|
||||
if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]:
|
||||
ClientAPIAccessDeniedExit()
|
||||
if GM.Globals[GM.CURRENT_SVCACCT_API]:
|
||||
SvcAcctAPIAccessDeniedExit()
|
||||
_m.systemErrorExit(_m.API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED)
|
||||
|
||||
def checkEntityDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0):
|
||||
_m = _getMain()
|
||||
message = accessErrorMessage(cd)
|
||||
if message:
|
||||
_m.systemErrorExit(_m.INVALID_DOMAIN_RC, message)
|
||||
_m.entityDoesNotExistWarning(entityType, entityName, i, count)
|
||||
|
||||
def checkEntityAFDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0):
|
||||
_m = _getMain()
|
||||
message = accessErrorMessage(cd)
|
||||
if message:
|
||||
_m.systemErrorExit(_m.INVALID_DOMAIN_RC, message)
|
||||
_m.entityActionFailedWarning([entityType, entityName], Msg.DOES_NOT_EXIST, i, count)
|
||||
|
||||
def checkEntityItemValueAFDNEorAccessErrorExit(cd, entityType, entityName, itemType, itemValue, i=0, count=0):
|
||||
_m = _getMain()
|
||||
message = accessErrorMessage(cd)
|
||||
if message:
|
||||
_m.systemErrorExit(_m.INVALID_DOMAIN_RC, message)
|
||||
_m.entityActionFailedWarning([entityType, entityName, itemType, itemValue], Msg.DOES_NOT_EXIST, i, count)
|
||||
|
||||
def entityUnknownWarning(entityType, entityName, i=0, count=0):
|
||||
_m = _getMain()
|
||||
domain = _m.getEmailAddressDomain(entityName)
|
||||
if (domain.endswith(GC.Values[GC.DOMAIN])) or (domain.endswith('google.com')):
|
||||
_m.entityDoesNotExistWarning(entityType, entityName, i, count)
|
||||
else:
|
||||
_m.entityServiceNotApplicableWarning(entityType, entityName, i, count)
|
||||
|
||||
def entityOrEntityUnknownWarning(entity1Type, entity1Name, entity2Type, entity2Name, i=0, count=0):
|
||||
_m = _getMain()
|
||||
_m.setSysExitRC(_m.ENTITY_DOES_NOT_EXIST_RC)
|
||||
_m.writeStderr(_m.formatKeyValueList(Ind.Spaces(),
|
||||
[f'{Msg.EITHER} {Ent.Singular(entity1Type)}', entity1Name, _m.getPhraseDNEorSNA(entity1Name), None,
|
||||
f'{Msg.OR} {Ent.Singular(entity2Type)}', entity2Name, _m.getPhraseDNEorSNA(entity2Name)],
|
||||
_m.currentCountNL(i, count)))
|
||||
|
||||
def duplicateAliasGroupUserWarning(cd, entityValueList, i=0, count=0):
|
||||
_m = _getMain()
|
||||
email = entityValueList[1]
|
||||
try:
|
||||
result = _m.callGAPI(cd.users(), 'get',
|
||||
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
||||
userKey=email, fields='id,primaryEmail')
|
||||
if (result['primaryEmail'].lower() == email) or (result['id'] == email):
|
||||
kvList = [Ent.USER, email]
|
||||
else:
|
||||
kvList = [Ent.USER_ALIAS, email, Ent.USER, result['primaryEmail']]
|
||||
except (GAPI.userNotFound, GAPI.badRequest,
|
||||
GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError, GAPI.systemError):
|
||||
try:
|
||||
result = _m.callGAPI(cd.groups(), 'get',
|
||||
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
||||
groupKey=email, fields='id,email')
|
||||
if (result['email'].lower() == email) or (result['id'] == email):
|
||||
kvList = [Ent.GROUP, email]
|
||||
else:
|
||||
kvList = [Ent.GROUP_ALIAS, email, Ent.GROUP, result['email']]
|
||||
except (GAPI.groupNotFound,
|
||||
GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
|
||||
kvList = [Ent.EMAIL, email]
|
||||
_m.writeStderr(_m.formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+
|
||||
[Act.Failed(), Msg.DUPLICATE]+
|
||||
Ent.FormatEntityValueList(kvList),
|
||||
_m.currentCountNL(i, count)))
|
||||
_m.setSysExitRC(_m.ENTITY_DUPLICATE_RC)
|
||||
return kvList[0]
|
||||
1545
src/gam/util/api.py
Normal file
1545
src/gam/util/api.py
Normal file
File diff suppressed because it is too large
Load Diff
1622
src/gam/util/args.py
Normal file
1622
src/gam/util/args.py
Normal file
File diff suppressed because it is too large
Load Diff
1111
src/gam/util/batch.py
Normal file
1111
src/gam/util/batch.py
Normal file
File diff suppressed because it is too large
Load Diff
1104
src/gam/util/config.py
Normal file
1104
src/gam/util/config.py
Normal file
File diff suppressed because it is too large
Load Diff
354
src/gam/util/connection.py
Normal file
354
src/gam/util/connection.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""Network diagnostics, version display, and usage functions.
|
||||
|
||||
Extracted from gam/__init__.py to reduce monolith size.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import arrow
|
||||
import httplib2
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from importlib.metadata import version as lib_version
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glglobals as GM
|
||||
from gamlib import glmsgs as Msg
|
||||
from gamlib import glverlibs
|
||||
|
||||
|
||||
class _InstanceProxy:
|
||||
"""Lazy proxy that delegates attribute access to a named instance in the gam module."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
def __getattr__(self, attr):
|
||||
return getattr(getattr(sys.modules['gam'], self._name), attr)
|
||||
|
||||
Cmd = _InstanceProxy('Cmd')
|
||||
|
||||
def _getEnt():
|
||||
return sys.modules['gam'].Ent
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
|
||||
# --- Constants ---
|
||||
|
||||
def _buildTimeOffsetUnits():
|
||||
m = _getMain()
|
||||
return [('day', m.SECONDS_PER_DAY), ('hour', m.SECONDS_PER_HOUR),
|
||||
('minute', m.SECONDS_PER_MINUTE), ('second', 1)]
|
||||
|
||||
MACOS_CODENAMES = {
|
||||
10: {
|
||||
6: 'Snow Leopard',
|
||||
7: 'Lion',
|
||||
8: 'Mountain Lion',
|
||||
9: 'Mavericks',
|
||||
10: 'Yosemite',
|
||||
11: 'El Capitan',
|
||||
12: 'Sierra',
|
||||
13: 'High Sierra',
|
||||
14: 'Mojave',
|
||||
15: 'Catalina',
|
||||
16: 'Big Sur'
|
||||
},
|
||||
11: 'Big Sur',
|
||||
12: 'Monterey',
|
||||
13: 'Ventura',
|
||||
14: 'Sonoma',
|
||||
15: 'Sequoia',
|
||||
26: 'Tahoe',
|
||||
}
|
||||
|
||||
# --- Functions ---
|
||||
|
||||
def getLocalGoogleTimeOffset(testLocation=None):
|
||||
m = _getMain()
|
||||
if testLocation is None:
|
||||
testLocation = m.GOOGLE_TIMECHECK_LOCATION
|
||||
TIME_OFFSET_UNITS = _buildTimeOffsetUnits()
|
||||
# If local time is well off, it breaks https because the server certificate will be seen as too old or new and thus invalid; http doesn't have that issue.
|
||||
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds), retry with https as it should be OK
|
||||
httpObj = m.getHttpObj()
|
||||
for prot in ['http', 'https']:
|
||||
try:
|
||||
headerData = httpObj.request(f'{prot}://'+testLocation, 'HEAD')
|
||||
googleUTC = arrow.Arrow.strptime(headerData[0]['date'], '%a, %d %b %Y %H:%M:%S %Z', tzinfo='UTC')
|
||||
except (httplib2.HttpLib2Error, RuntimeError) as e:
|
||||
m.handleServerError(e)
|
||||
except httplib2.socks.HTTPError as e:
|
||||
# If user has specified an HTTPS proxy, the http request will probably fail as httplib2
|
||||
# turns a GET into a CONNECT which is not valid for an http address
|
||||
if prot == 'http':
|
||||
continue
|
||||
m.handleServerError(e)
|
||||
except (ValueError, KeyError):
|
||||
if prot == 'http':
|
||||
continue
|
||||
m.systemErrorExit(m.NETWORK_ERROR_RC, Msg.INVALID_HTTP_HEADER.format(str(headerData)))
|
||||
offset = remainder = int(abs((arrow.utcnow()-googleUTC).total_seconds()))
|
||||
if offset < m.MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
|
||||
continue
|
||||
timeoff = []
|
||||
for tou in TIME_OFFSET_UNITS:
|
||||
uval, remainder = divmod(remainder, tou[1])
|
||||
if uval:
|
||||
timeoff.append(f'{uval} {tou[0]}{"s" if uval != 1 else ""}')
|
||||
if not timeoff:
|
||||
timeoff.append(Msg.LESS_THAN_1_SECOND)
|
||||
nicetime = ', '.join(timeoff)
|
||||
return (offset, nicetime)
|
||||
|
||||
def _getServerTLSUsed(location):
|
||||
m = _getMain()
|
||||
url = 'https://'+location
|
||||
_, netloc, _, _, _, _ = urlparse(url)
|
||||
conn = 'https:'+netloc
|
||||
httpObj = m.getHttpObj()
|
||||
triesLimit = 5
|
||||
for n in range(1, triesLimit+1):
|
||||
try:
|
||||
httpObj.request(url, headers={'user-agent': m.GAM_USER_AGENT})
|
||||
cipher_name, tls_ver, _ = httpObj.connections[conn].sock.cipher()
|
||||
return tls_ver, cipher_name
|
||||
except (httplib2.HttpLib2Error, RuntimeError) as e:
|
||||
if n != triesLimit:
|
||||
httpObj.connections = {}
|
||||
m.waitOnFailure(n, triesLimit, m.NETWORK_ERROR_RC, str(e))
|
||||
continue
|
||||
m.handleServerError(e)
|
||||
|
||||
def getOSPlatform():
|
||||
myos = platform.system()
|
||||
if myos == 'Linux':
|
||||
import distro
|
||||
pltfrm = ' '.join(distro.linux_distribution(full_distribution_name=False)).title()
|
||||
elif myos == 'Windows':
|
||||
pltfrm = ' '.join(platform.win32_ver())
|
||||
elif myos == 'Darwin':
|
||||
myos = 'macOS'
|
||||
mac_ver = platform.mac_ver()[0]
|
||||
major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
|
||||
minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
|
||||
if major_ver == 10:
|
||||
codename = MACOS_CODENAMES[major_ver].get(minor_ver, '')
|
||||
else:
|
||||
codename = MACOS_CODENAMES.get(major_ver, '')
|
||||
pltfrm = ' '.join([codename, mac_ver])
|
||||
else:
|
||||
pltfrm = platform.platform()
|
||||
return f'{myos} {pltfrm}'
|
||||
|
||||
def inspect_untrusted_cert(url):
|
||||
"""Bypasses validation momentarily to extract the untrusted Issuer."""
|
||||
parsed = urlparse(url if '://' in url else f'https://{url}')
|
||||
host = parsed.hostname
|
||||
port = parsed.port or 443
|
||||
# Create an unverified context purely for diagnostic extraction
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=5) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
der_cert = ssock.getpeercert(binary_form=True)
|
||||
cert = x509.load_der_x509_certificate(der_cert, default_backend())
|
||||
issuer = cert.issuer.rfc4514_string()
|
||||
subject = cert.subject.rfc4514_string()
|
||||
try:
|
||||
san_ext = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
||||
# Loop through the list of SANs (DNS names, IP addresses, etc.)
|
||||
sans = [str(name.value) for name in san_ext.value]
|
||||
san_str = ", ".join(sans)
|
||||
except x509.ExtensionNotFound:
|
||||
san_str = "None"
|
||||
return f"Untrusted Issuer: {issuer}\n Server Subject: {subject}\n SANs: {san_str}"
|
||||
except Exception as e:
|
||||
return f"Failed to retrieve diagnostic certificate: {e}"
|
||||
|
||||
# gam checkconnection
|
||||
def doCheckConnection():
|
||||
m = _getMain()
|
||||
|
||||
def check_host(host):
|
||||
nonlocal try_count, okay, not_okay, success_count
|
||||
try_count += 1
|
||||
dns_err = None
|
||||
ip = 'unknown'
|
||||
try:
|
||||
ip = socket.getaddrinfo(host, None)[0][-1][0] # works with ipv6
|
||||
except socket.gaierror as e:
|
||||
dns_err = f'{not_okay}\n DNS failure: {str(e)}\n'
|
||||
except Exception as e:
|
||||
dns_err = f'{not_okay}\n Unknown DNS failure: {str(e)}\n'
|
||||
check_line = f'Checking {host} ({ip}) ({try_count})...'
|
||||
m.writeStdout(f'{check_line:<100}')
|
||||
m.flushStdout()
|
||||
if dns_err:
|
||||
m.writeStdout(dns_err)
|
||||
return
|
||||
gen_firewall = 'You probably have security software or a firewall on your machine or network that is preventing GAM from making Internet connections. Check your network configuration or try running GAM on a hotspot or home network to see if the problem exists only on your organization\'s network.'
|
||||
try:
|
||||
if host.startswith('http'):
|
||||
url = host
|
||||
else:
|
||||
url = f'https://{host}:443/'
|
||||
httpObj.request(url, 'HEAD', headers=headers)
|
||||
success_count += 1
|
||||
m.writeStdout(f'{okay}\n')
|
||||
except ConnectionRefusedError:
|
||||
m.writeStdout(f'{not_okay}\n Connection refused. {gen_firewall}\n')
|
||||
except ConnectionResetError:
|
||||
m.writeStdout(f'{not_okay}\n Connection reset by peer. {gen_firewall}\n')
|
||||
except httplib2.error.ServerNotFoundError:
|
||||
m.writeStdout(f'{not_okay}\n Failed to find server. Your DNS is probably misconfigured.\n')
|
||||
except ssl.SSLCertVerificationError as e:
|
||||
diag_info = inspect_untrusted_cert(host)
|
||||
# e.verify_message contains the specific OpenSSL error string
|
||||
m.writeStdout(f'{not_okay}\n Certificate verification failed: {e.verify_message}\n Diagnostic Info:\n {diag_info}\nIf you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.\n')
|
||||
except ssl.SSLError as e:
|
||||
if e.reason == 'SSLV3_ALERT_HANDSHAKE_FAILURE':
|
||||
m.writeStdout(f'{not_okay}\n GAM expects to connect with TLS 1.3 or newer and that failed. If your firewall / proxy server is not compatible with TLS 1.3 then you can tell GAM to allow TLS 1.2 by setting tls_min_version = TLSv1.2 in gam.cfg.\n')
|
||||
elif e.reason == 'CERTIFICATE_VERIFY_FAILED':
|
||||
m.writeStdout(f'{not_okay}\n Certificate verification failed. If you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.\n')
|
||||
elif e.strerror and e.strerror.startswith('TLS/SSL connection has been closed\n'):
|
||||
m.writeStdout(f'{not_okay}\n TLS connection was closed. {gen_firewall}\n')
|
||||
else:
|
||||
m.writeStdout(f'{not_okay}\n {str(e)}\n')
|
||||
except TimeoutError:
|
||||
m.writeStdout(f'{not_okay}\n Timed out trying to connect to host\n')
|
||||
except Exception as e:
|
||||
m.writeStdout(f'{not_okay}\n {str(e)}\n')
|
||||
|
||||
try_count = 0
|
||||
httpObj = m.getHttpObj(timeout=30)
|
||||
httpObj.follow_redirects = False
|
||||
headers = {'user-agent': m.GAM_USER_AGENT}
|
||||
okay = m.createGreenText('OK')
|
||||
not_okay = m.createRedText('ERROR')
|
||||
success_count = 0
|
||||
initial_hosts = ['api.github.com',
|
||||
'raw.githubusercontent.com',
|
||||
'accounts.google.com',
|
||||
'oauth2.googleapis.com',
|
||||
'www.googleapis.com']
|
||||
for host in initial_hosts:
|
||||
check_host(host)
|
||||
api_hosts = ['apps-apis.google.com',
|
||||
'www.google.com']
|
||||
for host in api_hosts:
|
||||
check_host(host)
|
||||
# For v2 discovery APIs, GAM gets discovery file from <api>.googleapis.com so
|
||||
# add those domains.
|
||||
disc_hosts = []
|
||||
for api, config in API._INFO.items():
|
||||
if config.get('v2discovery') and not config.get('localdiscovery'):
|
||||
if mapped_api := config.get('mappedAPI'):
|
||||
api = mapped_api
|
||||
host = f'{api}.googleapis.com'
|
||||
if host not in disc_hosts:
|
||||
disc_hosts.append(host)
|
||||
for host in disc_hosts:
|
||||
check_host(host)
|
||||
checked_hosts = initial_hosts + api_hosts + disc_hosts
|
||||
# now we need to "build" each API and check it's base URL host
|
||||
# if we haven't already. This may not be any hosts at all but
|
||||
# to ensure we are checking all hosts GAM may use we should
|
||||
# keep this.
|
||||
for api in API._INFO:
|
||||
if api in [API.CONTACTS, API.EMAIL_AUDIT]:
|
||||
continue
|
||||
svc = m.getService(api, httpObj)
|
||||
base_url = svc._rootDesc.get('baseUrl')
|
||||
parsed_base_url = urlparse(base_url)
|
||||
base_host = parsed_base_url.netloc
|
||||
if base_host not in checked_hosts:
|
||||
m.writeStdout(f'Checking {base_host} for {api}\n')
|
||||
check_host(base_host)
|
||||
checked_hosts.append(base_host)
|
||||
if success_count == try_count:
|
||||
m.writeStdout(m.createGreenText('All hosts passed!\n'))
|
||||
else:
|
||||
m.systemErrorExit(3, m.createYellowText('Some hosts failed to connect! Please follow the recommendations for those hosts to correct any issues and try again.'))
|
||||
|
||||
# gam comment
|
||||
def doComment():
|
||||
m = _getMain()
|
||||
m.writeStdout(Cmd.QuotedArgumentList(Cmd.Remaining())+'\n')
|
||||
|
||||
# gam version [check|checkrc|simple|extended] [timeoffset] [nooffseterror] [location <HostName>]
|
||||
def doVersion(checkForArgs=True):
|
||||
m = _getMain()
|
||||
Ent = _getEnt()
|
||||
forceCheck = 0
|
||||
extended = noOffsetError = timeOffset = simple = False
|
||||
testLocation = m.GOOGLE_TIMECHECK_LOCATION
|
||||
if checkForArgs:
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = m.getArgument()
|
||||
if myarg == 'check':
|
||||
forceCheck = 1
|
||||
elif myarg == 'checkrc':
|
||||
forceCheck = -1
|
||||
elif myarg == 'simple':
|
||||
simple = True
|
||||
elif myarg == 'extended':
|
||||
extended = timeOffset = True
|
||||
elif myarg == 'timeoffset':
|
||||
timeOffset = True
|
||||
elif myarg == 'nooffseterror':
|
||||
noOffsetError = True
|
||||
elif myarg == 'location':
|
||||
testLocation = m.getString(Cmd.OB_HOST_NAME)
|
||||
else:
|
||||
m.unknownArgumentExit()
|
||||
if simple:
|
||||
m.writeStdout(m.__version__)
|
||||
return
|
||||
m.writeStdout((f'{m.GAM} {m.__version__} - {m.GAM_URL} - {GM.Globals[GM.GAM_TYPE]}\n'
|
||||
f'{m.__author__}\n'
|
||||
f'Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {struct.calcsize("P")*8}-bit {sys.version_info[3]}\n'
|
||||
f'{getOSPlatform()} {platform.machine()}\n'
|
||||
f'Path: {GM.Globals[GM.GAM_PATH]}\n'
|
||||
f'{Ent.Singular(Ent.CONFIG_FILE)}: {GM.Globals[GM.GAM_CFG_FILE]}, {Ent.Singular(Ent.SECTION)}: {GM.Globals[GM.GAM_CFG_SECTION_NAME]}, '
|
||||
f'{GC.CUSTOMER_ID}: {GC.Values[GC.CUSTOMER_ID]}, {GC.DOMAIN}: {GC.Values[GC.DOMAIN]}\n'
|
||||
f'Time: {m.ISOformatTimeStamp(m.todaysTime())}\n'
|
||||
))
|
||||
if sys.platform.startswith('win') and str(struct.calcsize('P')*8).find('32') != -1 and platform.machine().find('64') != -1:
|
||||
m.printKeyValueList([Msg.UPDATE_GAM_TO_64BIT])
|
||||
if timeOffset:
|
||||
offsetSeconds, offsetFormatted = getLocalGoogleTimeOffset(testLocation)
|
||||
m.printKeyValueList([Msg.YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE.format(testLocation, offsetFormatted)])
|
||||
if offsetSeconds > m.MAX_LOCAL_GOOGLE_TIME_OFFSET:
|
||||
if not noOffsetError:
|
||||
m.systemErrorExit(m.NETWORK_ERROR_RC, Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
|
||||
m.stderrWarningMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
|
||||
if forceCheck:
|
||||
m.doGAMCheckForUpdates(forceCheck)
|
||||
if extended:
|
||||
m.printKeyValueList([ssl.OPENSSL_VERSION])
|
||||
tls_ver, cipher_name = _getServerTLSUsed(testLocation)
|
||||
for lib in glverlibs.GAM_VER_LIBS:
|
||||
try:
|
||||
m.writeStdout(f'{lib} {lib_version(lib)}\n')
|
||||
except:
|
||||
pass
|
||||
m.printKeyValueList([f'{testLocation} connects using {tls_ver} {cipher_name}'])
|
||||
|
||||
# gam help
|
||||
def doUsage():
|
||||
m = _getMain()
|
||||
m.printBlankLine()
|
||||
doVersion(checkForArgs=False)
|
||||
m.writeStdout(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], m.FN_GAMCOMMANDS_TXT)))
|
||||
m.writeStdout(Msg.HELP_WIKI.format(m.GAM_WIKI))
|
||||
1988
src/gam/util/csv_pf.py
Normal file
1988
src/gam/util/csv_pf.py
Normal file
File diff suppressed because it is too large
Load Diff
570
src/gam/util/display.py
Normal file
570
src/gam/util/display.py
Normal file
@@ -0,0 +1,570 @@
|
||||
"""GAM display utilities — printing, warning, action, and entity display functions.
|
||||
|
||||
Thin wrappers around writeStdout/writeStderr that format entity values, actions,
|
||||
and messages for user-facing output. Depends only on gamlib modules and
|
||||
util.output.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glglobals as GM
|
||||
from gamlib import glmsgs as Msg
|
||||
|
||||
|
||||
class _InstanceProxy:
|
||||
"""Lazy proxy that delegates attribute access to a named instance in the gam module."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
def __getattr__(self, attr):
|
||||
return getattr(getattr(sys.modules['gam'], self._name), attr)
|
||||
|
||||
Act = _InstanceProxy('Act')
|
||||
Ind = _InstanceProxy('Ind')
|
||||
|
||||
|
||||
from util.output import (
|
||||
currentCountNL,
|
||||
formatKeyValueList,
|
||||
printWarningMessage,
|
||||
setSysExitRC,
|
||||
writeStderr,
|
||||
writeStdout,
|
||||
)
|
||||
|
||||
# Constants duplicated from __init__.py to avoid circular imports.
|
||||
ACTION_FAILED_RC = 50
|
||||
ACTION_NOT_PERFORMED_RC = 51
|
||||
BAD_REQUEST_RC = 53
|
||||
ENTITY_DOES_NOT_EXIST_RC = 56
|
||||
ENTITY_DUPLICATE_RC = 57
|
||||
SERVICE_NOT_APPLICABLE_RC = 73
|
||||
ERROR = 'ERROR'
|
||||
FIRST_ITEM_MARKER = '%%first_item%%'
|
||||
LAST_ITEM_MARKER = '%%last_item%%'
|
||||
TOTAL_ITEMS_MARKER = '%%total_items%%'
|
||||
|
||||
|
||||
def _getEnt():
|
||||
"""Get the Ent instance from the main module (lazy accessor)."""
|
||||
return sys.modules['gam'].Ent
|
||||
|
||||
|
||||
def _getEscapeCRsNLs():
|
||||
"""Get the escapeCRsNLs function from the main module (lazy accessor)."""
|
||||
return sys.modules['gam'].escapeCRsNLs
|
||||
|
||||
|
||||
# --- Warnings ---
|
||||
|
||||
def badRequestWarning(entityType, itemType, itemValue):
|
||||
Ent = _getEnt()
|
||||
printWarningMessage(BAD_REQUEST_RC,
|
||||
f'{Msg.GOT} 0 {Ent.Plural(entityType)}: {Msg.INVALID} {Ent.Singular(itemType)} - {itemValue}')
|
||||
|
||||
def emptyQuery(query, entityType):
|
||||
Ent = _getEnt()
|
||||
return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.NO_ENTITIES_FOUND.format(Ent.Plural(entityType))}'
|
||||
|
||||
def invalidQuery(query):
|
||||
Ent = _getEnt()
|
||||
return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.INVALID}'
|
||||
|
||||
def invalidMember(query):
|
||||
Ent = _getEnt()
|
||||
if query:
|
||||
badRequestWarning(Ent.GROUP, Ent.QUERY, invalidQuery(query))
|
||||
return True
|
||||
return False
|
||||
|
||||
def invalidUserSchema(schema):
|
||||
Ent = _getEnt()
|
||||
if isinstance(schema, list):
|
||||
return f'{Ent.Singular(Ent.USER_SCHEMA)} ({",".join(schema)}) {Msg.INVALID}'
|
||||
return f'{Ent.Singular(Ent.USER_SCHEMA)} {schema}) {Msg.INVALID}'
|
||||
|
||||
|
||||
# --- Service Not Enabled Warnings ---
|
||||
|
||||
def userServiceNotEnabledWarning(entityName, service, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Singular(Ent.USER), entityName, Msg.SERVICE_NOT_ENABLED.format(service)],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def userAlertsServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Alerts', i, count)
|
||||
|
||||
def userAnalyticsServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Alerts', i, count)
|
||||
|
||||
def userCalServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Calendar', i, count)
|
||||
|
||||
def userChatServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Chat', i, count)
|
||||
|
||||
def userContactDelegateServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Contact Delegate', i, count)
|
||||
|
||||
def userDriveServiceNotEnabledWarning(user, errMessage, i=0, count=0):
|
||||
# if errMessage.find('Drive apps') == -1 and errMessage.find('Active session is invalid') == -1:
|
||||
# entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
||||
if errMessage.find('Drive apps') >= 0 or errMessage.find('Active session is invalid') >= 0:
|
||||
userServiceNotEnabledWarning(user, 'Drive', i, count)
|
||||
else:
|
||||
entityActionNotPerformedWarning([_getEnt().USER, user], errMessage, i, count)
|
||||
|
||||
def userKeepServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Keep', i, count)
|
||||
|
||||
def userGmailServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Gmail', i, count)
|
||||
|
||||
def userLookerStudioServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Looker Studio', i, count)
|
||||
|
||||
def userPeopleServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'People', i, count)
|
||||
|
||||
def userTasksServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'Tasks', i, count)
|
||||
|
||||
def userYouTubeServiceNotEnabledWarning(entityName, i=0, count=0):
|
||||
userServiceNotEnabledWarning(entityName, 'YouTube', i, count)
|
||||
|
||||
|
||||
# --- Entity Warning Functions ---
|
||||
|
||||
def entityServiceNotApplicableWarning(entityType, entityName, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Singular(entityType), entityName, Msg.SERVICE_NOT_APPLICABLE],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityDoesNotExistWarning(entityType, entityName, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Singular(entityType), entityName, Msg.DOES_NOT_EXIST],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityListDoesNotExistWarning(entityValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityDoesNotHaveItemWarning(entityValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityDuplicateWarning(entityValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ENTITY_DUPLICATE_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), Msg.DUPLICATE],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityActionFailedWarning(entityValueList, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_FAILED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierItemValueListActionFailedWarning(entityValueList, modifier, infoTypeValueList, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_FAILED_RC)
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList)+[Act.Failed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierActionFailedWarning(entityValueList, modifier, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_FAILED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', Act.Failed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierNewValueActionFailedWarning(entityValueList, modifier, newValue, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_FAILED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue, Act.Failed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityNumEntitiesActionFailedWarning(entityType, entityName, itemType, itemCount, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_FAILED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Singular(entityType), entityName,
|
||||
Ent.Choose(itemType, itemCount), itemCount,
|
||||
Act.Failed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityActionNotPerformedWarning(entityValueList, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_NOT_PERFORMED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityItemValueListActionNotPerformedWarning(entityValueList, infoTypeValueList, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), '']+Ent.FormatEntityValueList(infoTypeValueList)+[errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierItemValueListActionNotPerformedWarning(entityValueList, modifier, infoTypeValueList, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.NotPerformed()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList)+[errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityNumEntitiesActionNotPerformedWarning(entityValueList, itemType, itemCount, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_NOT_PERFORMED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Ent.Choose(itemType, itemCount), itemCount, Act.NotPerformed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityBadRequestWarning(entityValueList, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(BAD_REQUEST_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[ERROR, errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
|
||||
# --- Getting / Paging Display ---
|
||||
|
||||
def printGettingAllAccountEntities(entityType, query='', qualifier='', accountType=None):
|
||||
Ent = _getEnt()
|
||||
if accountType is None:
|
||||
accountType = Ent.ACCOUNT
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
if query:
|
||||
Ent.SetGettingQuery(entityType, query)
|
||||
elif qualifier:
|
||||
Ent.SetGettingQualifier(entityType, qualifier)
|
||||
else:
|
||||
Ent.SetGetting(entityType)
|
||||
writeStderr(f'{Msg.GETTING_ALL} {Ent.PluralGetting()}{Ent.GettingPreQualifier()}{Ent.MayTakeTime(accountType)}\n')
|
||||
|
||||
def printGotAccountEntities(count):
|
||||
Ent = _getEnt()
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
writeStderr(f'{Msg.GOT} {count} {Ent.ChooseGetting(count)}{Ent.GettingPostQualifier()}\n')
|
||||
|
||||
def setGettingAllEntityItemsForWhom(entityItem, forWhom, query='', qualifier=''):
|
||||
Ent = _getEnt()
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
if query:
|
||||
Ent.SetGettingQuery(entityItem, query)
|
||||
elif qualifier:
|
||||
Ent.SetGettingQualifier(entityItem, qualifier)
|
||||
else:
|
||||
Ent.SetGetting(entityItem)
|
||||
Ent.SetGettingForWhom(forWhom)
|
||||
|
||||
def printGettingAllEntityItemsForWhom(entityItem, forWhom, i=0, count=0, query='', qualifier='', entityType=None):
|
||||
Ent = _getEnt()
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
setGettingAllEntityItemsForWhom(entityItem, forWhom, query=query, qualifier=qualifier)
|
||||
writeStderr(f'{Msg.GETTING_ALL} {Ent.PluralGetting()}{Ent.GettingPreQualifier()} {Msg.FOR} {forWhom}{Ent.MayTakeTime(entityType)}{currentCountNL(i, count)}')
|
||||
|
||||
def printGotEntityItemsForWhom(count):
|
||||
Ent = _getEnt()
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
writeStderr(f'{Msg.GOT} {count} {Ent.ChooseGetting(count)}{Ent.GettingPostQualifier()} {Msg.FOR} {Ent.GettingForWhom()}\n')
|
||||
|
||||
def printGettingEntityItem(entityType, entityItem, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
writeStderr(f'{Msg.GETTING} {Ent.Singular(entityType)} {entityItem}{currentCountNL(i, count)}')
|
||||
|
||||
def printGettingEntityItemForWhom(entityItem, forWhom, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
if GC.Values[GC.SHOW_GETTINGS]:
|
||||
Ent.SetGetting(entityItem)
|
||||
Ent.SetGettingForWhom(forWhom)
|
||||
writeStderr(f'{Msg.GETTING} {Ent.PluralGetting()} {Msg.FOR} {forWhom}{currentCountNL(i, count)}')
|
||||
|
||||
def stderrEntityMessage(entityValueList, message, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[message],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def getPageMessage(showFirstLastItems=False, showDate=None):
|
||||
if not GC.Values[GC.SHOW_GETTINGS]:
|
||||
return None
|
||||
pageMessage = f'{Msg.GOT} {TOTAL_ITEMS_MARKER} {{0}}'
|
||||
if showDate:
|
||||
pageMessage += f' on {showDate}'
|
||||
if showFirstLastItems:
|
||||
pageMessage += f': {FIRST_ITEM_MARKER} - {LAST_ITEM_MARKER}'
|
||||
else:
|
||||
pageMessage += '...'
|
||||
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
|
||||
pageMessage += '\n'
|
||||
else:
|
||||
GM.Globals[GM.LAST_GOT_MSG_LEN] = 0
|
||||
return pageMessage
|
||||
|
||||
def getPageMessageForWhom(forWhom=None, showFirstLastItems=False, showDate=None, clearLastGotMsgLen=True):
|
||||
Ent = _getEnt()
|
||||
if not GC.Values[GC.SHOW_GETTINGS]:
|
||||
return None
|
||||
if forWhom:
|
||||
Ent.SetGettingForWhom(forWhom)
|
||||
pageMessage = f'{Msg.GOT} {TOTAL_ITEMS_MARKER} {{0}}{Ent.GettingPostQualifier()} {Msg.FOR} {Ent.GettingForWhom()}'
|
||||
if showDate:
|
||||
pageMessage += f' on {showDate}'
|
||||
if showFirstLastItems:
|
||||
pageMessage += f': {FIRST_ITEM_MARKER} - {LAST_ITEM_MARKER}'
|
||||
else:
|
||||
pageMessage += '...'
|
||||
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
|
||||
pageMessage += '\n'
|
||||
elif clearLastGotMsgLen:
|
||||
GM.Globals[GM.LAST_GOT_MSG_LEN] = 0
|
||||
return pageMessage
|
||||
|
||||
|
||||
# --- Print Utilities ---
|
||||
|
||||
def printLine(message):
|
||||
writeStdout(message+'\n')
|
||||
|
||||
def printBlankLine():
|
||||
writeStdout('\n')
|
||||
|
||||
def printKeyValueList(kvList):
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(), kvList, '\n'))
|
||||
|
||||
def printKeyValueListWithCount(kvList, i, count):
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(), kvList, currentCountNL(i, count)))
|
||||
|
||||
def printKeyValueDict(kvDict):
|
||||
for key, value in kvDict.items():
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(), [key, value], '\n'))
|
||||
|
||||
def printKeyValueWithCRsNLs(key, value):
|
||||
escapeCRsNLs = _getEscapeCRsNLs()
|
||||
if value.find('\n') >= 0 or value.find('\r') >= 0:
|
||||
if GC.Values[GC.SHOW_CONVERT_CR_NL]:
|
||||
printKeyValueList([key, escapeCRsNLs(value)])
|
||||
else:
|
||||
printKeyValueList([key, ''])
|
||||
Ind.Increment()
|
||||
printKeyValueList([Ind.MultiLineText(value)])
|
||||
Ind.Decrement()
|
||||
else:
|
||||
printKeyValueList([key, value])
|
||||
|
||||
def printJSONKey(key):
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(), [key, None], ''))
|
||||
|
||||
def printJSONValue(value):
|
||||
writeStdout(formatKeyValueList(' ', [value], '\n'))
|
||||
|
||||
def printEntity(entityValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList),
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def printEntityMessage(entityValueList, message, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[message],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def printEntitiesCount(entityType, entityList):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Plural(entityType), None if entityList is None else f'({len(entityList)})'],
|
||||
'\n'))
|
||||
|
||||
def printEntityKVList(entityValueList, infoKVList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+infoKVList,
|
||||
currentCountNL(i, count)))
|
||||
|
||||
|
||||
# --- performAction / entityPerformAction ---
|
||||
|
||||
def performAction(entityType, entityValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
[f'{Act.ToPerform()} {Ent.Singular(entityType)} {entityValue}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def performActionNumItems(itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def performActionModifierNumItems(modifier, itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
[f'{Act.ToPerform()} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def actionPerformedNumItems(itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Performed()} '],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def actionFailedNumItems(itemCount, itemType, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Failed()}: {errMessage} '],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def actionNotPerformedNumItemsWarning(itemCount, itemType, errMessage, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
setSysExitRC(ACTION_NOT_PERFORMED_RC)
|
||||
writeStderr(formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Choose(itemType, itemCount), itemCount, Act.NotPerformed(), errMessage],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformAction(entityValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionNumItems(entityValueList, itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionModifierNumItems(entityValueList, modifier, itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionNumItemsModifier(entityValueList, itemCount, itemType, modifier, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionSubItemModifierNumItems(entityValueList, subitemType, modifier, itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {Ent.Plural(subitemType)} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionSubItemModifierNumItemsModifierNewValue(entityValueList, subitemType, modifier1, itemCount, itemType, modifier2, newValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+
|
||||
[f'{Act.ToPerform()} {Ent.Plural(subitemType)} {modifier1} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier2}', newValue],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionModifierNumItemsModifier(entityValueList, modifier1, itemCount, itemType, modifier2, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier1} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier2}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionModifierItemValueList(entityValueList, modifier, infoTypeValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList),
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionModifierNewValue(entityValueList, modifier, newValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionModifierNewValueItemValueList(entityValueList, modifier, newValue, infoTypeValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue]+Ent.FormatEntityValueList(infoTypeValueList),
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionItemValue(entityValueList, itemType, itemValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.ToPerform(), None, Ent.Singular(itemType), itemValue],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityPerformActionInfo(entityValueList, infoValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.ToPerform(), infoValue],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
|
||||
# --- entityActionPerformed / entityModifier ---
|
||||
|
||||
def entityActionPerformed(entityValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.Performed()],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityActionPerformedMessage(entityValueList, message, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
if message:
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.Performed(), message],
|
||||
currentCountNL(i, count)))
|
||||
else:
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.Performed()],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityNumItemsActionPerformed(entityValueList, itemCount, itemType, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Performed()}'],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierActionPerformed(entityValueList, modifier, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', None],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierItemValueListActionPerformed(entityValueList, modifier, infoTypeValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList),
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierNewValueActionPerformed(entityValueList, modifier, newValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierNewValueItemValueListActionPerformed(entityValueList, modifier, newValue, infoTypeValueList, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue]+Ent.FormatEntityValueList(infoTypeValueList),
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityModifierNewValueKeyValueActionPerformed(entityValueList, modifier, newValue, infoKey, infoValue, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
writeStdout(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue, infoKey, infoValue],
|
||||
currentCountNL(i, count)))
|
||||
193
src/gam/util/email.py
Normal file
193
src/gam/util/email.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""GAM email utilities.
|
||||
|
||||
Extracted from gam/__init__.py. Provides email attachment handling
|
||||
and email sending via Gmail API or SMTP.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import smtplib
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.audio import MIMEAudio
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glgapi as GAPI
|
||||
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
|
||||
# Add attachements to an email message
|
||||
def _addAttachmentsToMessage(message, attachments):
|
||||
gam = _getMain()
|
||||
for attachment in attachments:
|
||||
try:
|
||||
attachFilename = gam.setFilePath(attachment[0], GC.INPUT_DIR)
|
||||
attachContentType, attachEncoding = mimetypes.guess_type(attachFilename)
|
||||
if attachContentType is None or attachEncoding is not None:
|
||||
attachContentType = 'application/octet-stream'
|
||||
main_type, sub_type = attachContentType.split('/', 1)
|
||||
if main_type == 'text':
|
||||
msg = MIMEText(gam.readFile(attachFilename, 'r', attachment[1]), _subtype=sub_type, _charset=gam.UTF8)
|
||||
elif main_type == 'image':
|
||||
msg = MIMEImage(gam.readFile(attachFilename, 'rb'), _subtype=sub_type)
|
||||
elif main_type == 'audio':
|
||||
msg = MIMEAudio(gam.readFile(attachFilename, 'rb'), _subtype=sub_type)
|
||||
elif main_type == 'application':
|
||||
msg = MIMEApplication(gam.readFile(attachFilename, 'rb'), _subtype=sub_type)
|
||||
else:
|
||||
msg = MIMEBase(main_type, sub_type)
|
||||
msg.set_payload(gam.readFile(attachFilename, 'rb'))
|
||||
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(attachFilename))
|
||||
message.attach(msg)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
gam.usageErrorExit(f'{attachFilename}: {str(e)}')
|
||||
|
||||
# Add embedded images to an email message
|
||||
def _addEmbeddedImagesToMessage(message, embeddedImages):
|
||||
gam = _getMain()
|
||||
for embeddedImage in embeddedImages:
|
||||
try:
|
||||
imageFilename = gam.setFilePath(embeddedImage[0], GC.INPUT_DIR)
|
||||
imageContentType, imageEncoding = mimetypes.guess_type(imageFilename)
|
||||
if imageContentType is None or imageEncoding is not None:
|
||||
imageContentType = 'application/octet-stream'
|
||||
main_type, sub_type = imageContentType.split('/', 1)
|
||||
if main_type == 'image':
|
||||
msg = MIMEImage(gam.readFile(imageFilename, 'rb'), _subtype=sub_type)
|
||||
else:
|
||||
msg = MIMEBase(main_type, sub_type)
|
||||
msg.set_payload(gam.readFile(imageFilename, 'rb'))
|
||||
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(imageFilename))
|
||||
msg.add_header('Content-ID', f'<{embeddedImage[1]}>')
|
||||
message.attach(msg)
|
||||
except (IOError, UnicodeDecodeError) as e:
|
||||
gam.usageErrorExit(f'{imageFilename}: {str(e)}')
|
||||
|
||||
# Send an email
|
||||
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
|
||||
html=False, charset=None, attachments=None, embeddedImages=None,
|
||||
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None,
|
||||
action=None):
|
||||
gam = _getMain()
|
||||
Act = gam.Act
|
||||
Ent = gam.Ent
|
||||
if charset is None:
|
||||
charset = gam.UTF8
|
||||
if action is None:
|
||||
action = Act.SENDEMAIL
|
||||
|
||||
def checkResult(entityType, recipients):
|
||||
if not recipients:
|
||||
return
|
||||
toSent = set(recipients.split(','))
|
||||
toFailed = {}
|
||||
for addr, err in result.items():
|
||||
if addr in toSent:
|
||||
toSent.remove(addr)
|
||||
toFailed[addr] = f'{err[0]}: {err[1]}'
|
||||
if toSent:
|
||||
gam.entityActionPerformed([entityType, ','.join(toSent), Ent.MESSAGE, msgSubject], i, count)
|
||||
for addr, errMsg in toFailed.items():
|
||||
gam.entityActionFailedWarning([entityType, addr, Ent.MESSAGE, msgSubject], errMsg, i, count)
|
||||
|
||||
def cleanAddr(emailAddr):
|
||||
match = gam.NAME_EMAIL_ADDRESS_PATTERN.match(emailAddr)
|
||||
if match:
|
||||
emailName = match.group(1)
|
||||
emailAddr = gam.normalizeEmailAddressOrUID(match.group(2), noUid=True, noLower=True)
|
||||
return (f'{emailName} <{emailAddr}>', emailAddr)
|
||||
emailAddr = gam.normalizeEmailAddressOrUID(emailAddr, noUid=True, noLower=True)
|
||||
return (emailAddr, emailAddr)
|
||||
|
||||
if msgFrom is None:
|
||||
msgFrom = gam._getAdminEmail()
|
||||
# Force ASCII for RFC compliance
|
||||
# xmlcharref seems to work to display at least
|
||||
# some unicode in HTML body and is ignored in
|
||||
# plain text body.
|
||||
# msgBody = msgBody.encode('ascii', 'xmlcharrefreplace').decode(gam.UTF8)
|
||||
if not attachments and not embeddedImages:
|
||||
message = MIMEText(msgBody, ['plain', 'html'][html], charset)
|
||||
else:
|
||||
message = MIMEMultipart()
|
||||
msg = MIMEText(msgBody, ['plain', 'html'][html], charset)
|
||||
message.attach(msg)
|
||||
if attachments:
|
||||
_addAttachmentsToMessage(message, attachments)
|
||||
if embeddedImages:
|
||||
_addEmbeddedImagesToMessage(message, embeddedImages)
|
||||
message['Subject'] = msgSubject
|
||||
message['From'], msgFromAddr = cleanAddr(msgFrom)
|
||||
if msgReplyTo is not None:
|
||||
message['Reply-To'], _ = cleanAddr(msgReplyTo)
|
||||
if ccRecipients:
|
||||
message['Cc'] = ccRecipients.lower()
|
||||
if bccRecipients:
|
||||
message['Bcc'] = bccRecipients.lower()
|
||||
if msgHeaders:
|
||||
for header, value in msgHeaders.items():
|
||||
if header not in {'Subject', 'From', 'To', 'Reply-To', 'Cc', 'Bcc'}:
|
||||
message[header] = value
|
||||
if mailBox is None:
|
||||
mailBox = msgFromAddr
|
||||
_, mailBoxAddr = cleanAddr(mailBox)
|
||||
parentAction = Act.Get()
|
||||
Act.Set(action)
|
||||
if not GC.Values[GC.SMTP_HOST]:
|
||||
if not clientAccess:
|
||||
userId, gmail = gam.buildGAPIServiceObject(API.GMAIL, mailBoxAddr)
|
||||
if not gmail:
|
||||
Act.Set(parentAction)
|
||||
return
|
||||
else:
|
||||
userId = mailBoxAddr
|
||||
gmail = gam.buildGAPIObject(API.GMAIL)
|
||||
message['To'] = msgTo if msgTo else userId
|
||||
body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}
|
||||
if threadId is not None:
|
||||
body['threadId'] = threadId
|
||||
try:
|
||||
result = gam.callGAPI(gmail.users().messages(), 'send',
|
||||
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
|
||||
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
||||
userId=userId, body=body, fields='id')
|
||||
gam.entityActionPerformedMessage([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], f"{result['id']}", i, count)
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
|
||||
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied) as e:
|
||||
gam.entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
|
||||
else:
|
||||
message['To'] = msgTo if msgTo else mailBoxAddr
|
||||
server = None
|
||||
try:
|
||||
server = smtplib.SMTP(GC.Values[GC.SMTP_HOST], 587, GC.Values[GC.SMTP_FQDN])
|
||||
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
||||
server.set_debuglevel(1)
|
||||
server.starttls(context=ssl.create_default_context(cafile=GC.Values[GC.CACERTS_PEM]))
|
||||
if GC.Values[GC.SMTP_USERNAME] and GC.Values[GC.SMTP_PASSWORD]:
|
||||
if isinstance(GC.Values[GC.SMTP_PASSWORD], bytes):
|
||||
server.login(GC.Values[GC.SMTP_USERNAME], base64.b64decode(GC.Values[GC.SMTP_PASSWORD]).decode(gam.UTF8))
|
||||
else:
|
||||
server.login(GC.Values[GC.SMTP_USERNAME], GC.Values[GC.SMTP_PASSWORD])
|
||||
result = server.send_message(message)
|
||||
checkResult(Ent.RECIPIENT, message['To'])
|
||||
checkResult(Ent.RECIPIENT_CC, ccRecipients)
|
||||
checkResult(Ent.RECIPIENT_BCC, bccRecipients)
|
||||
except smtplib.SMTPException as e:
|
||||
gam.entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
|
||||
if server:
|
||||
try:
|
||||
server.quit()
|
||||
except Exception:
|
||||
pass
|
||||
Act.Set(parentAction)
|
||||
1558
src/gam/util/entity.py
Normal file
1558
src/gam/util/entity.py
Normal file
File diff suppressed because it is too large
Load Diff
213
src/gam/util/errors.py
Normal file
213
src/gam/util/errors.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""Error, exit, and argument validation functions.
|
||||
|
||||
Extracted from gam/__init__.py to reduce monolith size.
|
||||
Functions here depend only on gamlib modules and util/output.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glglobals as GM
|
||||
from gamlib import glmsgs as Msg
|
||||
|
||||
|
||||
class _InstanceProxy:
|
||||
"""Lazy proxy that delegates attribute access to a named instance in the gam module."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
def __getattr__(self, attr):
|
||||
return getattr(getattr(sys.modules['gam'], self._name), attr)
|
||||
|
||||
Act = _InstanceProxy('Act')
|
||||
Cmd = _InstanceProxy('Cmd')
|
||||
Ind = _InstanceProxy('Ind')
|
||||
|
||||
|
||||
|
||||
from util.output import (
|
||||
currentCountNL,
|
||||
formatKeyValueList,
|
||||
stderrErrorMsg,
|
||||
stderrWarningMsg,
|
||||
systemErrorExit,
|
||||
writeStderr,
|
||||
)
|
||||
|
||||
# Lazy accessor for Ent (runtime instance)
|
||||
def _getEnt():
|
||||
return sys.modules['gam'].Ent
|
||||
|
||||
# Constants imported from __init__.py at module level via lazy accessor
|
||||
def _getConst(name):
|
||||
return getattr(sys.modules['gam'], name)
|
||||
|
||||
# --- Return code constants (imported lazily) ---
|
||||
ACTION_FAILED_RC = None
|
||||
CLIENT_SECRETS_JSON_REQUIRED_RC = None
|
||||
ENTITY_DOES_NOT_EXIST_RC = None
|
||||
ENTITY_IS_NOT_UNIQUE_RC = None
|
||||
INVALID_JSON_RC = None
|
||||
OAUTH2_TXT_REQUIRED_RC = None
|
||||
OAUTH2SERVICE_JSON_REQUIRED_RC = None
|
||||
USAGE_ERROR_RC = None
|
||||
|
||||
def _ensureConstants():
|
||||
global ACTION_FAILED_RC, CLIENT_SECRETS_JSON_REQUIRED_RC, ENTITY_DOES_NOT_EXIST_RC
|
||||
global ENTITY_IS_NOT_UNIQUE_RC, INVALID_JSON_RC, OAUTH2_TXT_REQUIRED_RC
|
||||
global OAUTH2SERVICE_JSON_REQUIRED_RC, USAGE_ERROR_RC
|
||||
if ACTION_FAILED_RC is None:
|
||||
m = sys.modules['gam']
|
||||
ACTION_FAILED_RC = m.ACTION_FAILED_RC
|
||||
CLIENT_SECRETS_JSON_REQUIRED_RC = m.CLIENT_SECRETS_JSON_REQUIRED_RC
|
||||
ENTITY_DOES_NOT_EXIST_RC = m.ENTITY_DOES_NOT_EXIST_RC
|
||||
ENTITY_IS_NOT_UNIQUE_RC = m.ENTITY_IS_NOT_UNIQUE_RC
|
||||
INVALID_JSON_RC = m.INVALID_JSON_RC
|
||||
OAUTH2_TXT_REQUIRED_RC = m.OAUTH2_TXT_REQUIRED_RC
|
||||
OAUTH2SERVICE_JSON_REQUIRED_RC = m.OAUTH2SERVICE_JSON_REQUIRED_RC
|
||||
USAGE_ERROR_RC = m.USAGE_ERROR_RC
|
||||
|
||||
# --- Credential file errors ---
|
||||
|
||||
def invalidClientSecretsJsonExit(errMsg):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.CLIENT_SECRETS_JSON_FILE), GC.Values[GC.CLIENT_SECRETS_JSON], errMsg))
|
||||
writeStderr(Msg.INSTRUCTIONS_CLIENT_SECRETS_JSON)
|
||||
systemErrorExit(CLIENT_SECRETS_JSON_REQUIRED_RC, None)
|
||||
|
||||
def invalidOauth2serviceJsonExit(errMsg):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.OAUTH2SERVICE_JSON_FILE), GC.Values[GC.OAUTH2SERVICE_JSON], errMsg))
|
||||
writeStderr(Msg.INSTRUCTIONS_OAUTH2SERVICE_JSON)
|
||||
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, None)
|
||||
|
||||
def invalidOauth2TxtExit(errMsg):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.OAUTH2_TXT_FILE), GC.Values[GC.OAUTH2_TXT], errMsg))
|
||||
writeStderr(Msg.EXECUTE_GAM_OAUTH_CREATE)
|
||||
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, None)
|
||||
|
||||
def expiredRevokedOauth2TxtExit():
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
stderrErrorMsg(Msg.IS_EXPIRED_OR_REVOKED.format(Ent.Singular(Ent.OAUTH2_TXT_FILE), GC.Values[GC.OAUTH2_TXT]))
|
||||
writeStderr(Msg.EXECUTE_GAM_OAUTH_CREATE)
|
||||
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, None)
|
||||
|
||||
def invalidDiscoveryJsonExit(fileName, errMsg):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.DISCOVERY_JSON_FILE), fileName, errMsg))
|
||||
systemErrorExit(INVALID_JSON_RC, None)
|
||||
|
||||
# --- Entity error exits ---
|
||||
|
||||
def entityActionFailedExit(entityValueList, errMsg, i=0, count=0):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), errMsg],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityDoesNotExistExit(entityType, entityName, i=0, count=0, errMsg=None):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
Cmd.Backup()
|
||||
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
||||
systemErrorExit(ENTITY_DOES_NOT_EXIST_RC, formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Singular(entityType), entityName, errMsg or Msg.DOES_NOT_EXIST],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityDoesNotHaveItemExit(entityValueList, i=0, count=0):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
Cmd.Backup()
|
||||
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
||||
systemErrorExit(ENTITY_DOES_NOT_EXIST_RC, formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def entityIsNotUniqueExit(entityType, entityName, valueType, valueList, i=0, count=0):
|
||||
_ensureConstants()
|
||||
Ent = _getEnt()
|
||||
Cmd.Backup()
|
||||
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
||||
systemErrorExit(ENTITY_IS_NOT_UNIQUE_RC, formatKeyValueList(Ind.Spaces(),
|
||||
[Ent.Singular(entityType), entityName, Msg.IS_NOT_UNIQUE.format(Ent.Plural(valueType), ','.join(valueList))],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
# --- Usage/argument errors ---
|
||||
|
||||
def usageErrorExit(message, extraneous=False):
|
||||
_ensureConstants()
|
||||
writeStderr(Cmd.CommandLineWithBadArgumentMarked(extraneous))
|
||||
stderrErrorMsg(message)
|
||||
FN_GAMCOMMANDS_TXT = _getConst('FN_GAMCOMMANDS_TXT')
|
||||
GAM_WIKI = _getConst('GAM_WIKI')
|
||||
writeStderr(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
|
||||
writeStderr(Msg.HELP_WIKI.format(GAM_WIKI))
|
||||
sys.exit(USAGE_ERROR_RC)
|
||||
|
||||
def csvFieldErrorExit(fieldName, fieldNames, backupArg=False, checkForCharset=False):
|
||||
if backupArg:
|
||||
Cmd.Backup()
|
||||
if checkForCharset and Cmd.Previous() == 'charset':
|
||||
Cmd.Backup()
|
||||
Cmd.Backup()
|
||||
usageErrorExit(Msg.HEADER_NOT_FOUND_IN_CSV_HEADERS.format(fieldName, ','.join(fieldNames)))
|
||||
|
||||
def csvDataAlreadySavedErrorExit():
|
||||
Cmd.Backup()
|
||||
usageErrorExit(Msg.CSV_DATA_ALREADY_SAVED)
|
||||
|
||||
# The last thing shown is unknown
|
||||
def unknownArgumentExit():
|
||||
Cmd.Backup()
|
||||
usageErrorExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1])
|
||||
|
||||
# Argument describes what's expected
|
||||
def expectedArgumentExit(problem, argument):
|
||||
usageErrorExit(f'{problem}: {Msg.EXPECTED} <{argument}>')
|
||||
|
||||
def blankArgumentExit(argument):
|
||||
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_BLANK][1], f'{Msg.NON_BLANK} {argument}')
|
||||
|
||||
def emptyArgumentExit(argument):
|
||||
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_EMPTY][1], f'{Msg.NON_EMPTY} {argument}')
|
||||
|
||||
def invalidArgumentExit(argument):
|
||||
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1], argument)
|
||||
|
||||
def missingArgumentExit(argument):
|
||||
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_MISSING][1], argument)
|
||||
|
||||
def deprecatedArgument(argument):
|
||||
Cmd.Backup()
|
||||
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
||||
Cmd.Advance()
|
||||
stderrWarningMsg(f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_DEPRECATED][1]}: {Msg.IGNORED} <{argument}>')
|
||||
|
||||
def deprecatedArgumentExit(argument):
|
||||
usageErrorExit(f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_DEPRECATED][1]}: <{argument}>')
|
||||
|
||||
def deprecatedCommandExit():
|
||||
_ensureConstants()
|
||||
systemErrorExit(USAGE_ERROR_RC, Msg.SITES_COMMAND_DEPRECATED.format(Cmd.CommandDeprecated()))
|
||||
|
||||
# Choices is the valid set of choices that was expected
|
||||
def formatChoiceList(choices):
|
||||
choiceList = [c if c else "''" for c in choices]
|
||||
if len(choiceList) <= 5:
|
||||
return '|'.join(choiceList)
|
||||
return '|'.join(sorted(choiceList))
|
||||
|
||||
def invalidChoiceExit(choice, choices, backupArg):
|
||||
if backupArg:
|
||||
Cmd.Backup()
|
||||
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID_CHOICE][1].format(choice), formatChoiceList(choices))
|
||||
|
||||
def missingChoiceExit(choices):
|
||||
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_MISSING][1], formatChoiceList(choices))
|
||||
289
src/gam/util/fileio.py
Normal file
289
src/gam/util/fileio.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""GAM file I/O helpers — open/read/write/delete files, file path utilities.
|
||||
|
||||
Pure file operations that depend only on gamlib modules and util.output.
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from pathvalidate import sanitize_filename, sanitize_filepath
|
||||
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glentity
|
||||
from gamlib import glglobals as GM
|
||||
from gamlib import glmsgs as Msg
|
||||
|
||||
|
||||
class _InstanceProxy:
|
||||
"""Lazy proxy that delegates attribute access to a named instance in the gam module."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
def __getattr__(self, attr):
|
||||
return getattr(getattr(sys.modules['gam'], self._name), attr)
|
||||
|
||||
Act = _InstanceProxy('Act')
|
||||
Ind = _InstanceProxy('Ind')
|
||||
|
||||
|
||||
from util.output import (
|
||||
stderrErrorMsg,
|
||||
stderrWarningMsg,
|
||||
setSysExitRC,
|
||||
systemErrorExit,
|
||||
formatKeyValueList,
|
||||
currentCountNL,
|
||||
writeStderr,
|
||||
flushStderr,
|
||||
)
|
||||
|
||||
# Constants duplicated from __init__.py to avoid circular imports.
|
||||
UTF8_SIG = 'utf-8-sig'
|
||||
DEFAULT_FILE_READ_MODE = 'r'
|
||||
DEFAULT_FILE_WRITE_MODE = 'w'
|
||||
UNKNOWN = 'Unknown'
|
||||
FILE_ERROR_RC = 6
|
||||
ACTION_FAILED_RC = 50
|
||||
CONFIG_ERROR_RC = 13
|
||||
WARNING = 'WARNING'
|
||||
WARNING_PREFIX = WARNING + ': '
|
||||
|
||||
# Ent is an instance of GamEntity created in __init__.py.
|
||||
# For the default parameter value of fileErrorMessage, we use the
|
||||
# class-level constant directly.
|
||||
_ENT_FILE = glentity.GamEntity.FILE
|
||||
|
||||
|
||||
def _getEnt():
|
||||
"""Get the Ent instance from the main module (lazy accessor)."""
|
||||
return sys.modules['gam'].Ent
|
||||
|
||||
|
||||
def cleanFilename(filename):
|
||||
return sanitize_filename(filename, '_')
|
||||
|
||||
def setFilePath(filename, cfgDir):
|
||||
if filename.startswith('./') or filename.startswith('.\\'):
|
||||
return os.path.join(os.getcwd(), filename[2:])
|
||||
if filename == '-':
|
||||
return filename
|
||||
filename = os.path.expanduser(filename)
|
||||
if os.path.isabs(filename):
|
||||
return filename
|
||||
return os.path.join(GC.Values[cfgDir], filename)
|
||||
|
||||
def uniqueFilename(targetFolder, filetitle, overwrite, extension=None):
|
||||
filename = filetitle
|
||||
y = 0
|
||||
while True:
|
||||
if extension is not None and filename.lower()[-len(extension):] != extension.lower():
|
||||
filename += extension
|
||||
filepath = os.path.join(targetFolder, filename)
|
||||
if overwrite or not os.path.isfile(filepath):
|
||||
return (filepath, filename)
|
||||
y += 1
|
||||
filename = f'({y})-{filetitle}'
|
||||
|
||||
def cleanFilepath(filepath):
|
||||
return sanitize_filepath(filepath, platform='auto')
|
||||
|
||||
def fileErrorMessage(filename, e, entityType=_ENT_FILE):
|
||||
Ent = _getEnt()
|
||||
return f'{Ent.Singular(entityType)}: {filename}, {str(e)}'
|
||||
|
||||
|
||||
def fdErrorMessage(f, defaultFilename, e):
|
||||
return fileErrorMessage(getattr(f, 'name') if hasattr(f, 'name') else defaultFilename, e)
|
||||
|
||||
# Set file encoding to handle UTF8 BOM
|
||||
def setEncoding(mode, encoding):
|
||||
if 'b' in mode:
|
||||
return {}
|
||||
if not encoding:
|
||||
encoding = GM.Globals[GM.SYS_ENCODING]
|
||||
if 'r' in mode and encoding.lower().replace('-', '') == 'utf8':
|
||||
encoding = UTF8_SIG
|
||||
return {'encoding': encoding}
|
||||
|
||||
def StringIOobject(initbuff=None):
|
||||
if initbuff is None:
|
||||
return io.StringIO()
|
||||
return io.StringIO(initbuff)
|
||||
|
||||
# Open a file
|
||||
def openFile(filename, mode=DEFAULT_FILE_READ_MODE, encoding=None, errors=None, newline=None,
|
||||
continueOnError=False, displayError=True, stripUTFBOM=False):
|
||||
try:
|
||||
if filename != '-':
|
||||
kwargs = setEncoding(mode, encoding)
|
||||
f = open(os.path.expanduser(filename), mode, errors=errors, newline=newline, **kwargs)
|
||||
if stripUTFBOM:
|
||||
if 'b' in mode:
|
||||
if f.read(3) != b'\xef\xbb\xbf':
|
||||
f.seek(0)
|
||||
elif not kwargs['encoding'].lower().startswith('utf'):
|
||||
if f.read(3).encode('iso-8859-1', 'replace') != codecs.BOM_UTF8:
|
||||
f.seek(0)
|
||||
else:
|
||||
if f.read(1) != '\ufeff':
|
||||
f.seek(0)
|
||||
return f
|
||||
if 'r' in mode:
|
||||
return StringIOobject(str(sys.stdin.read()))
|
||||
if 'b' not in mode:
|
||||
return sys.stdout
|
||||
return os.fdopen(os.dup(sys.stdout.fileno()), 'wb')
|
||||
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
||||
if continueOnError:
|
||||
if displayError:
|
||||
stderrWarningMsg(fileErrorMessage(filename, e))
|
||||
setSysExitRC(FILE_ERROR_RC)
|
||||
return None
|
||||
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
||||
|
||||
# Close a file
|
||||
def closeFile(f, forceFlush=False):
|
||||
try:
|
||||
if forceFlush:
|
||||
# Necessary to make sure file is flushed by both Python and OS
|
||||
# https://stackoverflow.com/a/13762137/1503886
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
f.close()
|
||||
return True
|
||||
except IOError as e:
|
||||
stderrErrorMsg(fdErrorMessage(f, UNKNOWN, e))
|
||||
setSysExitRC(FILE_ERROR_RC)
|
||||
return False
|
||||
|
||||
# Read a file
|
||||
def readFile(filename, mode=DEFAULT_FILE_READ_MODE, encoding=None, newline=None,
|
||||
continueOnError=False, displayError=True):
|
||||
try:
|
||||
if filename != '-':
|
||||
kwargs = setEncoding(mode, encoding)
|
||||
with open(os.path.expanduser(filename), mode, newline=newline, **kwargs) as f:
|
||||
return f.read()
|
||||
return str(sys.stdin.read())
|
||||
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
||||
if continueOnError:
|
||||
if displayError:
|
||||
stderrWarningMsg(fileErrorMessage(filename, e))
|
||||
setSysExitRC(FILE_ERROR_RC)
|
||||
return None
|
||||
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
||||
|
||||
# Write a file
|
||||
def writeFile(filename, data, mode=DEFAULT_FILE_WRITE_MODE,
|
||||
continueOnError=False, displayError=True):
|
||||
try:
|
||||
if filename != '-':
|
||||
kwargs = setEncoding(mode, None)
|
||||
with open(os.path.expanduser(filename), mode, **kwargs) as f:
|
||||
f.write(data)
|
||||
return True
|
||||
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).write(data)
|
||||
return True
|
||||
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
||||
if continueOnError:
|
||||
if displayError:
|
||||
stderrErrorMsg(fileErrorMessage(filename, e))
|
||||
setSysExitRC(FILE_ERROR_RC)
|
||||
return False
|
||||
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
||||
|
||||
# Write a file, return error
|
||||
def writeFileReturnError(filename, data, mode=DEFAULT_FILE_WRITE_MODE):
|
||||
try:
|
||||
kwargs = {'encoding': GM.Globals[GM.SYS_ENCODING]} if 'b' not in mode else {}
|
||||
with open(os.path.expanduser(filename), mode, **kwargs) as f:
|
||||
f.write(data)
|
||||
return (True, None)
|
||||
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
||||
return (False, e)
|
||||
|
||||
# Delete a file
|
||||
def deleteFile(filename, continueOnError=False, displayError=True):
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
os.remove(filename)
|
||||
except OSError as e:
|
||||
if continueOnError:
|
||||
if displayError:
|
||||
stderrWarningMsg(fileErrorMessage(filename, e))
|
||||
return
|
||||
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
||||
|
||||
def getGDocSheetDataRetryWarning(entityValueList, errMsg, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
action = Act.Get()
|
||||
Act.Set(Act.RETRIEVE_DATA)
|
||||
stderrWarningMsg(formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg, 'Retry', ''],
|
||||
currentCountNL(i, count)))
|
||||
Act.Set(action)
|
||||
|
||||
def getGDocSheetDataFailedExit(entityValueList, errMsg, i=0, count=0):
|
||||
Ent = _getEnt()
|
||||
Act.Set(Act.RETRIEVE_DATA)
|
||||
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
|
||||
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg],
|
||||
currentCountNL(i, count)))
|
||||
|
||||
def incrAPICallsRetryData(errMsg, delta):
|
||||
GM.Globals[GM.API_CALLS_RETRY_DATA].setdefault(errMsg, [0, 0.0])
|
||||
GM.Globals[GM.API_CALLS_RETRY_DATA][errMsg][0] += 1
|
||||
GM.Globals[GM.API_CALLS_RETRY_DATA][errMsg][1] += delta
|
||||
|
||||
def initAPICallsRateCheck():
|
||||
GM.Globals[GM.RATE_CHECK_COUNT] = 0
|
||||
GM.Globals[GM.RATE_CHECK_START] = time.time()
|
||||
|
||||
def checkAPICallsRate():
|
||||
GM.Globals[GM.RATE_CHECK_COUNT] += 1
|
||||
if GM.Globals[GM.RATE_CHECK_COUNT] >= GC.Values[GC.API_CALLS_RATE_LIMIT]:
|
||||
current = time.time()
|
||||
delta = int(current-GM.Globals[GM.RATE_CHECK_START])
|
||||
if 0 <= delta < 60:
|
||||
delta = (60-delta)+3
|
||||
error_message = f'API calls per 60 seconds limit {GC.Values[GC.API_CALLS_RATE_LIMIT]} exceeded'
|
||||
writeStderr(f'{WARNING_PREFIX}{error_message}: Backing off: {delta} seconds\n')
|
||||
flushStderr()
|
||||
time.sleep(delta)
|
||||
if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]:
|
||||
incrAPICallsRetryData(error_message, delta)
|
||||
GM.Globals[GM.RATE_CHECK_START] = time.time()
|
||||
else:
|
||||
GM.Globals[GM.RATE_CHECK_START] = current
|
||||
GM.Globals[GM.RATE_CHECK_COUNT] = 0
|
||||
|
||||
def openGAMCommandLog(Globals, name):
|
||||
try:
|
||||
Globals[GM.CMDLOG_LOGGER] = logging.getLogger(name)
|
||||
Globals[GM.CMDLOG_LOGGER].setLevel(logging.INFO)
|
||||
Globals[GM.CMDLOG_HANDLER] = RotatingFileHandler(GC.Values[GC.CMDLOG],
|
||||
maxBytes=1024*GC.Values[GC.CMDLOG_MAX_KILO_BYTES],
|
||||
backupCount=GC.Values[GC.CMDLOG_MAX_BACKUPS],
|
||||
encoding=GC.Values[GC.CHARSET])
|
||||
Globals[GM.CMDLOG_LOGGER].addHandler(Globals[GM.CMDLOG_HANDLER])
|
||||
except Exception as e:
|
||||
Globals[GM.CMDLOG_LOGGER] = None
|
||||
systemErrorExit(CONFIG_ERROR_RC, Msg.LOGGING_INITIALIZATION_ERROR.format(str(e)))
|
||||
|
||||
def writeGAMCommandLog(Globals, logCmd, sysRC):
|
||||
import sys as _sys # noqa: PLC0415
|
||||
gam = _sys.modules['gam']
|
||||
Globals[GM.CMDLOG_LOGGER].info(f'{gam.currentISOformatTimeStamp()},{sysRC},{logCmd}')
|
||||
|
||||
def closeGAMCommandLog(Globals):
|
||||
try:
|
||||
Globals[GM.CMDLOG_HANDLER].flush()
|
||||
Globals[GM.CMDLOG_HANDLER].close()
|
||||
Globals[GM.CMDLOG_LOGGER].removeHandler(Globals[GM.CMDLOG_HANDLER])
|
||||
except Exception:
|
||||
pass
|
||||
Globals[GM.CMDLOG_LOGGER] = None
|
||||
275
src/gam/util/gdoc.py
Normal file
275
src/gam/util/gdoc.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""GAM GDoc/GSheet/Cloud Storage data retrieval utilities.
|
||||
|
||||
Extracted from gam/__init__.py. Provides functions for reading data from
|
||||
Google Docs, Google Sheets, Cloud Storage buckets, and CSV files.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from tempfile import TemporaryFile
|
||||
from urllib.parse import unquote
|
||||
|
||||
import googleapiclient.http
|
||||
import httplib2
|
||||
import google.auth.exceptions
|
||||
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glentity as Ent
|
||||
from gamlib import glgapi as GAPI
|
||||
from gamlib import glglobals as GM
|
||||
from gamlib import glmsgs as Msg
|
||||
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
|
||||
GDOC_FORMAT_MIME_TYPES = {
|
||||
'gcsv': 'text/csv',
|
||||
'gdoc': 'text/plain',
|
||||
'ghtml': 'text/html',
|
||||
}
|
||||
|
||||
# gdoc <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity>
|
||||
def getGDocData(gformat):
|
||||
mimeType = GDOC_FORMAT_MIME_TYPES[gformat]
|
||||
user = _getMain().getEmailAddress()
|
||||
fileIdEntity = _getMain().getDriveFileEntity(queryShortcutsOK=False)
|
||||
if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]:
|
||||
_, drive = _getMain().buildGAPIServiceObject(API.DRIVE3, user)
|
||||
else:
|
||||
drive = _getMain().buildGAPIObject(API.DRIVE3)
|
||||
if not drive:
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
_, _, jcount = _getMain()._validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive)
|
||||
if jcount == 0:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
|
||||
if jcount > 1:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
|
||||
fileId = fileIdEntity['list'][0]
|
||||
f = None
|
||||
try:
|
||||
result = _getMain().callGAPI(drive.files(), 'get',
|
||||
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
||||
fileId=fileId, fields='name,mimeType,exportLinks',
|
||||
supportsAllDrives=True)
|
||||
# Google Doc
|
||||
if 'exportLinks' in result:
|
||||
if mimeType not in result['exportLinks']:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
||||
Msg.INVALID_MIMETYPE.format(result['mimeType'], mimeType))
|
||||
f = TemporaryFile(mode='w+', encoding=_getMain().UTF8)
|
||||
_, content = drive._http.request(uri=result['exportLinks'][mimeType], method='GET')
|
||||
f.write(content.decode(_getMain().UTF8_SIG))
|
||||
f.seek(0)
|
||||
return f
|
||||
# Drive File
|
||||
if result['mimeType'] != mimeType:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
||||
Msg.INVALID_MIMETYPE.format(result['mimeType'], mimeType))
|
||||
fb = TemporaryFile(mode='wb+')
|
||||
request = drive.files().get_media(fileId=fileId)
|
||||
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
|
||||
done = False
|
||||
while not done:
|
||||
_, done = downloader.next_chunk()
|
||||
f = TemporaryFile(mode='w+', encoding=_getMain().UTF8)
|
||||
fb.seek(0)
|
||||
f.write(fb.read().decode(_getMain().UTF8_SIG))
|
||||
fb.close()
|
||||
f.seek(0)
|
||||
return f
|
||||
except GAPI.fileNotFound:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.DOCUMENT, fileId], Msg.DOES_NOT_EXIST)
|
||||
except (IOError, httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
||||
if f:
|
||||
f.close()
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.DOCUMENT, fileId], str(e))
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
||||
_getMain().userDriveServiceNotEnabledWarning(user, str(e))
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
|
||||
HTML_TITLE_PATTERN = re.compile(r'.*<title>(.+)</title>')
|
||||
|
||||
# gsheet <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity> <SheetEntity>
|
||||
def getGSheetData():
|
||||
user = _getMain().getEmailAddress()
|
||||
fileIdEntity = _getMain().getDriveFileEntity(queryShortcutsOK=False)
|
||||
sheetEntity = _getMain().getSheetEntity(False)
|
||||
if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]:
|
||||
user, drive = _getMain().buildGAPIServiceObject(API.DRIVE3, user)
|
||||
else:
|
||||
drive = _getMain().buildGAPIObject(API.DRIVE3)
|
||||
if not drive:
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
_, _, jcount = _getMain()._validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive)
|
||||
if jcount == 0:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
|
||||
if jcount > 1:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
|
||||
if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]:
|
||||
_, sheet = _getMain().buildGAPIServiceObject(API.SHEETS, user)
|
||||
else:
|
||||
sheet = _getMain().buildGAPIObject(API.SHEETS)
|
||||
if not sheet:
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
fileId = fileIdEntity['list'][0]
|
||||
f = None
|
||||
try:
|
||||
result = _getMain().callGAPI(drive.files(), 'get',
|
||||
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
||||
fileId=fileId, fields='name,mimeType', supportsAllDrives=True)
|
||||
if result['mimeType'] != _getMain().MIMETYPE_GA_SPREADSHEET:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
||||
Msg.INVALID_MIMETYPE.format(result['mimeType'], _getMain().MIMETYPE_GA_SPREADSHEET))
|
||||
spreadsheet = _getMain().callGAPI(sheet.spreadsheets(), 'get',
|
||||
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
||||
spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))')
|
||||
sheetId = _getMain().getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
|
||||
if sheetId is None:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], Msg.NOT_FOUND)
|
||||
spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?format=csv&id={fileId}&gid={sheetId}'
|
||||
f = TemporaryFile(mode='w+', encoding=_getMain().UTF8)
|
||||
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
||||
sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n')
|
||||
triesLimit = 3
|
||||
for n in range(1, triesLimit+1):
|
||||
_, content = drive._http.request(uri=spreadsheetUrl, method='GET')
|
||||
# Check for HTML error message instead of data
|
||||
if content[0:15] != b'<!DOCTYPE html>':
|
||||
break
|
||||
tg = HTML_TITLE_PATTERN.match(content[0:600].decode('utf-8'))
|
||||
errMsg = tg.group(1) if tg else 'Unknown error'
|
||||
_getMain().getGDocSheetDataRetryWarning([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg, n, triesLimit)
|
||||
time.sleep(20)
|
||||
else:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg)
|
||||
f.write(content.decode(_getMain().UTF8_SIG))
|
||||
f.seek(0)
|
||||
return f
|
||||
except GAPI.fileNotFound:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId], Msg.DOES_NOT_EXIST)
|
||||
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
||||
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
||||
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId, sheetEntity['sheetType'], sheetEntity['sheetValue']], str(e))
|
||||
except (IOError, httplib2.HttpLib2Error) as e:
|
||||
if f:
|
||||
f.close()
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId, sheetEntity['sheetType'], sheetEntity['sheetValue']], str(e))
|
||||
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
||||
_getMain().userDriveServiceNotEnabledWarning(user, str(e))
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
|
||||
|
||||
BUCKET_OBJECT_PATTERNS = [
|
||||
{'pattern': re.compile(r'https://storage.(?:googleapis|cloud.google).com/(.+?)/(.+)'), 'unquote': True},
|
||||
{'pattern': re.compile(r'gs://(.+?)/(.+)'), 'unquote': False},
|
||||
{'pattern': re.compile(r'(.+?)/(.+)'), 'unquote': False},
|
||||
]
|
||||
|
||||
def getBucketObjectName():
|
||||
Cmd = _getMain().Cmd
|
||||
uri = _getMain().getString(Cmd.OB_STRING)
|
||||
for pattern in BUCKET_OBJECT_PATTERNS:
|
||||
mg = re.search(pattern['pattern'], uri)
|
||||
if mg:
|
||||
bucket = mg.group(1)
|
||||
s_object = mg.group(2) if not pattern['unquote'] else unquote(mg.group(2))
|
||||
return (bucket, s_object, f'{bucket}/{s_object}')
|
||||
_getMain().systemErrorExit(_getMain().ACTION_NOT_PERFORMED_RC, f'Invalid <StorageBucketObjectName>: {uri}')
|
||||
|
||||
GCS_FORMAT_MIME_TYPES = {
|
||||
'gcscsv': 'text/csv',
|
||||
'gcsdoc': 'text/plain',
|
||||
'gcshtml': 'text/html',
|
||||
}
|
||||
|
||||
# gcscsv|gcshtml|gcsdoc <StorageBucketObjectName>
|
||||
def getStorageFileData(gcsformat, returnData=True):
|
||||
mimeType = GCS_FORMAT_MIME_TYPES[gcsformat]
|
||||
bucket, s_object, bucketObject = getBucketObjectName()
|
||||
s = _getMain().buildGAPIObject(API.STORAGEREAD)
|
||||
try:
|
||||
result = _getMain().callGAPI(s.objects(), 'get',
|
||||
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
||||
bucket=bucket, object=s_object, projection='noAcl', fields='contentType')
|
||||
except GAPI.notFound:
|
||||
_getMain().entityDoesNotExistExit(Ent.CLOUD_STORAGE_FILE, bucketObject)
|
||||
except GAPI.forbidden as e:
|
||||
_getMain().entityActionFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject], str(e))
|
||||
if result['contentType'] != mimeType:
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject],
|
||||
Msg.INVALID_MIMETYPE.format(result['contentType'], mimeType))
|
||||
fb = TemporaryFile(mode='wb+')
|
||||
try:
|
||||
request = s.objects().get_media(bucket=bucket, object=s_object)
|
||||
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
|
||||
done = False
|
||||
while not done:
|
||||
_, done = downloader.next_chunk()
|
||||
fb.seek(0)
|
||||
if returnData:
|
||||
data = fb.read().decode(_getMain().UTF8)
|
||||
fb.close()
|
||||
return data
|
||||
f = TemporaryFile(mode='w+', encoding=_getMain().UTF8)
|
||||
f.write(fb.read().decode(_getMain().UTF8_SIG))
|
||||
fb.close()
|
||||
f.seek(0)
|
||||
return f
|
||||
except googleapiclient.http.HttpError as e:
|
||||
mg = _getMain().HTTP_ERROR_PATTERN.match(str(e))
|
||||
_getMain().getGDocSheetDataFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject], mg.group(1) if mg else str(e))
|
||||
|
||||
# <CSVFileInput>
|
||||
def openCSVFileReader(filename, fieldnames=None):
|
||||
Cmd = _getMain().Cmd
|
||||
filenameLower = filename.lower()
|
||||
if filenameLower == 'gsheet':
|
||||
f = getGSheetData()
|
||||
_getMain().getCharSet()
|
||||
elif filenameLower in {'gcsv', 'gdoc'}:
|
||||
f = getGDocData(filenameLower)
|
||||
_getMain().getCharSet()
|
||||
elif filenameLower in {'gcscsv', 'gcsdoc'}:
|
||||
f = getStorageFileData(filenameLower, False)
|
||||
_getMain().getCharSet()
|
||||
else:
|
||||
encoding = _getMain().getCharSet()
|
||||
filename = _getMain().setFilePath(filename, GC.INPUT_DIR)
|
||||
f = _getMain().openFile(filename, mode=_getMain().DEFAULT_CSV_READ_MODE, encoding=encoding)
|
||||
if _getMain().checkArgumentPresent('warnifnodata'):
|
||||
loc = f.tell()
|
||||
try:
|
||||
if not f.readline() or not f.readline():
|
||||
_getMain().stderrWarningMsg(_getMain().fileErrorMessage(filename, Msg.NO_CSV_FILE_DATA_FOUND))
|
||||
sys.exit(_getMain().NO_ENTITIES_FOUND_RC)
|
||||
f.seek(loc)
|
||||
except (IOError, UnicodeDecodeError, UnicodeError) as e:
|
||||
_getMain().systemErrorExit(_getMain().FILE_ERROR_RC, _getMain().fileErrorMessage(filename, e))
|
||||
if _getMain().checkArgumentPresent('columndelimiter'):
|
||||
columnDelimiter = _getMain().getCharacter()
|
||||
else:
|
||||
columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
|
||||
if _getMain().checkArgumentPresent('noescapechar'):
|
||||
noEscapeChar = _getMain().getBoolean()
|
||||
else:
|
||||
noEscapeChar = GC.Values[GC.CSV_INPUT_NO_ESCAPE_CHAR]
|
||||
if _getMain().checkArgumentPresent('quotechar'):
|
||||
quotechar = _getMain().getCharacter()
|
||||
else:
|
||||
quotechar = GC.Values[GC.CSV_INPUT_QUOTE_CHAR]
|
||||
if not _getMain().checkArgumentPresent('endcsv') and _getMain().checkArgumentPresent('fields'):
|
||||
fieldnames = _getMain().shlexSplitList(_getMain().getString(Cmd.OB_FIELD_NAME_LIST))
|
||||
try:
|
||||
csvFile = csv.DictReader(f, fieldnames=fieldnames,
|
||||
delimiter=columnDelimiter,
|
||||
escapechar='\\' if not noEscapeChar else None,
|
||||
quotechar=quotechar)
|
||||
return (f, csvFile, csvFile.fieldnames if csvFile.fieldnames is not None else [])
|
||||
except (csv.Error, UnicodeDecodeError, UnicodeError) as e:
|
||||
_getMain().systemErrorExit(_getMain().FILE_ERROR_RC, e)
|
||||
56
src/gam/util/html.py
Normal file
56
src/gam/util/html.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""GAM HTML utilities.
|
||||
|
||||
Extracted from gam/__init__.py. Provides HTML-to-plain-text conversion.
|
||||
"""
|
||||
|
||||
import re
|
||||
from html.entities import name2codepoint
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
class _DeHTMLParser(HTMLParser): #pylint: disable=abstract-method
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
self.__text = []
|
||||
|
||||
def handle_data(self, data):
|
||||
self.__text.append(data)
|
||||
|
||||
def handle_charref(self, name):
|
||||
self.__text.append(chr(int(name[1:], 16)) if name.startswith('x') else chr(int(name)))
|
||||
|
||||
def handle_entityref(self, name):
|
||||
cp = name2codepoint.get(name)
|
||||
if cp:
|
||||
self.__text.append(chr(cp))
|
||||
else:
|
||||
self.__text.append('&'+name)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'p':
|
||||
self.__text.append('\n\n')
|
||||
elif tag == 'br':
|
||||
self.__text.append('\n')
|
||||
elif tag == 'a':
|
||||
for attr in attrs:
|
||||
if attr[0] == 'href':
|
||||
self.__text.append(f'({attr[1]}) ')
|
||||
break
|
||||
elif tag == 'div':
|
||||
if not attrs:
|
||||
self.__text.append('\n')
|
||||
elif tag in {'http:', 'https'}:
|
||||
self.__text.append(f' ({tag}//{attrs[0][0]}) ')
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
if tag == 'br':
|
||||
self.__text.append('\n\n')
|
||||
|
||||
def text(self):
|
||||
return re.sub(r'\n{2}\n+', '\n\n', re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
|
||||
|
||||
def dehtml(text):
|
||||
parser = _DeHTMLParser()
|
||||
parser.feed(str(text))
|
||||
parser.close()
|
||||
return parser.text()
|
||||
133
src/gam/util/orgunits.py
Normal file
133
src/gam/util/orgunits.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""GAM OrgUnit helper utilities.
|
||||
|
||||
Extracted from gam/__init__.py. Provides OrgUnit path/ID resolution
|
||||
and parent OrgUnit traversal.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glgapi as GAPI
|
||||
from gamlib import glglobals as GM
|
||||
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
|
||||
def getOrgUnitItem(pathOnly=False, absolutePath=True, cd=None):
|
||||
Cmd = _getMain().Cmd
|
||||
if Cmd.ArgumentsRemaining():
|
||||
path = Cmd.Current().strip()
|
||||
if path == 'root':
|
||||
path = '/'
|
||||
if path:
|
||||
if pathOnly and (path.startswith('id:') or path.startswith('uid:')) and cd is not None:
|
||||
try:
|
||||
result = _getMain().callGAPI(cd.orgunits(), 'get',
|
||||
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
||||
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=path,
|
||||
fields='orgUnitPath')
|
||||
Cmd.Advance()
|
||||
if absolutePath:
|
||||
return _getMain().makeOrgUnitPathAbsolute(result['orgUnitPath'])
|
||||
return _getMain().makeOrgUnitPathRelative(result['orgUnitPath'])
|
||||
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
|
||||
GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
_getMain().checkEntityAFDNEorAccessErrorExit(cd, _getMain().Ent.ORGANIZATIONAL_UNIT, path)
|
||||
_getMain().invalidArgumentExit(Cmd.OB_ORGUNIT_PATH)
|
||||
Cmd.Advance()
|
||||
if absolutePath:
|
||||
return _getMain().makeOrgUnitPathAbsolute(path)
|
||||
return _getMain().makeOrgUnitPathRelative(path)
|
||||
_getMain().missingArgumentExit([Cmd.OB_ORGUNIT_ITEM, Cmd.OB_ORGUNIT_PATH][pathOnly])
|
||||
|
||||
def getTopLevelOrgId(cd, parentOrgUnitPath):
|
||||
Ent = _getMain().Ent
|
||||
if parentOrgUnitPath != '/':
|
||||
try:
|
||||
result = _getMain().callGAPI(cd.orgunits(), 'get',
|
||||
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
||||
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=_getMain().encodeOrgUnitPath(_getMain().makeOrgUnitPathRelative(parentOrgUnitPath)),
|
||||
fields='orgUnitId')
|
||||
return result['orgUnitId']
|
||||
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
||||
return None
|
||||
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
_getMain().checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, parentOrgUnitPath)
|
||||
return None
|
||||
try:
|
||||
result = _getMain().callGAPI(cd.orgunits(), 'list',
|
||||
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
||||
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath='/', type='allIncludingParent',
|
||||
fields='organizationUnits(orgUnitId,orgUnitPath)')
|
||||
for orgUnit in result.get('organizationUnits', []):
|
||||
if orgUnit['orgUnitPath'] == '/':
|
||||
return orgUnit['orgUnitId']
|
||||
return None
|
||||
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
||||
return None
|
||||
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
_getMain().checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, parentOrgUnitPath)
|
||||
return None
|
||||
|
||||
def getOrgUnitId(cd=None, orgUnit=None):
|
||||
Ent = _getMain().Ent
|
||||
if cd is None:
|
||||
cd = _getMain().buildGAPIObject(API.DIRECTORY)
|
||||
if orgUnit is None:
|
||||
orgUnit = getOrgUnitItem()
|
||||
try:
|
||||
if orgUnit == '/':
|
||||
result = _getMain().callGAPI(cd.orgunits(), 'list',
|
||||
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
||||
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath='/', type='children',
|
||||
fields='organizationUnits(parentOrgUnitId,parentOrgUnitPath)')
|
||||
if result.get('organizationUnits', []):
|
||||
return (result['organizationUnits'][0]['parentOrgUnitPath'], result['organizationUnits'][0]['parentOrgUnitId'])
|
||||
topLevelOrgId = getTopLevelOrgId(cd, '/')
|
||||
if topLevelOrgId:
|
||||
return (orgUnit, topLevelOrgId)
|
||||
return (orgUnit, '/') #Bogus but should never happen
|
||||
result = _getMain().callGAPI(cd.orgunits(), 'get',
|
||||
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
||||
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=_getMain().encodeOrgUnitPath(_getMain().makeOrgUnitPathRelative(orgUnit)),
|
||||
fields='orgUnitId,orgUnitPath')
|
||||
return (result['orgUnitPath'], result['orgUnitId'])
|
||||
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
||||
_getMain().entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, orgUnit)
|
||||
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
_getMain().accessErrorExit(cd)
|
||||
|
||||
def getAllParentOrgUnitsForUser(cd, user):
|
||||
Ent = _getMain().Ent
|
||||
try:
|
||||
result = _getMain().callGAPI(cd.users(), 'get',
|
||||
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
||||
userKey=user, fields='orgUnitPath', projection='basic')
|
||||
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden):
|
||||
_getMain().entityDoesNotExistExit(Ent.USER, user)
|
||||
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
_getMain().accessErrorExit(cd)
|
||||
parentPath = result['orgUnitPath']
|
||||
if parentPath == '/':
|
||||
orgUnitPath, orgUnitId = getOrgUnitId(cd, '/')
|
||||
return {orgUnitId: orgUnitPath}
|
||||
parentPath = _getMain().encodeOrgUnitPath(_getMain().makeOrgUnitPathRelative(parentPath))
|
||||
orgUnits = {}
|
||||
while True:
|
||||
try:
|
||||
result = _getMain().callGAPI(cd.orgunits(), 'get',
|
||||
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
||||
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=parentPath,
|
||||
fields='orgUnitId,orgUnitPath,parentOrgUnitId')
|
||||
orgUnits[result['orgUnitId']] = result['orgUnitPath']
|
||||
if 'parentOrgUnitId' not in result:
|
||||
break
|
||||
parentPath = result['parentOrgUnitId']
|
||||
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
||||
_getMain().entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, parentPath)
|
||||
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
||||
_getMain().accessErrorExit(cd)
|
||||
return orgUnits
|
||||
184
src/gam/util/output.py
Normal file
184
src/gam/util/output.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""GAM output helpers — write/flush/print/format functions.
|
||||
|
||||
These are pure output functions that write to stdout/stderr via GM.Globals.
|
||||
No circular dependency risk — they only depend on gamlib modules and
|
||||
simple string constants.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glglobals as GM
|
||||
|
||||
|
||||
class _InstanceProxy:
|
||||
"""Lazy proxy that delegates attribute access to a named instance in the gam module."""
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
def __getattr__(self, attr):
|
||||
return getattr(getattr(sys.modules['gam'], self._name), attr)
|
||||
|
||||
Ind = _InstanceProxy('Ind')
|
||||
|
||||
|
||||
# These constants are duplicated from __init__.py to avoid circular imports.
|
||||
# They are simple string literals that never change.
|
||||
ERROR = 'ERROR'
|
||||
ERROR_PREFIX = ERROR + ': '
|
||||
WARNING = 'WARNING'
|
||||
WARNING_PREFIX = WARNING + ': '
|
||||
STDOUT_STDERR_ERROR_RC = 66
|
||||
|
||||
|
||||
# stdin/stdout/stderr
|
||||
def readStdin(prompt):
|
||||
return input(prompt)
|
||||
|
||||
def stdErrorExit(e):
|
||||
try:
|
||||
sys.stderr.write(f'\n{ERROR_PREFIX}{str(e)}\n')
|
||||
except IOError:
|
||||
pass
|
||||
sys.exit(STDOUT_STDERR_ERROR_RC)
|
||||
|
||||
def writeStdout(data):
|
||||
try:
|
||||
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).write(data)
|
||||
except IOError as e:
|
||||
stdErrorExit(e)
|
||||
|
||||
def flushStdout():
|
||||
try:
|
||||
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).flush()
|
||||
except IOError as e:
|
||||
stdErrorExit(e)
|
||||
|
||||
def writeStderr(data):
|
||||
flushStdout()
|
||||
try:
|
||||
GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr).write(data)
|
||||
except IOError as e:
|
||||
stdErrorExit(e)
|
||||
|
||||
def flushStderr():
|
||||
try:
|
||||
GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr).flush()
|
||||
except IOError as e:
|
||||
stdErrorExit(e)
|
||||
|
||||
# Error messages
|
||||
def setSysExitRC(sysRC):
|
||||
GM.Globals[GM.SYSEXITRC] = sysRC
|
||||
|
||||
def stderrErrorMsg(message):
|
||||
writeStderr(f'\n{ERROR_PREFIX}{message}\n')
|
||||
|
||||
def stderrWarningMsg(message):
|
||||
writeStderr(f'\n{WARNING_PREFIX}{message}\n')
|
||||
|
||||
def systemErrorExit(sysRC, message):
|
||||
if message:
|
||||
stderrErrorMsg(message)
|
||||
sys.exit(sysRC)
|
||||
|
||||
def printErrorMessage(sysRC, message):
|
||||
setSysExitRC(sysRC)
|
||||
writeStderr(f'\n{Ind.Spaces()}{ERROR_PREFIX}{message}\n')
|
||||
|
||||
def printWarningMessage(sysRC, message):
|
||||
setSysExitRC(sysRC)
|
||||
writeStderr(f'\n{Ind.Spaces()}{WARNING_PREFIX}{message}\n')
|
||||
|
||||
def supportsColoredText():
|
||||
"""Determines if the current terminal environment supports colored text.
|
||||
|
||||
Returns:
|
||||
Bool, True if the current terminal environment supports colored text via
|
||||
ANSI escape characters.
|
||||
"""
|
||||
# Make a rudimentary check for Windows. Though Windows does seem to support
|
||||
# colorization with VT100 emulation, it is disabled by default. Therefore,
|
||||
# we'll simply disable it in GAM on Windows for now.
|
||||
return not sys.platform.startswith('win')
|
||||
|
||||
def createColoredText(text, color):
|
||||
"""Uses ANSI escape characters to create colored text in supported terminals.
|
||||
|
||||
See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
|
||||
Args:
|
||||
text: String, The text to colorize using ANSI escape characters.
|
||||
color: String, An ANSI escape sequence denoting the color of the text to be
|
||||
created. See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
|
||||
Returns:
|
||||
The input text with appropriate ANSI escape characters to create
|
||||
colorization in a supported terminal environment.
|
||||
"""
|
||||
END_COLOR_SEQUENCE = '\033[0m' # Ends the applied color formatting
|
||||
if supportsColoredText():
|
||||
return color + text + END_COLOR_SEQUENCE
|
||||
return text # Hand back the plain text, uncolorized.
|
||||
|
||||
def createRedText(text):
|
||||
"""Uses ANSI encoding to create red colored text if supported."""
|
||||
return createColoredText(text, '\033[91m')
|
||||
|
||||
def createGreenText(text):
|
||||
"""Uses ANSI encoding to create green colored text if supported."""
|
||||
return createColoredText(text, '\u001b[32m')
|
||||
|
||||
def createYellowText(text):
|
||||
"""Uses ANSI encoding to create yellow text if supported."""
|
||||
return createColoredText(text, '\u001b[33m')
|
||||
|
||||
def executeBatch(dbatch):
|
||||
dbatch.execute()
|
||||
if GC.Values[GC.INTER_BATCH_WAIT] > 0:
|
||||
time.sleep(GC.Values[GC.INTER_BATCH_WAIT])
|
||||
|
||||
def _stripControlCharsFromName(name):
|
||||
for cc in ['\x00', '\r', '\n']:
|
||||
name = name.replace(cc, '')
|
||||
return name
|
||||
|
||||
def currentCount(i, count):
|
||||
return f' ({i}/{count})' if (count > GC.Values[GC.SHOW_COUNTS_MIN]) else ''
|
||||
|
||||
def currentCountNL(i, count):
|
||||
return f' ({i}/{count})\n' if (count > GC.Values[GC.SHOW_COUNTS_MIN]) else '\n'
|
||||
|
||||
# Format a key value list
|
||||
# key, value -> "key: value" + ", " if not last item
|
||||
# key, '' -> "key:" + ", " if not last item
|
||||
# key, None -> "key" + " " if not last item
|
||||
def formatKeyValueList(prefixStr, kvList, suffixStr):
|
||||
msg = prefixStr
|
||||
i = 0
|
||||
l = len(kvList)
|
||||
while i < l:
|
||||
if isinstance(kvList[i], (bool, float, int)):
|
||||
msg += str(kvList[i])
|
||||
else:
|
||||
msg += kvList[i]
|
||||
i += 1
|
||||
if i < l:
|
||||
val = kvList[i]
|
||||
if (val is not None) or (i == l-1):
|
||||
msg += ':'
|
||||
if (val is not None) and (not isinstance(val, str) or val):
|
||||
msg += ' '
|
||||
if isinstance(val, (bool, float, int)):
|
||||
msg += str(val)
|
||||
else:
|
||||
msg += val
|
||||
i += 1
|
||||
if i < l:
|
||||
msg += ', '
|
||||
else:
|
||||
i += 1
|
||||
if i < l:
|
||||
msg += ' '
|
||||
msg += suffixStr
|
||||
return msg
|
||||
Reference in New Issue
Block a user