chat message searching, user attributes, license cleanup
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Daily Dependency Pinning (2-Week Buffer) / pin-deps (push) Has been cancelled

This commit is contained in:
Ross Scroggs
2026-05-27 11:15:31 -07:00
parent 9d8442c7ad
commit 4ba9385c23
6 changed files with 291 additions and 16 deletions

View File

@@ -270,7 +270,7 @@ If an item contains spaces, it should be surrounded by ".
assuredcontrolsplus | 1010390002 | Assured Controls Plus |
bce | beyondcorp | beyondcorpenterprise | cep | chromeenterprisepremium | 1010400001 | Chrome Enterprise Premium |
cdm | chrome | googlechromedevicemanagement | Google-Chrome-Device-Management |
cloudidentity | identity | 1010010001 | Cloud Identity |
cloudidentityfree | cloudidentity | identity | 1010010001 | Cloud Identity Free |
cloudidentitypremium | identitypremium | 1010050001 | Cloud Identity Premium |
cloudsearch | 1010350001 | Cloud Search |
colabpro | 1010500001 | Colab Pro |
@@ -6768,13 +6768,41 @@ gam <UserTypeEntity> info chatmessage name <ChatMessage>
[formatjson]
gam <UserTypeEntity> show chatmessages
<ChatSpace>+
[showdeleted [<Boolean>]] [filter <String>]
[showdeleted [<Boolean>]
[([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
[fields <ChatMessageFieldNameList>]
[orderby createtime [ascending|descending]]
[formatjson]
gam <UserTypeEntity> print chatmessages [todrive <ToDriveAttribute>*]
<ChatSpace>+
[showdeleted [<Boolean>]] [filter <String>]
[showdeleted [<Boolean>]
[([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
[thread <ChatThread>])
[fields <ChatMessageFieldNameList>]
[orderby createtime [ascending|descending]]
[formatjson [quotechar <Character>]]
gam <UserTypeEntity> show chatsearchmessages
keywords <StringList>
<ChatSpace>*
[displaynames [all|any] <StringList>]
[senders <EmailAddressEntity>]*
[usermentions [all|any] <EmailAddressEntity>]*
[([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
[hasattachment]
[fields <ChatMessageFieldNameList>]
[orderby createtime|relevance]
[formatjson]
gam <UserTypeEntity> print chatsearchmessages [todrive <ToDriveAttribute>*]
keywords <StringList>
<ChatSpace>*
[displaynames [all|any] <StringList>]
[senders <EmailAddressEntity>]*
[usermentions [all|any] <EmailAddressEntity>]*
[([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
[hasattachment]
[fields <ChatMessageFieldNameList>]
[orderby createtime|relevance]
[formatjson [quotechar <Character>]]
gam <UserTypeEntity> info chatevent name <ChatEvent>

View File

@@ -1,3 +1,27 @@
7.44.00
Added support for User data `archivalTime` and `suspensionTime` that is available
when fields `archived` and `suspended` are requested in `gam info user` and `gam print users`.
Added the following options to `gam <UserTypeEntity> show chatmessages` to simplify specifying a filter.
```
start|starttime <Date>|<Time>
end|endtime <Date>|<Time>
range <Date>|<Time> <Date>|<Time>
thread <ChatThread>
```
Added commands to search for and display chat messages.
* See: https://github.com/GAM-team/GAM/wiki/Users-Chat#display-chat-messages-by-searching
These commands are in Developer Preview; to use them you must have these values set in `gam.cfg`.
```
developer_preview_apis = chat
developer_preview_api_key = <DeveloperPreviewKey>
```
Upgraded to Python 3.14.5.
7.43.10
Updated `gam <UserTypeEntity> forward message|thread [recipient|to] <RecipientEntity>` to not forward messages

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
"""
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.43.10'
__version__ = '7.44.00'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
# pylint: disable=wrong-import-position
@@ -1002,6 +1002,13 @@ def getChoiceAndValue(item, choices, delimiter):
missingArgumentExit(item)
invalidChoiceExit(choice, choices, False)
AND_OR_CONJUNCTION_MAP = {
'and': 'AND',
'or': 'OR',
'all': 'AND',
'any': 'OR',
}
SUSPENDED_ARGUMENTS = {'notsuspended', 'suspended', 'issuspended'}
SUSPENDED_CHOICE_MAP = {'notsuspended': False, 'suspended': True}
def _getIsSuspended(myarg):
@@ -29161,24 +29168,37 @@ def infoChatMessage(users):
def doInfoChatMessage():
infoChatMessage([None])
CHAT_MESSAGES_ORDERBY_CHOICE_MAP = {
'createtime': 'createTime'
}
# gam <UserTypeEntity> show chatmessages
# <ChatSpace>+
# [showdeleted [<Boolean>]] [filter <String>]
# [showdeleted [<Boolean>]]
# [([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
# [thread <ChatThread>])
# [fields <ChatMessageFieldNameList>]
# [orderby createtime [ascending|descending]]
# [formatjson]
# gam <UserTypeEntity> print chatmessages [todrive <ToDriveAttribute>*]
# <ChatSpace>+
# [showdeleted [<Boolean>]] [filter <String>]
# [([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
# [thread <ChatThread>])
# [fields <ChatMessageFieldNameList>]
# [orderby createtime [ascending|descending]]
# [formatjson [quotechar <Character>]]
def printShowChatMessages(users):
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
OBY = OrderBy(CHAT_MESSAGES_ORDERBY_CHOICE_MAP, ascendingKeyword='ASC', descendingKeyword='DESC')
fieldsList = []
pfilter = None
parentList = []
showDeleted = False
startEndTime = StartEndTime()
threadName = ''
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
@@ -29191,10 +29211,35 @@ def printShowChatMessages(users):
showDeleted = getBoolean()
elif myarg =='filter':
pfilter = getString(Cmd.OB_STRING)
elif myarg in {'start', 'starttime', 'end', 'endtime', 'range'}:
startEndTime.Get(myarg)
elif myarg == 'thread':
threadName = getString(Cmd.OB_CHAT_THREAD)
elif myarg == 'orderby':
OBY.GetChoice()
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not parentList:
missingArgumentExit('space')
if startEndTime.startDateTime is not None or startEndTime.endDateTime is not None:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += '('
if startEndTime.startDateTime is not None:
pfilter += f'createTime > "{startEndTime.startDateTime}"'
if startEndTime.endDateTime is not None:
pfilter += ' AND '
if startEndTime.endDateTime is not None:
pfilter += f'createTime < "{startEndTime.endDateTime}"'
pfilter += ')'
if threadName:
if pfilter:
pfilter += ' AND '
else:
pfilter = ''
pfilter += f'thread.name = {threadName}'
chatSenders = {}
fields = getItemFieldsFromFieldsList('messages', fieldsList)
i, count, users = getEntityArgument(users)
@@ -29225,7 +29270,8 @@ def printShowChatMessages(users):
pageMessage=_getChatPageMessage(Ent.CHAT_MESSAGE, user, i, count, qfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
pageSize=GC.Values[GC.CHAT_MAX_RESULTS], parent=parentName, filter=pfilter, showDeleted=showDeleted,
pageSize=GC.Values[GC.CHAT_MAX_RESULTS], parent=parentName,
filter=pfilter, showDeleted=showDeleted, orderBy=OBY.orderBy,
fields=fields)
for message in messages:
if 'sender' in message:
@@ -29256,6 +29302,183 @@ def printShowChatMessages(users):
if csvPF:
csvPF.writeCSVfile('Chat Messages')
def _getChatSpaceDisplayName(chat, space, chatSpaces):
spaceName = space['name']
if spaceName not in chatSpaces:
try:
result = callGAPI(chat.spaces(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
name=spaceName, fields='displayName')
spaceDisplayName = result.get('displayName', 'None')
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError, GAPI.permissionDenied, GAPI.failedPrecondition):
spaceDisplayName = 'None'
chatSpaces[spaceName] = spaceDisplayName
space['displayName'] = chatSpaces[spaceName]
CHAT_SEARCHMESSAGES_ORDERBY_CHOICE_MAP = {
'createtime': 'createTime',
'relevance': 'relevance',
}
CHAT_SEARCHMESSAGES_VIEW_CHOICE_MAP = {'basic': 'SEARCH_MESSAGES_VIEW_BASIC', 'full': 'SEARCH_MESSAGES_VIEW_FULL'}
# gam <UserTypeEntity> show chatsearchmessages
# keywords <StringList>
# <ChatSpace>*
# [displaynames [all|any] <StringList>]
# [senders <EmailAddressEntity>]*
# [usermentions [all|any] <EmailAddressEntity>]*
# [([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
# [hasattachment [<Boolean>]]
# [fields <ChatMessageFieldNameList>]
# [orderby createtime|relevance]
# [basic|full]
# [formatjson]
# gam <UserTypeEntity> print chatsearchmessages [todrive <ToDriveAttribute>*]
# keywords <StringList>
# <ChatSpace>*
# [displaynames [all|any] <StringList>]
# [senders <EmailAddressEntity>]*
# [usermentions [all|any] <EmailAddressEntity>]*
# [([start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)]
# [hasattachment [<Boolean>]]
# [fields <ChatMessageFieldNameList>]
# [orderby createtime|relevance]
# [basic|full]
# [formatjson [quotechar <Character>]]
def printShowChatSearchMessages(users):
if API.CHAT not in GM.Globals[GM.DEVELOPER_PREVIEW_APIS]:
Cmd.Backup()
usageErrorExit(Msg.DEVELOPER_PREVIEW_REQUIRED)
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
orderBy = None
fieldsList = []
keywordList = []
spaceList = []
displayNameConjunction = ''
displayNameList = []
senderList = []
userMentionList = []
startEndTime = StartEndTime()
hasAttachment = False
body = {'view': CHAT_SEARCHMESSAGES_VIEW_CHOICE_MAP['basic'],
'pageSize': GC.Values[GC.CHAT_MAX_RESULTS], 'pageToken': None}
parent = 'spaces/-'
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg =='keywords':
keywordList = getString(Cmd.OB_STRING_LIST, minLen=0).replace(',', ' ').split()
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
spaceList.append(getSpaceName(myarg))
elif myarg == 'displaynames':
displayNameConjunction = getChoice(AND_OR_CONJUNCTION_MAP, mapChoice=True, defaultChoice='OR')
displayNameList = getString(Cmd.OB_STRING_LIST, minLen=0).replace(',', ' ').split()
elif myarg == 'senders':
senderList.extend(getNormalizedEmailAddressEntity(noUid=False))
elif myarg == 'usermentions':
userMentionConjunction = getChoice(AND_OR_CONJUNCTION_MAP, mapChoice=True, defaultChoice='OR')
userMentionList.extend(getNormalizedEmailAddressEntity(noUid=False))
elif myarg in {'start', 'starttime', 'end', 'endtime`', 'range'}:
startEndTime.Get(myarg)
elif myarg == 'hasattachment':
hasAttachment = True
elif myarg == 'orderby':
orderBy = getChoice(CHAT_SEARCHMESSAGES_ORDERBY_CHOICE_MAP, mapChoice=True)
elif myarg in CHAT_SEARCHMESSAGES_VIEW_CHOICE_MAP:
body['view'] = CHAT_SEARCHMESSAGES_VIEW_CHOICE_MAP[myarg]
elif getFieldsList(myarg, CHAT_MESSAGES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
pass
else:
FJQC.GetFormatJSONQuoteChar(myarg, True)
if not keywordList:
missingArgumentExit('keywords')
if orderBy is not None:
body['orderBy'] = f'{orderBy} desc'
body['filter'] = f'({" ".join(keywordList)})'
if spaceList:
body['filter'] += ' AND ('
for space in spaceList:
body['filter'] += f'space.name = "{space}" OR '
body['filter'] = body['filter'][:-4] + ')'
if displayNameList:
body['filter'] += ' AND ('
for displayName in displayNameList:
body['filter'] += f'space.display_name:{displayName}" {displayNameConjunction} '
body['filter'] = body['filter'][:-(len(displayNameConjunction)+2)] + ')'
if senderList:
body['filter'] += ' AND ('
for sender in senderList:
body['filter'] += f'sender.name = "users/{sender}" OR '
body['filter'] = body['filter'][:-4] + ')'
if userMentionList:
body['filter'] += ' AND ('
for userMention in userMentionList:
body['filter'] += f'annotations.user_mentions.user.name:"users/{userMention}" {userMentionConjunction} '
body['filter'] = body['filter'][:-(len(userMentionConjunction)+2)] + ')'
if startEndTime.startDateTime is not None or startEndTime.endDateTime is not None:
body['filter'] += ' AND ('
if startEndTime.startDateTime is not None:
body['filter'] += f'createTime >= "{startEndTime.startDateTime}"'
if startEndTime.endDateTime is not None:
body['filter'] += ' AND '
if startEndTime.endDateTime is not None:
body['filter'] += f'createTime < "{startEndTime.endDateTime}"'
body['filter'] += ')'
if hasAttachment:
body['filter'] += ' AND attachment:*'
chatSenders = {}
chatSpaces = {}
fields = getItemFieldsFromFieldsList('results(message', fieldsList)
if fields:
fields += ')'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_SPACE, None])
if not chat:
continue
_, chatsp, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, [Ent.CHAT_SPACE, None])
if not chat:
continue
try:
results = callGAPIpages(chat.spaces().messages(), 'search', 'results',
pageMessage=_getChatPageMessage(Ent.CHAT_MESSAGE, user, i, count, body['filter']),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
parent='spaces/-', body=body, fields=fields, pageArgsInBody=True)
for result in results:
if 'sender' in result['message']:
_getChatSenderEmail(cd, result['message']['sender'], chatSenders)
if 'space' in result['message']:
_getChatSpaceDisplayName(chatsp, result['message']['space'], chatSpaces)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
continue
except GAPI.failedPrecondition:
userChatServiceNotEnabledWarning(user, i, count)
break
if not csvPF:
jcount = len(results)
if not FJQC.formatJSON:
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MESSAGE, i, count)
Ind.Increment()
j = 0
for result in results:
j += 1
_showChatItem(result['message'], Ent.CHAT_MESSAGE, FJQC, j, jcount)
Ind.Decrement()
elif results:
for result in results:
_printChatItem(user, result['message'], parent, Ent.CHAT_MESSAGE, csvPF, FJQC)
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
csvPF.WriteRowNoFilter({'User': user})
if csvPF:
csvPF.writeCSVfile('Chat Messages')
# gam <UserTypeEntity> info chatevent name <ChatEvent>
# [formatjson]
def infoChatEvent(users):
@@ -80170,17 +80393,12 @@ def CAABuildCondition():
unknownArgumentExit()
return condition
CAA_COMBINING_FUNCTIONS_MAP = {
'and': 'AND',
'or': 'OR',
}
def CAABuildBasicLevel():
basic_level = {'conditions': []}
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'combiningfunction':
basic_level['combiningFunction'] = getChoice(CAA_COMBINING_FUNCTIONS_MAP, mapChoice=True)
basic_level['combiningFunction'] = getChoice(AND_OR_CONJUNCTION_MAP, mapChoice=True)
elif myarg == 'condition':
basic_level['conditions'].append(CAABuildCondition())
else:
@@ -81701,6 +81919,7 @@ USER_COMMANDS_WITH_OBJECTS = {
Cmd.ARG_CHATEVENT: printShowChatEvents,
Cmd.ARG_CHATMEMBER: printShowChatMembers,
Cmd.ARG_CHATMESSAGE: printShowChatMessages,
Cmd.ARG_CHATSEARCHMESSAGE: printShowChatSearchMessages,
Cmd.ARG_CHATSECTION: printShowChatSections,
Cmd.ARG_CHATSECTIONITEM: printShowChatSectionItems,
Cmd.ARG_CHATSPACE: printShowChatSpaces,
@@ -81819,6 +82038,7 @@ USER_COMMANDS_WITH_OBJECTS = {
Cmd.ARG_CHATEVENT: printShowChatEvents,
Cmd.ARG_CHATMEMBER: printShowChatMembers,
Cmd.ARG_CHATMESSAGE: printShowChatMessages,
Cmd.ARG_CHATSEARCHMESSAGE: printShowChatSearchMessages,
Cmd.ARG_CHATSECTION: printShowChatSections,
Cmd.ARG_CHATSECTIONITEM: printShowChatSectionItems,
Cmd.ARG_CHATSPACE: printShowChatSpaces,
@@ -82047,6 +82267,7 @@ USER_COMMANDS_OBJ_ALIASES = {
Cmd.ARG_CHATEVENTS: Cmd.ARG_CHATEVENT,
Cmd.ARG_CHATMEMBERS: Cmd.ARG_CHATMEMBER,
Cmd.ARG_CHATMESSAGES: Cmd.ARG_CHATMESSAGE,
Cmd.ARG_CHATSEARCHMESSAGES: Cmd.ARG_CHATSEARCHMESSAGE,
Cmd.ARG_CHATSECTIONS: Cmd.ARG_CHATSECTION,
Cmd.ARG_CHATSECTIONITEMS: Cmd.ARG_CHATSECTIONITEM,
Cmd.ARG_CHATSPACES: Cmd.ARG_CHATSPACE,

View File

@@ -846,6 +846,8 @@ class GamCLArgs():
ARG_CHATMEMBERS = 'chatmembers'
ARG_CHATMESSAGE = 'chatmessage'
ARG_CHATMESSAGES = 'chatmessages'
ARG_CHATSEARCHMESSAGE = 'chatsearchmessage'
ARG_CHATSEARCHMESSAGES = 'chatsearchmessages'
ARG_CHATSECTION = 'chatsection'
ARG_CHATSECTIONS = 'chatsections'
ARG_CHATSECTIONITEM = 'chatsectionitem'

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2025 Ross Scroggs All Rights Reserved.
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
@@ -22,7 +22,7 @@
# Products/SKUs
_PRODUCTS = {
'101001': 'Cloud Identity',
'101001': 'Cloud Identity Free',
'101005': 'Cloud Identity Premium',
'101031': 'Google Workspace for Education',
'101033': 'Google Voice',
@@ -44,7 +44,7 @@ _PRODUCTS = {
}
_SKUS = {
'1010010001': {
'product': '101001', 'aliases': ['identity', 'cloudidentity'], 'displayName': 'Cloud Identity Free'},
'product': '101001', 'aliases': ['identity', 'cloudidentity', 'cloudidentityfree'], 'displayName': 'Cloud Identity Free'},
'1010050001': {
'product': '101005', 'aliases': ['identitypremium', 'cloudidentitypremium'], 'displayName': 'Cloud Identity Premium'},
'1010070001': {

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#