Compare commits

..

7 Commits
main ... v3.43

Author SHA1 Message Date
Jay Lee
bdc763c91a Update README.md 2014-11-12 11:10:21 -05:00
Jay Lee
2ec19cc197 Update README.md 2014-10-07 04:34:25 -04:00
Jay Lee
387ad7f9d9 Update README.md 2014-10-07 04:33:07 -04:00
Jay Lee
ed8af7d81c Update README.md 2014-10-06 13:14:17 -04:00
Jay Lee
d955f49684 Update README.md 2014-10-06 13:13:24 -04:00
Jay Lee
bcd75a979f gitignore also 2014-10-06 13:11:29 -04:00
Jay Lee
5cc46fc4ee remove files from readme branch 2014-10-06 13:10:37 -04:00
309 changed files with 34 additions and 214962 deletions

View File

@@ -1,14 +0,0 @@
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
Please confirm the following:
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
* I am typing the command as described in the GAM Wiki at https://github.com/GAM-team/GAM/wiki
Full steps to reproduce the issue:
1.
2.
3.
Expected outcome (what are you trying to do?):
Actual outcome (what errors or bad behavior do you see instead?):

View File

@@ -1,14 +0,0 @@
---
name: Question about using GAM
about: Help with using GAM or running it for the first time
title: Please use the GAM discussion group
labels: invalid
assignees: ''
---
If you need help with GAM, please do not file an issue here, it will be closed and ignored.
Please post your question to the GAM discussion group where other admins are ready and willing to help:
https://groups.google.com/g/google-apps-manager

View File

@@ -1,23 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: jay0lee
---
The issue tracker is for reporting product deficiencies. "How do I?" questions should be posted to the discussion forum at https://groups.google.com/group/google-apps-manager. When in doubt, start at the discussion forum and return here only when instructed to do so.
Please confirm the following:
* I have upgraded to the latest GAM release from https://github.com/GAM-team/GAM/releases and I still have this issue.
* I am typing the command as described in the GAM Wiki at https://github.com/GAM-team/GAM/wiki.
Full steps to reproduce the issue:
1.
2.
3.
Expected outcome (what are you trying to do?):
Actual outcome (what errors or bad behavior do you see instead?):

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for GAM
title: ''
labels: enhancement
assignees: jay0lee
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,19 +0,0 @@
#!/bin/sh
credspath="$1"
if [ ! -d "$credspath" ]; then
echo "creating ${credspath}"
mkdir -p "$credspath"
fi
credsfile="${credspath}/oauth2.txt"
echo "$oa2" > "$credsfile"
echo "File size:"
wc -c "$credsfile"
echo "File type:"
file "$credsfile"
echo "Validation:"
jq -e . "$credsfile" > /dev/null
if [ $? -eq 0 ]; then
echo "Valid JSON"
else
echo "Invalid JSON"
fi

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- These are required for binaries built by PyInstaller -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

View File

@@ -1,6 +0,0 @@
oauth2.txt
nobrowser.txt
enabledasa.txt
lastupdatecheck.txt
*.lck
*.csv

61
.github/stale.yml vendored
View File

@@ -1,61 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- pinned
- enhancement
- help wanted
- security
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: wontfix
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
paths-ignore:
- 'wiki/**'
pull_request:
paths-ignore:
- 'wiki/**'
schedule:
- cron: '25 10 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@@ -1,93 +0,0 @@
name: Check for Google Root CA Updates
on:
schedule:
- cron: '23 23 * * *'
workflow_dispatch:
defaults:
run:
shell: bash
working-directory: src/gam
jobs:
check-certs:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
- name: Get Current cacerts.pem hash
run: |
export CURRENT_HASH=$(sha256sum ./cacerts.pem)
echo "Current hash is: ${CURRENT_HASH}"
echo "CURRENT_HASH=${CURRENT_HASH}" >> $GITHUB_ENV
- name: Generate GAM-specific bundle with LE + Google roots
run: |
OUTPUT_FILE="cacerts.pem"
> "$OUTPUT_FILE"
process_cert() {
local url="$1"
local op_ca="$2"
local label="$3"
local tmp_cert=$(mktemp)
curl "$url" > "$tmp_cert"
local issuer=$(openssl x509 -noout -issuer -in "$tmp_cert" | sed -e 's/^issuer= *//')
local subject=$(openssl x509 -noout -subject -in "$tmp_cert" | sed -e 's/^subject= *//')
local serial_hex=$(openssl x509 -noout -serial -in "$tmp_cert" | sed -e 's/^serial=//')
local serial_dec=$(python3 -c "print(int('$serial_hex', 16))")
local md5=$(openssl x509 -noout -fingerprint -md5 -in "$tmp_cert" | sed -e 's/.*=//' | tr '[:upper:]' '[:lower:]')
local sha1=$(openssl x509 -noout -fingerprint -sha1 -in "$tmp_cert" | sed -e 's/.*=//' | tr '[:upper:]' '[:lower:]')
local sha256=$(openssl x509 -noout -fingerprint -sha256 -in "$tmp_cert" | sed -e 's/.*=//' | tr '[:upper:]' '[:lower:]')
echo "# Operating CA: $op_ca" >> "$OUTPUT_FILE"
echo "# Issuer: $issuer" >> "$OUTPUT_FILE"
echo "# Subject: $subject" >> "$OUTPUT_FILE"
echo "# Label: \"$label\"" >> "$OUTPUT_FILE"
echo "# Serial: $serial_dec" >> "$OUTPUT_FILE"
echo "# MD5 Fingerprint: $md5" >> "$OUTPUT_FILE"
echo "# SHA1 Fingerprint: $sha1" >> "$OUTPUT_FILE"
echo "# SHA256 Fingerprint: $sha256" >> "$OUTPUT_FILE"
cat "$tmp_cert" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
rm "$tmp_cert"
}
echo "#" >> "$OUTPUT_FILE"
echo "# This is a custom certificate authority bundle for GAM" >> "$OUTPUT_FILE"
echo "# It's composed of Let's Encrypt Root CAs and Google's" >> "$OUTPUT_FILE"
echo "# certificate bundle. This should be the minimal list of" >> "$OUTPUT_FILE"
echo "# CAs required to talk to Google and Github." >> "$OUTPUT_FILE"
echo"" >> "$OUTPUT_FILE"
echo "Processing Let's Encrypt ISRG Root X1..."
process_cert "https://letsencrypt.org/certs/isrgrootx1.pem" "Let's Encrypt" "ISRG Root X1"
echo "Processing Let's Encrypt ISRG Root X2..."
process_cert "https://letsencrypt.org/certs/isrg-root-x2.pem" "Let's Encrypt" "ISRG Root X2"
echo "Appending Google's roots.pem..."
curl -s https://pki.goog/roots.pem >> "$OUTPUT_FILE"
echo "Done! The new bundle has been saved to $OUTPUT_FILE."
- name: Compare hashes
run: |
export NEW_HASH=$(sha256sum ./cacerts.pem)
if [ "$NEW_HASH" == "$CURRENT_HASH" ]; then
echo "Same file."
else
echo "New file content. Was ${CURRENT_HASH} and now is ${NEW_HASH}"
fi
- name: Commit file
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add cacerts.pem
git diff --quiet && git diff --staged --quiet || git commit -am '[ci skip] Updated cacerts.pem'
- name: Push changes
uses: ad-m/github-push-action@77c5b412c50b723d2a4fbc6d71fb5723bcd439aa
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,43 +0,0 @@
name: Push wiki
permissions:
contents: write
on:
push:
paths:
- 'wiki/**'
workflow_dispatch:
jobs:
pushwiki:
runs-on: ubuntu-latest
steps:
- name: Checkout GAM source
run: |
# checkout via normal shell here
# so we're not authorized to actually make any changes
git clone https://github.com/GAM-team/GAM
- name: Checkout Wiki source
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
with:
path: GAM.wiki
repository: GAM-team/GAM.wiki
persist-credentials: true
fetch-depth: 0
- name: Overwrite all Wiki repo files with /wiki from main git
run: |
# remove all wiki repo files so deletes work
rm -fv GAM.wiki/*.md
# copy all files from main GAM repo wiki folder
cp -fv GAM/wiki/*.md GAM.wiki/
- name: Commit Wiki changes
run: |
cd GAM.wiki
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
git commit -m "[no ci] Push Wiki changes"
git status
git push

View File

@@ -1,35 +0,0 @@
name: build and publish releases to PyPi
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
jobs:
pypi:
name: Upload release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/gam7
permissions:
id-token: write
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
with:
persist-credentials: false
fetch-depth: 0
- name: Install required packages to publish
run: |
python3 -m pip install --upgrade build
- name: Build packages
run: |
python -m build
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
attestation: true

View File

@@ -1,144 +0,0 @@
name: Daily Dependency Pinning (2-Week Buffer)
on:
schedule:
# Runs every day at midnight UTC
- cron: '0 0 * * *'
workflow_dispatch: # Allows you to trigger it manually from the UI
jobs:
pin-deps:
runs-on: ubuntu-slim
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: '3.14'
- name: Calculate and pin two-week old stable versions
shell: python
run: |
import json
import urllib.request
import re
import tomllib
from datetime import datetime, timedelta, timezone
from pathlib import Path
toml_path = Path("pyproject.toml")
if not toml_path.exists():
print("pyproject.toml not found!")
exit(1)
content = toml_path.read_text(encoding="utf-8")
parsed_toml = tomllib.loads(content)
target_deps = set()
# Standard [project.dependencies]
target_deps.update(parsed_toml.get("project", {}).get("dependencies", []))
# Optional [project.optional-dependencies]
for deps in parsed_toml.get("project", {}).get("optional-dependencies", {}).values():
target_deps.update(deps)
# Dev [dependency-groups] (uv / PEP 735 standard)
for deps in parsed_toml.get("dependency-groups", {}).values():
if isinstance(deps, list):
for d in deps:
if isinstance(d, str):
target_deps.add(d)
if not target_deps:
print("No dependencies found to process.")
exit(0)
updates = {}
two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14)
print(f"Evaluating dependencies against cutoff date: {two_weeks_ago.isoformat()}")
for dep in target_deps:
# Isolate base package name (e.g., "yubikey-manager>=5.6.1" -> "yubikey-manager")
pkg_name = re.split(r'[<>=!~;\s]', dep)[0].strip()
# Preserve environment markers if they exist
marker = ""
if ";" in dep:
marker = " ; " + dep.split(";", 1)[1].strip()
print(f"Fetching PyPI data for: {pkg_name}")
try:
url = f"https://pypi.org/pypi/{pkg_name}/json"
req = urllib.request.Request(url, headers={'User-Agent': 'GAM-CI-Script'})
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
valid_versions = []
for ver, files in data.get("releases", {}).items():
if not files:
continue
upload_time_str = files[0].get("upload_time_iso_8601")
if not upload_time_str:
continue
if upload_time_str.endswith('Z'):
upload_time_str = upload_time_str[:-1] + '+00:00'
upload_time = datetime.fromisoformat(upload_time_str)
# Filter: Must be older than 2 weeks and not a pre-release
if upload_time <= two_weeks_ago and not any(x in ver.lower() for x in ['a', 'b', 'rc', 'dev']):
valid_versions.append((upload_time, ver))
if valid_versions:
# Sort by upload time descending to get the newest valid option
valid_versions.sort(key=lambda x: x[0], reverse=True)
target_version = valid_versions[0][1]
pinned_dep = f"{pkg_name}=={target_version}{marker}"
if pinned_dep != dep:
updates[dep] = pinned_dep
print(f" -> Pinning: '{dep}' => '{pinned_dep}'")
else:
print(f" -> Already pinned correctly to {target_version}")
else:
print(f" -> No valid historical versions found.")
except urllib.error.HTTPError as e:
print(f" -> Package not found on PyPI or HTTP error: {e}")
except Exception as e:
print(f" -> Error processing {pkg_name}: {e}")
# 3. Replace the strings safely in the original file content
new_content = content
for old_dep, new_dep in updates.items():
# Regex targets the exact string inside either single or double quotes
# Using a lambda replacement ensures we don't trip over escape sequences in the new string
escaped_old = re.escape(old_dep)
pattern = r'([\'"])' + escaped_old + r'\1'
new_content = re.sub(pattern, lambda m: m.group(1) + new_dep + m.group(1), new_content)
# Write changes back to pyproject.toml
if content != new_content:
toml_path.write_text(new_content, encoding="utf-8")
print("\npyproject.toml updated successfully.")
else:
print("\nNo updates required.")
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: upgrade PyPi deps"
title: "Upgrade PyPi deps"
body: "Automated scan checking PyPI for package versions at least 2 weeks old."
branch: sys-deps-upgrade
force: false # Standard push, plays nice with rulesets

201
LICENSE
View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,46 +1,43 @@
GAM is a command line tool for Google Workspace admins to manage domain and user settings quickly and easily.
GAM
============================
GAM is a free, open source command line tool for
Google Apps Administrators to manage
domain and user settings quickly and easily. GAM supports
[![Build StatusM](https://github.com/GAM-team/GAM/actions/workflows/build.yml/badge.svg)](https://github.com/GAM-team/GAM/actions/workflows/build.yml)
* creating, deleting, and updating users, aliases, groups,
organizations, and resource calendars
* modifying user email settings such as IMAP, signatures,
vacation messages, profile sharing, email forwarding,
send as address, labels, and features.
* modifying calendar access rights for users and resource calendars.
* generating detailed reports for users, groups, resources,
account activity, email clients, and quotas.
* and many more commands
# Quick Start
## Linux / MacOS
Open a terminal and run:
```sh
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)
```
this will download GAM, install it and start setup.
## Windows
Download the EXE Installer from the [GitHub Releases] page. Run it and you'll be prompted to setup GAM.
## Use your own Python
If you'd prefer to install GAM as a Python package you can install with pip:
```
pip install gam7
```
# Documentation
Downloads
---------
You can download the current GAM release from
the [GitHub Releases] page.
Documentation
------------------
The GAM documentation is hosted in the [GitHub Wiki]
# Mailing List / Discussion group
Mailing List / Discussion group
-------------------------------
The GAM mailing list / discussion group is hosted
on [Google Groups]. You can join the list and interact
via email, or just post from the web itself.
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
Source Repository
-----------------
The official GAM source repository is on [GitHub] in the master branch.
# Chat Room
Author
------
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>.
There is a public chat room hosted in Google Chat. [Instructions to join](https://github.com/GAM-team/GAM/wiki/GAM-Public-Chat-Room).
# Author
GAM is maintained by [Jay (James) Lee](mailto:jay0lee@gmail.com) and [Ross Scroggs](mailto:ross.scroggs@gmail.com). Please direct "how do I?" questions to [Google Groups].
[GAM release]: https://github.com/GAM-team/GAM/releases
[GitHub Releases]: https://github.com/GAM-team/GAM/releases
[GitHub]: https://github.com/GAM-team/GAM/tree/master
[GitHub Wiki]: https://github.com/GAM-team/GAM/wiki/
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
[GitHub]: https://github.com/jay0lee/GAM/tree/master
[GitHub Wiki]: https://github.com/jay0lee/GAM/wiki/
[Google Groups]: http://groups.google.com/group/google-apps-manager

View File

@@ -1,6 +0,0 @@
# overrides uv.lock to force newer dependencies
# when old deps are vulnerable. These should be set
# to expire after 2 weeks when the fixed version will
# be automatically picked up anyway.
# Format: package_requirement | MM/DD/YYYY
urllib3>=2.7.0 | 05/22/2026

View File

@@ -1,72 +0,0 @@
[project]
name = "gam7"
dynamic = [
"version",
]
authors = [
{ name = "Jay Lee", email = "jay0lee@gmail.com" },
{ name = "Ross Scroggs", email = "Ross.Scroggs@gmail.com" },
]
dependencies = [
"arrow==1.4.0",
"chardet==7.4.3",
"cryptography==48.0.0",
"distro==1.9.0 ; sys_platform=='linux'",
"filelock==3.29.0",
"google-api-python-client==2.196.0",
"google-auth-httplib2==0.4.0",
"google-auth-oauthlib==1.4.0",
"google-auth==2.53.0",
"httplib2==0.31.2",
"lxml==6.1.1",
"passlib==1.7.4",
"pathvalidate==3.3.1",
"pysocks==1.7.1",
]
description = "CLI tool to manage Google Workspace"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
]
license-files = [
"LICEN[CS]E*",
]
[project.license]
text = "Apache License (2.0)"
[project.optional-dependencies]
yubikey = [
"yubikey-manager==5.9.1",
]
[project.scripts]
gam = "gam.__main__:main"
[project.urls]
Homepage = "https://github.com/GAM-team/GAM"
Issues = "https://github.com/GAM-team/GAM/issues"
Discussion = "https://groups.google.com/group/google-apps-manager"
Chat = "https://git.io/gam-chat"
[tool.hatch.version]
path = "src/gam/__init__.py"
[tool.hatch.build.targets.wheel]
packages = [
"src/gam",
]
[build-system]
requires = [
"hatchling",
]
build-backend = "hatchling.build"

72
src/.gitignore vendored
View File

@@ -1,72 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
# Sphinx documentation
docs/_build/
# GAM specific
client_secrets.json
oauth2.txt*
oauth2service*
debug.gam
lastupdatecheck.txt
noupdatecheck.txt
nodito.txt
nobrowser.txt
nocache.txt
noverifyssl.txt
gamcache/
dist/
gam-64/
*.zip
*.msi
*.wixobj
*.wixpdb

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
../LICENSE

View File

@@ -1 +0,0 @@
../README.md

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Sample Python script to call GAM"""
import multiprocessing
import platform
from gam import initializeLogging, CallGAMCommand
if __name__ == '__main__':
# One time initialization
if platform.system() != 'Linux':
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn', force=True)
initializeLogging()
#
CallGAMCommand(['gam', 'version'])
# Issue command, output goes to stdout/stderr
rc = CallGAMCommand(['gam', 'info', 'domain'])
# Issue command, redirect stdout/stderr
rc = CallGAMCommand(['gam', 'redirect', 'stdout', 'domain.txt', 'redirect', 'stderr', 'stdout', 'info', 'domain'])

View File

@@ -1,482 +0,0 @@
#!/usr/bin/env bash
usage()
{
cat << EOF
GAM installation script.
OPTIONS:
-h show help.
-d Directory where gam folder will be installed. Default is \$HOME/bin/
-a Architecture to install (x86_64, arm64). Default is to detect your arch with "uname -m".
-o OS we are running (linux, macos). Default is to detect your OS with "uname -s".
-b OS version. Default is to detect on macOS and Linux.
-l Just upgrade GAM to latest version. Skips project creation and auth.
-p Profile update (true, false). Should script add gam command to environment. Default is true.
-u Admin user email address to use with GAM. Default is to prompt.
-r Regular user email address. Used to test service account access to user data. Default is to prompt.
-v Version to install (latest, prerelease, draft, 3.8, etc). Default is latest.
-s Strip gam component from extracted files, files will be downloaded directly to $target_dir
EOF
}
target_dir="$HOME/bin"
target_folder="$target_dir/gam7"
gamarch=$(uname -m)
gamos=$(uname -s)
osversion=""
update_profile=true
upgrade_only=false
gamversion="latest"
adminuser=""
regularuser=""
strip_gam="--strip-components 0"
while getopts "hd:a:o:b:lp:u:r:v:s" OPTION
do
case $OPTION in
h) usage; exit;;
d) target_dir="${OPTARG%/}"; target_folder="$target_dir/gam7";;
a) gamarch="$OPTARG";;
o) gamos="$OPTARG";;
b) osversion="$OPTARG";;
l) upgrade_only=true;;
p) update_profile="$OPTARG";;
u) adminuser="$OPTARG";;
r) regularuser="$OPTARG";;
v) gamversion="$OPTARG";;
s) strip_gam="--strip-components 1"; target_folder="$target_dir";;
?) usage; exit;;
esac
done
target_gam="$target_folder/gam"
update_profile() {
[ "$2" -eq 1 ] || [ -f "$1" ] || return 1
grep -F "$alias_line" "$1" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo_yellow "Adding gam alias to profile file $1."
echo -e "\n$alias_line" >> "$1"
else
echo_yellow "gam alias already exists in profile file $1. Skipping add."
fi
}
echo_red()
{
echo -e "\x1B[1;31m$1"
echo -e '\x1B[0m'
}
echo_green()
{
echo -e "\x1B[1;32m$1"
echo -e '\x1B[0m'
}
echo_yellow()
{
echo -e "\x1B[1;33m$1"
echo -e '\x1B[0m'
}
version_gt()
{
if [ "${1}" = "${2}" ]; then
true
else
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
fi
}
if [ "$gamversion" == "latest" ]; then
release_url="https://api.github.com/repos/GAM-team/GAM/releases/latest"
elif [ "$gamversion" == "prerelease" -o "$gamversion" == "draft" ]; then
release_url="https://api.github.com/repos/GAM-team/GAM/releases"
else
release_url="https://api.github.com/repos/GAM-team/GAM/releases/tags/v$gamversion"
fi
if [ -z ${GHCLIENT+x} ]; then
check_type="unauthenticated"
curl_opts=( )
else
check_type="authenticated"
curl_opts=( "$GHCLIENT" )
fi
curl_ver=$(curl --version|head -1|cut -d " " -f 2)
if [[ "${curl_ver:0:4}" < "7.76" ]]; then
curl_fail=( )
else
curl_fail=( "--fail-with-body" )
fi
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release ($check_type)..."
release_json=$(curl \
--silent \
"${curl_opts[@]}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$release_url" \
"${curl_fail[@]}")
curl_exit_code=$?
if [ $curl_exit_code -ne 0 ]; then
echo_red "ERROR retrieving URL: ${release_json}"
exit
else
echo_green "done"
fi
echo_yellow "Calculating download URL for this device..."
# Python is sadly the nearest to universal way to safely handle JSON with Bash
# At least this code should be compatible with just about any Python version ever
# unlike GAM itself. If some users don't have Python we can try grep / sed / etc
# but that gets really ugly
pycode="import json
import sys
attrib = sys.argv[1]
gamversion = sys.argv[2]
release = json.load(sys.stdin)
if type(release) is list:
for a_release in release:
if a_release['prerelease'] and gamversion != 'prerelease':
continue
elif a_release['draft'] and gamversion != 'draft':
continue
release = a_release
break
try:
for asset in release['assets']:
print(asset[attrib])
#else:
# print('ERROR: Attribute: {0} for version {1} not found'.format(attrib, gamversion))
except KeyError:
print('ERROR: assets value not found in JSON value of:\n\n%s' % release)"
pycmd="python3"
$pycmd -V >/dev/null 2>&1
rc=$?
if (( $rc != 0 )); then
pycmd="python"
fi
$pycmd -V >/dev/null 2>&1
rc=$?
if (( $rc != 0 )); then
pycmd="/usr/bin/python3"
fi
$pycmd -V >/dev/null 2>&1
rc=$?
if (( $rc != 0 )); then
pycmd="python2"
fi
$pycmd -V >/dev/null 2>&1
rc=$?
if (( $rc != 0 )); then
echo_red "ERROR: No version of python installed."
exit
fi
# also sort the URLs once so we're evaluating newest OS version first
download_urls=$(echo "$release_json" | \
$pycmd -c "$pycode" browser_download_url "$gamversion" | \
sort --version-sort --reverse)
if [[ ${download_urls:0:5} = "ERROR" ]]; then
echo_red "${download_urls}"
exit
fi
case $gamos in
[lL]inux)
gamos="linux"
download_urls=$(echo -e "$download_urls" | grep -e "-linux-")
if [ "$osversion" == "" ]; then
this_glibc_ver=$(ldd --version | awk '/ldd/{print $NF}')
else
this_glibc_ver=$osversion
fi
echo "This Linux distribution uses glibc $this_glibc_ver"
case $gamarch in
x86_64)
download_urls=$(echo -e "$download_urls" | grep -e "-x86_64-")
gam_x86_64_glibc_vers=$(echo -e "$download_urls" | \
grep --only-matching 'glibc[0-9\.]*\.tar\.xz$' \
| cut -c 6-9 )
useglibc="legacy"
for gam_glibc_ver in $gam_x86_64_glibc_vers; do
if version_gt $this_glibc_ver $gam_glibc_ver; then
useglibc="glibc$gam_glibc_ver"
echo_green "Using GAM compiled against $useglibc"
break
fi
done
download_url=$(echo -e "$download_urls" | grep "$useglibc")
;;
arm|arm64|aarch64)
download_urls=$(echo -e "$download_urls" | grep -e "-arm64-\|-aarch64-")
gam_arm64_glibc_vers=$(echo -e "$download_urls" | \
grep --only-matching 'glibc[0-9\.]*\.tar\.xz$' | \
cut -c 6-9)
useglibc="legacy"
for gam_glibc_ver in $gam_arm64_glibc_vers; do
if version_gt $this_glibc_ver $gam_glibc_ver; then
useglibc="glibc$gam_glibc_ver"
echo_green "Using GAM compiled against $useglibc"
break
fi
done
download_url=$(echo -e "$download_urls" | grep "$useglibc")
;;
*)
echo_red "ERROR: this installer currently only supports x86_64 and arm64 Linux. Looks like you're running on $gamarch. Exiting."
exit
esac
;;
[Mm]ac[Oo][sS]|[Dd]arwin)
gamos="macos"
currentversion=$(sw_vers -productVersion | awk -F '.' '{print $1 "." $2}')
# override osversion only if it wasn't set by cli arguments
osversion=${osversion:-${currentversion}}
# override osversion only if it wasn't set by cli arguments
download_urls=$(echo -e "$download_urls" | grep -e "-macos")
case $gamarch in
x86_64)
archgrep="-x86_64"
;;
arm|arm64|aarch64)
archgrep="-arm64\|-aarch64"
;;
*)
echo_red "ERROR: this installer currently only supports x86_64 and arm64 macOS. Looks like you're running on ${gamarch}. Exiting."
exit
;;
esac
gam_macos_urls=$(echo -e "$download_urls" | \
grep -e $archgrep)
versionless_urls=$(echo -e "$gam_macos_urls" | \
grep -e "-macos-")
if [ "$versionless_urls" == "" ]; then
# versions after 7.00.38 include macOS version info
gam_macos_vers=$(echo -e "$gam_macos_urls" | \
grep --only-matching -e '-macos[0-9\.]*' | \
cut -c 7-10)
for gam_mac_ver in $gam_macos_vers; do
if version_gt $currentversion $gam_mac_ver; then
download_url=$(echo -e "$gam_macos_urls" | grep "$gam_mac_ver")
echo_green "You are running macOS ${currentversion} Using GAM compiled against ${gam_mac_ver}"
break
fi
done
if [ -z ${download_url+x} ]; then
echo_red "Sorry, you are running macOS ${osversion} but GAM on ${gamarch} requires macOS ${gam_mac_ver} or newer. Exiting."
exit
fi
else
# versions 7.00.38 and older don't include version info
case $gamarch in
x86_64)
minimum_version=13
;;
arm|arm64|aarch64)
minimum_version=14
;;
esac
download_url=$(echo -e "$download_urls" | grep -e $archgrep)
if version_gt "$osversion" "$minimum_version"; then
echo_green "You are running macOS ${osversion}, good. Downloading GAM from ${download_url}."
else
echo_red "Sorry, you are running macOS ${osversion} but GAM on ${gamarch} requires macOS ${minimum_version}. Exiting."
exit
fi
if [ -z ${download_url+x} ]; then
echo_red "Sorry, you are running macOS ${currentversion} but GAM on ${gamarch} requires macOS ${minimum_version}. Exiting."
exit
fi
fi
;;
MINGW64_NT*)
gamos="windows"
echo "You are running Windows"
download_url=$(echo -e "$download_urls" | \
grep -e "-windows-" | \
grep ".zip")
;;
*)
echo_red "Sorry, this installer currently only supports Linux and macOS. Looks like you're running on ${gamos}. Exiting."
exit
;;
esac
# Temp dir for archive
temp_archive_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
# Clean up after ourselves even if we are killed with CTRL-C
trap "rm -rf $temp_archive_dir" EXIT
# hack to grab the end of the URL which should be the filename.
name=$(echo -e "$download_url" | rev | cut -f1 -d "/" | rev)
echo_yellow "Downloading ${download_url} to $temp_archive_dir ($check_type)..."
# Save archive to temp w/o losing our path
(cd "$temp_archive_dir" && curl -O -L -s "${curl_opts[@]}" "$download_url")
mkdir -p "$target_folder"
echo_yellow "Deleting contents of $target_folder/lib"
rm -frv "$target_folder/lib"
echo_yellow "Extracting archive to $target_dir"
if [[ "$name" =~ tar.xz|tar.gz|tar ]]; then
tar $strip_gam -xf "$temp_archive_dir"/"$name" -C "$target_dir"
elif [[ "$name" == *.zip ]]; then
unzip -o "${temp_archive_dir}/${name}" -d "${target_dir}"
else
echo "I don't know what to do with files like ${name}. Giving up."
exit 1
fi
rc=$?
if (( $rc != 0 )); then
echo_red "ERROR: extracting the GAM archive with tar failed with error $rc. Exiting."
exit
else
echo_green "Finished extracting GAM archive."
fi
# Update profile to add gam command
if [ "$update_profile" = true ]; then
alias_line="alias gam=\"$target_gam\""
if [ "$gamos" == "linux" ]; then
update_profile "$HOME/.bash_aliases" 0 || update_profile "$HOME/.bash_profile" 0 || update_profile "$HOME/.bashrc" 0
update_profile "$HOME/.zshrc" 0
elif [ "$gamos" == "macos" ]; then
update_profile "$HOME/.bash_aliases" 0 || update_profile "$HOME/.bash_profile" 0 || update_profile "$HOME/.bashrc" 0 || update_profile "$HOME/.profile" 1
update_profile "$HOME/.zshrc" 1
fi
else
echo_yellow "skipping profile update."
fi
if [ "$upgrade_only" = true ]; then
echo_green "Here's information about your GAM upgrade:"
"$target_gam" version extended
rc=$?
if (( $rc != 0 )); then
echo_red "ERROR: Failed running GAM for the first time with return code $rc. Please report this error to GAM mailing list. Exiting."
exit
fi
echo_green "GAM upgrade complete!"
exit
fi
while true; do
read -p "Can you run a full browser on this machine? (usually Y for macOS, N for Linux if you SSH into this machine) " yn
case $yn in
[Yy]*)
"$target_gam" config no_browser false save
break
;;
[Nn]*)
"$target_gam" config no_browser true save
break
;;
*)
echo_red "Please answer yes or no."
;;
esac
done
echo
project_created=false
while true; do
read -p "GAM is now installed. Are you ready to set up a Google API project for GAM? (yes or no) " yn
case $yn in
[Yy]*)
if [ "$adminuser" == "" ]; then
read -p "Please enter your Google Workspace admin email address: " adminuser
fi
"$target_gam" create project $adminuser
rc=$?
if (( $rc == 0 )); then
echo_green "Project creation complete."
project_created=true
break
else
echo_red "Project creation failed. Trying again. Say N to skip project creation."
fi
;;
[Nn]*)
echo -e "\nYou can create an API project later by running:\n\ngam create project\n"
break
;;
*)
echo_red "Please answer yes or no."
;;
esac
done
admin_authorized=false
while $project_created; do
read -p "Are you ready to authorize GAM to perform Google Workspace management operations as your admin account? (yes or no) " yn
case $yn in
[Yy]*)
"$target_gam" oauth create $adminuser
rc=$?
if (( $rc == 0 )); then
echo_green "Admin authorization complete."
admin_authorized=true
break
else
echo_red "Admin authorization failed. Trying again. Say N to skip admin authorization."
fi
;;
[Nn]*)
echo -e "\nYou can authorize an admin later by running:\n\ngam oauth create\n"
break
;;
*)
echo_red "Please answer yes or no."
;;
esac
done
service_account_authorized=false
while $admin_authorized; do
read -p "Are you ready to authorize GAM to manage Google Workspace user data and settings? (yes or no) " yn
case $yn in
[Yy]*)
if [ "$regularuser" == "" ]; then
read -p "Please enter the email address of a regular Google Workspace user: " regularuser
fi
echo_yellow "Great! Checking service account scopes.This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console."
"$target_gam" user $regularuser check serviceaccount
rc=$?
if (( $rc == 0 )); then
echo_green "Service account authorization complete."
service_account_authorized=true
break
else
echo_red "Service account authorization failed. Confirm you entered the scopes correctly in the admin console. It can take a few minutes for scopes to PASS after they are entered in the admin console so if you're sure you entered them correctly, go grab a coffee and then hit Y to try again. Say N to skip admin authorization."
fi
;;
[Nn]*)
echo -e "\nYou can authorize a service account later by running:\n\ngam user $adminuser check serviceaccount\n"
break
;;
*)
echo_red "Please answer yes or no."
;;
esac
done
echo_green "Here's information about your new GAM installation:"
"$target_gam" version extended
rc=$?
if (( $rc != 0 )); then
echo_red "ERROR: Failed running GAM for the first time with $rc. Please report this error to GAM mailing list. Exiting."
exit
fi
echo_green "GAM installation and setup complete!"
if [ "$update_profile" = true ]; then
echo_green "Please restart your terminal shell or to get started right away run:\n\n$alias_line"
fi

View File

@@ -1,88 +0,0 @@
:neworupgrade
@echo.
@set /p nu= "If you have installed any version of GAM on any computer for your domain, enter u to upgrade, otherwise enter n? [u or n] "
@if /I "%nu%"=="u" (
@ echo GAM installation and setup complete!
@ goto alldone
)
@if /I not "%nu%"=="n" (
@ echo.
@ echo Please answer n or u.
@ goto neworupgrade
)
:createproject
@echo.
@set /p yn= "Are you ready to set up a Google API project for GAM? [y or n] "
@if /I "%yn%"=="n" (
@ echo.
@ echo You can create an API project later by running:
@ echo.
@ echo gam create project
@ goto alldone
)
@if /I not "%yn%"=="y" (
@ echo.
@ echo Please answer y or n.
@ goto createproject
)
@set /p adminemail= "Please enter your Google Workspace admin email address: "
@gam create project %adminemail%
@if not ERRORLEVEL 1 goto projectdone
@echo.
@echo Project creation failed. Trying again. Say n to skip project creation.
@goto createproject
:projectdone
:adminauth
@echo.
@set /p yn= "Are you ready to authorize GAM to perform Google Workspace management operations as your admin account? [y or n] "
@if /I "%yn%"=="n" (
@ echo.
@ echo You can authorize an admin later by running:
@ echo.
@ echo gam oauth create %adminemail%
@ goto admindone
)
@if /I not "%yn%"=="y" (
@ echo.
@ echo Please answer y or n.
@ goto adminauth
)
@gam oauth create %adminemail%
@if not ERRORLEVEL 1 goto admindone
@echo.
@echo Admin authorization failed. Trying again. Say n to skip admin authorization.
@goto adminauth
:admindone
:saauth
@echo.
@set /p yn= "Are you ready to authorize GAM to manage Google Workspace user data and settings? [y or n] "
@if /I "%yn%"=="n" (
@ echo.
@ echo You can authorize a service account later by running:
@ echo.
@ echo gam user %adminemail% check serviceaccount
@ goto sadone
)
@if /I not "%yn%"=="y" (
@ echo.
@ echo Please answer y or n.
@ goto saauth
)
@echo.
@set /p regularuser= "Please enter the email address of a regular Google Workspace user: "
@echo Great! Checking service account scopes. This will fail the first time. Follow the steps to authorize and retry. It can take a few minutes for scopes to PASS after they've been authorized in the admin console.
@gam user %regularuser% check serviceaccount
@if not ERRORLEVEL 1 goto sadone
@echo.
@echo Service account authorization failed. Confirm you entered the scopes correctly in the admin console. It can take a few minutes for scopes to PASS after they are entered in the admin console so if you're sure you entered them correctly, go grab a coffee and then hit Y to try again. Say N to skip admin authorization.
@goto saauth
:sadone
@echo GAM installation and setup complete!
:alldone
@pause

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 8.1 / Server 2012 R2 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10+ / Server 2016+ -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@@ -1,116 +0,0 @@
; --- 1. PREPROCESSOR DEFINITIONS ---
#define AppVersion GetEnv("GAMVERSION")
#if AppVersion == ""
#define AppVersion "7.0.0"
#endif
; Pull architecture directly from GitHub Actions environment variable
#define RunnerArch GetEnv("RUNNER_ARCH")
[Setup]
; --- 2. CORE APPLICATION INFO ---
AppId={{D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319}
AppName=GAM7
AppVersion={#AppVersion}
AppPublisher=GAM Team - google-apps-manager@googlegroups.com
DefaultDirName={sd}\GAM7
LicenseFile=dist\gam\gam7\LICENSE
PrivilegesRequired=admin
ChangesEnvironment=yes
; Tell Inno Setup to use a custom signtool defined via the command line
SignTool=gamsigntool
; --- 3. COMPRESSION & OPTIMIZATION ---
Compression=lzma2/ultra64
SolidCompression=yes
; --- 4. DYNAMIC ARCHITECTURE CONFIGURATION ---
; GitHub Actions RUNNER_ARCH is typically uppercase "ARM64" or "X64"
#if RunnerArch == "ARM64" || RunnerArch == "arm64"
ArchitecturesAllowed=arm64
ArchitecturesInstallIn64BitMode=arm64
OutputBaseFilename=gam-{#AppVersion}-windows-arm64
#else
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
OutputBaseFilename=gam-{#AppVersion}-windows-x86_64
#endif
[Messages]
; Custom error if an admin tries to run the ARM64 installer on an Intel machine
#if RunnerArch == "ARM64" || RunnerArch == "arm64"
WindowsVersionNotSupported=You downloaded the ARM64 version of GAM, but this computer has an Intel or AMD processor.%n%nPlease go back to the release page and download the x86_64 installer instead.
#endif
[Files]
; --- 5. DYNAMIC FILE INCLUSION ---
Source: "dist\gam\gam7\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Registry]
; --- 6. PATH ENVIRONMENT VARIABLE ---
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
Check: NeedsAddPath(ExpandConstant('{app}'))
[Code]
const
ERROR_SUCCESS = 0;
function MsiEnumRelatedProducts(lpUpgradeCode: string; dwReserved: Integer; iProductIndex: Integer; lpProductBuf: string): Integer;
external 'MsiEnumRelatedProductsW@msi.dll stdcall';
function UninstallWixMSI(): Boolean;
var
UpgradeCode: string;
ProductCode: string;
ResultCode: Integer;
begin
UpgradeCode := '{D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319}';
ProductCode := StringOfChar(' ', 39);
ResultCode := MsiEnumRelatedProducts(UpgradeCode, 0, 0, ProductCode);
if ResultCode = ERROR_SUCCESS then
begin
ProductCode := Trim(ProductCode);
Exec('msiexec.exe', '/x ' + ProductCode + ' /qn /norestart', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
Result := True;
end;
function InitializeSetup(): Boolean;
begin
// --- Architecture Warning for Emulation ---
#if RunnerArch != "ARM64" && RunnerArch != "arm64"
if IsArm64() then
begin
if MsgBox('Notice: You are installing the Intel (x86_64) build of GAM on an ARM processor.' + #13#10#13#10 +
'While this will work via Windows emulation, it will perform worse than the native ARM64 version.' + #13#10#13#10 +
'Do you want to continue with the installation anyway?',
mbConfirmation, MB_YESNO) = idNo then
begin
Result := False;
Exit;
end;
end;
#endif
UninstallWixMSI();
Result := True;
end;
function NeedsAddPath(Param: string): boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
'Path', OrigPath)
then begin
Result := True;
exit;
end;
Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0;
end;

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Provides backwards compatibility for calling gam as a single .py file"""
import multiprocessing
import platform
from gam.__main__ import main
# Run from command line
if __name__ == '__main__':
if platform.system() != 'Linux':
multiprocessing.freeze_support()
# Python 3.14.4 and PyInstaller 6.19.0 don't play nice with Linux forkserver
# use spawn everywhere for now.
multiprocessing.set_start_method('spawn', force=True)
main()

View File

@@ -1,155 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
from os import getenv
import re
from sys import platform
from PyInstaller.utils.hooks import copy_metadata
from gam.gamlib.glverlibs import GAM_VER_LIBS
with open("gam/__init__.py") as f:
version_file = f.read()
version = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M).group(1)
version_list = [int(i) for i in version.split('.')]
while len(version_list) < 4:
version_list.append(0)
version_tuple = tuple(version_list)
version_str = str(version_tuple)
with open("version_info.txt.in") as f:
version_info = f.read()
version_info = version_info.replace("{VERSION}", version).replace(
"{VERSION_TUPLE}", version_str
)
with open("version_info.txt", "w") as f:
f.write(version_info)
print(version_info)
datas = []
for pkg in GAM_VER_LIBS:
datas += copy_metadata(pkg, recursive=True)
datas += [('gam/cbcm-v1.1beta1.json', '.')]
datas += [('gam/contactdelegation-v1.json', '.')]
datas += [('gam/datastudio-v1.json', '.')]
datas += [('gam/meet-v2beta.json', '.')]
datas += [('gam/serviceaccountlookup-v1.json', '.')]
hiddenimports = [
'gam.gamlib.yubikey',
]
excludes = [
'pkg_resources',
]
runtime_hooks = []
a = Analysis(
['gam/__main__.py'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=['tools/hooks'],
hooksconfig={},
runtime_hooks=runtime_hooks,
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
#print(f"datas from analysis:\n{a.datas}")
for d in a.datas:
if 'pyconfig' in d[0]:
a.datas.remove(d)
break
#print(f"datas after pyconfig cleanup:\n{a.datas}")
pyz = PYZ(a.pure,
a.zipped_data,
cipher=None)
# requires Python 3.10+ but no one should be compiling
# GAM with older versions anyway
target_arch = None
codesign_identity = None
entitlements_file = None
manifest = None
version = 'version_info.txt'
match platform:
case "darwin":
if getenv('arch') == 'universal2':
target_arch = "universal2"
codesign_identity = getenv('codesign_identity')
if codesign_identity:
entitlements_file = '../.github/actions/entitlements.plist'
strip = True
case "win32":
target_arch = None
strip = False
manifest = 'gam.exe.manifest'
case _:
target_arch = None
strip = True
name = 'gam'
debug = False
bootloader_ignore_signals = False
upx = False
console = True
disable_windowed_traceback = False
argv_emulation = False
if getenv('PYINSTALLER_BUILD_ONEDIR') == 'yes':
# Build one directory
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=name,
debug=debug,
bootloader_ignore_signals=bootloader_ignore_signals,
strip=strip,
manifest=manifest,
upx=upx,
console=console,
# put most everyting under a lib/ subfolder
contents_directory='lib',
disable_windowed_traceback=disable_windowed_traceback,
argv_emulation=argv_emulation,
target_arch=target_arch,
codesign_identity=codesign_identity,
entitlements_file=entitlements_file,
version=version,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=strip,
upx=upx,
upx_exclude=[],
name=name,
)
else:
# Build one file
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name=name,
debug=debug,
bootloader_ignore_signals=bootloader_ignore_signals,
manifest=manifest,
strip=strip,
upx=upx,
console=console,
disable_windowed_traceback=disable_windowed_traceback,
argv_emulation=argv_emulation,
target_arch=target_arch,
codesign_identity=codesign_identity,
entitlements_file=entitlements_file,
version=version,
)

View File

@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" >
<Product
Id="*"
Name="GAM7"
Language="1033"
Version="$(env.GAMVERSION)"
Manufacturer="GAM Team - google-apps-manager@googlegroups.com"
UpgradeCode="D86B52B2-EFE9-4F9D-8BA3-9D84B9B2D319">
<Package
InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade
DowngradeErrorMessage=
"A newer version of [ProductName] is already installed."
Schedule="afterInstallExecute" />
<MediaTemplate EmbedCab="yes" />
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />
<UIRef Id="WixUI_InstallDir" />
<Feature
Id="gam"
Title="GAM7"
Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
<Fragment>
<SetDirectory Id="WINDOWSVOLUME" Value="[WindowsVolume]"/>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="WINDOWSVOLUME">
<Directory Id="INSTALLFOLDER" Name="GAM7">
<Directory Id="lib" Name="lib">
</Directory>
</Directory>
</Directory>
</Directory>
</Fragment>
<Fragment>
<!-- Group of components that are our main application items -->
<ComponentGroup
Id="ProductComponents"
Directory="INSTALLFOLDER"
Source="dist/gam/gam7">
<Component Id="gam_exe" Guid="d046ea24-c9f8-40ca-84db-70b0119933ff">
<File Name="gam.exe" KeyPath="yes" />
<Environment Id="PATH" Name="PATH" Value="[INSTALLFOLDER]" Permanent="yes" Part="last" Action="set" System="yes" />
</Component>
<Component Id="license" Guid="c76864c5-d005-44d5-bb7c-a27e5923792d">
<File Name="LICENSE" KeyPath="yes" />
</Component>
<Component Id="gam_setup_bat" Guid="5e6bbacb-d86f-4d80-a10b-89b81ee63fcb">
<File Name="gam-setup.bat" KeyPath="yes" />
</Component>
<Component Id="GamCommands_txt" Guid="a2dca862-b222-469e-a637-95ea2a1c53e7">
<File Name="GamCommands.txt" KeyPath="yes" />
</Component>
<Component Id="GamUpdate_txt" Guid="1b7cdd48-0fff-4943-a219-102fcd14c755">
<File Name="GamUpdate.txt" KeyPath="yes" />
</Component>
<Component Id="cacerts_pem" Guid="61fe2b2d-1646-4bed-b844-193965e97727">
<File Name="cacerts.pem" KeyPath="yes" />
</Component>
<ComponentGroupRef Id="Lib" />
</ComponentGroup>
</Fragment>
<Fragment>
<InstallUISequence>
<ExecuteAction />
<Show Dialog="WelcomeDlg" Before="ProgressDlg" />
</InstallUISequence>
</Fragment>
</Wix>

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# GAM
#
# Copyright 2023, All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import multiprocessing
import platform
import sys
import gam
def main():
gam.initializeLogging()
rc = gam.ProcessGAMCommand(sys.argv)
try:
sys.stdout.flush()
except (IOError, ValueError):
pass
sys.exit(rc)
# Run from command line
if __name__ == '__main__':
if getattr(sys, 'frozen', False): # we're frozen:
multiprocessing.freeze_support()
#if platform.system() == 'Linux':
# set explictly since it's not default in Python < 3.14, forkserver should
# be safer than fork and less likely to see bulk command hangs.
#multiprocessing.set_start_method('forkserver')
#else:
# Python 3.14.4 and PyInstaller 6.19.0 don't play nice with forkserver
# on Linux. For the time being use spawn everywhere.
multiprocessing.set_start_method('spawn')
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2009 Google Inc.
#
# Licensed under the Apache License 2.0;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This module is used for version 2 of the Google Data APIs.
# __author__ = 'j.s@google.com (Jeff Scudder)'
import base64
class BasicAuth(object):
"""Sets the Authorization header as defined in RFC1945"""
def __init__(self, user_id, password):
self.basic_cookie = base64.encodestring(
'%s:%s' % (user_id, password)).strip()
def modify_request(self, http_request):
http_request.headers['Authorization'] = 'Basic %s' % self.basic_cookie
ModifyRequest = modify_request
class NoAuth(object):
def modify_request(self, http_request):
pass

View File

@@ -1,214 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2009 Google Inc.
#
# Licensed under the Apache License 2.0;
"""AtomPubClient provides CRUD ops. in line with the Atom Publishing Protocol.
"""
# __author__ = 'j.s@google.com (Jeff Scudder)'
import atom.http_core
class Error(Exception):
pass
class MissingHost(Error):
pass
class AtomPubClient(object):
host = None
auth_token = None
ssl = False # Whether to force all requests over https
xoauth_requestor_id = None
def __init__(self, http_client=None, host=None, auth_token=None, source=None,
xoauth_requestor_id=None, **kwargs):
"""Creates a new AtomPubClient instance.
Args:
source: The name of your application.
http_client: An object capable of performing HTTP requests through a
request method. This object is used to perform the request
when the AtomPubClient's request method is called. Used to
allow HTTP requests to be directed to a mock server, or use
an alternate library instead of the default of httplib to
make HTTP requests.
host: str The default host name to use if a host is not specified in the
requested URI.
auth_token: An object which sets the HTTP Authorization header when its
modify_request method is called.
"""
self.http_client = http_client or atom.http_core.ProxiedHttpClient()
if host is not None:
self.host = host
if auth_token is not None:
self.auth_token = auth_token
self.xoauth_requestor_id = xoauth_requestor_id
self.source = source
def request(self, method=None, uri=None, auth_token=None,
http_request=None, **kwargs):
"""Performs an HTTP request to the server indicated.
Uses the http_client instance to make the request.
Args:
method: The HTTP method as a string, usually one of 'GET', 'POST',
'PUT', or 'DELETE'
uri: The URI desired as a string or atom.http_core.Uri.
http_request:
auth_token: An authorization token object whose modify_request method
sets the HTTP Authorization header.
Returns:
The results of calling self.http_client.request. With the default
http_client, this is an HTTP response object.
"""
# Modify the request based on the AtomPubClient settings and parameters
# passed in to the request.
http_request = self.modify_request(http_request)
if isinstance(uri, str):
uri = atom.http_core.Uri.parse_uri(uri)
if uri is not None:
uri.modify_request(http_request)
if isinstance(method, str):
http_request.method = method
# Any unrecognized arguments are assumed to be capable of modifying the
# HTTP request.
for name, value in kwargs.items():
if value is not None:
if hasattr(value, 'modify_request'):
value.modify_request(http_request)
else:
http_request.uri.query[name] = str(value)
# Default to an http request if the protocol scheme is not set.
if http_request.uri.scheme is None:
http_request.uri.scheme = 'http'
# Override scheme. Force requests over https.
if self.ssl:
http_request.uri.scheme = 'https'
if http_request.uri.path is None:
http_request.uri.path = '/'
# Add the Authorization header at the very end. The Authorization header
# value may need to be calculated using information in the request.
if auth_token:
auth_token.modify_request(http_request)
elif self.auth_token:
self.auth_token.modify_request(http_request)
# Check to make sure there is a host in the http_request.
if http_request.uri.host is None:
raise MissingHost('No host provided in request %s %s' % (
http_request.method, str(http_request.uri)))
# Perform the fully specified request using the http_client instance.
# Sends the request to the server and returns the server's response.
return self.http_client.request(http_request)
Request = request
def get(self, uri=None, auth_token=None, http_request=None, **kwargs):
"""Performs a request using the GET method, returns an HTTP response."""
return self.request(method='GET', uri=uri, auth_token=auth_token,
http_request=http_request, **kwargs)
Get = get
def post(self, uri=None, data=None, auth_token=None, http_request=None,
**kwargs):
"""Sends data using the POST method, returns an HTTP response."""
return self.request(method='POST', uri=uri, auth_token=auth_token,
http_request=http_request, data=data, **kwargs)
Post = post
def put(self, uri=None, data=None, auth_token=None, http_request=None,
**kwargs):
"""Sends data using the PUT method, returns an HTTP response."""
return self.request(method='PUT', uri=uri, auth_token=auth_token,
http_request=http_request, data=data, **kwargs)
Put = put
def delete(self, uri=None, auth_token=None, http_request=None, **kwargs):
"""Performs a request using the DELETE method, returns an HTTP response."""
return self.request(method='DELETE', uri=uri, auth_token=auth_token,
http_request=http_request, **kwargs)
Delete = delete
def modify_request(self, http_request):
"""Changes the HTTP request before sending it to the server.
Sets the User-Agent HTTP header and fills in the HTTP host portion
of the URL if one was not included in the request (for this it uses
the self.host member if one is set). This method is called in
self.request.
Args:
http_request: An atom.http_core.HttpRequest() (optional) If one is
not provided, a new HttpRequest is instantiated.
Returns:
An atom.http_core.HttpRequest() with the User-Agent header set and
if this client has a value in its host member, the host in the request
URL is set.
"""
if http_request is None:
http_request = atom.http_core.HttpRequest()
if self.host is not None and http_request.uri.host is None:
http_request.uri.host = self.host
if self.xoauth_requestor_id is not None:
http_request.uri.query['xoauth_requestor_id'] = self.xoauth_requestor_id
# Set the user agent header for logging purposes.
if self.source:
http_request.headers['User-Agent'] = '%s gdata-py/2.0.18' % self.source
else:
http_request.headers['User-Agent'] = 'gdata-py/2.0.17'
return http_request
ModifyRequest = modify_request
class CustomHeaders(object):
"""Add custom headers to an http_request.
Usage:
>>> custom_headers = atom.client.CustomHeaders(header1='value1',
header2='value2')
>>> client.get(uri, custom_headers=custom_headers)
"""
def __init__(self, **kwargs):
"""Creates a CustomHeaders instance.
Initialize the headers dictionary with the arguments list.
"""
self.headers = kwargs
def modify_request(self, http_request):
"""Changes the HTTP request before sending it to the server.
Adds the custom headers to the HTTP request.
Args:
http_request: An atom.http_core.HttpRequest().
Returns:
An atom.http_core.HttpRequest() with the added custom headers.
"""
for name, value in self.headers.items():
if value is not None:
http_request.headers[name] = value
return http_request

View File

@@ -1,535 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This module is used for version 2 of the Google Data APIs.
# __author__ = 'j.s@google.com (Jeff Scudder)'
import inspect
import lxml.etree as ElementTree
try:
from xml.dom.minidom import parseString as xmlString
except ImportError:
xmlString = None
STRING_ENCODING = 'utf-8'
class XmlElement(object):
"""Represents an element node in an XML document.
The text member is a UTF-8 encoded str or unicode.
"""
_qname = None
_other_elements = None
_other_attributes = None
# The rule set contains mappings for XML qnames to child members and the
# appropriate member classes.
_rule_set = None
_members = None
text = None
def __init__(self, text=None, *args, **kwargs):
if ('_members' not in self.__class__.__dict__
or self.__class__._members is None):
self.__class__._members = tuple(self.__class__._list_xml_members())
for member_name, member_type in self.__class__._members:
if member_name in kwargs:
setattr(self, member_name, kwargs[member_name])
else:
if isinstance(member_type, list):
setattr(self, member_name, [])
else:
setattr(self, member_name, None)
self._other_elements = []
self._other_attributes = {}
if text is not None:
self.text = text
def _list_xml_members(cls):
"""Generator listing all members which are XML elements or attributes.
The following members would be considered XML members:
foo = 'abc' - indicates an XML attribute with the qname abc
foo = SomeElement - indicates an XML child element
foo = [AnElement] - indicates a repeating XML child element, each instance
will be stored in a list in this member
foo = ('att1', '{http://example.com/namespace}att2') - indicates an XML
attribute which has different parsing rules in different versions of
the protocol. Version 1 of the XML parsing rules will look for an
attribute with the qname 'att1' but verion 2 of the parsing rules will
look for a namespaced attribute with the local name of 'att2' and an
XML namespace of 'http://example.com/namespace'.
"""
members = []
for pair in inspect.getmembers(cls):
if not pair[0].startswith('_') and pair[0] != 'text':
member_type = pair[1]
if (isinstance(member_type, tuple) or isinstance(member_type, list)
or isinstance(member_type, str)
or (inspect.isclass(member_type)
and issubclass(member_type, XmlElement))):
members.append(pair)
return members
_list_xml_members = classmethod(_list_xml_members)
def _get_rules(cls, version):
"""Initializes the _rule_set for the class which is used when parsing XML.
This method is used internally for parsing and generating XML for an
XmlElement. It is not recommended that you call this method directly.
Returns:
A tuple containing the XML parsing rules for the appropriate version.
The tuple looks like:
(qname, {sub_element_qname: (member_name, member_class, repeating), ..},
{attribute_qname: member_name})
To give a couple of concrete example, the atom.data.Control _get_rules
with version of 2 will return:
('{http://www.w3.org/2007/app}control',
{'{http://www.w3.org/2007/app}draft': ('draft',
<class 'atom.data.Draft'>,
False)},
{})
Calling _get_rules with version 1 on gdata.data.FeedLink will produce:
('{http://schemas.google.com/g/2005}feedLink',
{'{http://www.w3.org/2005/Atom}feed': ('feed',
<class 'gdata.data.GDFeed'>,
False)},
{'href': 'href', 'readOnly': 'read_only', 'countHint': 'count_hint',
'rel': 'rel'})
"""
# Initialize the _rule_set to make sure there is a slot available to store
# the parsing rules for this version of the XML schema.
# Look for rule set in the class __dict__ proxy so that only the
# _rule_set for this class will be found. By using the dict proxy
# we avoid finding rule_sets defined in superclasses.
# The four lines below provide support for any number of versions, but it
# runs a bit slower then hard coding slots for two versions, so I'm using
# the below two lines.
# if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
# cls._rule_set = []
# while len(cls.__dict__['_rule_set']) < version:
# cls._rule_set.append(None)
# If there is no rule set cache in the class, provide slots for two XML
# versions. If and when there is a version 3, this list will need to be
# expanded.
if '_rule_set' not in cls.__dict__ or cls._rule_set is None:
cls._rule_set = [None, None]
# If a version higher than 2 is requested, fall back to version 2 because
# 2 is currently the highest supported version.
if version > 2:
return cls._get_rules(2)
# Check the dict proxy for the rule set to avoid finding any rule sets
# which belong to the superclass. We only want rule sets for this class.
if cls._rule_set[version - 1] is None:
# The rule set for each version consists of the qname for this element
# ('{namespace}tag'), a dictionary (elements) for looking up the
# corresponding class member when given a child element's qname, and a
# dictionary (attributes) for looking up the corresponding class member
# when given an XML attribute's qname.
elements = {}
attributes = {}
if ('_members' not in cls.__dict__ or cls._members is None):
cls._members = tuple(cls._list_xml_members())
for member_name, target in cls._members:
if isinstance(target, list):
# This member points to a repeating element.
elements[_get_qname(target[0], version)] = (member_name, target[0],
True)
elif isinstance(target, tuple):
# This member points to a versioned XML attribute.
if version <= len(target):
attributes[target[version - 1]] = member_name
else:
attributes[target[-1]] = member_name
elif isinstance(target, str):
# This member points to an XML attribute.
attributes[target] = member_name
elif issubclass(target, XmlElement):
# This member points to a single occurance element.
elements[_get_qname(target, version)] = (member_name, target, False)
version_rules = (_get_qname(cls, version), elements, attributes)
cls._rule_set[version - 1] = version_rules
return version_rules
else:
return cls._rule_set[version - 1]
_get_rules = classmethod(_get_rules)
def get_elements(self, tag=None, namespace=None, version=1):
"""Find all sub elements which match the tag and namespace.
To find all elements in this object, call get_elements with the tag and
namespace both set to None (the default). This method searches through
the object's members and the elements stored in _other_elements which
did not match any of the XML parsing rules for this class.
Args:
tag: str
namespace: str
version: int Specifies the version of the XML rules to be used when
searching for matching elements.
Returns:
A list of the matching XmlElements.
"""
matches = []
ignored1, elements, ignored2 = self.__class__._get_rules(version)
if elements:
for qname, element_def in elements.items():
member = getattr(self, element_def[0])
if member:
if _qname_matches(tag, namespace, qname):
if element_def[2]:
# If this is a repeating element, copy all instances into the
# result list.
matches.extend(member)
else:
matches.append(member)
for element in self._other_elements:
if _qname_matches(tag, namespace, element._qname):
matches.append(element)
return matches
GetElements = get_elements
# FindExtensions and FindChildren are provided for backwards compatibility
# to the atom.AtomBase class.
# However, FindExtensions may return more results than the v1 atom.AtomBase
# method does, because get_elements searches both the expected children
# and the unexpected "other elements". The old AtomBase.FindExtensions
# method searched only "other elements" AKA extension_elements.
FindExtensions = get_elements
FindChildren = get_elements
def get_attributes(self, tag=None, namespace=None, version=1):
"""Find all attributes which match the tag and namespace.
To find all attributes in this object, call get_attributes with the tag
and namespace both set to None (the default). This method searches
through the object's members and the attributes stored in
_other_attributes which did not fit any of the XML parsing rules for this
class.
Args:
tag: str
namespace: str
version: int Specifies the version of the XML rules to be used when
searching for matching attributes.
Returns:
A list of XmlAttribute objects for the matching attributes.
"""
matches = []
ignored1, ignored2, attributes = self.__class__._get_rules(version)
if attributes:
for qname, attribute_def in attributes.items():
if isinstance(attribute_def, (list, tuple)):
attribute_def = attribute_def[0]
member = getattr(self, attribute_def)
# TODO: ensure this hasn't broken existing behavior.
# member = getattr(self, attribute_def[0])
if member:
if _qname_matches(tag, namespace, qname):
matches.append(XmlAttribute(qname, member))
for qname, value in self._other_attributes.items():
if _qname_matches(tag, namespace, qname):
matches.append(XmlAttribute(qname, value))
return matches
GetAttributes = get_attributes
def _harvest_tree(self, tree, version=1):
"""Populates object members from the data in the tree Element."""
qname, elements, attributes = self.__class__._get_rules(version)
for element in tree:
if elements and element.tag in elements:
definition = elements[element.tag]
# If this is a repeating element, make sure the member is set to a
# list.
if definition[2]:
if getattr(self, definition[0]) is None:
setattr(self, definition[0], [])
getattr(self, definition[0]).append(_xml_element_from_tree(element,
definition[1], version))
else:
setattr(self, definition[0], _xml_element_from_tree(element,
definition[1], version))
else:
self._other_elements.append(_xml_element_from_tree(element, XmlElement,
version))
for attrib, value in tree.attrib.items():
if attributes and attrib in attributes:
setattr(self, attributes[attrib], value)
else:
self._other_attributes[attrib] = value
if tree.text:
self.text = tree.text
def _to_tree(self, version=1):
new_tree = ElementTree.Element(_get_qname(self, version))
self._attach_members(new_tree, version)
return new_tree
def _attach_members(self, tree, version=1):
"""Convert members to XML elements/attributes and add them to the tree.
Args:
tree: An ElementTree.Element which will be modified. The members of
this object will be added as child elements or attributes
according to the rules described in _expected_elements and
_expected_attributes. The elements and attributes stored in
other_attributes and other_elements are also added a children
of this tree.
version: int Ingnored in this method but used by VersionedElement.
encoding: str (optional)
"""
qname, elements, attributes = self.__class__._get_rules(version)
encoding = STRING_ENCODING
# Add the expected elements and attributes to the tree.
if elements:
for tag, element_def in elements.items():
member = getattr(self, element_def[0])
# If this is a repeating element and there are members in the list.
if member and element_def[2]:
for instance in member:
instance._become_child(tree, version)
elif member:
member._become_child(tree, version)
if attributes:
for attribute_tag, member_name in attributes.items():
value = getattr(self, member_name)
if value:
tree.attrib[attribute_tag] = value
# Add the unexpected (other) elements and attributes to the tree.
for element in self._other_elements:
element._become_child(tree, version)
for key, value in self._other_attributes.items():
# I'm not sure if unicode can be used in the attribute name, so for now
# we assume the encoding is correct for the attribute name.
if not isinstance(value, str):
value = value.decode(encoding)
tree.attrib[key] = value
if self.text:
if isinstance(self.text, str):
tree.text = self.text
else:
tree.text = self.text.decode(encoding)
def to_string(self, version=1, encoding=None, pretty_print=None):
"""Converts this object to XML."""
tree_string = ElementTree.tostring(self._to_tree(version))
if pretty_print and xmlString is not None:
return xmlString(tree_string).toprettyxml()
return tree_string
ToString = to_string
def __str__(self):
return self.to_string()
def _become_child(self, tree, version=1):
"""Adds a child element to tree with the XML data in self."""
new_child = ElementTree.Element(_get_qname(self, version))
tree.append(new_child)
new_child.tag = _get_qname(self, version)
self._attach_members(new_child, version)
def __get_extension_elements(self):
return self._other_elements
def __set_extension_elements(self, elements):
self._other_elements = elements
extension_elements = property(__get_extension_elements,
__set_extension_elements,
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
def __get_extension_attributes(self):
return self._other_attributes
def __set_extension_attributes(self, attributes):
self._other_attributes = attributes
extension_attributes = property(__get_extension_attributes,
__set_extension_attributes,
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
def _get_tag(self, version=1):
qname = _get_qname(self, version)
if qname:
return qname[qname.find('}') + 1:]
return None
def _get_namespace(self, version=1):
qname = _get_qname(self, version)
if qname.startswith('{'):
return qname[1:qname.find('}')]
else:
return None
def _set_tag(self, tag):
if isinstance(self._qname, tuple):
self._qname = self._qname.copy()
if self._qname[0].startswith('{'):
self._qname[0] = '{%s}%s' % (self._get_namespace(1), tag)
else:
self._qname[0] = tag
else:
if self._qname is not None and self._qname.startswith('{'):
self._qname = '{%s}%s' % (self._get_namespace(), tag)
else:
self._qname = tag
def _set_namespace(self, namespace):
tag = self._get_tag(1)
if tag is None:
tag = ''
if isinstance(self._qname, tuple):
self._qname = self._qname.copy()
if namespace:
self._qname[0] = '{%s}%s' % (namespace, tag)
else:
self._qname[0] = tag
else:
if namespace:
self._qname = '{%s}%s' % (namespace, tag)
else:
self._qname = tag
tag = property(_get_tag, _set_tag,
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
namespace = property(_get_namespace, _set_namespace,
"""Provides backwards compatibility for v1 atom.AtomBase classes.""")
# Provided for backwards compatibility to atom.ExtensionElement
children = extension_elements
attributes = extension_attributes
def _get_qname(element, version):
if isinstance(element._qname, tuple):
if version <= len(element._qname):
return element._qname[version - 1]
else:
return element._qname[-1]
else:
return element._qname
def _qname_matches(tag, namespace, qname):
"""Logic determines if a QName matches the desired local tag and namespace.
This is used in XmlElement.get_elements and XmlElement.get_attributes to
find matches in the element's members (among all expected-and-unexpected
elements-and-attributes).
Args:
expected_tag: string
expected_namespace: string
qname: string in the form '{xml_namespace}localtag' or 'tag' if there is
no namespace.
Returns:
boolean True if the member's tag and namespace fit the expected tag and
namespace.
"""
# If there is no expected namespace or tag, then everything will match.
if qname is None:
member_tag = None
member_namespace = None
else:
if qname.startswith('{'):
member_namespace = qname[1:qname.index('}')]
member_tag = qname[qname.index('}') + 1:]
else:
member_namespace = None
member_tag = qname
return ((tag is None and namespace is None)
# If there is a tag, but no namespace, see if the local tag matches.
or (namespace is None and member_tag == tag)
# There was no tag, but there was a namespace so see if the namespaces
# match.
or (tag is None and member_namespace == namespace)
# There was no tag, and the desired elements have no namespace, so check
# to see that the member's namespace is None.
or (tag is None and namespace == ''
and member_namespace is None)
# The tag and the namespace both match.
or (tag == member_tag
and namespace == member_namespace)
# The tag matches, and the expected namespace is the empty namespace,
# check to make sure the member's namespace is None.
or (tag == member_tag and namespace == ''
and member_namespace is None))
def parse(xml_string, target_class=None, version=1):
"""Parses the XML string according to the rules for the target_class.
Args:
xml_string: bytes
target_class: XmlElement or a subclass. If None is specified, the
XmlElement class is used.
version: int (optional) The version of the schema which should be used when
converting the XML into an object. The default is 1.
encoding: str (optional) The character encoding of the bytes in the
xml_string. Default is 'UTF-8'.
"""
if target_class is None:
target_class = XmlElement
if not isinstance(xml_string, bytes):
raise Exception("This function only accepts bytes")
tree = ElementTree.fromstring(xml_string)
return _xml_element_from_tree(tree, target_class, version)
Parse = parse
xml_element_from_string = parse
XmlElementFromString = xml_element_from_string
def _xml_element_from_tree(tree, target_class, version=1):
if target_class._qname is None:
instance = target_class()
instance._qname = tree.tag
instance._harvest_tree(tree, version)
return instance
# TODO handle the namespace-only case
# Namespace only will be used with Google Spreadsheets rows and
# Google Base item attributes.
elif tree.tag == _get_qname(target_class, version):
instance = target_class()
instance._harvest_tree(tree, version)
return instance
return None
class XmlAttribute(object):
def __init__(self, qname, value):
self._qname = qname
self.value = value

View File

@@ -1,327 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2009 Google Inc.
#
# Licensed under the Apache License 2.0;
# This module is used for version 2 of the Google Data APIs.
# __author__ = 'j.s@google.com (Jeff Scudder)'
import atom.core
XML_TEMPLATE = '{http://www.w3.org/XML/1998/namespace}%s'
ATOM_TEMPLATE = '{http://www.w3.org/2005/Atom}%s'
APP_TEMPLATE_V1 = '{http://purl.org/atom/app#}%s'
APP_TEMPLATE_V2 = '{http://www.w3.org/2007/app}%s'
class Name(atom.core.XmlElement):
"""The atom:name element."""
_qname = ATOM_TEMPLATE % 'name'
class Email(atom.core.XmlElement):
"""The atom:email element."""
_qname = ATOM_TEMPLATE % 'email'
class Uri(atom.core.XmlElement):
"""The atom:uri element."""
_qname = ATOM_TEMPLATE % 'uri'
class Person(atom.core.XmlElement):
"""A foundation class which atom:author and atom:contributor extend.
A person contains information like name, email address, and web page URI for
an author or contributor to an Atom feed.
"""
name = Name
email = Email
uri = Uri
class Author(Person):
"""The atom:author element.
An author is a required element in Feed unless each Entry contains an Author.
"""
_qname = ATOM_TEMPLATE % 'author'
class Contributor(Person):
"""The atom:contributor element."""
_qname = ATOM_TEMPLATE % 'contributor'
class Link(atom.core.XmlElement):
"""The atom:link element."""
_qname = ATOM_TEMPLATE % 'link'
href = 'href'
rel = 'rel'
type = 'type'
hreflang = 'hreflang'
title = 'title'
length = 'length'
class Generator(atom.core.XmlElement):
"""The atom:generator element."""
_qname = ATOM_TEMPLATE % 'generator'
uri = 'uri'
version = 'version'
class Text(atom.core.XmlElement):
"""A foundation class from which atom:title, summary, etc. extend.
This class should never be instantiated.
"""
type = 'type'
class Title(Text):
"""The atom:title element."""
_qname = ATOM_TEMPLATE % 'title'
class Subtitle(Text):
"""The atom:subtitle element."""
_qname = ATOM_TEMPLATE % 'subtitle'
class Rights(Text):
"""The atom:rights element."""
_qname = ATOM_TEMPLATE % 'rights'
class Summary(Text):
"""The atom:summary element."""
_qname = ATOM_TEMPLATE % 'summary'
class Content(Text):
"""The atom:content element."""
_qname = ATOM_TEMPLATE % 'content'
src = 'src'
class Category(atom.core.XmlElement):
"""The atom:category element."""
_qname = ATOM_TEMPLATE % 'category'
term = 'term'
scheme = 'scheme'
label = 'label'
class Id(atom.core.XmlElement):
"""The atom:id element."""
_qname = ATOM_TEMPLATE % 'id'
class Icon(atom.core.XmlElement):
"""The atom:icon element."""
_qname = ATOM_TEMPLATE % 'icon'
class Logo(atom.core.XmlElement):
"""The atom:logo element."""
_qname = ATOM_TEMPLATE % 'logo'
class Draft(atom.core.XmlElement):
"""The app:draft element which indicates if this entry should be public."""
_qname = (APP_TEMPLATE_V1 % 'draft', APP_TEMPLATE_V2 % 'draft')
class Control(atom.core.XmlElement):
"""The app:control element indicating restrictions on publication.
The APP control element may contain a draft element indicating whether or
not this entry should be publicly available.
"""
_qname = (APP_TEMPLATE_V1 % 'control', APP_TEMPLATE_V2 % 'control')
draft = Draft
class Date(atom.core.XmlElement):
"""A parent class for atom:updated, published, etc."""
class Updated(Date):
"""The atom:updated element."""
_qname = ATOM_TEMPLATE % 'updated'
class Published(Date):
"""The atom:published element."""
_qname = ATOM_TEMPLATE % 'published'
class LinkFinder(object):
"""An "interface" providing methods to find link elements
Entry elements often contain multiple links which differ in the rel
attribute or content type. Often, developers are interested in a specific
type of link so this class provides methods to find specific classes of
links.
This class is used as a mixin in Atom entries and feeds.
"""
def find_url(self, rel):
"""Returns the URL (as a string) in a link with the desired rel value."""
for link in self.link:
if link.rel == rel and link.href:
return link.href
return None
FindUrl = find_url
def get_link(self, rel):
"""Returns a link object which has the desired rel value.
If you are interested in the URL instead of the link object,
consider using find_url instead.
"""
for link in self.link:
if link.rel == rel and link.href:
return link
return None
GetLink = get_link
def find_self_link(self):
"""Find the first link with rel set to 'self'
Returns:
A str containing the link's href or None if none of the links had rel
equal to 'self'
"""
return self.find_url('self')
FindSelfLink = find_self_link
def get_self_link(self):
return self.get_link('self')
GetSelfLink = get_self_link
def find_edit_link(self):
return self.find_url('edit')
FindEditLink = find_edit_link
def get_edit_link(self):
return self.get_link('edit')
GetEditLink = get_edit_link
def find_edit_media_link(self):
link = self.find_url('edit-media')
# Search for media-edit as well since Picasa API used media-edit instead.
if link is None:
return self.find_url('media-edit')
return link
FindEditMediaLink = find_edit_media_link
def get_edit_media_link(self):
link = self.get_link('edit-media')
if link is None:
return self.get_link('media-edit')
return link
GetEditMediaLink = get_edit_media_link
def find_next_link(self):
return self.find_url('next')
FindNextLink = find_next_link
def get_next_link(self):
return self.get_link('next')
GetNextLink = get_next_link
def find_license_link(self):
return self.find_url('license')
FindLicenseLink = find_license_link
def get_license_link(self):
return self.get_link('license')
GetLicenseLink = get_license_link
def find_alternate_link(self):
return self.find_url('alternate')
FindAlternateLink = find_alternate_link
def get_alternate_link(self):
return self.get_link('alternate')
GetAlternateLink = get_alternate_link
class FeedEntryParent(atom.core.XmlElement, LinkFinder):
"""A super class for atom:feed and entry, contains shared attributes"""
author = [Author]
category = [Category]
contributor = [Contributor]
id = Id
link = [Link]
rights = Rights
title = Title
updated = Updated
def __init__(self, atom_id=None, text=None, *args, **kwargs):
if atom_id is not None:
self.id = atom_id
atom.core.XmlElement.__init__(self, text=text, *args, **kwargs)
class Source(FeedEntryParent):
"""The atom:source element."""
_qname = ATOM_TEMPLATE % 'source'
generator = Generator
icon = Icon
logo = Logo
subtitle = Subtitle
class Entry(FeedEntryParent):
"""The atom:entry element."""
_qname = ATOM_TEMPLATE % 'entry'
content = Content
published = Published
source = Source
summary = Summary
control = Control
class Feed(Source):
"""The atom:feed element which contains entries."""
_qname = ATOM_TEMPLATE % 'feed'
entry = [Entry]
class ExtensionElement(atom.core.XmlElement):
"""Provided for backwards compatibility to the v1 atom.ExtensionElement."""
def __init__(self, tag=None, namespace=None, attributes=None,
children=None, text=None, *args, **kwargs):
if namespace:
self._qname = '{%s}%s' % (namespace, tag)
else:
self._qname = tag
self.children = children or []
self.attributes = attributes or {}
self.text = text
_BecomeChildElement = atom.core.XmlElement._become_child

View File

@@ -1,354 +0,0 @@
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
"""HttpClients in this module use httplib to make HTTP requests.
This module make HTTP requests based on httplib, but there are environments
in which an httplib based approach will not work (if running in Google App
Engine for example). In those cases, higher level classes (like AtomService
and GDataService) can swap out the HttpClient to transparently use a
different mechanism for making HTTP requests.
HttpClient: Contains a request method which performs an HTTP call to the
server.
ProxiedHttpClient: Contains a request method which connects to a proxy using
settings stored in operating system environment variables then
performs an HTTP call to the endpoint server.
"""
# __author__ = 'api.jscudder (Jeff Scudder)'
import base64
import http.client
import os
import socket
import atom.http_core
import atom.http_interface
import atom.url
ssl_imported = False
ssl = None
try:
import ssl
ssl_imported = True
except ImportError:
pass
class ProxyError(atom.http_interface.Error):
pass
class TestConfigurationError(Exception):
pass
DEFAULT_CONTENT_TYPE = 'application/atom+xml'
class HttpClient(atom.http_interface.GenericHttpClient):
# Added to allow old v1 HttpClient objects to use the new
# http_code.HttpClient. Used in unit tests to inject a mock client.
v2_http_client = None
def __init__(self, headers=None):
self.debug = False
self.headers = headers or {}
def request(self, operation, url, data=None, headers=None):
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
DELETE.
Usage example, perform and HTTP GET on http://www.google.com/:
import atom.http
client = atom.http.HttpClient()
http_response = client.request('GET', 'http://www.google.com/')
Args:
operation: str The HTTP operation to be performed. This is usually one
of 'GET', 'POST', 'PUT', or 'DELETE'
data: filestream, list of parts, or other object which can be converted
to a string. Should be set to None when performing a GET or DELETE.
If data is a file-like object which can be read, this method will
read a chunk of 100K bytes at a time and send them.
If the data is a list of parts to be sent, each part will be
evaluated and sent.
url: The full URL to which the request should be sent. Can be a string
or atom.url.Url.
headers: dict of strings. HTTP headers which should be sent
in the request.
"""
all_headers = self.headers.copy()
if headers:
all_headers.update(headers)
# If the list of headers does not include a Content-Length, attempt to
# calculate it based on the data object.
if data and 'Content-Length' not in all_headers:
if isinstance(data, (str,)):
all_headers['Content-Length'] = str(len(data))
else:
raise atom.http_interface.ContentLengthRequired('Unable to calculate '
'the length of the data parameter. Specify a value for '
'Content-Length')
# Set the content type to the default value if none was set.
if 'Content-Type' not in all_headers:
all_headers['Content-Type'] = DEFAULT_CONTENT_TYPE
if self.v2_http_client is not None:
http_request = atom.http_core.HttpRequest(method=operation)
atom.http_core.Uri.parse_uri(str(url)).modify_request(http_request)
http_request.headers = all_headers
if data:
http_request._body_parts.append(data)
return self.v2_http_client.request(http_request=http_request)
if not isinstance(url, atom.url.Url):
if isinstance(url, str):
url = atom.url.parse_url(url)
else:
raise atom.http_interface.UnparsableUrlObject('Unable to parse url parameter because it was not a string or atom.url.Url')
connection = self._prepare_connection(url, all_headers)
if self.debug:
connection.debuglevel = 1
connection.putrequest(operation, self._get_access_url(url), skip_host=True)
if url.port is not None:
connection.putheader('Host', '%s:%s' % (url.host, url.port))
else:
connection.putheader('Host', url.host)
# Overcome a bug in Python 2.4 and 2.5
# httplib.HTTPConnection.putrequest adding
# HTTP request header 'Host: www.google.com:443' instead of
# 'Host: www.google.com', and thus resulting the error message
# 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
if (url.protocol == 'https' and int(url.port or 443) == 443 and
hasattr(connection, '_buffer') and
isinstance(connection._buffer, list)):
header_line = 'Host: %s:443' % url.host
replacement_header_line = 'Host: %s' % url.host
try:
connection._buffer[connection._buffer.index(header_line)] = (
replacement_header_line)
except ValueError: # header_line missing from connection._buffer
pass
# Send the HTTP headers.
for header_name in all_headers:
connection.putheader(header_name, all_headers[header_name])
connection.endheaders()
# If there is data, send it in the request.
if data:
if isinstance(data, list):
for data_part in data:
_send_data_part(data_part, connection)
else:
_send_data_part(data, connection)
# Return the HTTP Response from the server.
return connection.getresponse()
def _prepare_connection(self, url, headers):
if not isinstance(url, atom.url.Url):
if isinstance(url, (str,)):
url = atom.url.parse_url(url)
else:
raise atom.http_interface.UnparsableUrlObject('Unable to parse url '
'parameter because it was not a string or atom.url.Url')
if url.protocol == 'https':
if not url.port:
return http.client.HTTPSConnection(url.host)
return http.client.HTTPSConnection(url.host, int(url.port))
else:
if not url.port:
return http.client.HTTPConnection(url.host)
return http.client.HTTPConnection(url.host, int(url.port))
def _get_access_url(self, url):
return url.to_string()
class ProxiedHttpClient(HttpClient):
"""Performs an HTTP request through a proxy.
The proxy settings are obtained from enviroment variables. The URL of the
proxy server is assumed to be stored in the environment variables
'https_proxy' and 'http_proxy' respectively. If the proxy server requires
a Basic Auth authorization header, the username and password are expected to
be in the 'proxy-username' or 'proxy_username' variable and the
'proxy-password' or 'proxy_password' variable, or in 'http_proxy' or
'https_proxy' as "protocol://[username:password@]host:port".
After connecting to the proxy server, the request is completed as in
HttpClient.request.
"""
def _prepare_connection(self, url, headers):
proxy_settings = os.environ.get('%s_proxy' % url.protocol)
if not proxy_settings:
# The request was HTTP or HTTPS, but there was no appropriate proxy set.
return HttpClient._prepare_connection(self, url, headers)
else:
proxy_auth = _get_proxy_auth(proxy_settings)
proxy_netloc = _get_proxy_net_location(proxy_settings)
if url.protocol == 'https':
# Set any proxy auth headers
if proxy_auth:
proxy_auth = 'Proxy-Authorization: %s' % proxy_auth
# Construct the proxy connect command.
port = url.port
if not port:
port = '443'
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (url.host, port)
# Set the user agent to send to the proxy
if headers and 'User-Agent' in headers:
user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
else:
user_agent = 'User-Agent: python\r\n'
proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
# Find the proxy host and port.
proxy_url = atom.url.parse_url(proxy_netloc)
if not proxy_url.port:
proxy_url.port = '80'
# Connect to the proxy server, very simple recv and error checking
p_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p_sock.connect((proxy_url.host, int(proxy_url.port)))
#p_sock.sendall(proxy_pieces)
p_sock.sendall(proxy_pieces.encode('utf-8'))
response = ''
# Wait for the full response.
while response.find("\r\n\r\n") == -1:
#response += p_sock.recv(8192)
response += p_sock.recv(8192).decode('utf-8')
p_status = response.split()[1]
if p_status != str(200):
raise ProxyError('Error status=%s' % str(p_status))
# Trivial setup for ssl socket.
sslobj = None
if ssl_imported:
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.minimum_version = ssl.TLSVersion.TLSv1_2
sslobj = context.wrap_socket(p_sock, server_hostname=url.host)
else:
sock_ssl = socket.ssl(p_sock, None, None)
sslobj = http.client.FakeSocket(p_sock, sock_ssl)
# Initalize httplib and replace with the proxy socket.
connection = http.client.HTTPConnection(proxy_url.host)
connection.sock = sslobj
return connection
else:
# If protocol was not https.
# Find the proxy host and port.
proxy_url = atom.url.parse_url(proxy_netloc)
if not proxy_url.port:
proxy_url.port = '80'
if proxy_auth:
headers['Proxy-Authorization'] = proxy_auth.strip()
return http.client.HTTPConnection(proxy_url.host, int(proxy_url.port))
def _get_access_url(self, url):
return url.to_string()
def _get_proxy_auth(proxy_settings):
"""Returns proxy authentication string for header.
Will check environment variables for proxy authentication info, starting with
proxy(_/-)username and proxy(_/-)password before checking the given
proxy_settings for a [protocol://]username:password@host[:port] string.
Args:
proxy_settings: String from http_proxy or https_proxy environment variable.
Returns:
Authentication string for proxy, or empty string if no proxy username was
found.
"""
proxy_username = None
proxy_password = None
proxy_username = os.environ.get('proxy-username')
if not proxy_username:
proxy_username = os.environ.get('proxy_username')
proxy_password = os.environ.get('proxy-password')
if not proxy_password:
proxy_password = os.environ.get('proxy_password')
if not proxy_username:
if '@' in proxy_settings:
protocol_and_proxy_auth = proxy_settings.split('@')[0].split(':')
if len(protocol_and_proxy_auth) == 3:
# 3 elements means we have [<protocol>, //<user>, <password>]
proxy_username = protocol_and_proxy_auth[1].lstrip('/')
proxy_password = protocol_and_proxy_auth[2]
elif len(protocol_and_proxy_auth) == 2:
# 2 elements means we have [<user>, <password>]
proxy_username = protocol_and_proxy_auth[0]
proxy_password = protocol_and_proxy_auth[1]
if proxy_username:
user_auth = base64.b64encode(('%s:%s' % (proxy_username, proxy_password)).encode('utf-8'))
return 'Basic %s\r\n' % (user_auth.strip().decode('utf-8'))
else:
return ''
def _get_proxy_net_location(proxy_settings):
"""Returns proxy host and port.
Args:
proxy_settings: String from http_proxy or https_proxy environment variable.
Must be in the form of protocol://[username:password@]host:port
Returns:
String in the form of protocol://host:port
"""
if '@' in proxy_settings:
protocol = proxy_settings.split(':')[0]
netloc = proxy_settings.split('@')[1]
return '%s://%s' % (protocol, netloc)
else:
return proxy_settings
def _send_data_part(data, connection):
if isinstance(data, (str,)):
connection.send(data)
return
# Check to see if data is a file-like object that has a read method.
elif hasattr(data, 'read'):
# Read the file and send it a chunk at a time.
while 1:
binarydata = data.read(100000)
if binarydata == b'': break
connection.send(binarydata)
return
else:
# The data object was not a file.
# Try to convert to a string and send the data.
#connection.send(str(data))
connection.send(str(data).encode('utf-8'))
return

View File

@@ -1,599 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2009 Google Inc.
#
# Licensed under the Apache License 2.0;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This module is used for version 2 of the Google Data APIs.
# TODO: add proxy handling.
# __author__ = 'j.s@google.com (Jeff Scudder)'
import http.client
import io
import os
import urllib.error
import urllib.parse
import urllib.request
ssl = None
try:
import ssl
except ImportError:
pass
class Error(Exception):
pass
class UnknownSize(Error):
pass
class ProxyError(Error):
pass
MIME_BOUNDARY = 'END_OF_PART'
def get_headers(http_response):
"""Retrieves all HTTP headers from an HTTP response from the server.
This method is provided for backwards compatibility for Python2.2 and 2.3.
The httplib.HTTPResponse object in 2.2 and 2.3 does not have a getheaders
method so this function will use getheaders if available, but if not it
will retrieve a few using getheader.
"""
if hasattr(http_response, 'getheaders'):
return http_response.getheaders()
else:
headers = []
for header in (
'location', 'content-type', 'content-length', 'age', 'allow',
'cache-control', 'content-location', 'content-encoding', 'date',
'etag', 'expires', 'last-modified', 'pragma', 'server',
'set-cookie', 'transfer-encoding', 'vary', 'via', 'warning',
'www-authenticate', 'gdata-version'):
value = http_response.getheader(header, None)
if value is not None:
headers.append((header, value))
return headers
class HttpRequest(object):
"""Contains all of the parameters for an HTTP 1.1 request.
The HTTP headers are represented by a dictionary, and it is the
responsibility of the user to ensure that duplicate field names are combined
into one header value according to the rules in section 4.2 of RFC 2616.
"""
method = None
uri = None
def __init__(self, uri=None, method=None, headers=None):
"""Construct an HTTP request.
Args:
uri: The full path or partial path as a Uri object or a string.
method: The HTTP method for the request, examples include 'GET', 'POST',
etc.
headers: dict of strings The HTTP headers to include in the request.
"""
self.headers = headers or {}
self._body_parts = []
if method is not None:
self.method = method
if isinstance(uri, str):
uri = Uri.parse_uri(uri)
self.uri = uri or Uri()
def add_body_part(self, data, mime_type, size=None):
"""Adds data to the HTTP request body.
If more than one part is added, this is assumed to be a mime-multipart
request. This method is designed to create MIME 1.0 requests as specified
in RFC 1341.
Args:
data: str or a file-like object containing a part of the request body.
mime_type: str The MIME type describing the data
size: int Required if the data is a file like object. If the data is a
string, the size is calculated so this parameter is ignored.
"""
if hasattr(data, '__len__'):
size = len(data)
if size is None:
# TODO: support chunked transfer if some of the body is of unknown size.
raise UnknownSize('Each part of the body must have a known size.')
if 'Content-Length' in self.headers:
content_length = int(self.headers['Content-Length'])
else:
content_length = 0
# If this is the first part added to the body, then this is not a multipart
# request.
if len(self._body_parts) == 0:
self.headers['Content-Type'] = mime_type
content_length = size
self._body_parts.append(data)
elif len(self._body_parts) == 1:
# This is the first member in a mime-multipart request, so change the
# _body_parts list to indicate a multipart payload.
self._body_parts.insert(0, 'Media multipart posting')
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
content_length += len(boundary_string) + size
self._body_parts.insert(1, boundary_string)
content_length += len('Media multipart posting')
# Put the content type of the first part of the body into the multipart
# payload.
original_type_string = 'Content-Type: %s\r\n\r\n' % (
self.headers['Content-Type'],)
self._body_parts.insert(2, original_type_string)
content_length += len(original_type_string)
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
self._body_parts.append(boundary_string)
content_length += len(boundary_string)
# Change the headers to indicate this is now a mime multipart request.
self.headers['Content-Type'] = 'multipart/related; boundary="%s"' % (
MIME_BOUNDARY,)
self.headers['MIME-version'] = '1.0'
# Include the mime type of this part.
type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
self._body_parts.append(type_string)
content_length += len(type_string)
self._body_parts.append(data)
ending_boundary_string = '\r\n--%s--' % (MIME_BOUNDARY,)
self._body_parts.append(ending_boundary_string)
content_length += len(ending_boundary_string)
else:
# This is a mime multipart request.
boundary_string = '\r\n--%s\r\n' % (MIME_BOUNDARY,)
self._body_parts.insert(-1, boundary_string)
content_length += len(boundary_string) + size
# Include the mime type of this part.
type_string = 'Content-Type: %s\r\n\r\n' % (mime_type)
self._body_parts.insert(-1, type_string)
content_length += len(type_string)
self._body_parts.insert(-1, data)
self.headers['Content-Length'] = str(content_length)
# I could add an "append_to_body_part" method as well.
AddBodyPart = add_body_part
def add_form_inputs(self, form_data,
mime_type='application/x-www-form-urlencoded'):
"""Form-encodes and adds data to the request body.
Args:
form_data: dict or sequnce or two member tuples which contains the
form keys and values.
mime_type: str The MIME type of the form data being sent. Defaults
to 'application/x-www-form-urlencoded'.
"""
body = urllib.parse.urlencode(form_data)
self.add_body_part(body, bytes(mime_type, 'ascii'))
AddFormInputs = add_form_inputs
def _copy(self):
"""Creates a deep copy of this request."""
copied_uri = Uri(self.uri.scheme, self.uri.host, self.uri.port,
self.uri.path, self.uri.query.copy())
new_request = HttpRequest(uri=copied_uri, method=self.method,
headers=self.headers.copy())
new_request._body_parts = self._body_parts[:]
return new_request
def _dump(self):
"""Converts to a printable string for debugging purposes.
In order to preserve the request, it does not read from file-like objects
in the body.
"""
output = 'HTTP Request\n method: %s\n url: %s\n headers:\n' % (
self.method, str(self.uri))
for header, value in self.headers.items():
output += ' %s: %s\n' % (header, value)
output += ' body sections:\n'
i = 0
for part in self._body_parts:
if isinstance(part, str):
output += ' %s: %s\n' % (i, part)
else:
output += ' %s: <file like object>\n' % i
i += 1
return output
def _apply_defaults(http_request):
if http_request.uri.scheme is None:
if http_request.uri.port == 443:
http_request.uri.scheme = 'https'
else:
http_request.uri.scheme = 'http'
class Uri(object):
"""A URI as used in HTTP 1.1"""
scheme = None
host = None
port = None
path = None
def __init__(self, scheme=None, host=None, port=None, path=None, query=None):
"""Constructor for a URI.
Args:
scheme: str This is usually 'http' or 'https'.
host: str The host name or IP address of the desired server.
post: int The server's port number.
path: str The path of the resource following the host. This begins with
a /, example: '/calendar/feeds/default/allcalendars/full'
query: dict of strings The URL query parameters. The keys and values are
both escaped so this dict should contain the unescaped values.
For example {'my key': 'val', 'second': '!!!'} will become
'?my+key=val&second=%21%21%21' which is appended to the path.
"""
self.query = query or {}
if scheme is not None:
self.scheme = scheme
if host is not None:
self.host = host
if port is not None:
self.port = port
if path:
self.path = path
def _get_query_string(self):
param_pairs = []
for key, value in self.query.items():
quoted_key = urllib.parse.quote_plus(str(key))
if value is None:
param_pairs.append(quoted_key)
else:
quoted_value = urllib.parse.quote_plus(str(value))
param_pairs.append('%s=%s' % (quoted_key, quoted_value))
return '&'.join(param_pairs)
def _get_relative_path(self):
"""Returns the path with the query parameters escaped and appended."""
param_string = self._get_query_string()
if self.path is None:
path = '/'
else:
path = self.path
if param_string:
return '?'.join([path, param_string])
else:
return path
def _to_string(self):
if self.scheme is None and self.port == 443:
scheme = 'https'
elif self.scheme is None:
scheme = 'http'
else:
scheme = self.scheme
if self.path is None:
path = '/'
else:
path = self.path
if self.port is None:
return '%s://%s%s' % (scheme, self.host, self._get_relative_path())
else:
return '%s://%s:%s%s' % (scheme, self.host, str(self.port),
self._get_relative_path())
def __str__(self):
return self._to_string()
def modify_request(self, http_request=None):
"""Sets HTTP request components based on the URI."""
if http_request is None:
http_request = HttpRequest()
if http_request.uri is None:
http_request.uri = Uri()
# Determine the correct scheme.
if self.scheme:
http_request.uri.scheme = self.scheme
if self.port:
http_request.uri.port = self.port
if self.host:
http_request.uri.host = self.host
# Set the relative uri path
if self.path:
http_request.uri.path = self.path
if self.query:
http_request.uri.query = self.query.copy()
return http_request
ModifyRequest = modify_request
def parse_uri(uri_string):
"""Creates a Uri object which corresponds to the URI string.
This method can accept partial URIs, but it will leave missing
members of the Uri unset.
"""
parts = urllib.parse.urlparse(uri_string)
uri = Uri()
if parts[0]:
uri.scheme = parts[0]
if parts[1]:
host_parts = parts[1].split(':')
if host_parts[0]:
uri.host = host_parts[0]
if len(host_parts) > 1:
uri.port = int(host_parts[1])
if parts[2]:
uri.path = parts[2]
if parts[4]:
param_pairs = parts[4].split('&')
for pair in param_pairs:
pair_parts = pair.split('=')
if len(pair_parts) > 1:
uri.query[urllib.parse.unquote_plus(pair_parts[0])] = (
urllib.parse.unquote_plus(pair_parts[1]))
elif len(pair_parts) == 1:
uri.query[urllib.parse.unquote_plus(pair_parts[0])] = None
return uri
parse_uri = staticmethod(parse_uri)
ParseUri = parse_uri
parse_uri = Uri.parse_uri
ParseUri = Uri.parse_uri
class HttpResponse(object):
status = None
reason = None
_body = None
def __init__(self, status=None, reason=None, headers=None, body=None):
self._headers = headers or {}
if status is not None:
self.status = status
if reason is not None:
self.reason = reason
if body is not None:
if hasattr(body, 'read'):
self._body = body
else:
self._body = io.StringIO(body)
def getheader(self, name, default=None):
if name in self._headers:
return self._headers[name]
else:
return default
def getheaders(self):
return self._headers
def read(self, amt=None):
if self._body is None:
return None
if not amt:
return self._body.read()
else:
return self._body.read(amt)
def _dump_response(http_response):
"""Converts to a string for printing debug messages.
Does not read the body since that may consume the content.
"""
output = 'HttpResponse\n status: %s\n reason: %s\n headers:' % (
http_response.status, http_response.reason)
headers = get_headers(http_response)
if isinstance(headers, dict):
for header, value in headers.items():
output += ' %s: %s\n' % (header, value)
else:
for pair in headers:
output += ' %s: %s\n' % (pair[0], pair[1])
return output
class HttpClient(object):
"""Performs HTTP requests using httplib."""
debug = None
def request(self, http_request):
return self._http_request(http_request.method, http_request.uri,
http_request.headers, http_request._body_parts)
Request = request
def _get_connection(self, uri, headers=None):
"""Opens a socket connection to the server to set up an HTTP request.
Args:
uri: The full URL for the request as a Uri object.
headers: A dict of string pairs containing the HTTP headers for the
request.
"""
connection = None
if uri.scheme == 'https':
if not uri.port:
connection = http.client.HTTPSConnection(uri.host)
else:
connection = http.client.HTTPSConnection(uri.host, int(uri.port))
else:
if not uri.port:
connection = http.client.HTTPConnection(uri.host)
else:
connection = http.client.HTTPConnection(uri.host, int(uri.port))
return connection
def _http_request(self, method, uri, headers=None, body_parts=None):
"""Makes an HTTP request using httplib.
Args:
method: str example: 'GET', 'POST', 'PUT', 'DELETE', etc.
uri: str or atom.http_core.Uri
headers: dict of strings mapping to strings which will be sent as HTTP
headers in the request.
body_parts: list of strings, objects with a read method, or objects
which can be converted to strings using str. Each of these
will be sent in order as the body of the HTTP request.
"""
if isinstance(uri, str):
uri = Uri.parse_uri(uri)
connection = self._get_connection(uri, headers=headers)
if self.debug:
connection.debuglevel = 1
if connection.host != uri.host:
connection.putrequest(method, str(uri))
else:
connection.putrequest(method, uri._get_relative_path())
# Overcome a bug in Python 2.4 and 2.5
# httplib.HTTPConnection.putrequest adding
# HTTP request header 'Host: www.google.com:443' instead of
# 'Host: www.google.com', and thus resulting the error message
# 'Token invalid - AuthSub token has wrong scope' in the HTTP response.
if (uri.scheme == 'https' and int(uri.port or 443) == 443 and
hasattr(connection, '_buffer') and
isinstance(connection._buffer, list)):
header_line = 'Host: %s:443' % uri.host
replacement_header_line = 'Host: %s' % uri.host
try:
connection._buffer[connection._buffer.index(header_line)] = (
replacement_header_line)
except ValueError: # header_line missing from connection._buffer
pass
# Send the HTTP headers.
for header_name, value in headers.items():
connection.putheader(header_name, value)
connection.endheaders()
# If there is data, send it in the request.
if body_parts and [x for x in body_parts if x != '']:
for part in body_parts:
_send_data_part(part, connection)
# Return the HTTP Response from the server.
return connection.getresponse()
def _send_data_part(data, connection):
if isinstance(data, str):
# I might want to just allow str, not unicode.
connection.send(data.encode())
# Check to see if data is a file-like object that has a read method.
elif hasattr(data, 'read'):
# Read the file and send it a chunk at a time.
while 1:
binarydata = data.read(100000)
if binarydata == '': break
connection.send(binarydata)
else:
# The data object was not a file.
# Try to convert to a string and send the data.
connection.send(data)
class ProxiedHttpClient(HttpClient):
def _get_connection(self, uri, headers=None):
# Check to see if there are proxy settings required for this request.
proxy = None
if uri.scheme == 'https':
proxy = os.environ.get('https_proxy')
elif uri.scheme == 'http':
proxy = os.environ.get('http_proxy')
if not proxy:
return HttpClient._get_connection(self, uri, headers=headers)
# Now we have the URL of the appropriate proxy server.
# Get a username and password for the proxy if required.
proxy_auth = _get_proxy_auth()
if uri.scheme == 'https':
import socket
if proxy_auth:
proxy_auth = 'Proxy-Authorization: %s' % proxy_auth
# Construct the proxy connect command.
port = uri.port
if not port:
port = 443
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (uri.host, port)
# Set the user agent to send to the proxy
user_agent = ''
if headers and 'User-Agent' in headers:
user_agent = 'User-Agent: %s\r\n' % (headers['User-Agent'])
proxy_pieces = '%s%s%s\r\n' % (proxy_connect, proxy_auth, user_agent)
# Find the proxy host and port.
proxy_uri = Uri.parse_uri(proxy)
if not proxy_uri.port:
proxy_uri.port = '80'
# Connect to the proxy server, very simple recv and error checking
p_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p_sock.connect((proxy_uri.host, int(proxy_uri.port)))
#p_sock.sendall(proxy_pieces)
p_sock.sendall(proxy_pieces.encode('utf-8'))
response = ''
# Wait for the full response.
while response.find("\r\n\r\n") == -1:
#response += p_sock.recv(8192)
response += p_sock.recv(8192).decode('utf-8')
p_status = response.split()[1]
if p_status != str(200):
raise ProxyError('Error status=%s' % str(p_status))
# Trivial setup for ssl socket.
sslobj = None
if ssl is not None:
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.minimum_version = ssl.TLSVersion.TLSv1_2
sslobj = context.wrap_socket(p_sock, server_hostname=uri.host)
else:
sock_ssl = socket.ssl(p_sock, None, None)
sslobj = http.client.FakeSocket(p_sock, sock_ssl)
# Initalize httplib and replace with the proxy socket.
connection = http.client.HTTPConnection(proxy_uri.host)
connection.sock = sslobj
return connection
elif uri.scheme == 'http':
proxy_uri = Uri.parse_uri(proxy)
if not proxy_uri.port:
proxy_uri.port = '80'
if proxy_auth:
headers['Proxy-Authorization'] = proxy_auth.strip()
return http.client.HTTPConnection(proxy_uri.host, int(proxy_uri.port))
return None
def _get_proxy_auth():
import base64
proxy_username = os.environ.get('proxy-username')
if not proxy_username:
proxy_username = os.environ.get('proxy_username')
proxy_password = os.environ.get('proxy-password')
if not proxy_password:
proxy_password = os.environ.get('proxy_password')
if proxy_username:
user_auth = base64.b64encode(('%s:%s' % (proxy_username, proxy_password)).encode('utf-8'))
return 'Basic %s\r\n' % (user_auth.strip().decode('utf-8'))
else:
return ''

View File

@@ -1,144 +0,0 @@
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
"""This module provides a common interface for all HTTP requests.
HttpResponse: Represents the server's response to an HTTP request. Provides
an interface identical to httplib.HTTPResponse which is the response
expected from higher level classes which use HttpClient.request.
GenericHttpClient: Provides an interface (superclass) for an object
responsible for making HTTP requests. Subclasses of this object are
used in AtomService and GDataService to make requests to the server. By
changing the http_client member object, the AtomService is able to make
HTTP requests using different logic (for example, when running on
Google App Engine, the http_client makes requests using the App Engine
urlfetch API).
"""
# __author__ = 'api.jscudder (Jeff Scudder)'
import io
USER_AGENT = '%s GData-Python/2.0.18'
class Error(Exception):
pass
class UnparsableUrlObject(Error):
pass
class ContentLengthRequired(Error):
pass
class HttpResponse(object):
def __init__(self, body=None, status=None, reason=None, headers=None):
"""Constructor for an HttpResponse object.
HttpResponse represents the server's response to an HTTP request from
the client. The HttpClient.request method returns a httplib.HTTPResponse
object and this HttpResponse class is designed to mirror the interface
exposed by httplib.HTTPResponse.
Args:
body: A file like object, with a read() method. The body could also
be a string, and the constructor will wrap it so that
HttpResponse.read(self) will return the full string.
status: The HTTP status code as an int. Example: 200, 201, 404.
reason: The HTTP status message which follows the code. Example:
OK, Created, Not Found
headers: A dictionary containing the HTTP headers in the server's
response. A common header in the response is Content-Length.
"""
if body:
if hasattr(body, 'read'):
self._body = body
else:
self._body = io.StringIO(body)
else:
self._body = None
if status is not None:
self.status = int(status)
else:
self.status = None
self.reason = reason
self._headers = headers or {}
def getheader(self, name, default=None):
if name in self._headers:
return self._headers[name]
else:
return default
def read(self, amt=None):
if not amt:
return self._body.read()
else:
return self._body.read(amt)
class GenericHttpClient(object):
debug = False
def __init__(self, http_client, headers=None):
"""
Args:
http_client: An object which provides a request method to make an HTTP
request. The request method in GenericHttpClient performs a
call-through to the contained HTTP client object.
headers: A dictionary containing HTTP headers which should be included
in every HTTP request. Common persistent headers include
'User-Agent'.
"""
self.http_client = http_client
self.headers = headers or {}
def request(self, operation, url, data=None, headers=None):
all_headers = self.headers.copy()
if headers:
all_headers.update(headers)
return self.http_client.request(operation, url, data=data,
headers=all_headers)
def get(self, url, headers=None):
return self.request('GET', url, headers=headers)
def post(self, url, data, headers=None):
return self.request('POST', url, data=data, headers=headers)
def put(self, url, data, headers=None):
return self.request('PUT', url, data=data, headers=headers)
def delete(self, url, headers=None):
return self.request('DELETE', url, headers=headers)
class GenericToken(object):
"""Represents an Authorization token to be added to HTTP requests.
Some Authorization headers included calculated fields (digital
signatures for example) which are based on the parameters of the HTTP
request. Therefore the token is responsible for signing the request
and adding the Authorization header.
"""
def perform_request(self, http_client, operation, url, data=None,
headers=None):
"""For the GenericToken, no Authorization token is set."""
return http_client.request(operation, url, data=data, headers=headers)
def valid_for_scope(self, url):
"""Tells the caller if the token authorizes access to the desired URL.
Since the generic token doesn't add an auth header, it is not valid for
any scope.
"""
return False

View File

@@ -1,123 +0,0 @@
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
# __author__ = 'api.jscudder (Jeff Scudder)'
import atom.http_interface
import atom.url
class Error(Exception):
pass
class NoRecordingFound(Error):
pass
class MockRequest(object):
"""Holds parameters of an HTTP request for matching against future requests.
"""
def __init__(self, operation, url, data=None, headers=None):
self.operation = operation
if isinstance(url, str):
url = atom.url.parse_url(url)
self.url = url
self.data = data
self.headers = headers
class MockResponse(atom.http_interface.HttpResponse):
"""Simulates an httplib.HTTPResponse object."""
def __init__(self, body=None, status=None, reason=None, headers=None):
if body and hasattr(body, 'read'):
self.body = body.read()
else:
self.body = body
if status is not None:
self.status = int(status)
else:
self.status = None
self.reason = reason
self._headers = headers or {}
def read(self):
return self.body
class MockHttpClient(atom.http_interface.GenericHttpClient):
def __init__(self, headers=None, recordings=None, real_client=None):
"""An HttpClient which responds to request with stored data.
The request-response pairs are stored as tuples in a member list named
recordings.
The MockHttpClient can be switched from replay mode to record mode by
setting the real_client member to an instance of an HttpClient which will
make real HTTP requests and store the server's response in list of
recordings.
Args:
headers: dict containing HTTP headers which should be included in all
HTTP requests.
recordings: The initial recordings to be used for responses. This list
contains tuples in the form: (MockRequest, MockResponse)
real_client: An HttpClient which will make a real HTTP request. The
response will be converted into a MockResponse and stored in
recordings.
"""
self.recordings = recordings or []
self.real_client = real_client
self.headers = headers or {}
def add_response(self, response, operation, url, data=None, headers=None):
"""Adds a request-response pair to the recordings list.
After the recording is added, future matching requests will receive the
response.
Args:
response: MockResponse
operation: str
url: str
data: str, Currently the data is ignored when looking for matching
requests.
headers: dict of strings: Currently the headers are ignored when
looking for matching requests.
"""
request = MockRequest(operation, url, data=data, headers=headers)
self.recordings.append((request, response))
def request(self, operation, url, data=None, headers=None):
"""Returns a matching MockResponse from the recordings.
If the real_client is set, the request will be passed along and the
server's response will be added to the recordings and also returned.
If there is no match, a NoRecordingFound error will be raised.
"""
if self.real_client is None:
if isinstance(url, str):
url = atom.url.parse_url(url)
for recording in self.recordings:
if recording[0].operation == operation and recording[0].url == url:
return recording[1]
raise NoRecordingFound('No recordings found for %s %s' % (
operation, url))
else:
# There is a real HTTP client, so make the request, and record the
# response.
response = self.real_client.request(operation, url, data=data,
headers=headers)
# TODO: copy the headers
stored_response = MockResponse(body=response, status=response.status,
reason=response.reason)
self.add_response(stored_response, operation, url, data=data,
headers=headers)
return stored_response

View File

@@ -1,313 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2009 Google Inc.
#
# Licensed under the Apache License 2.0;
# This module is used for version 2 of the Google Data APIs.
# __author__ = 'j.s@google.com (Jeff Scudder)'
import io
import os.path
import pickle
import tempfile
import atom.http_core
class Error(Exception):
pass
class NoRecordingFound(Error):
pass
class MockHttpClient(object):
debug = None
real_client = None
last_request_was_live = False
# The following members are used to construct the session cache temp file
# name.
# These are combined to form the file name
# /tmp/cache_prefix.cache_case_name.cache_test_name
cache_name_prefix = 'gdata_live_test'
cache_case_name = ''
cache_test_name = ''
def __init__(self, recordings=None, real_client=None):
self._recordings = recordings or []
if real_client is not None:
self.real_client = real_client
def add_response(self, http_request, status, reason, headers=None,
body=None):
response = MockHttpResponse(status, reason, headers, body)
# TODO Scrub the request and the response.
self._recordings.append((http_request._copy(), response))
AddResponse = add_response
def request(self, http_request):
"""Provide a recorded response, or record a response for replay.
If the real_client is set, the request will be made using the
real_client, and the response from the server will be recorded.
If the real_client is None (the default), this method will examine
the recordings and find the first which matches.
"""
request = http_request._copy()
_scrub_request(request)
if self.real_client is None:
self.last_request_was_live = False
for recording in self._recordings:
if _match_request(recording[0], request):
return recording[1]
else:
# Pass along the debug settings to the real client.
self.real_client.debug = self.debug
# Make an actual request since we can use the real HTTP client.
self.last_request_was_live = True
response = self.real_client.request(http_request)
scrubbed_response = _scrub_response(response)
self.add_response(request, scrubbed_response.status,
scrubbed_response.reason,
dict(atom.http_core.get_headers(scrubbed_response)),
scrubbed_response.read())
# Return the recording which we just added.
return self._recordings[-1][1]
raise NoRecordingFound('No recoding was found for request: %s %s' % (
request.method, str(request.uri)))
Request = request
def _save_recordings(self, filename):
recording_file = open(os.path.join(tempfile.gettempdir(), filename),
'wb')
pickle.dump(self._recordings, recording_file)
recording_file.close()
def _load_recordings(self, filename):
recording_file = open(os.path.join(tempfile.gettempdir(), filename),
'rb')
self._recordings = pickle.load(recording_file)
recording_file.close()
def _delete_recordings(self, filename):
full_path = os.path.join(tempfile.gettempdir(), filename)
if os.path.exists(full_path):
os.remove(full_path)
def _load_or_use_client(self, filename, http_client):
if os.path.exists(os.path.join(tempfile.gettempdir(), filename)):
self._load_recordings(filename)
else:
self.real_client = http_client
def use_cached_session(self, name=None, real_http_client=None):
"""Attempts to load recordings from a previous live request.
If a temp file with the recordings exists, then it is used to fulfill
requests. If the file does not exist, then a real client is used to
actually make the desired HTTP requests. Requests and responses are
recorded and will be written to the desired temprary cache file when
close_session is called.
Args:
name: str (optional) The file name of session file to be used. The file
is loaded from the temporary directory of this machine. If no name
is passed in, a default name will be constructed using the
cache_name_prefix, cache_case_name, and cache_test_name of this
object.
real_http_client: atom.http_core.HttpClient the real client to be used
if the cached recordings are not found. If the default
value is used, this will be an
atom.http_core.HttpClient.
"""
if real_http_client is None:
real_http_client = atom.http_core.HttpClient()
if name is None:
self._recordings_cache_name = self.get_cache_file_name()
else:
self._recordings_cache_name = name
self._load_or_use_client(self._recordings_cache_name, real_http_client)
def close_session(self):
"""Saves recordings in the temporary file named in use_cached_session."""
if self.real_client is not None:
self._save_recordings(self._recordings_cache_name)
def delete_session(self, name=None):
"""Removes recordings from a previous live request."""
if name is None:
self._delete_recordings(self._recordings_cache_name)
else:
self._delete_recordings(name)
def get_cache_file_name(self):
return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name,
self.cache_test_name)
def _dump(self):
"""Provides debug information in a string."""
output = 'MockHttpClient\n real_client: %s\n cache file name: %s\n' % (
self.real_client, self.get_cache_file_name())
output += ' recordings:\n'
i = 0
for recording in self._recordings:
output += ' recording %i is for: %s %s\n' % (
i, recording[0].method, str(recording[0].uri))
i += 1
return output
def _match_request(http_request, stored_request):
"""Determines whether a request is similar enough to a stored request
to cause the stored response to be returned."""
# Check to see if the host names match.
if (http_request.uri.host is not None
and http_request.uri.host != stored_request.uri.host):
return False
# Check the request path in the URL (/feeds/private/full/x)
elif http_request.uri.path != stored_request.uri.path:
return False
# Check the method used in the request (GET, POST, etc.)
elif http_request.method != stored_request.method:
return False
# If there is a gsession ID in either request, make sure that it is matched
# exactly.
elif ('gsessionid' in http_request.uri.query
or 'gsessionid' in stored_request.uri.query):
if 'gsessionid' not in stored_request.uri.query:
return False
elif 'gsessionid' not in http_request.uri.query:
return False
elif (http_request.uri.query['gsessionid']
!= stored_request.uri.query['gsessionid']):
return False
# Ignores differences in the query params (?start-index=5&max-results=20),
# the body of the request, the port number, HTTP headers, just to name a
# few.
return True
def _scrub_request(http_request):
""" Removes email address and password from a client login request.
Since the mock server saves the request and response in plantext, sensitive
information like the password should be removed before saving the
recordings. At the moment only requests sent to a ClientLogin url are
scrubbed.
"""
if (http_request and http_request.uri and http_request.uri.path and
http_request.uri.path.endswith('ClientLogin')):
# Remove the email and password from a ClientLogin request.
http_request._body_parts = []
http_request.add_form_inputs(
{'form_data': 'client login request has been scrubbed'})
else:
# We can remove the body of the post from the recorded request, since
# the request body is not used when finding a matching recording.
http_request._body_parts = []
return http_request
def _scrub_response(http_response):
return http_response
class EchoHttpClient(object):
"""Sends the request data back in the response.
Used to check the formatting of the request as it was sent. Always responds
with a 200 OK, and some information from the HTTP request is returned in
special Echo-X headers in the response. The following headers are added
in the response:
'Echo-Host': The host name and port number to which the HTTP connection is
made. If no port was passed in, the header will contain
host:None.
'Echo-Uri': The path portion of the URL being requested. /example?x=1&y=2
'Echo-Scheme': The beginning of the URL, usually 'http' or 'https'
'Echo-Method': The HTTP method being used, 'GET', 'POST', 'PUT', etc.
"""
def request(self, http_request):
return self._http_request(http_request.uri, http_request.method,
http_request.headers, http_request._body_parts)
def _http_request(self, uri, method, headers=None, body_parts=None):
body = io.StringIO()
response = atom.http_core.HttpResponse(status=200, reason='OK', body=body)
if headers is None:
response._headers = {}
else:
# Copy headers from the request to the response but convert values to
# strings. Server response headers always come in as strings, so an int
# should be converted to a corresponding string when echoing.
for header, value in headers.items():
response._headers[header] = str(value)
response._headers['Echo-Host'] = '%s:%s' % (uri.host, str(uri.port))
response._headers['Echo-Uri'] = uri._get_relative_path()
response._headers['Echo-Scheme'] = uri.scheme
response._headers['Echo-Method'] = method
for part in body_parts:
if isinstance(part, str):
body.write(part)
elif hasattr(part, 'read'):
body.write(part.read())
body.seek(0)
return response
class SettableHttpClient(object):
"""An HTTP Client which responds with the data given in set_response."""
def __init__(self, status, reason, body, headers):
"""Configures the response for the server.
See set_response for details on the arguments to the constructor.
"""
self.set_response(status, reason, body, headers)
self.last_request = None
def set_response(self, status, reason, body, headers):
"""Determines the response which will be sent for each request.
Args:
status: An int for the HTTP status code, example: 200, 404, etc.
reason: String for the HTTP reason, example: OK, NOT FOUND, etc.
body: The body of the HTTP response as a string or a file-like
object (something with a read method).
headers: dict of strings containing the HTTP headers in the response.
"""
self.response = atom.http_core.HttpResponse(status=status, reason=reason,
body=body)
self.response._headers = headers.copy()
def request(self, http_request):
self.last_request = http_request
return self.response
class MockHttpResponse(atom.http_core.HttpResponse):
def __init__(self, status=None, reason=None, headers=None, body=None):
self._headers = headers or {}
if status is not None:
self.status = status
if reason is not None:
self.reason = reason
if body is not None:
# Instead of using a file-like object for the body, store as a string
# so that reads can be repeated.
if hasattr(body, 'read'):
self._body = body.read()
else:
self._body = body
def read(self):
return self._body

View File

@@ -1,235 +0,0 @@
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
"""MockService provides CRUD ops. for mocking calls to AtomPub services.
MockService: Exposes the publicly used methods of AtomService to provide
a mock interface which can be used in unit tests.
"""
import pickle
import atom.service
# __author__ = 'api.jscudder (Jeffrey Scudder)'
# Recordings contains pairings of HTTP MockRequest objects with MockHttpResponse objects.
recordings = []
# If set, the mock service HttpRequest are actually made through this object.
real_request_handler = None
def ConcealValueWithSha(source):
import sha
return sha.new(source[:-5]).hexdigest()
def DumpRecordings(conceal_func=ConcealValueWithSha):
if conceal_func:
for recording_pair in recordings:
recording_pair[0].ConcealSecrets(conceal_func)
return pickle.dumps(recordings)
def LoadRecordings(recordings_file_or_string):
if isinstance(recordings_file_or_string, str):
atom.mock_service.recordings = pickle.loads(recordings_file_or_string)
elif hasattr(recordings_file_or_string, 'read'):
atom.mock_service.recordings = pickle.loads(
recordings_file_or_string.read())
def HttpRequest(service, operation, data, uri, extra_headers=None,
url_params=None, escape_params=True, content_type='application/atom+xml'):
"""Simulates an HTTP call to the server, makes an actual HTTP request if
real_request_handler is set.
This function operates in two different modes depending on if
real_request_handler is set or not. If real_request_handler is not set,
HttpRequest will look in this module's recordings list to find a response
which matches the parameters in the function call. If real_request_handler
is set, this function will call real_request_handler.HttpRequest, add the
response to the recordings list, and respond with the actual response.
Args:
service: atom.AtomService object which contains some of the parameters
needed to make the request. The following members are used to
construct the HTTP call: server (str), additional_headers (dict),
port (int), and ssl (bool).
operation: str The HTTP operation to be performed. This is usually one of
'GET', 'POST', 'PUT', or 'DELETE'
data: ElementTree, filestream, list of parts, or other object which can be
converted to a string.
Should be set to None when performing a GET or PUT.
If data is a file-like object which can be read, this method will read
a chunk of 100K bytes at a time and send them.
If the data is a list of parts to be sent, each part will be evaluated
and sent.
uri: The beginning of the URL to which the request should be sent.
Examples: '/', '/base/feeds/snippets',
'/m8/feeds/contacts/default/base'
extra_headers: dict of strings. HTTP headers which should be sent
in the request. These headers are in addition to those stored in
service.additional_headers.
url_params: dict of strings. Key value pairs to be added to the URL as
URL parameters. For example {'foo':'bar', 'test':'param'} will
become ?foo=bar&test=param.
escape_params: bool default True. If true, the keys and values in
url_params will be URL escaped when the form is constructed
(Special characters converted to %XX form.)
content_type: str The MIME type for the data being sent. Defaults to
'application/atom+xml', this is only used if data is set.
"""
full_uri = atom.service.BuildUri(uri, url_params, escape_params)
(server, port, ssl, uri) = atom.service.ProcessUrl(service, uri)
current_request = MockRequest(operation, full_uri, host=server, ssl=ssl,
data=data, extra_headers=extra_headers, url_params=url_params,
escape_params=escape_params, content_type=content_type)
# If the request handler is set, we should actually make the request using
# the request handler and record the response to replay later.
if real_request_handler:
response = real_request_handler.HttpRequest(service, operation, data, uri,
extra_headers=extra_headers, url_params=url_params,
escape_params=escape_params, content_type=content_type)
# TODO: need to copy the HTTP headers from the real response into the
# recorded_response.
recorded_response = MockHttpResponse(body=response.read(),
status=response.status, reason=response.reason)
# Insert a tuple which maps the request to the response object returned
# when making an HTTP call using the real_request_handler.
recordings.append((current_request, recorded_response))
return recorded_response
else:
# Look through available recordings to see if one matches the current
# request.
for request_response_pair in recordings:
if request_response_pair[0].IsMatch(current_request):
return request_response_pair[1]
return None
class MockRequest(object):
"""Represents a request made to an AtomPub server.
These objects are used to determine if a client request matches a recorded
HTTP request to determine what the mock server's response will be.
"""
def __init__(self, operation, uri, host=None, ssl=False, port=None,
data=None, extra_headers=None, url_params=None, escape_params=True,
content_type='application/atom+xml'):
"""Constructor for a MockRequest
Args:
operation: str One of 'GET', 'POST', 'PUT', or 'DELETE' this is the
HTTP operation requested on the resource.
uri: str The URL describing the resource to be modified or feed to be
retrieved. This should include the protocol (http/https) and the host
(aka domain). For example, these are some valud full_uris:
'http://example.com', 'https://www.google.com/accounts/ClientLogin'
host: str (optional) The server name which will be placed at the
beginning of the URL if the uri parameter does not begin with 'http'.
Examples include 'example.com', 'www.google.com', 'www.blogger.com'.
ssl: boolean (optional) If true, the request URL will begin with https
instead of http.
data: ElementTree, filestream, list of parts, or other object which can be
converted to a string. (optional)
Should be set to None when performing a GET or PUT.
If data is a file-like object which can be read, the constructor
will read the entire file into memory. If the data is a list of
parts to be sent, each part will be evaluated and stored.
extra_headers: dict (optional) HTTP headers included in the request.
url_params: dict (optional) Key value pairs which should be added to
the URL as URL parameters in the request. For example uri='/',
url_parameters={'foo':'1','bar':'2'} could become '/?foo=1&bar=2'.
escape_params: boolean (optional) Perform URL escaping on the keys and
values specified in url_params. Defaults to True.
content_type: str (optional) Provides the MIME type of the data being
sent.
"""
self.operation = operation
self.uri = _ConstructFullUrlBase(uri, host=host, ssl=ssl)
self.data = data
self.extra_headers = extra_headers
self.url_params = url_params or {}
self.escape_params = escape_params
self.content_type = content_type
def ConcealSecrets(self, conceal_func):
"""Conceal secret data in this request."""
if 'Authorization' in self.extra_headers:
self.extra_headers['Authorization'] = conceal_func(
self.extra_headers['Authorization'])
def IsMatch(self, other_request):
"""Check to see if the other_request is equivalent to this request.
Used to determine if a recording matches an incoming request so that a
recorded response should be sent to the client.
The matching is not exact, only the operation and URL are examined
currently.
Args:
other_request: MockRequest The request which we want to check this
(self) MockRequest against to see if they are equivalent.
"""
# More accurate matching logic will likely be required.
return (self.operation == other_request.operation and self.uri ==
other_request.uri)
def _ConstructFullUrlBase(uri, host=None, ssl=False):
"""Puts URL components into the form http(s)://full.host.strinf/uri/path
Used to construct a roughly canonical URL so that URLs which begin with
'http://example.com/' can be compared to a uri of '/' when the host is
set to 'example.com'
If the uri contains 'http://host' already, the host and ssl parameters
are ignored.
Args:
uri: str The path component of the URL, examples include '/'
host: str (optional) The host name which should prepend the URL. Example:
'example.com'
ssl: boolean (optional) If true, the returned URL will begin with https
instead of http.
Returns:
String which has the form http(s)://example.com/uri/string/contents
"""
if uri.startswith('http'):
return uri
if ssl:
return 'https://%s%s' % (host, uri)
else:
return 'http://%s%s' % (host, uri)
class MockHttpResponse(object):
"""Returned from MockService crud methods as the server's response."""
def __init__(self, body=None, status=None, reason=None, headers=None):
"""Construct a mock HTTPResponse and set members.
Args:
body: str (optional) The HTTP body of the server's response.
status: int (optional)
reason: str (optional)
headers: dict (optional)
"""
self.body = body
self.status = status
self.reason = reason
self.headers = headers or {}
def read(self):
return self.body
def getheader(self, header_name):
return self.headers[header_name]

View File

@@ -1,723 +0,0 @@
#
# Copyright (C) 2006, 2007, 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
"""AtomService provides CRUD ops. in line with the Atom Publishing Protocol.
AtomService: Encapsulates the ability to perform insert, update and delete
operations with the Atom Publishing Protocol on which GData is
based. An instance can perform query, insertion, deletion, and
update.
HttpRequest: Function that performs a GET, POST, PUT, or DELETE HTTP request
to the specified end point. An AtomService object or a subclass can be
used to specify information about the request.
"""
# __author__ = 'api.jscudder (Jeff Scudder)'
import base64
import http.client
import os
import socket
import urllib.error
import urllib.parse
import urllib.request
import warnings
import atom.http
import atom.http_interface
import atom.token_store
import atom.url
import lxml.etree as ElementTree
import atom
class AtomService(object):
"""Performs Atom Publishing Protocol CRUD operations.
The AtomService contains methods to perform HTTP CRUD operations.
"""
# Default values for members
port = 80
ssl = False
# Set the current_token to force the AtomService to use this token
# instead of searching for an appropriate token in the token_store.
current_token = None
auto_store_tokens = True
auto_set_current_token = True
def _get_override_token(self):
return self.current_token
def _set_override_token(self, token):
self.current_token = token
override_token = property(_get_override_token, _set_override_token)
# @atom.v1_deprecated('Please use atom.client.AtomPubClient instead.')
def __init__(self, server=None, additional_headers=None,
application_name='', http_client=None, token_store=None):
"""Creates a new AtomService client.
Args:
server: string (optional) The start of a URL for the server
to which all operations should be directed. Example:
'www.google.com'
additional_headers: dict (optional) Any additional HTTP headers which
should be included with CRUD operations.
http_client: An object responsible for making HTTP requests using a
request method. If none is provided, a new instance of
atom.http.ProxiedHttpClient will be used.
token_store: Keeps a collection of authorization tokens which can be
applied to requests for a specific URLs. Critical methods are
find_token based on a URL (atom.url.Url or a string), add_token,
and remove_token.
"""
self.http_client = http_client or atom.http.ProxiedHttpClient()
self.token_store = token_store or atom.token_store.TokenStore()
self.server = server
self.additional_headers = additional_headers or {}
self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % (
application_name,)
# If debug is True, the HTTPConnection will display debug information
self._set_debug(False)
__init__ = atom.v1_deprecated(
'Please use atom.client.AtomPubClient instead.')(
__init__)
def _get_debug(self):
return self.http_client.debug
def _set_debug(self, value):
self.http_client.debug = value
debug = property(_get_debug, _set_debug,
doc='If True, HTTP debug information is printed.')
def use_basic_auth(self, username, password, scopes=None):
if username is not None and password is not None:
if scopes is None:
scopes = [atom.token_store.SCOPE_ALL]
base_64_string = base64.encodestring('%s:%s' % (username, password))
token = BasicAuthToken('Basic %s' % base_64_string.strip(),
scopes=[atom.token_store.SCOPE_ALL])
if self.auto_set_current_token:
self.current_token = token
if self.auto_store_tokens:
return self.token_store.add_token(token)
return True
return False
def UseBasicAuth(self, username, password, for_proxy=False):
"""Sets an Authenticaiton: Basic HTTP header containing plaintext.
Deprecated, use use_basic_auth instead.
The username and password are base64 encoded and added to an HTTP header
which will be included in each request. Note that your username and
password are sent in plaintext.
Args:
username: str
password: str
"""
self.use_basic_auth(username, password)
# @atom.v1_deprecated('Please use atom.client.AtomPubClient for requests.')
def request(self, operation, url, data=None, headers=None,
url_params=None):
if isinstance(url, str):
if url.startswith('http:') and self.ssl:
# Force all requests to be https if self.ssl is True.
url = atom.url.parse_url('https:' + url[5:])
elif not url.startswith('http') and self.ssl:
url = atom.url.parse_url('https://%s%s' % (self.server, url))
elif not url.startswith('http'):
url = atom.url.parse_url('http://%s%s' % (self.server, url))
else:
url = atom.url.parse_url(url)
if url_params:
for name, value in url_params.items():
url.params[name] = value
all_headers = self.additional_headers.copy()
if headers:
all_headers.update(headers)
# If the list of headers does not include a Content-Length, attempt to
# calculate it based on the data object.
if data and 'Content-Length' not in all_headers:
content_length = CalculateDataLength(data)
if content_length:
all_headers['Content-Length'] = str(content_length)
# Find an Authorization token for this URL if one is available.
if self.override_token:
auth_token = self.override_token
else:
auth_token = self.token_store.find_token(url)
return auth_token.perform_request(self.http_client, operation, url,
data=data, headers=all_headers)
request = atom.v1_deprecated(
'Please use atom.client.AtomPubClient for requests.')(
request)
# CRUD operations
def Get(self, uri, extra_headers=None, url_params=None, escape_params=True):
"""Query the APP server with the given URI
The uri is the portion of the URI after the server value
(server example: 'www.google.com').
Example use:
To perform a query against Google Base, set the server to
'base.google.com' and set the uri to '/base/feeds/...', where ... is
your query. For example, to find snippets for all digital cameras uri
should be set to: '/base/feeds/snippets?bq=digital+camera'
Args:
uri: string The query in the form of a URI. Example:
'/base/feeds/snippets?bq=digital+camera'.
extra_headers: dicty (optional) Extra HTTP headers to be included
in the GET request. These headers are in addition to
those stored in the client's additional_headers property.
The client automatically sets the Content-Type and
Authorization headers.
url_params: dict (optional) Additional URL parameters to be included
in the query. These are translated into query arguments
in the form '&dict_key=value&...'.
Example: {'max-results': '250'} becomes &max-results=250
escape_params: boolean (optional) If false, the calling code has already
ensured that the query will form a valid URL (all
reserved characters have been escaped). If true, this
method will escape the query and any URL parameters
provided.
Returns:
httplib.HTTPResponse The server's response to the GET request.
"""
return self.request('GET', uri, data=None, headers=extra_headers,
url_params=url_params)
def Post(self, data, uri, extra_headers=None, url_params=None,
escape_params=True, content_type='application/atom+xml'):
"""Insert data into an APP server at the given URI.
Args:
data: string, ElementTree._Element, or something with a __str__ method
The XML to be sent to the uri.
uri: string The location (feed) to which the data should be inserted.
Example: '/base/feeds/items'.
extra_headers: dict (optional) HTTP headers which are to be included.
The client automatically sets the Content-Type,
Authorization, and Content-Length headers.
url_params: dict (optional) Additional URL parameters to be included
in the URI. These are translated into query arguments
in the form '&dict_key=value&...'.
Example: {'max-results': '250'} becomes &max-results=250
escape_params: boolean (optional) If false, the calling code has already
ensured that the query will form a valid URL (all
reserved characters have been escaped). If true, this
method will escape the query and any URL parameters
provided.
Returns:
httplib.HTTPResponse Server's response to the POST request.
"""
if extra_headers is None:
extra_headers = {}
if content_type:
extra_headers['Content-Type'] = content_type
return self.request('POST', uri, data=data, headers=extra_headers,
url_params=url_params)
def Put(self, data, uri, extra_headers=None, url_params=None,
escape_params=True, content_type='application/atom+xml'):
"""Updates an entry at the given URI.
Args:
data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The
XML containing the updated data.
uri: string A URI indicating entry to which the update will be applied.
Example: '/base/feeds/items/ITEM-ID'
extra_headers: dict (optional) HTTP headers which are to be included.
The client automatically sets the Content-Type,
Authorization, and Content-Length headers.
url_params: dict (optional) Additional URL parameters to be included
in the URI. These are translated into query arguments
in the form '&dict_key=value&...'.
Example: {'max-results': '250'} becomes &max-results=250
escape_params: boolean (optional) If false, the calling code has already
ensured that the query will form a valid URL (all
reserved characters have been escaped). If true, this
method will escape the query and any URL parameters
provided.
Returns:
httplib.HTTPResponse Server's response to the PUT request.
"""
if extra_headers is None:
extra_headers = {}
if content_type:
extra_headers['Content-Type'] = content_type
return self.request('PUT', uri, data=data, headers=extra_headers,
url_params=url_params)
def Delete(self, uri, extra_headers=None, url_params=None,
escape_params=True):
"""Deletes the entry at the given URI.
Args:
uri: string The URI of the entry to be deleted. Example:
'/base/feeds/items/ITEM-ID'
extra_headers: dict (optional) HTTP headers which are to be included.
The client automatically sets the Content-Type and
Authorization headers.
url_params: dict (optional) Additional URL parameters to be included
in the URI. These are translated into query arguments
in the form '&dict_key=value&...'.
Example: {'max-results': '250'} becomes &max-results=250
escape_params: boolean (optional) If false, the calling code has already
ensured that the query will form a valid URL (all
reserved characters have been escaped). If true, this
method will escape the query and any URL parameters
provided.
Returns:
httplib.HTTPResponse Server's response to the DELETE request.
"""
return self.request('DELETE', uri, data=None, headers=extra_headers,
url_params=url_params)
class BasicAuthToken(atom.http_interface.GenericToken):
def __init__(self, auth_header, scopes=None):
"""Creates a token used to add Basic Auth headers to HTTP requests.
Args:
auth_header: str The value for the Authorization header.
scopes: list of str or atom.url.Url specifying the beginnings of URLs
for which this token can be used. For example, if scopes contains
'http://example.com/foo', then this token can be used for a request to
'http://example.com/foo/bar' but it cannot be used for a request to
'http://example.com/baz'
"""
self.auth_header = auth_header
self.scopes = scopes or []
def perform_request(self, http_client, operation, url, data=None,
headers=None):
"""Sets the Authorization header to the basic auth string."""
if headers is None:
headers = {'Authorization': self.auth_header}
else:
headers['Authorization'] = self.auth_header
return http_client.request(operation, url, data=data, headers=headers)
def __str__(self):
return self.auth_header
def valid_for_scope(self, url):
"""Tells the caller if the token authorizes access to the desired URL.
"""
if isinstance(url, str):
url = atom.url.parse_url(url)
for scope in self.scopes:
if scope == atom.token_store.SCOPE_ALL:
return True
if isinstance(scope, str):
scope = atom.url.parse_url(scope)
if scope == url:
return True
# Check the host and the path, but ignore the port and protocol.
elif scope.host == url.host and not scope.path:
return True
elif scope.host == url.host and scope.path and not url.path:
continue
elif scope.host == url.host and url.path.startswith(scope.path):
return True
return False
def PrepareConnection(service, full_uri):
"""Opens a connection to the server based on the full URI.
This method is deprecated, instead use atom.http.HttpClient.request.
Examines the target URI and the proxy settings, which are set as
environment variables, to open a connection with the server. This
connection is used to make an HTTP request.
Args:
service: atom.AtomService or a subclass. It must have a server string which
represents the server host to which the request should be made. It may also
have a dictionary of additional_headers to send in the HTTP request.
full_uri: str Which is the target relative (lacks protocol and host) or
absolute URL to be opened. Example:
'https://www.google.com/accounts/ClientLogin' or
'base/feeds/snippets' where the server is set to www.google.com.
Returns:
A tuple containing the httplib.HTTPConnection and the full_uri for the
request.
"""
deprecation('calling deprecated function PrepareConnection')
(server, port, ssl, partial_uri) = ProcessUrl(service, full_uri)
if ssl:
# destination is https
proxy = os.environ.get('https_proxy')
if proxy:
(p_server, p_port, p_ssl, p_uri) = ProcessUrl(service, proxy, True)
proxy_username = os.environ.get('proxy-username')
if not proxy_username:
proxy_username = os.environ.get('proxy_username')
proxy_password = os.environ.get('proxy-password')
if not proxy_password:
proxy_password = os.environ.get('proxy_password')
if proxy_username:
user_auth = base64.encodestring('%s:%s' % (proxy_username,
proxy_password))
proxy_authorization = ('Proxy-authorization: Basic %s\r\n' % (
user_auth.strip()))
else:
proxy_authorization = ''
proxy_connect = 'CONNECT %s:%s HTTP/1.0\r\n' % (server, port)
user_agent = 'User-Agent: %s\r\n' % (
service.additional_headers['User-Agent'])
proxy_pieces = (proxy_connect + proxy_authorization + user_agent
+ '\r\n')
# now connect, very simple recv and error checking
p_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p_sock.connect((p_server, p_port))
p_sock.sendall(proxy_pieces)
response = ''
# Wait for the full response.
while response.find("\r\n\r\n") == -1:
response += p_sock.recv(8192)
p_status = response.split()[1]
if p_status != str(200):
raise atom.http.ProxyError('Error status=%s' % p_status)
# Trivial setup for ssl socket.
ssl = socket.ssl(p_sock, None, None)
fake_sock = http.client.FakeSocket(p_sock, ssl)
# Initalize httplib and replace with the proxy socket.
connection = http.client.HTTPConnection(server)
connection.sock = fake_sock
full_uri = partial_uri
else:
connection = http.client.HTTPSConnection(server, port)
full_uri = partial_uri
else:
# destination is http
proxy = os.environ.get('http_proxy')
if proxy:
(p_server, p_port, p_ssl, p_uri) = ProcessUrl(service.server, proxy, True)
proxy_username = os.environ.get('proxy-username')
if not proxy_username:
proxy_username = os.environ.get('proxy_username')
proxy_password = os.environ.get('proxy-password')
if not proxy_password:
proxy_password = os.environ.get('proxy_password')
if proxy_username:
UseBasicAuth(service, proxy_username, proxy_password, True)
connection = http.client.HTTPConnection(p_server, p_port)
if not full_uri.startswith("http://"):
if full_uri.startswith("/"):
full_uri = "http://%s%s" % (service.server, full_uri)
else:
full_uri = "http://%s/%s" % (service.server, full_uri)
else:
connection = http.client.HTTPConnection(server, port)
full_uri = partial_uri
return (connection, full_uri)
def UseBasicAuth(service, username, password, for_proxy=False):
"""Sets an Authenticaiton: Basic HTTP header containing plaintext.
Deprecated, use AtomService.use_basic_auth insread.
The username and password are base64 encoded and added to an HTTP header
which will be included in each request. Note that your username and
password are sent in plaintext. The auth header is added to the
additional_headers dictionary in the service object.
Args:
service: atom.AtomService or a subclass which has an
additional_headers dict as a member.
username: str
password: str
"""
deprecation('calling deprecated function UseBasicAuth')
base_64_string = base64.encodestring('%s:%s' % (username, password))
base_64_string = base_64_string.strip()
if for_proxy:
header_name = 'Proxy-Authorization'
else:
header_name = 'Authorization'
service.additional_headers[header_name] = 'Basic %s' % (base_64_string,)
def ProcessUrl(service, url, for_proxy=False):
"""Processes a passed URL. If the URL does not begin with https?, then
the default value for server is used
This method is deprecated, use atom.url.parse_url instead.
"""
if not isinstance(url, atom.url.Url):
url = atom.url.parse_url(url)
server = url.host
ssl = False
port = 80
if not server:
if hasattr(service, 'server'):
server = service.server
else:
server = service
if not url.protocol and hasattr(service, 'ssl'):
ssl = service.ssl
if hasattr(service, 'port'):
port = service.port
else:
if url.protocol == 'https':
ssl = True
elif url.protocol == 'http':
ssl = False
if url.port:
port = int(url.port)
elif port == 80 and ssl:
port = 443
return (server, port, ssl, url.get_request_uri())
def DictionaryToParamList(url_parameters, escape_params=True):
"""Convert a dictionary of URL arguments into a URL parameter string.
This function is deprcated, use atom.url.Url instead.
Args:
url_parameters: The dictionaty of key-value pairs which will be converted
into URL parameters. For example,
{'dry-run': 'true', 'foo': 'bar'}
will become ['dry-run=true', 'foo=bar'].
Returns:
A list which contains a string for each key-value pair. The strings are
ready to be incorporated into a URL by using '&'.join([] + parameter_list)
"""
# Choose which function to use when modifying the query and parameters.
# Use quote_plus when escape_params is true.
transform_op = [str, urllib.parse.quote_plus][bool(escape_params)]
# Create a list of tuples containing the escaped version of the
# parameter-value pairs.
parameter_tuples = [(transform_op(param), transform_op(value))
for param, value in list((url_parameters or {}).items())]
# Turn parameter-value tuples into a list of strings in the form
# 'PARAMETER=VALUE'.
return ['='.join(x) for x in parameter_tuples]
def BuildUri(uri, url_params=None, escape_params=True):
"""Converts a uri string and a collection of parameters into a URI.
This function is deprcated, use atom.url.Url instead.
Args:
uri: string
url_params: dict (optional)
escape_params: boolean (optional)
uri: string The start of the desired URI. This string can alrady contain
URL parameters. Examples: '/base/feeds/snippets',
'/base/feeds/snippets?bq=digital+camera'
url_parameters: dict (optional) Additional URL parameters to be included
in the query. These are translated into query arguments
in the form '&dict_key=value&...'.
Example: {'max-results': '250'} becomes &max-results=250
escape_params: boolean (optional) If false, the calling code has already
ensured that the query will form a valid URL (all
reserved characters have been escaped). If true, this
method will escape the query and any URL parameters
provided.
Returns:
string The URI consisting of the escaped URL parameters appended to the
initial uri string.
"""
# Prepare URL parameters for inclusion into the GET request.
parameter_list = DictionaryToParamList(url_params, escape_params)
# Append the URL parameters to the URL.
if parameter_list:
if uri.find('?') != -1:
# If there are already URL parameters in the uri string, add the
# parameters after a new & character.
full_uri = '&'.join([uri] + parameter_list)
else:
# The uri string did not have any URL parameters (no ? character)
# so put a ? between the uri and URL parameters.
full_uri = '%s%s' % (uri, '?%s' % ('&'.join([] + parameter_list)))
else:
full_uri = uri
return full_uri
def HttpRequest(service, operation, data, uri, extra_headers=None,
url_params=None, escape_params=True, content_type='application/atom+xml'):
"""Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
This method is deprecated, use atom.http.HttpClient.request instead.
Usage example, perform and HTTP GET on http://www.google.com/:
import atom.service
client = atom.service.AtomService()
http_response = client.Get('http://www.google.com/')
or you could set the client.server to 'www.google.com' and use the
following:
client.server = 'www.google.com'
http_response = client.Get('/')
Args:
service: atom.AtomService object which contains some of the parameters
needed to make the request. The following members are used to
construct the HTTP call: server (str), additional_headers (dict),
port (int), and ssl (bool).
operation: str The HTTP operation to be performed. This is usually one of
'GET', 'POST', 'PUT', or 'DELETE'
data: ElementTree, filestream, list of parts, or other object which can be
converted to a string.
Should be set to None when performing a GET or PUT.
If data is a file-like object which can be read, this method will read
a chunk of 100K bytes at a time and send them.
If the data is a list of parts to be sent, each part will be evaluated
and sent.
uri: The beginning of the URL to which the request should be sent.
Examples: '/', '/base/feeds/snippets',
'/m8/feeds/contacts/default/base'
extra_headers: dict of strings. HTTP headers which should be sent
in the request. These headers are in addition to those stored in
service.additional_headers.
url_params: dict of strings. Key value pairs to be added to the URL as
URL parameters. For example {'foo':'bar', 'test':'param'} will
become ?foo=bar&test=param.
escape_params: bool default True. If true, the keys and values in
url_params will be URL escaped when the form is constructed
(Special characters converted to %XX form.)
content_type: str The MIME type for the data being sent. Defaults to
'application/atom+xml', this is only used if data is set.
"""
deprecation('call to deprecated function HttpRequest')
full_uri = BuildUri(uri, url_params, escape_params)
(connection, full_uri) = PrepareConnection(service, full_uri)
if extra_headers is None:
extra_headers = {}
# Turn on debug mode if the debug member is set.
if service.debug:
connection.debuglevel = 1
connection.putrequest(operation, full_uri)
# If the list of headers does not include a Content-Length, attempt to
# calculate it based on the data object.
if (data and 'Content-Length' not in service.additional_headers and
'Content-Length' not in extra_headers):
content_length = CalculateDataLength(data)
if content_length:
extra_headers['Content-Length'] = str(content_length)
if content_type:
extra_headers['Content-Type'] = content_type
# Send the HTTP headers.
if isinstance(service.additional_headers, dict):
for header in service.additional_headers:
connection.putheader(header, service.additional_headers[header])
if isinstance(extra_headers, dict):
for header in extra_headers:
connection.putheader(header, extra_headers[header])
connection.endheaders()
# If there is data, send it in the request.
if data:
if isinstance(data, list):
for data_part in data:
__SendDataPart(data_part, connection)
else:
__SendDataPart(data, connection)
# Return the HTTP Response from the server.
return connection.getresponse()
def __SendDataPart(data, connection):
"""This method is deprecated, use atom.http._send_data_part"""
deprecated('call to deprecated function __SendDataPart')
if isinstance(data, str):
# TODO add handling for unicode.
connection.send(data)
return
elif ElementTree.iselement(data):
connection.send(ElementTree.tostring(data))
return
# Check to see if data is a file-like object that has a read method.
elif hasattr(data, 'read'):
# Read the file and send it a chunk at a time.
while 1:
binarydata = data.read(100000)
if binarydata == '': break
connection.send(binarydata)
return
else:
# The data object was not a file.
# Try to convert to a string and send the data.
connection.send(str(data))
return
def CalculateDataLength(data):
"""Attempts to determine the length of the data to send.
This method will respond with a length only if the data is a string or
and ElementTree element.
Args:
data: object If this is not a string or ElementTree element this funtion
will return None.
"""
if isinstance(data, str):
return len(data)
elif isinstance(data, list):
return None
elif ElementTree.iselement(data):
return len(ElementTree.tostring(data))
elif hasattr(data, 'read'):
# If this is a file-like object, don't try to guess the length.
return None
else:
return len(str(data).encode('utf-8'))
def deprecation(message):
warnings.warn(message, DeprecationWarning, stacklevel=2)

View File

@@ -1,105 +0,0 @@
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
"""This module provides a TokenStore class which is designed to manage
auth tokens required for different services.
Each token is valid for a set of scopes which is the start of a URL. An HTTP
client will use a token store to find a valid Authorization header to send
in requests to the specified URL. If the HTTP client determines that a token
has expired or been revoked, it can remove the token from the store so that
it will not be used in future requests.
"""
# __author__ = 'api.jscudder (Jeff Scudder)'
import atom.http_interface
import atom.url
SCOPE_ALL = 'http'
class TokenStore(object):
"""Manages Authorization tokens which will be sent in HTTP headers."""
def __init__(self, scoped_tokens=None):
self._tokens = scoped_tokens or {}
def add_token(self, token):
"""Adds a new token to the store (replaces tokens with the same scope).
Args:
token: A subclass of http_interface.GenericToken. The token object is
responsible for adding the Authorization header to the HTTP request.
The scopes defined in the token are used to determine if the token
is valid for a requested scope when find_token is called.
Returns:
True if the token was added, False if the token was not added becase
no scopes were provided.
"""
if not hasattr(token, 'scopes') or not token.scopes:
return False
for scope in token.scopes:
self._tokens[str(scope)] = token
return True
def find_token(self, url):
"""Selects an Authorization header token which can be used for the URL.
Args:
url: str or atom.url.Url or a list containing the same.
The URL which is going to be requested. All
tokens are examined to see if any scopes begin match the beginning
of the URL. The first match found is returned.
Returns:
The token object which should execute the HTTP request. If there was
no token for the url (the url did not begin with any of the token
scopes available), then the atom.http_interface.GenericToken will be
returned because the GenericToken calls through to the http client
without adding an Authorization header.
"""
if url is None:
return None
if isinstance(url, str):
url = atom.url.parse_url(url)
if url in self._tokens:
token = self._tokens[url]
if token.valid_for_scope(url):
return token
else:
del self._tokens[url]
for scope, token in self._tokens.items():
if token.valid_for_scope(url):
return token
return atom.http_interface.GenericToken()
def remove_token(self, token):
"""Removes the token from the token_store.
This method is used when a token is determined to be invalid. If the
token was found by find_token, but resulted in a 401 or 403 error stating
that the token was invlid, then the token should be removed to prevent
future use.
Returns:
True if a token was found and then removed from the token
store. False if the token was not in the TokenStore.
"""
token_found = False
scopes_to_delete = []
for scope, stored_token in self._tokens.items():
if stored_token == token:
scopes_to_delete.append(scope)
token_found = True
for scope in scopes_to_delete:
del self._tokens[scope]
return token_found
def remove_all_tokens(self):
self._tokens = {}

View File

@@ -1,130 +0,0 @@
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License 2.0;
# __author__ = 'api.jscudder (Jeff Scudder)'
import urllib.error
import urllib.parse
import urllib.parse
import urllib.request
DEFAULT_PROTOCOL = 'http'
DEFAULT_PORT = 80
def parse_url(url_string):
"""Creates a Url object which corresponds to the URL string.
This method can accept partial URLs, but it will leave missing
members of the Url unset.
"""
parts = urllib.parse.urlparse(url_string)
url = Url()
if parts[0]:
url.protocol = parts[0]
if parts[1]:
host_parts = parts[1].split(':')
if host_parts[0]:
url.host = host_parts[0]
if len(host_parts) > 1:
url.port = host_parts[1]
if parts[2]:
url.path = parts[2]
if parts[4]:
param_pairs = parts[4].split('&')
for pair in param_pairs:
pair_parts = pair.split('=')
if len(pair_parts) > 1:
url.params[urllib.parse.unquote_plus(pair_parts[0])] = (
urllib.parse.unquote_plus(pair_parts[1]))
elif len(pair_parts) == 1:
url.params[urllib.parse.unquote_plus(pair_parts[0])] = None
return url
class Url(object):
"""Represents a URL and implements comparison logic.
URL strings which are not identical can still be equivalent, so this object
provides a better interface for comparing and manipulating URLs than
strings. URL parameters are represented as a dictionary of strings, and
defaults are used for the protocol (http) and port (80) if not provided.
"""
def __init__(self, protocol=None, host=None, port=None, path=None,
params=None):
self.protocol = protocol
self.host = host
self.port = port
self.path = path
self.params = params or {}
def to_string(self):
url_parts = ['', '', '', '', '', '']
if self.protocol:
url_parts[0] = self.protocol
if self.host:
if self.port:
url_parts[1] = ':'.join((self.host, str(self.port)))
else:
url_parts[1] = self.host
if self.path:
url_parts[2] = self.path
if self.params:
url_parts[4] = self.get_param_string()
return urllib.parse.urlunparse(url_parts)
def get_param_string(self):
param_pairs = []
for key, value in self.params.items():
param_pairs.append('='.join((urllib.parse.quote_plus(key),
urllib.parse.quote_plus(str(value)))))
return '&'.join(param_pairs)
def get_request_uri(self):
"""Returns the path with the parameters escaped and appended."""
param_string = self.get_param_string()
if param_string:
return '?'.join([self.path, param_string])
else:
return self.path
def __cmp__(self, other):
if not isinstance(other, Url):
return cmp(self.to_string(), str(other))
difference = 0
# Compare the protocol
if self.protocol and other.protocol:
difference = cmp(self.protocol, other.protocol)
elif self.protocol and not other.protocol:
difference = cmp(self.protocol, DEFAULT_PROTOCOL)
elif not self.protocol and other.protocol:
difference = cmp(DEFAULT_PROTOCOL, other.protocol)
if difference != 0:
return difference
# Compare the host
difference = cmp(self.host, other.host)
if difference != 0:
return difference
# Compare the port
if self.port and other.port:
difference = cmp(self.port, other.port)
elif self.port and not other.port:
difference = cmp(self.port, DEFAULT_PORT)
elif not self.port and other.port:
difference = cmp(DEFAULT_PORT, other.port)
if difference != 0:
return difference
# Compare the path
difference = cmp(self.path, other.path)
if difference != 0:
return difference
# Compare the parameters
return cmp(self.params, other.params)
def __str__(self):
return self.to_string()

View File

@@ -1,694 +0,0 @@
#
# This is a custom certificate authority bundle for GAM
# It's composed of Let's Encrypt Root CAs and Google's
# certificate bundle. This should be the minimal list of
# CAs required to talk to Google and Github.
# Operating CA: Let's Encrypt
# Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
# Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1
# Label: "ISRG Root X1"
# Serial: 172886928669790476064670243504169061120
# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e
# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8
# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
# Operating CA: Let's Encrypt
# Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X2
# Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X2
# Label: "ISRG Root X2"
# Serial: 87493402998870891108772069816698636114
# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5
# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af
# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70
-----BEGIN CERTIFICATE-----
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
/q4AaOeMSQ+2b1tbFfLn
-----END CERTIFICATE-----
# Operating CA: DigiCert
# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Assured ID Root G2"
# Serial: 15385348160840213938643033620894905419
# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d
# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f
# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85
-----BEGIN CERTIFICATE-----
MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG
EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA
n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc
biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp
EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA
bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu
YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB
AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW
BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI
QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I
0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni
lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9
B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv
ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo
IhNzbM8m9Yop5w==
-----END CERTIFICATE-----
# Operating CA: DigiCert
# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Assured ID Root G3"
# Serial: 15459312981008553731928384953135426796
# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb
# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89
# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2
-----BEGIN CERTIFICATE-----
MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw
CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq
hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf
Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q
RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD
AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY
JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv
6pZjamVFkpUBtA==
-----END CERTIFICATE-----
# Operating CA: DigiCert
# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Global Root G2"
# Serial: 4293743540046975378534879503202253541
# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44
# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4
# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f
-----BEGIN CERTIFICATE-----
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
MrY=
-----END CERTIFICATE-----
# Operating CA: DigiCert
# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Global Root G3"
# Serial: 7089244469030293291760083333884364146
# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca
# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e
# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0
-----BEGIN CERTIFICATE-----
MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw
CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe
Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw
EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x
IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG
fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO
Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd
BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx
AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/
oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8
sycX
-----END CERTIFICATE-----
# Operating CA: DigiCert
# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Trusted Root G4"
# Serial: 7451500558977370777930084869016614236
# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49
# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4
# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88
-----BEGIN CERTIFICATE-----
MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
-----END CERTIFICATE-----
# Operating CA: GlobalSign
# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3
# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3
# Label: "GlobalSign Root CA - R3"
# Serial: 4835703278459759426209954
# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28
# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad
# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b
-----BEGIN CERTIFICATE-----
MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
WD9f
-----END CERTIFICATE-----
# Operating CA: GlobalSign
# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5
# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5
# Label: "GlobalSign ECC Root CA - R5"
# Serial: 32785792099990507226680698011560947931244
# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08
# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa
# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24
-----BEGIN CERTIFICATE-----
MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk
MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH
bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX
DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD
QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc
8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke
hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI
KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg
515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO
xwy8p2Fp8fc74SrL+SvzZpA3
-----END CERTIFICATE-----
# Operating CA: GlobalSign
# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6
# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6
# Label: "GlobalSign Root CA - R6"
# Serial: 1417766617973444989252670301619537
# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae
# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1
# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69
-----BEGIN CERTIFICATE-----
MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg
MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh
bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx
MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET
MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ
KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI
xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k
ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD
aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw
LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw
1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX
k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2
SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h
bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n
WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY
rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce
MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD
AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu
bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN
nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt
Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61
55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj
vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf
cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz
oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp
nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs
pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v
JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R
8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4
5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=
-----END CERTIFICATE-----
# Note: "GlobalSign Root CA - R7" not added on purpose. It is P-521.
# Operating CA: GoDaddy
# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.
# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc.
# Label: "Go Daddy Root Certificate Authority - G2"
# Serial: 0
# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01
# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b
# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
4uJEvlz36hz1
-----END CERTIFICATE-----
# Operating CA: GoDaddy
# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.
# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc.
# Label: "Starfield Root Certificate Authority - G2"
# Serial: 0
# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96
# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e
# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5
-----BEGIN CERTIFICATE-----
MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw
MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp
Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg
nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1
HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N
Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN
dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0
HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G
CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU
sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3
4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg
8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1
mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
-----END CERTIFICATE-----
# Operating CA: Sectigo
# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited
# Subject: CN=COMODO Certification Authority O=COMODO CA Limited
# Label: "COMODO Certification Authority"
# Serial: 43390818032842818540635488309124489234
# MD5 Fingerprint: 20:E7:4F:82:C2:7E:94:80:34:82:8A:13:A9:17:1D:97
# SHA1 Fingerprint EE:86:93:87:FF:FD:83:49:AB:5A:D1:43:22:58:87:89:A4:57:B0:12
# SHA256 Fingerprint: 1A:0D:20:44:5D:E5:BA:18:62:D1:9E:F8:80:85:8C:BC:E5:01:02:B3:6E:8F:0A:04:0C:3C:69:E7:45:22:FE:6E
-----BEGIN CERTIFICATE-----
MIID0DCCArigAwIBAgIQIKTEf93f4cdTYwcTiHdgEjANBgkqhkiG9w0BAQUFADCB
gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMTAxMDEwMDAw
MDBaFw0zMDEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl
YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P
RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0
aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3
UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI
2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8
Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp
+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+
DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O
nKVIrLsm9wIDAQABo0IwQDAdBgNVHQ4EFgQUC1jli8ZMFTekQKkwqSG+RzZaVv8w
DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
ggEBAC/JxBwHO89hAgCx2SFRdXIDMLDEFh9sAIsQrK/xR9SuEDwMGvjUk2ysEDd8
t6aDZK3N3w6HM503sMZ7OHKx8xoOo/lVem0DZgMXlUrxsXrfViEGQo+x06iF3u6X
HWLrp+cxEmbDD6ZLLkGC9/3JG6gbr+48zuOcrigHoSybJMIPIyaDMouGDx8rEkYl
Fo92kANr3ryqImhrjKGsKxE5pttwwn1y6TPn/CbxdFqR5p2ErPioBhlG5qfpqjQi
pKGfeq23sqSaM4hxAjwu1nqyH6LKwN0vEJT9s4yEIHlG1QXUEOTS22RPuFvuG8Ug
R1uUq27UlTMdphVx8fiUylQ5PsE=
-----END CERTIFICATE-----
# Operating CA: Sectigo
# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited
# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited
# Label: "COMODO ECC Certification Authority"
# Serial: 41578283867086692638256921589707938090
# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23
# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11
# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7
-----BEGIN CERTIFICATE-----
MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT
IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw
MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy
ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N
T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR
FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J
cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW
BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm
fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
-----END CERTIFICATE-----
# Operating CA: Sectigo
# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited
# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited
# Label: "COMODO RSA Certification Authority"
# Serial: 101909084537582093308941363524873193117
# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18
# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4
# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34
-----BEGIN CERTIFICATE-----
MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB
hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5
MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh
dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR
6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X
pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC
9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV
/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf
Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z
+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w
qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah
SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC
u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf
Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq
crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E
FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB
/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl
wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM
4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV
2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna
FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ
CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK
boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke
jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL
S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb
QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl
0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB
NVOFBkpdn627G190
-----END CERTIFICATE-----
# Operating CA: Sectigo
# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network
# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network
# Label: "USERTrust ECC Certification Authority"
# Serial: 123013823720199481456569720443997572134
# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1
# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0
# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a
-----BEGIN CERTIFICATE-----
MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl
eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT
JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx
MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg
VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm
aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo
I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng
o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G
A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD
VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB
zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW
RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=
-----END CERTIFICATE-----
# Operating CA: Sectigo
# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network
# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network
# Label: "USERTrust RSA Certification Authority"
# Serial: 2645093764781058787591871645665788717
# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5
# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e
# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2
-----BEGIN CERTIFICATE-----
MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
jjxDah2nGN59PRbxYvnKkKj9
-----END CERTIFICATE-----
# Operating CA: Google Trust Services LLC
# Subject: C = US, O = Google Trust Services LLC, CN = GTS Root R1
# Issuer: C = US, O = Google Trust Services LLC, CN = GTS Root R1
# Label: "GTS Root R1"
# Serial: 0203E5936F31B01349886BA217
# MD5 Fingerprint: 05:FE:D0:BF:71:A8:A3:76:63:DA:01:E0:D8:52:DC:40
# SHA1 Fingerprint: E5:8C:1C:C4:91:3B:38:63:4B:E9:10:6E:E3:AD:8E:6B:9D:D9:81:4A
# SHA256 Fingerprint: D9:47:43:2A:BD:E7:B7:FA:90:FC:2E:6B:59:10:1B:12:80:E0:E1:C7:E4:E4:0F:A3:C6:88:7F:FF:57:A7:F4:CF
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
-----END CERTIFICATE-----
# Operating CA: Google Trust Services LLC
# Subject: C = US, O = Google Trust Services LLC, CN = GTS Root R2
# Issuer: C = US, O = Google Trust Services LLC, CN = GTS Root R2
# Label: "GTS Root R2"
# Serial: 0203E5AEC58D04251AAB1125AA
# MD5 Fingerprint=1E:39:C0:53:E6:1E:29:82:0B:CA:52:55:36:5D:57:DC
# SHA1 Fingerprint=9A:44:49:76:32:DB:DE:FA:D0:BC:FB:5A:7B:17:BD:9E:56:09:24:94
# SHA256 Fingerprint=8D:25:CD:97:22:9D:BF:70:35:6B:DA:4E:B3:CC:73:40:31:E2:4C:F0:0F:AF:CF:D3:2D:C7:6E:B5:84:1C:7E:A8
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA
A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt
nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY
6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu
MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k
RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg
f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV
+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo
dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW
Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa
G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq
gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID
AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H
vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8
0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC
B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u
NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg
yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev
HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6
xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR
TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg
JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV
7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl
6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL
-----END CERTIFICATE-----
# Operating CA: Google Trust Services LLC
# Subject: C = US, O = Google Trust Services LLC, CN = GTS Root R3
# Issuer: C = US, O = Google Trust Services LLC, CN = GTS Root R3
# Label: "GTS Root R3"
# Serial: 0203E5B882EB20F825276D3D66
# MD5 Fingerprint: 3E:E7:9D:58:02:94:46:51:94:E5:E0:22:4A:8B:E7:73
# SHA1 Fingerprint: ED:E5:71:80:2B:C8:92:B9:5B:83:3C:D2:32:68:3F:09:CD:A0:1E:46
# SHA256 Fingerprint: 34:D8:A7:3E:E2:08:D9:BC:DB:0D:95:65:20:93:4B:4E:40:E6:94:82:59:6E:8B:6F:73:C8:42:6B:01:0A:6F:48
-----BEGIN CERTIFICATE-----
MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
-----END CERTIFICATE-----
# Operating CA: Google Trust Services LLC
# Subject: C = US, O = Google Trust Services LLC, CN = GTS Root R4
# Issuer: C = US, O = Google Trust Services LLC, CN = GTS Root R4
# Label: "GTS Root R4"
# Serial: 0203E5C068EF631A9C72905052
# MD5 Fingerprint=43:96:83:77:19:4D:76:B3:9D:65:52:E4:1D:22:A5:E8
# SHA1 Fingerprint=77:D3:03:67:B5:E0:0C:15:F6:0C:38:61:DF:7C:E1:3B:92:46:4D:47
# SHA256 Fingerprint=34:9D:FA:40:58:C5:E2:63:12:3B:39:8A:E7:95:57:3C:4E:13:13:C8:3F:E6:8F:93:55:6C:D5:E8:03:1B:3C:7D
-----BEGIN CERTIFICATE-----
MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi
QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR
HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D
9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8
p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD
-----END CERTIFICATE-----
# Operating CA: Google Trust Services LLC
# Subject: OU = GlobalSign ECC Root CA - R4, O = GlobalSign, CN = GlobalSign
# Issuer: OU = GlobalSign ECC Root CA - R4, O = GlobalSign, CN = GlobalSign
# Label: "GlobalSign R4"
# Serial: 0203E57EF53F93FDA50921B2A6
# MD5 Fingerprint: 26:29:F8:6D:E1:88:BF:A2:65:7F:AA:C4:CD:0F:7F:FC
# SHA1 Fingerprint: 6B:A0:B0:98:E1:71:EF:5A:AD:FE:48:15:80:77:10:F4:BD:6F:0B:28
# SHA256 Fingerprint: B0:85:D7:0B:96:4F:19:1A:73:E4:AF:0D:54:AE:7A:0E:07:AA:FD:AF:9B:71:DD:08:62:13:8A:B7:32:5A:24:A2
-----BEGIN CERTIFICATE-----
MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD
VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh
bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw
MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g
UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT
BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx
uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV
HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
-----END CERTIFICATE-----

View File

@@ -1,593 +0,0 @@
{
"auth": {
"oauth2": {
"scopes": {
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers": {
"description": "View and manage your Chrome browsers registered with Cloud Management"
},
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly": {
"description": "View your Chrome browsers registered with Cloud Management"
}
}
}
},
"basePath": "",
"baseUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
"batchPath": "batch",
"canonicalName": "cbcm",
"discoveryVersion": "v1",
"documentationLink": "https://support.google.com/chrome/a/answer/9681204",
"fullyEncodeReservedExpansion": true,
"icons": {
"x16": "http://www.google.com/images/icons/product/search-16.gif",
"x32": "http://www.google.com/images/icons/product/search-32.gif"
},
"id": "cbcm:v1.1beta1",
"kind": "discovery#restDescription",
"mtlsRootUrl": "https://admin.mtls.googleapis.com/",
"name": "cbcm",
"ownerDomain": "google.com",
"ownerName": "Jay Lee",
"packagePath": "cbcm",
"parameters": {
"$.xgafv": {
"description": "V1 error format.",
"enum": [
"1",
"2"
],
"enumDescriptions": [
"v1 error format",
"v2 error format"
],
"location": "query",
"type": "string"
},
"access_token": {
"description": "OAuth access token.",
"location": "query",
"type": "string"
},
"alt": {
"default": "json",
"description": "Data format for response.",
"enum": [
"json",
"media",
"proto"
],
"enumDescriptions": [
"Responses with Content-Type of application/json",
"Media download with context-dependent Content-Type",
"Responses with Content-Type of application/x-protobuf"
],
"location": "query",
"type": "string"
},
"callback": {
"description": "JSONP",
"location": "query",
"type": "string"
},
"fields": {
"description": "Selector specifying which fields to include in a partial response.",
"location": "query",
"type": "string"
},
"key": {
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"location": "query",
"type": "string"
},
"oauth_token": {
"description": "OAuth 2.0 token for the current user.",
"location": "query",
"type": "string"
},
"prettyPrint": {
"default": "true",
"description": "Returns response with indentations and line breaks.",
"location": "query",
"type": "boolean"
},
"quotaUser": {
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
"location": "query",
"type": "string"
},
"uploadType": {
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
"location": "query",
"type": "string"
},
"upload_protocol": {
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\").",
"location": "query",
"type": "string"
}
},
"protocol": "rest",
"resources": {
"chromebrowsers": {
"methods": {
"delete": {
"description": "Deletes a browser.",
"flatPath": "{customer}/devices/chromebrowsers/{deviceId}",
"httpMethod": "DELETE",
"id": "cbcm.chromebrowsers.delete",
"parameterOrder": [
"customer",
"deviceId"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
},
"deviceId": {
"description": "Immutable ID of the browser.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "{customer}/devices/chromebrowsers/{deviceId}",
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
},
"get": {
"description": "Retrieves a browser.",
"flatPath": "{customer}/devices/chromebrowsers/{deviceId}",
"httpMethod": "GET",
"id": "cbcm.chromebrowsers.get",
"parameterOrder": [
"customer",
"deviceId"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
},
"deviceId": {
"description": "Immutable ID of the browser.",
"location": "path",
"required": true,
"type": "string"
},
"projection": {
"description": "Restrict information returned to a set of selected fields. FULL or BASIC.",
"location": "query",
"type": "string"
}
},
"path": "{customer}/devices/chromebrowsers/{deviceId}",
"response": {
"$ref": "ChromeBrowser"
},
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers",
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly"
]
},
"list": {
"description": "Retrieves a paginated list of all the browsers in a domain.",
"flatPath": "{customer}/devices/chromebrowsers",
"httpMethod": "GET",
"id": "cbcm.chromebrowsers.list",
"parameterOrder": [
"customer"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
},
"maxResults": {
"description": "Maximum number of results to return.",
"format": "int32",
"location": "query",
"maximum": "100",
"minimum": "1",
"type": "integer"
},
"orderBy": {
"description": "property to use for sorting results.",
"location": "query",
"type": "string"
},
"orgUnitPath": {
"description": "The full path of the organizational unit or its unique ID.",
"location": "query",
"type": "string"
},
"pageToken": {
"description": "Token to specify the next page in the list.",
"location": "query",
"type": "string"
},
"projection": {
"description": "Restrict information returned to a set of selected fields. FULL or BASIC.",
"location": "query",
"type": "string"
},
"query": {
"description": "Search string using the list page query language.",
"location": "query",
"type": "string"
},
"sortOrder": {
"description": "Whether to return results in ascending or descending order. Must be used with the orderBy parameter.",
"location": "query",
"type": "string"
}
},
"path": "{customer}/devices/chromebrowsers",
"response": {
"$ref": "ChromeBrowsers"
},
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers",
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly"
]
},
"moveChromeBrowsersToOu": {
"description": "Move Chrome Browsers Device between Organization Units",
"flatPath": "{customer}/devices/chromebrowsers/moveChromeBrowsersToOu",
"httpMethod": "POST",
"id": "cbcm.chromebrowsers.moveChromeBrowsersToOu",
"parameterOrder": [
"customer"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "{customer}/devices/chromebrowsers/moveChromeBrowsersToOu",
"request": {
"$ref": "MoveChromeBrowsersRequest"
},
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
},
"update": {
"description": "Updates a browser.",
"flatPath": "{customer}/devices/chromebrowsers/{deviceId}",
"httpMethod": "PUT",
"id": "cbcm.chromebrowsers.update",
"parameterOrder": [
"customer",
"deviceId"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
},
"deviceId": {
"description": "Immutable ID of the browser.",
"location": "path",
"required": true,
"type": "string"
},
"projection": {
"description": "BASIC or FULL",
"location": "query",
"type": "string"
}
},
"path": "{customer}/devices/chromebrowsers/{deviceId}",
"request": {
"$ref": "ChromeBrowser"
},
"response": {
"$ref": "ChromeBrowser"
},
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
}
}
},
"enrollmentTokens": {
"methods": {
"list": {
"description": "Retrieves a paginated list of all the browser entollment tokens in a domain.",
"flatPath": "{customer}/chrome/enrollmentTokens",
"httpMethod": "GET",
"id": "cbcm.enrollmentTokens.list",
"parameterOrder": [
"customer"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
},
"pageSize": {
"description": "Maximum number of results to return.",
"format": "int32",
"location": "query",
"maximum": "100",
"minimum": "1",
"type": "integer"
},
"orgUnitPath": {
"description": "The full path of the organizational unit or its unique ID.",
"location": "query",
"type": "string"
},
"pageToken": {
"description": "Token to specify the next page in the list.",
"location": "query",
"type": "string"
},
"query": {
"description": "Search string using the list page query language.",
"location": "query",
"type": "string"
}
},
"path": "{customer}/chrome/enrollmentTokens",
"response": {
"$ref": "EnrollmentTokens"
},
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers",
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers.readonly"
]
},
"create": {
"description": "Creates a browser enrollment token in a domain.",
"flatPath": "{customer}/chrome/enrollmentTokens",
"httpMethod": "POST",
"id": "cbcm.enrollmentTokens.create",
"parameterOrder": [
"customer"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "{customer}/chrome/enrollmentTokens",
"request": {
"$ref": "CreateEnrollmentTokenRequest"
},
"response": {
"$ref": "EnrollmentToken"
},
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
},
"revoke": {
"description": "Revokes a browser enrollment token in a domain.",
"flatPath": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
"httpMethod": "POST",
"id": "cbcm.enrollmentTokens.revoke",
"parameterOrder": [
"customer",
"tokenPermanentId"
],
"parameters": {
"customer": {
"description": "Immutable ID of the G Suite account.",
"location": "path",
"required": true,
"type": "string"
},
"tokenPermanentId": {
"description": "Unique identifier for an enrollment token.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "{customer}/chrome/enrollmentTokens/{tokenPermanentId}:revoke",
"scopes": [
"https://www.googleapis.com/auth/admin.directory.device.chromebrowsers"
]
}
}
}
},
"revision": "20201203",
"rootUrl": "https://admin.googleapis.com/admin/directory/v1.1beta1/customer/",
"schemas": {
"ChromeBrowser": {
"id": "ChromeBrowser",
"properties": {
"annotatedAssetId": {
"description": "Asset identifier as annotated by the administrator or specified during enrollment.",
"type": "string"
},
"annotatedLocation": {
"description": "Address or location of the device as annotated by the administrator.",
"type": "string"
},
"annotatedNotes": {
"description": "Notes about this device as annotated by the administrator",
"type": "string"
},
"annotatedUser": {
"description": "User of the device as annotated by the administrator.",
"type": "string"
},
"deviceId": {
"annotations": {
"required": [
"cbcm.chromebrowsers.update"
]
},
"description": "The unique ID of the device.",
"type": "string"
}
},
"type": "object"
},
"ChromeBrowsers": {
"id": "ChromeBrowsers",
"properties": {
"browsers": {
"description": "List of Chrome browser objects.",
"items": {
"$ref": "ChromeBrowser"
},
"type": "array"
},
"etag": {
"description": "ETag of the resource.",
"type": "string"
},
"kind": {
"default": "admin#directory#chromeosdevices",
"description": "Kind of resource this is.",
"type": "string"
},
"nextPageToken": {
"description": "Token used to access the next page of this result. To access the next page, use this token's value in the `pageToken` query string of this request.",
"type": "string"
}
},
"type": "object"
},
"EnrollmentToken": {
"id": "EnrollmentToken",
"properties": {
"kind": {
"default": "admin#directory#chromeEnrollmentToken",
"description": "Kind of resource this is.",
"type": "string"
},
"tokenId": {
"description": "Enrollment Token ID.",
"type": "string"
},
"tokenPermanentId": {
"description": "Enrollment Token Permanent ID.",
"type": "string"
},
"customerId": {
"description": "Immutable ID of the G Suite account.",
"type": "string"
},
"orgUnitPath": {
"description": "The full path of the organizational unit or its unique ID.",
"type": "string"
},
"creatorId": {
"description": "Creator ID.",
"type": "string"
},
"createTime": {
"description": "Creation Time.",
"type": "string"
},
"revokerId": {
"description": "Revoker ID.",
"type": "string"
},
"revokeTime": {
"description": "Revoke Time",
"type": "string"
}
},
"type": "object"
},
"EnrollmentTokens": {
"id": "EnrollmentTokens",
"properties": {
"chrome_enrollment_tokens": {
"description": "List of Chrome browser enrollment token objects.",
"items": {
"$ref": "EnrollmentToken"
},
"type": "array"
},
"kind": {
"default": "admin#directory#chromeEnrollmentTokens",
"description": "Kind of resource this is.",
"type": "string"
},
"nextPageToken": {
"description": "Token used to access the next page of this result. To access the next page, use this token's value in the `pageToken` query string of this request.",
"type": "string"
}
},
"type": "object"
},
"CreateEnrollmentTokenRequest": {
"id": "CreateEnrollmentTokenRequest",
"properties": {
"org_unit_path": {
"description": "The full path of the organizational unit or its unique ID.",
"type": "string"
},
"expire_time": {
"description": "Expiration Time.",
"type": "string"
},
"token_type": {
"id": "token_type",
"annotations": {
"required": [
"cbcm.enrollmentTokens.create"
]
},
"description": "CHROME_BROWSER.",
"type": "string"
}
}
},
"MoveChromeBrowsersRequest": {
"id": "MoveChromeBrowsersRequest",
"type": "object",
"properties": {
"org_unit_path": {
"annotations": {
"required": [
"cbcm.chromebrowsers.moveChromeBrowsersToOu"
]
},
"description": "Destination organization unit to move devices to. Full path of the organizational unit or its ID prefixed with id:",
"type": "string"
},
"resource_ids": {
"annotations": {
"required": [
"cbcm.chromebrowsers.moveChromeBrowsersToOu"
]
},
"description": "List of unique device IDs of Chrome Browser Devices to move. A maximum of 600 browsers may be moved per request.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"servicePath": "",
"title": "Admin SDK API",
"version": "cbcm_v1.1beta1"
}

View File

@@ -1,249 +0,0 @@
{
"auth": {
"oauth2": {
"scopes": {
"https://www.googleapis.com/auth/admin.contact.delegation": {
"description": "View and manage your Contact Delegation"
},
"https://www.googleapis.com/auth/admin.contact.delegation.readonly": {
"description": "View your Contact Delegation"
}
}
}
},
"basePath": "",
"baseUrl": "https://admin.googleapis.com/admin/contacts/v1/",
"batchPath": "batch",
"canonicalName": "contactdelegation",
"description": "The Contact Delegation API allows Admins to delegate access of one user's, called the delegator, contacts to another user, called the delegate.",
"discoveryVersion": "v1",
"documentationLink": "https://developers.google.com/admin-sdk/contact-delegation",
"fullyEncodeReservedExpansion": true,
"icons": {
"x16": "http://www.google.com/images/icons/product/search-16.gif",
"x32": "http://www.google.com/images/icons/product/search-32.gif"
},
"id": "contactdelegation:v1",
"kind": "discovery#restDescription",
"name": "contactdelegation",
"ownerDomain": "google.com",
"ownerName": "Google",
"packagePath": "admin",
"parameters": {
"$.xgafv": {
"description": "V1 error format.",
"enum": [
"1",
"2"
],
"enumDescriptions": [
"v1 error format",
"v2 error format"
],
"location": "query",
"type": "string"
},
"access_token": {
"description": "OAuth access token.",
"location": "query",
"type": "string"
},
"alt": {
"default": "json",
"description": "Data format for response.",
"enum": [
"json",
"media",
"proto"
],
"enumDescriptions": [
"Responses with Content-Type of application/json",
"Media download with context-dependent Content-Type",
"Responses with Content-Type of application/x-protobuf"
],
"location": "query",
"type": "string"
},
"callback": {
"description": "JSONP",
"location": "query",
"type": "string"
},
"fields": {
"description": "Selector specifying which fields to include in a partial response.",
"location": "query",
"type": "string"
},
"key": {
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"location": "query",
"type": "string"
},
"oauth_token": {
"description": "OAuth 2.0 token for the current user.",
"location": "query",
"type": "string"
},
"prettyPrint": {
"default": "true",
"description": "Returns response with indentations and line breaks.",
"location": "query",
"type": "boolean"
},
"quotaUser": {
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
"location": "query",
"type": "string"
},
"uploadType": {
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
"location": "query",
"type": "string"
},
"upload_protocol": {
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\").",
"location": "query",
"type": "string"
}
},
"protocol": "rest",
"resources": {
"delegates": {
"methods": {
"create": {
"description": "Creates a contact delegations",
"flatPath": "users/{user}/delegates",
"httpMethod": "POST",
"id": "contactdelegations.delegates.create",
"parameterOrder": [
"user"
],
"parameters": {
"user": {
"description": "Email address of the delegator.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "users/{user}/delegates/{delegate}",
"request": {
"$ref": "Delegate"
},
"scopes": [
"https://www.googleapis.com/auth/admin.contact.delegation"
]
},
"delete": {
"description": "Deletes a contact delegation.",
"flatPath": "users/{user}/delegates/{delegate}",
"httpMethod": "DELETE",
"id": "contactdelegations.delegates.delete",
"parameterOrder": [
"user",
"delegate"
],
"parameters": {
"delegate": {
"description": "Email address of the delegate",
"location": "path",
"required": true,
"type": "string"
},
"user": {
"description": "Email address of the delegator.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "users/{user}/delegates/{delegate}",
"scopes": [
"https://www.googleapis.com/auth/admin.contact.delegation"
]
},
"list": {
"description": "Lists contact delegates for a user",
"flatPath": "users/{user}/delegates",
"httpMethod": "GET",
"id": "contactdelegations.delegates.list",
"parameterOrder": [
"user"
],
"parameters": {
"pageSize": {
"description": "Determines how many delegates are returned in each response. ",
"format": "int32",
"location": "query",
"minimum": "1",
"type": "integer"
},
"pageToken": {
"description": "Token to specify the next page in the list.",
"location": "query",
"type": "string"
},
"user": {
"description": "Email address of the delegator.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "users/{user}/delegates",
"response": {
"$ref": "Delegates"
},
"scopes": [
"https://www.googleapis.com/auth/admin.contact.delegation",
"https://www.googleapis.com/auth/admin.contact.delegation.readonly"
]
}
}
}
},
"rootUrl": "https://admin.googleapis.com/admin/contacts/v1/",
"schemas": {
"Delegate": {
"description": "JSON template for a delegate.",
"id": "Delegate",
"properties": {
"email": {
"description": "Email of the delegate.",
"type": "string"
}
},
"type": "object"
},
"Delegates": {
"id": "Delegates",
"properties": {
"delegates": {
"description": "List of delegates.",
"items": {
"$ref": "Delegate"
},
"type": "array"
},
"etag": {
"description": "ETag of the resource.",
"type": "string"
},
"kind": {
"default": "",
"description": "Kind of resource this is.",
"type": "string"
},
"nextPageToken": {
"description": "Token used to access the next page of this result. To access the next page, use this token's value in the `pageToken` query string of this request.",
"type": "string"
}
},
"type": "object"
}
},
"servicePath": "",
"title": "Contact Delegation API",
"version": "v1",
"version_module": true
}

View File

@@ -1,486 +0,0 @@
{
"basePath": "",
"discoveryVersion": "v1",
"documentationLink": "https://support.google.com/datastudio",
"canonicalName": "Data Studio",
"id": "datastudio:v1",
"ownerName": "Google",
"description": "Allows programmatic viewing and editing of Data Studio assets.",
"rootUrl": "https://datastudio.googleapis.com/",
"ownerDomain": "google.com",
"mtlsRootUrl": "https://datastudio.mtls.googleapis.com/",
"batchPath": "batch",
"version_module": true,
"version": "v1",
"schemas": {
"Asset": {
"id": "Asset",
"properties": {
"title": {
"description": "The title of the asset.",
"type": "string"
},
"createTime": {
"format": "google-datetime",
"description": "Date the asset was created.",
"type": "string"
},
"lastViewByMeTime": {
"type": "string",
"description": "Date the asset was last viewed by me.",
"format": "google-datetime"
},
"owner": {
"type": "string",
"description": "The owner of the asset."
},
"name": {
"type": "string",
"description": "The name of the asset."
},
"trashed": {
"type": "boolean",
"description": "Value indicating if the asset is in the trash."
},
"updateTime": {
"format": "google-datetime",
"description": "Date the asset was last modified.",
"type": "string"
},
"updateByMeTime": {
"description": "Date the asset was last modified by me.",
"type": "string",
"format": "google-datetime"
},
"assetType": {
"enumDescriptions": [
"Asset type not specified.",
"A report asset.",
"A data Source asset."
],
"enum": [
"ASSET_TYPE_UNSPECIFIED",
"REPORT",
"DATA_SOURCE"
],
"description": "The type of the asset.",
"type": "string"
}
},
"description": "A Data Studio asset.",
"type": "object"
},
"SearchAssetsResponse": {
"id": "SearchAssetsResponse",
"properties": {
"assets": {
"items": {
"$ref": "Asset"
},
"type": "array",
"description": "The list of assets."
},
"nextPageToken": {
"type": "string",
"description": "A token to retrieve next page of results. Pass this value in the SearchAssetsRequest.page_token field in the subsequent call to `SearchAssets` method to retrieve the next page of results."
}
},
"description": "Response message for DataStudioService.SearchAssets",
"type": "object"
},
"UpdatePermissionsRequest": {
"description": "Request message for DataStudioService.UpdatePermissions",
"properties": {
"updateMask": {
"description": "The list of fields to be updated. Currently not supported.",
"type": "string",
"format": "google-fieldmask"
},
"permissions": {
"description": "The permissions object to update.",
"$ref": "Permissions"
}
},
"id": "UpdatePermissionsRequest",
"type": "object"
},
"AddMembersRequest": {
"properties": {
"members": {
"type": "array",
"items": {
"type": "string"
},
"description": "Required. The members to add to the role. The format of a member is one of - user:alice@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-app@appspot.gserviceaccount.com"
},
"role": {
"type": "string",
"enumDescriptions": [
"Role not specified.",
"A viewer.",
"An editor.",
"An owner.",
"Link shared viewer.",
"Link shared editor."
],
"enum": [
"ROLE_UNSPECIFIED",
"VIEWER",
"EDITOR",
"OWNER",
"LINK_VIEWER",
"LINK_EDITOR"
],
"description": "Required. The role to add members to."
}
},
"type": "object",
"id": "AddMembersRequest",
"description": "Request message for DataStudioService.AddMembers"
},
"Permissions": {
"type": "object",
"id": "Permissions",
"description": "A Data Studio asset's Permissions.",
"properties": {
"permissions": {
"description": "A map from a Role to a list of members. Role is a string representation of the Role enum. One of: - OWNER - EDITOR - VIEWER",
"additionalProperties": {
"$ref": "Members"
},
"type": "object"
},
"etag": {
"format": "byte",
"description": "etag to detect and fail concurrent modifications",
"type": "string"
}
}
},
"RevokeAllPermissionsRequest": {
"description": "Request message for DataStudioService.RevokeAllPermissions",
"id": "RevokeAllPermissionsRequest",
"type": "object",
"properties": {
"members": {
"description": "Required. The members that are having their access revoked. The format of a member is one of - user:alice@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-app@appspot.gserviceaccount.com",
"items": {
"type": "string"
},
"type": "array"
}
}
},
"Members": {
"description": "A wrapper message for a list of members.",
"properties": {
"members": {
"description": "Format of string is one of - user:alice@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-app@appspot.gserviceaccount.com",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object",
"id": "Members"
}
},
"name": "datastudio",
"protocol": "rest",
"baseUrl": "https://datastudio.googleapis.com/",
"title": "Data Studio API",
"revision": "20210412",
"fullyEncodeReservedExpansion": true,
"icons": {
"x32": "http://www.google.com/images/icons/product/search-32.gif",
"x16": "http://www.google.com/images/icons/product/search-16.gif"
},
"parameters": {
"quotaUser": {
"location": "query",
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
"type": "string"
},
"prettyPrint": {
"location": "query",
"type": "boolean",
"description": "Returns response with indentations and line breaks.",
"default": "true"
},
"callback": {
"location": "query",
"type": "string",
"description": "JSONP"
},
"uploadType": {
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
"type": "string",
"location": "query"
},
"upload_protocol": {
"type": "string",
"location": "query",
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\")."
},
"$.xgafv": {
"enumDescriptions": [
"v1 error format",
"v2 error format"
],
"description": "V1 error format.",
"location": "query",
"type": "string",
"enum": [
"1",
"2"
]
},
"oauth_token": {
"type": "string",
"location": "query",
"description": "OAuth 2.0 token for the current user."
},
"alt": {
"type": "string",
"enum": [
"json",
"media",
"proto"
],
"location": "query",
"description": "Data format for response.",
"enumDescriptions": [
"Responses with Content-Type of application/json",
"Media download with context-dependent Content-Type",
"Responses with Content-Type of application/x-protobuf"
],
"default": "json"
},
"fields": {
"description": "Selector specifying which fields to include in a partial response.",
"location": "query",
"type": "string"
},
"access_token": {
"type": "string",
"description": "OAuth access token.",
"location": "query"
},
"key": {
"type": "string",
"location": "query",
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token."
}
},
"servicePath": "",
"kind": "discovery#restDescription",
"resources": {
"assets": {
"methods": {
"getPermissions": {
"parameters": {
"name": {
"type": "string",
"location": "path",
"description": "Required. The name of the asset.",
"required": true
},
"role": {
"enumDescriptions": [
"Role not specified.",
"A viewer.",
"An editor.",
"An owner.",
"Link shared viewer.",
"Link shared editor."
],
"type": "string",
"location": "query",
"description": "The role of the permssion.",
"enum": [
"ROLE_UNSPECIFIED",
"VIEWER",
"EDITOR",
"OWNER",
"LINK_VIEWER",
"LINK_EDITOR"
]
}
},
"id": "datastudio.assets.getPermissions",
"response": {
"$ref": "Permissions"
},
"flatPath": "v1/assets/{name}/permissions",
"path": "v1/assets/{name}/permissions",
"description": "Gets the asset's permission for a given role.",
"parameterOrder": [
"name"
],
"httpMethod": "GET"
},
"updatePermissions": {
"id": "datastudio.assets.updatePermissions",
"parameterOrder": [
"name"
],
"flatPath": "v1/assets/{name}/permissions",
"description": "Updates a permission.",
"request": {
"$ref": "UpdatePermissionsRequest"
},
"path": "v1/assets/{name}/permissions",
"parameters": {
"name": {
"description": "Required. The name of the asset.",
"location": "path",
"type": "string",
"required": true
}
},
"response": {
"$ref": "Permissions"
},
"httpMethod": "PATCH"
},
"get": {
"path": "v1/{+name}",
"id": "datastudio.assets.get",
"parameterOrder": [
"name"
],
"description": "Gets the asset by name.",
"parameters": {
"name": {
"description": "Required. The name of the asset. Format: assets/{asset}",
"location": "path",
"required": true,
"pattern": "^assets/[^/]+$",
"type": "string"
}
},
"flatPath": "v1/assets/{assetsId}",
"response": {
"$ref": "Asset"
},
"httpMethod": "GET"
},
"search": {
"response": {
"$ref": "SearchAssetsResponse"
},
"path": "v1/assets:search",
"parameters": {
"pageToken": {
"type": "string",
"location": "query",
"description": "A token identifying a page of results the server should return. Use the value of SearchAssetsResponse.next_page_token returned from the previous call to `SearchAssets` method."
},
"assetTypes": {
"type": "string",
"repeated": true,
"description": "Exactly one AssetType must be specified.",
"enumDescriptions": [
"Asset type not specified.",
"A report asset.",
"A data Source asset."
],
"location": "query",
"enum": [
"ASSET_TYPE_UNSPECIFIED",
"REPORT",
"DATA_SOURCE"
]
},
"title": {
"description": "The title of assets to include. Not an exact match, works the same as search from the UI.",
"location": "query",
"type": "string"
},
"owner": {
"type": "string",
"location": "query",
"description": "The email of the owner of the asset."
},
"pageSize": {
"description": "Requested page size. If unspecified, server will pick an appropriate default.",
"location": "query",
"type": "integer",
"format": "int32"
},
"orderBy": {
"location": "query",
"description": "How the results should be ordered. Valid options are: - title",
"type": "string"
},
"includeTrashed": {
"location": "query",
"type": "boolean",
"description": "Value indicating if assets in trash should be included."
}
},
"flatPath": "v1/assets:search",
"httpMethod": "GET",
"description": "Searches assets.",
"id": "datastudio.assets.search",
"parameterOrder": []
}
},
"resources": {
"permissions": {
"methods": {
"revokeAllPermissions": {
"path": "v1/assets/{name}/permissions:revokeAllPermissions",
"response": {
"$ref": "Permissions"
},
"flatPath": "v1/assets/{name}/permissions:revokeAllPermissions",
"id": "datastudio.assets.permissions.revokeAllPermissions",
"description": "Revokes one or more members' access to an asset.",
"parameters": {
"name": {
"required": true,
"type": "string",
"location": "path",
"description": "Required. The name of the asset."
}
},
"parameterOrder": [
"name"
],
"httpMethod": "POST",
"request": {
"$ref": "RevokeAllPermissionsRequest"
}
},
"addMembers": {
"path": "v1/assets/{name}/permissions:addMembers",
"parameters": {
"name": {
"required": true,
"location": "path",
"type": "string",
"description": "Required. The name of the asset."
}
},
"httpMethod": "POST",
"parameterOrder": [
"name"
],
"response": {
"$ref": "Permissions"
},
"id": "datastudio.assets.permissions.addMembers",
"request": {
"$ref": "AddMembersRequest"
},
"description": "Adds one or more members to a role.",
"flatPath": "v1/assets/{name}/permissions:addMembers"
}
}
}
}
}
}
}

View File

@@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@@ -1,312 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM action processing
"""
class GamAction():
# Keys into NAMES; arbitrary values but must be unique
ACCEPT = 'acpt'
ADD = 'add '
ADD_PREVIEW = 'addp'
APPEND = 'apnd'
APPROVE = 'aprv'
ARCHIVE = 'arch'
BACKUP = 'back'
BLOCK = 'blok'
CANCEL = 'canc'
CANCEL_WIPE = 'canw'
CHECK = 'chek'
CLAIM = 'clai'
CLAIM_OWNERSHIP = 'clow'
CLEAR = 'clea'
CLOSE = 'clos'
COLLECT = 'coll'
COMMENT = 'comm'
COPY = 'copy'
COPY_MERGE = 'copm'
CREATE = 'crea'
CREATE_PREVIEW = 'crep'
CREATE_SHORTCUT = 'crsc'
DEDUP = 'dedu'
DELETE = 'dele'
DELETE_EMPTY = 'delm'
DELETE_PREVIEW = 'delp'
DELETE_SHORTCUT = 'desc'
DEPROVISION = 'depr'
DISABLE = 'disa'
DOWNLOAD = 'down'
DRAFT = 'draf'
EMPTY = 'empt'
ENABLE = 'enbl'
END = 'end '
EXISTS = 'exis'
EXPORT = 'expo'
EXTRACT = 'extr'
GET_COMMAND_RESULT = 'gtcr'
FETCH = 'fetc'
FORWARD = 'forw'
HIDE = 'hide'
IMPORT = 'impo'
INFO = 'info'
INITIALIZE = 'init'
INSERT = 'insr'
INVALIDATE = 'inva'
ISSUE_COMMAND = 'isco'
LIST = 'list'
LOOKUP = 'look'
MERGE = 'merg'
MODIFY = 'modi'
MOVE = 'move'
MOVE_MERGE = 'movm'
NOACTION = 'noac'
NOACTION_PREVIEW = 'noap'
OBLITERATE = 'obli'
PERFORM = 'perf'
PRE_PROVISIONED_DISABLE ='ppdi'
PRE_PROVISIONED_REENABLE ='ppre'
PRINT = 'prin'
PROCESS = 'proc'
PROCESS_PREVIEW = 'prop'
PURGE = 'purg'
RECREATE = 'recr'
REENABLE = 'reen'
REFRESH = 'refr'
RELABEL = 'rela'
REMOVE = 'remo'
REMOVE_PREVIEW = 'remp'
RENAME = 'rena'
REOPEN = 'reop'
REPLACE = 'repl'
REPLACE_DOMAIN = 'repd'
REPORT = 'repo'
RESET_YUBIKEY_PIV = 'rpiv'
RESPOND = 'resp'
RESTORE = 'rest'
RESUBMIT = 'res'
RETAIN = 'reta'
RETRIEVE_DATA = 'retd'
REVOKE = 'revo'
SAVE = 'save'
SEND = 'send'
SENDEMAIL = 'snem'
SENDREPLY = 'sner'
SET = 'set '
SETUP = 'setu'
SHARE = 'shar'
SHOW = 'show'
SIGNOUT = 'siou'
SKIP = 'skip'
SPAM = 'spam'
SUBMIT = 'subm'
SUSPEND = 'susp'
SYNC = 'sync'
TRANSFER = 'tran'
TRANSFER_OWNERSHIP = 'trow'
TRASH = 'tras'
TURNOFF2SV = 'to2s'
UNDELETE = 'unde'
UNHIDE = 'unhi'
UNSUSPEND = 'unsu'
UNTRASH = 'untr'
UPDATE = 'upda'
UPDATE_MOVE = 'upmo'
UPDATE_OWNER = 'updo'
UPDATE_PREVIEW = 'updp'
UPLOAD = 'uplo'
UNZIP = 'unzi'
USE = 'use '
VERIFY = 'vrfy'
VERIFYITEMEXISTS = 'vexi'
WAITFORMAILBOX = 'wamb'
WATCH = 'watc'
WIPE = 'wipe'
WIPE_PREVIEW = 'wipp'
# Usage:
# ACTION_NAMES[1] n Items - Delete 10 Users
# Item xxx ACTION_NAMES[0] - User xxx Deleted
# These values can be translated into other languages
_NAMES = {
ACCEPT: ['Accepted', 'Accept'],
ADD: ['Added', 'Add'],
ADD_PREVIEW: ['Added (Preview)', 'Add (Preview)'],
APPEND: ['Appended', 'Append'],
APPROVE: ['Approved', 'Approve'],
ARCHIVE: ['Archived', 'Archive'],
BACKUP: ['Backed up', 'Backup'],
BLOCK: ['Blocked', 'Block'],
CANCEL: ['Cancelled', 'Cancel'],
CANCEL_WIPE: ['Wipe Cancelled', 'Cancel Wipe'],
CHECK: ['Checked', 'Check'],
CLAIM: ['Claimed', 'Claim'],
CLAIM_OWNERSHIP: ['Ownership Claimed', 'Claim Ownership'],
CLEAR: ['Cleared', 'Clear'],
CLOSE: ['Closed', 'Close'],
COLLECT: ['Collected', 'Collect'],
COMMENT: ['Commented', 'Comment'],
COPY: ['Copied', 'Copy'],
COPY_MERGE: ['Copied(Merge)', 'Copy(Merge)'],
CREATE: ['Created', 'Create'],
CREATE_PREVIEW: ['Created (Preview)', 'Create (Preview)'],
CREATE_SHORTCUT: ['Created Shortcut', 'Create Shortcut'],
DEDUP: ['Duplicates Deleted', 'Delete Duplicates'],
DELETE: ['Deleted', 'Delete'],
DELETE_EMPTY: ['Deleted', 'Delete Empty'],
DELETE_PREVIEW: ['Deleted (Preview)', 'Delete (Preview)'],
DELETE_SHORTCUT: ['Deleted Shortcut', 'Delete Shortcut'],
DEPROVISION: ['Deprovisioned', 'Deprovision'],
DISABLE: ['Disabled', 'Disable'],
DOWNLOAD: ['Downloaded', 'Download'],
DRAFT: ['Drafted', 'Draft'],
EMPTY: ['Emptied', 'Empty'],
ENABLE: ['Enabled', 'Enable'],
END: ['Ended', 'End'],
EXISTS: ['Exists', 'Exists'],
EXPORT: ['Exported', 'Export'],
EXTRACT: ['Extracted', 'Extract'],
FORWARD: ['Forwarded', 'Forward'],
GET_COMMAND_RESULT: ['Got Command Result', 'Get Command Result'],
HIDE: ['Hidden', 'Hide'],
IMPORT: ['Imported', 'Import'],
INFO: ['Shown', 'Show Info'],
INITIALIZE: ['Initialized', 'Initialize'],
INSERT: ['Inserted', 'Insert'],
INVALIDATE: ['Invalidated', 'Invalidate'],
ISSUE_COMMAND: ['Command Issued', 'Issue Command'],
LIST: ['Listed', 'List'],
LOOKUP: ['Lookedup', 'Lookup'],
MERGE: ['Merged', 'Merge'],
MODIFY: ['Modified', 'Modify'],
MOVE: ['Moved', 'Move'],
MOVE_MERGE: ['Moved(Merge)', 'Move(Merge)'],
NOACTION: ['No Action', 'No Action'],
NOACTION_PREVIEW: ['No Action (Preview)', 'No Action (Preview)'],
OBLITERATE: ['Obliterated', 'Obliterate'],
PERFORM: ['Action Performed', 'Perform Action'],
PRE_PROVISIONED_DISABLE: ['PreProvisioned Disabled', 'PreProvisioned Disable'],
PRE_PROVISIONED_REENABLE: ['PreProvisioned Reenabled', 'PreProvisioned Reenable'],
PRINT: ['Printed', 'Print'],
PROCESS: ['Processed', 'Process'],
PROCESS_PREVIEW: ['Processed (Preview)', 'Process (Preview)'],
PURGE: ['Purged', 'Purge'],
RECREATE: ['Recreated', 'Recreate'],
REENABLE: ['Reenabled', 'Reenable'],
REFRESH: ['Refreshed', 'Refresh'],
RELABEL: ['Relabeled', 'Relabel'],
REMOVE: ['Removed', 'Remove'],
REMOVE_PREVIEW: ['Removed (Preview)', 'Remove (Preview)'],
RENAME: ['Renamed', 'Rename'],
REOPEN: ['Reopened', 'Reopen'],
REPLACE: ['Replaced', 'Replace'],
REPLACE_DOMAIN: ['Domain Replaced', 'Replace Domain'],
REPORT: ['Reported', 'Report'],
RESET_YUBIKEY_PIV: ['Yubikey PIV Reset', 'Reset Yubikey PIV'],
RESPOND: ['Responded', 'Respond'],
RESTORE: ['Restored', 'Restore'],
RESUBMIT: ['Resubmitted', 'Resubmit'],
RETAIN: ['Retained', 'Retain'],
RETRIEVE_DATA: ['Data Retrieved', 'Retrieve Data'],
REVOKE: ['Revoked', 'Revoke'],
SAVE: ['Saved', 'Save'],
SEND: ['Sent', 'Send'],
SENDEMAIL: ['Email Sent', 'Send Email'],
SENDREPLY: ['Reply Sent', 'Send Reply'],
SET: ['Set', 'Set'],
SETUP: ['Set Up', 'Set Up'],
SHARE: ['Shared', 'Share'],
SHOW: ['Shown', 'Show'],
SIGNOUT: ['Signed Out', 'Signout'],
SKIP: ['Skipped', 'Skip'],
SPAM: ['Marked as Spam', 'Mark as Spam'],
SUBMIT: ['Submitted', 'Submit'],
SUSPEND: ['Suspended', 'Suspend'],
SYNC: ['Synced', 'Sync'],
TRANSFER: ['Transferred', 'Transfer'],
TRANSFER_OWNERSHIP: ['Ownership Transferred', 'Transfer Ownership'],
TRASH: ['Trashed', 'Trash'],
TURNOFF2SV: ['2-Step Verification Turned Off', 'Turn Off 2-Step Verification'],
UNDELETE: ['Undeleted', 'Undelete'],
UNHIDE: ['Unhidden', 'Unhide'],
UNSUSPEND: ['Unsuspended', 'Unsuspend'],
UNTRASH: ['Untrashed', 'Untrash'],
UNZIP: ['Unzipped', 'Unzip'],
UPDATE: ['Updated', 'Update'],
UPDATE_MOVE: ['Updated/Moved', 'Update/Move'],
UPDATE_OWNER: ['Updated to Owner', 'Update to Owner'],
UPDATE_PREVIEW: ['Updated (Preview)', 'Update (Preview)'],
UPLOAD: ['Uploaded', 'Upload'],
USE: ['Used', 'Use'],
VERIFY: ['Verified', 'Verify'],
VERIFYITEMEXISTS: ['Verified Item Exists', 'Verify Item Exists'],
WAITFORMAILBOX: ['Mailbox is Setup', 'Check Mailbox is Setup'],
WATCH: ['Watched', 'Watch'],
WIPE: ['Wiped', 'Wipe'],
WIPE_PREVIEW: ['Wiped (Preview)', 'Wipe (Preview)'],
}
#
MODIFIER_CONTENTS_WITH = 'contents with'
MODIFIER_FOR = 'for'
MODIFIER_FROM = 'from'
MODIFIER_IN = 'in'
MODIFIER_INTO = 'into'
MODIFIER_PREVIOUSLY_IN = 'previously in'
MODIFIER_TO = 'to'
MODIFIER_WITH_COTEACHER_OWNER = 'with co-teacher as owner'
MODIFIER_WITH_NEW_TEACHER_OWNER = 'with new teacher as owner'
MODIFIER_WITH_CURRENT_OWNER = 'with current owner'
MODIFIER_WITH = 'with'
MODIFIER_WITH_CONTENT_FROM = 'with content from'
PREFIX_NOT = 'Not'
PREVIEW = 'Preview'
SUCCESS = 'Success'
SUFFIX_FAILED = 'Failed'
def __init__(self):
self.action = None
def Set(self, action):
self.action = action
def Get(self):
return self.action
def ToPerform(self):
return self._NAMES[self.action][1]
def Performed(self):
return self._NAMES[self.action][0]
def Failed(self):
return f'{self._NAMES[self.action][1]} {self.SUFFIX_FAILED}'
def NotPerformed(self):
actionWords = self._NAMES[self.action][0].split(' ')
if len(actionWords) != 2:
return f'{self.PREFIX_NOT} {self._NAMES[self.action][0]}'
return f'{actionWords[0]} {self.PREFIX_NOT} {actionWords[1]}'
def PerformedName(self, action):
return self._NAMES[action][0]
def ToPerformName(self, action):
return self._NAMES[action][1]
def csvFormat(self):
return self.action == self.PRINT

View File

@@ -1,859 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Google API resources
"""
# APIs
ACCESSCONTEXTMANAGER = 'accesscontextmanager'
ALERTCENTER = 'alertcenter'
ANALYTICS_ADMIN = 'analyticsadmin'
CALENDAR = 'calendar'
BUSINESSACCOUNTMANAGEMENT = 'mybusinessaccountmanagement'
CBCM = 'cbcm'
CHAT = 'chat'
CHAT_CUSTOM_EMOJIS = 'chatcustomemojis'
CHAT_EVENTS = 'chatevents'
CHAT_MEMBERSHIPS = 'chatmemberships'
CHAT_MEMBERSHIPS_ADMIN = 'chatmembershipsadmin'
CHAT_MESSAGES = 'chatmessages'
CHAT_SECTIONS = 'chatsections'
CHAT_SPACES = 'chatspaces'
CHAT_SPACES_ADMIN = 'chatspacesadmin'
CHAT_SPACES_DELETE = 'chatspacesdelete'
CHAT_SPACES_DELETE_ADMIN = 'chatspacesdeleteadmin'
CHROMEMANAGEMENT = 'chromemanagement'
CHROMEMANAGEMENT_APPDETAILS = 'chromemanagementappdetails'
CHROMEMANAGEMENT_CHROMEPROFILES = 'chromemanagementchromeprofiles'
CHROMEMANAGEMENT_TELEMETRY = 'chromemanagementtelemetry'
CHROMEPOLICY = 'chromepolicy'
CHROMEVERSIONHISTORY = 'versionhistory'
CLASSROOM = 'classroom'
CLOUDCHANNEL = 'cloudchannel'
CLOUDIDENTITY_DEVICES = 'cloudidentitydevices'
CLOUDIDENTITY_GROUPS = 'cloudidentitygroups'
CLOUDIDENTITY_INBOUND_SSO = 'cloudidentityinboundsso'
CLOUDIDENTITY_ORGUNITS = 'cloudidentityorgunits'
CLOUDIDENTITY_ORGUNITS_BETA = 'cloudidentityorgunitsbeta'
CLOUDIDENTITY_POLICY = 'cloudidentitypolicy'
CLOUDIDENTITY_POLICY_BETA = 'cloudidentitypolicybeta'
CLOUDIDENTITY_USERINVITATIONS = 'cloudidentityuserinvitations'
CLOUDRESOURCEMANAGER = 'cloudresourcemanager'
CLOUDRESOURCEMANAGERV1 = 'cloudresourcemanagerv1'
CONTACTS = 'contacts'
CONTACTDELEGATION = 'contactdelegation'
DATATRANSFER = 'datatransfer'
DIRECTORY = 'directory'
DOCS = 'docs'
DRIVE2 = 'drive2'
DRIVE3 = 'drive3'
DRIVETD = 'drivetd'
DRIVEACTIVITY = 'driveactivity'
DRIVELABELS = 'drivelabels'
DRIVELABELS_ADMIN = 'drivelabelsadmin'
DRIVELABELS_USER = 'drivelabelsuser'
EMAIL_AUDIT = 'email-audit'
FORMS = 'forms'
GMAIL = 'gmail'
GROUPSMIGRATION = 'groupsmigration'
GROUPSSETTINGS = 'groupssettings'
IAM = 'iam'
IAM_CREDENTIALS = 'iamcredentials'
KEEP = 'keep'
LICENSING = 'licensing'
LOOKERSTUDIO = 'datastudio'
MEET_SPACES = 'meet'
MEET_READONLY = 'meetreadonly'
OAUTH2 = 'oauth2'
ORGPOLICY = 'orgpolicy'
PEOPLE = 'people'
PEOPLE_DIRECTORY = 'peopledirectory'
PEOPLE_OTHERCONTACTS = 'peopleothercontacts'
PRINTERS = 'printers'
PUBSUB = 'pubsub'
REPORTS = 'reports'
RESELLER = 'reseller'
SEARCHCONSOLE = 'searchconsole'
SERVICEACCOUNTLOOKUP = 'serviceaccountlookup'
SERVICEMANAGEMENT = 'servicemanagement'
SERVICEUSAGE = 'serviceusage'
SHEETS = 'sheets'
SHEETSTD = 'sheetstd'
SITEVERIFICATION = 'siteVerification'
STORAGE = 'storage'
STORAGEREAD = 'storageread'
STORAGEWRITE = 'storagewrite'
TAGMANAGER = 'tagmanager'
TAGMANAGER_USERS = 'tagmanagerusers'
TASKS = 'tasks'
VAULT = 'vault'
YOUTUBE = 'youtube'
#
CHROMEVERSIONHISTORY_URL = 'https://versionhistory.googleapis.com/v1/chrome/platforms'
DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive'
DRIVE_FILE_SCOPE = 'https://www.googleapis.com/auth/drive.file'
DRIVE_READONLY_SCOPE = 'https://www.googleapis.com/auth/drive.readonly'
GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send'
GOOGLE_AUTH_PROVIDER_X509_CERT_URL = 'https://www.googleapis.com/oauth2/v1/certs'
GOOGLE_OAUTH2_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'
GOOGLE_OAUTH2_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'
CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
IAM_SCOPE = 'https://www.googleapis.com/auth/iam'
PEOPLE_SCOPE = 'https://www.googleapis.com/auth/contacts'
STORAGE_READONLY_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only'
STORAGE_READWRITE_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_write'
USERINFO_EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email' # email
USERINFO_PROFILE_SCOPE = 'https://www.googleapis.com/auth/userinfo.profile' # profile
REQUIRED_SCOPES = [USERINFO_EMAIL_SCOPE, USERINFO_PROFILE_SCOPE]
REQUIRED_SCOPES_SET = set(REQUIRED_SCOPES)
NUM_CLIENT_SCOPES_ERROR_LIMIT = 48
#
JWT_APIS = {
ACCESSCONTEXTMANAGER: [CLOUD_PLATFORM_SCOPE],
CHAT: ['https://www.googleapis.com/auth/chat.bot'],
CLOUDRESOURCEMANAGER: [CLOUD_PLATFORM_SCOPE],
IAM: [IAM_SCOPE],
ORGPOLICY: [CLOUD_PLATFORM_SCOPE],
}
#
SCOPELESS_APIS = {
CHROMEVERSIONHISTORY,
OAUTH2,
SERVICEACCOUNTLOOKUP,
}
#
# Scopes not in the discovery doc that are still valid for the API.
EXTRA_SCOPES = {
CLOUDRESOURCEMANAGER: ['https://www.googleapis.com/auth/cloudplatformfolders',
'https://www.googleapis.com/auth/cloudplatformfolders.readonly',
'https://www.googleapis.com/auth/cloudplatformprojects',
'https://www.googleapis.com/auth/cloudplatformprojects.readonly',
'https://www.googleapis.com/auth/cloudplatformorganizations',
'https://www.googleapis.com/auth/cloudplatformorganizations.readonly',
],
VAULT: ['https://www.googleapis.com/auth/ediscovery', 'https://www.googleapis.com/auth/ediscovery.readonly'],
}
EXTRA_SCOPES[CLOUDRESOURCEMANAGERV1] = EXTRA_SCOPES[CLOUDRESOURCEMANAGER]
APIS_NEEDING_ACCESS_TOKEN = {
CBCM: ['https://www.googleapis.com/auth/admin.directory.device.chromebrowsers']
}
#
DEPRECATED_SCOPES = {
'https://www.googleapis.com/auth/cloud-identity',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/iam',
}
#
REFRESH_PERM_ERRORS = [
'invalid_grant: reauth related error (rapt_required)', # no way to reauth today
'invalid_grant: Token has been expired or revoked',
]
OAUTH2_TOKEN_ERRORS = [
'access_denied',
'access_denied: Requested client not authorized',
'access_denied: Account restricted',
'internal_failure: Backend Error',
'internal_failure: None',
'invalid_account: Forbidden',
'invalid_grant',
'invalid_grant: Bad Request',
'invalid_grant: Invalid email or User ID',
'invalid_grant: Not a valid email',
'invalid_grant: Invalid JWT: No valid verifier found for issuer',
'invalid_grant: reauth related error (invalid_rapt)',
'invalid_grant: The account has been deleted',
'invalid_request: Invalid impersonation prn email address'
]
OAUTH2_UNAUTHORIZED_ERRORS = [
'unauthorized_client: Client is unauthorized to retrieve access tokens using this method',
'unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested',
'unauthorized_client: Unauthorized client or scope in request',
]
PROJECT_APIS = [
'accesscontextmanager.googleapis.com',
'admin.googleapis.com',
'alertcenter.googleapis.com',
'analyticsadmin.googleapis.com',
# 'audit.googleapis.com',
'mybusinessaccountmanagement.googleapis.com',
'calendar-json.googleapis.com',
'chat.googleapis.com',
'chromemanagement.googleapis.com',
'chromepolicy.googleapis.com',
'classroom.googleapis.com',
'cloudchannel.googleapis.com',
'cloudidentity.googleapis.com',
'cloudresourcemanager.googleapis.com',
'contacts.googleapis.com',
'datastudio.googleapis.com',
'docs.googleapis.com',
'drive.googleapis.com',
'driveactivity.googleapis.com',
'drivelabels.googleapis.com',
'forms.googleapis.com',
'gmail.googleapis.com',
'groupsmigration.googleapis.com',
'groupssettings.googleapis.com',
'iam.googleapis.com',
'keep.googleapis.com',
'licensing.googleapis.com',
'meet.googleapis.com',
'people.googleapis.com',
'pubsub.googleapis.com',
'reseller.googleapis.com',
'searchconsole.googleapis.com',
'sheets.googleapis.com',
'siteverification.googleapis.com',
'storage-api.googleapis.com',
'tagmanager.googleapis.com',
'tasks.googleapis.com',
'vault.googleapis.com',
'youtube.googleapis.com',
]
_INFO = {
ACCESSCONTEXTMANAGER: {'name': 'Access Context Manager API', 'version': 'v1', 'v2discovery': True},
ALERTCENTER: {'name': 'AlertCenter API', 'version': 'v1beta1', 'v2discovery': True},
ANALYTICS_ADMIN: {'name': 'Analytics Admin API', 'version': 'v1beta', 'v2discovery': True},
BUSINESSACCOUNTMANAGEMENT: {'name': 'Business Account Management API', 'version': 'v1', 'v2discovery': True},
CALENDAR: {'name': 'Calendar API', 'version': 'v3', 'v2discovery': True, 'mappedAPI': 'calendar-json'},
CBCM: {'name': 'Chrome Browser Cloud Management API', 'version': 'v1.1beta1', 'v2discovery': True, 'localjson': True},
CHAT: {'name': 'Chat API', 'version': 'v1', 'v2discovery': True},
CHAT_CUSTOM_EMOJIS: {'name': 'Chat API - Custom Emojis', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_EVENTS: {'name': 'Chat API - Events', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_MEMBERSHIPS: {'name': 'Chat API - Memberships', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_MEMBERSHIPS_ADMIN: {'name': 'Chat API - Memberships Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_MESSAGES: {'name': 'Chat API - Messages', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_SECTIONS: {'name': 'Chat API - User Sections', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_SPACES: {'name': 'Chat API - Spaces', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_SPACES_ADMIN: {'name': 'Chat API - Spaces Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_SPACES_DELETE: {'name': 'Chat API - Spaces Delete', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CHAT_SPACES_DELETE_ADMIN: {'name': 'Chat API - Spaces Delete Admin', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT},
CLASSROOM: {'name': 'Classroom API', 'version': 'v1', 'v2discovery': True},
CHROMEMANAGEMENT: {'name': 'Chrome Management API', 'version': 'v1', 'v2discovery': True},
CHROMEMANAGEMENT_APPDETAILS: {'name': 'Chrome Management API - AppDetails', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHROMEMANAGEMENT},
CHROMEMANAGEMENT_TELEMETRY: {'name': 'Chrome Management API - Telemetry', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHROMEMANAGEMENT},
CHROMEPOLICY: {'name': 'Chrome Policy API', 'version': 'v1', 'v2discovery': True},
CHROMEVERSIONHISTORY: {'name': 'Chrome Version History API', 'version': 'v1', 'v2discovery': True},
CLOUDCHANNEL: {'name': 'Cloud Channel API', 'version': 'v1', 'v2discovery': True},
CLOUDIDENTITY_DEVICES: {'name': 'Cloud Identity API - Devices', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_GROUPS: {'name': 'Cloud Identity API - Groups', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_INBOUND_SSO: {'name': 'Cloud Identity API - Inbound SSO Settings', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_ORGUNITS: {'name': 'Cloud Identity API - OrgUnits', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_ORGUNITS_BETA: {'name': 'Cloud Identity API - OrgUnits Beta', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_POLICY: {'name': 'Cloud Identity API - Policy', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_POLICY_BETA: {'name': 'Cloud Identity API - Policy Beta', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_USERINVITATIONS: {'name': 'Cloud Identity API - User Invitations', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDRESOURCEMANAGER: {'name': 'Resource Manager API v3', 'version': 'v3', 'v2discovery': True},
CLOUDRESOURCEMANAGERV1: {'name': 'Resource Manager API v1', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudresourcemanager'},
CONTACTS: {'name': 'Contacts API', 'version': 'v3', 'v2discovery': False},
CONTACTDELEGATION: {'name': 'Contact Delegation API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
DATATRANSFER: {'name': 'Data Transfer API', 'version': 'datatransfer_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
DIRECTORY: {'name': 'Directory API', 'version': 'directory_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
DOCS: {'name': 'Docs API', 'version': 'v1', 'v2discovery': True},
DRIVE2: {'name': 'Drive API v2', 'version': 'v2', 'v2discovery': False, 'mappedAPI': 'drive'},
DRIVE3: {'name': 'Drive API v3', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'},
DRIVETD: {'name': 'Drive API v3 - write todrive data', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'},
DRIVEACTIVITY: {'name': 'Drive Activity API v2', 'version': 'v2', 'v2discovery': True},
DRIVELABELS_ADMIN: {'name': 'Drive Labels API - Admin', 'version': 'v2', 'v2discovery': True, 'mappedAPI': DRIVELABELS},
DRIVELABELS_USER: {'name': 'Drive Labels API - User', 'version': 'v2', 'v2discovery': True, 'mappedAPI': DRIVELABELS},
EMAIL_AUDIT: {'name': 'Email Audit API', 'version': 'v1', 'v2discovery': False},
FORMS: {'name': 'Forms API', 'version': 'v1', 'v2discovery': True},
GMAIL: {'name': 'Gmail API', 'version': 'v1', 'v2discovery': True},
GROUPSMIGRATION: {'name': 'Groups Migration API', 'version': 'v1', 'v2discovery': True},
GROUPSSETTINGS: {'name': 'Groups Settings API', 'version': 'v1', 'v2discovery': True},
IAM: {'name': 'Identity and Access Management API', 'version': 'v1', 'v2discovery': True},
IAM_CREDENTIALS: {'name': 'Identity and Access Management Credentials API', 'version': 'v1', 'v2discovery': True},
KEEP: {'name': 'Keep API', 'version': 'v1', 'v2discovery': True},
LICENSING: {'name': 'License Manager API', 'version': 'v1', 'v2discovery': True},
LOOKERSTUDIO: {'name': 'Looker Studio API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
MEET_SPACES: {'name': 'Meet API - Manage/Display Meeting Spaces', 'version': 'v2', 'v2discovery': True},
MEET_READONLY: {'name': 'Meet API - Read Meeting Spaces metadata', 'version': 'v2', 'v2discovery': True, 'mappedAPI': MEET_SPACES},
OAUTH2: {'name': 'OAuth2 API', 'version': 'v2', 'v2discovery': False},
ORGPOLICY: {'name': 'Organization Policy API', 'version': 'v2', 'v2discovery': True},
PEOPLE: {'name': 'People API', 'version': 'v1', 'v2discovery': True},
PEOPLE_DIRECTORY: {'name': 'People Directory API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': PEOPLE},
PEOPLE_OTHERCONTACTS: {'name': 'People API - Other Contacts', 'version': 'v1', 'v2discovery': True, 'mappedAPI': PEOPLE},
PRINTERS: {'name': 'Directory API - Printers', 'version': 'directory_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
PUBSUB: {'name': 'Pub / Sub API', 'version': 'v1', 'v2discovery': True},
REPORTS: {'name': 'Reports API', 'version': 'reports_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
RESELLER: {'name': 'Reseller API', 'version': 'v1', 'v2discovery': True},
SEARCHCONSOLE: {'name': 'Search Console API', 'version': 'v1', 'v2discovery': True},
SERVICEACCOUNTLOOKUP: {'name': 'Service Account Lookup pseudo-API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
SERVICEMANAGEMENT: {'name': 'Service Management API', 'version': 'v1', 'v2discovery': True},
SERVICEUSAGE: {'name': 'Service Usage API', 'version': 'v1', 'v2discovery': True},
SHEETS: {'name': 'Sheets API', 'version': 'v4', 'v2discovery': True},
SHEETSTD: {'name': 'Sheets API - write todrive data', 'version': 'v4', 'v2discovery': True, 'mappedAPI': SHEETS},
SITEVERIFICATION: {'name': 'Site Verification API', 'version': 'v1', 'v2discovery': True},
STORAGE: {'name': 'Cloud Storage API', 'version': 'v1', 'v2discovery': True},
STORAGEREAD: {'name': 'Cloud Storage API - Read', 'version': 'v1', 'v2discovery': True, 'mappedAPI': STORAGE},
STORAGEWRITE: {'name': 'Cloud Storage API - Write', 'version': 'v1', 'v2discovery': True, 'mappedAPI': STORAGE},
TAGMANAGER: {'name': 'Tag Manager API - Accounts, Containers, Workspaces, Tags', 'version': 'v2', 'v2discovery': True},
TAGMANAGER_USERS: {'name': 'Tag Manager API - Users', 'version': 'v2', 'v2discovery': True, 'mappedAPI': TAGMANAGER},
TASKS: {'name': 'Tasks API', 'version': 'v1', 'v2discovery': True},
VAULT: {'name': 'Vault API', 'version': 'v1', 'v2discovery': True},
YOUTUBE: {'name': 'Youtube API', 'version': 'v3', 'v2discovery': True},
}
READONLY = ['readonly',]
_CLIENT_SCOPES = [
{'name': 'Calendar API',
'api': CALENDAR,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/calendar'},
{'name': 'Chrome Browser Cloud Management API',
'api': CBCM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers'},
{'name': 'Chrome Management API - readonly',
'api': CHROMEMANAGEMENT,
'scope': 'https://www.googleapis.com/auth/chrome.management.reports.readonly'},
{'name': 'Chrome Management API - AppDetails readonly',
'api': CHROMEMANAGEMENT_APPDETAILS,
'scope': 'https://www.googleapis.com/auth/chrome.management.appdetails.readonly'},
{'name': 'Chrome Management API - Profiles',
'api': CHROMEMANAGEMENT_CHROMEPROFILES,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chrome.management.profiles'},
{'name': 'Chrome Management API - Telemetry readonly',
'api': CHROMEMANAGEMENT_TELEMETRY,
'scope': 'https://www.googleapis.com/auth/chrome.management.telemetry.readonly'},
{'name': 'Chrome Policy API',
'api': CHROMEPOLICY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chrome.management.policy'},
{'name': 'Chrome Printer Management API',
'api': PRINTERS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.chrome.printers'},
{'name': 'Chrome Version History API',
'api': CHROMEVERSIONHISTORY,
'scope': ''},
{'name': 'Classroom API - Courses',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.courses'},
{'name': 'Classroom API - Course Announcements',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.announcements'},
{'name': 'Classroom API - Course Topics',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.topics'},
{'name': 'Classroom API - Course Work/Materials',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.courseworkmaterials'},
{'name': 'Classroom API - Course Work/Submissions',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.coursework.students'},
{'name': 'Classroom API - Student Guardians',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.guardianlinks.students'},
{'name': 'Classroom API - Profile Emails',
'api': CLASSROOM,
'scope': 'https://www.googleapis.com/auth/classroom.profile.emails'},
{'name': 'Classroom API - Profile Photos',
'api': CLASSROOM,
'scope': 'https://www.googleapis.com/auth/classroom.profile.photos'},
{'name': 'Classroom API - Rosters',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.rosters'},
{'name': 'Cloud Channel API',
'api': CLOUDCHANNEL,
'subscopes': READONLY,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/apps.order'},
{'name': 'Cloud Identity API - Groups',
'api': CLOUDIDENTITY_GROUPS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/cloud-identity.groups'},
{'name': 'Cloud Identity API - Inbound SSO Settings',
'api': CLOUDIDENTITY_INBOUND_SSO,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/cloud-identity.inboundsso'},
{'name': 'Cloud Identity API - OrgUnits Beta',
'api': CLOUDIDENTITY_ORGUNITS_BETA,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/cloud-identity.orgunits'},
{'name': 'Cloud Identity API - Policy',
'api': CLOUDIDENTITY_POLICY,
'subscopes': READONLY,
'roByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
{'name': 'Cloud Identity API - Policy Beta',
'api': CLOUDIDENTITY_POLICY_BETA,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
{'name': 'Cloud Identity API - User Invitations',
'api': CLOUDIDENTITY_USERINVITATIONS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/cloud-identity.userinvitations'},
{'name': 'Cloud Storage API (Read Only, Vault/Takeout Download, Cloud Storage)',
'api': STORAGEREAD,
'offByDefault': True,
'scope': STORAGE_READONLY_SCOPE},
{'name': 'Cloud Storage API (Read/Write, Vault/Takeout Copy/Download, Cloud Storage)',
'api': STORAGEWRITE,
'offByDefault': True,
'scope': STORAGE_READWRITE_SCOPE},
{'name': 'Contacts API - Domain Shared Contacts',
'api': CONTACTS,
'scope': 'https://www.google.com/m8/feeds'},
{'name': 'Contact Delegation API',
'api': CONTACTDELEGATION,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.contact.delegation'},
{'name': 'Data Transfer API',
'api': DATATRANSFER,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.datatransfer'},
{'name': 'Directory API - Chrome OS Devices',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.device.chromeos'},
{'name': 'Directory API - Customers',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.customer'},
{'name': 'Directory API - Domains',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.domain'},
{'name': 'Directory API - Groups',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.group'},
{'name': 'Directory API - Mobile Devices Directory',
'api': DIRECTORY,
'subscopes': ['readonly', 'actiononly'],
'scope': 'https://www.googleapis.com/auth/admin.directory.device.mobile'},
{'name': 'Directory API - Organizational Units',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.orgunit'},
{'name': 'Directory API - Resource Calendars',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.resource.calendar'},
{'name': 'Directory API - Roles',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.rolemanagement'},
{'name': 'Directory API - User Schemas',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.userschema'},
{'name': 'Directory API - User Security',
'api': DIRECTORY,
'scope': 'https://www.googleapis.com/auth/admin.directory.user.security'},
{'name': 'Directory API - Users',
'api': DIRECTORY,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.user'},
{'name': 'Email Audit API',
'api': EMAIL_AUDIT,
'offByDefault': True,
'scope': 'https://apps-apis.google.com/a/feeds/compliance/audit/'},
{'name': 'Groups Migration API',
'api': GROUPSMIGRATION,
'scope': 'https://www.googleapis.com/auth/apps.groups.migration'},
{'name': 'Groups Settings API',
'api': GROUPSSETTINGS,
'scope': 'https://www.googleapis.com/auth/apps.groups.settings'},
{'name': 'License Manager API',
'api': LICENSING,
'scope': 'https://www.googleapis.com/auth/apps.licensing'},
{'name': 'People Directory API - readonly',
'api': PEOPLE_DIRECTORY,
'scope': 'https://www.googleapis.com/auth/directory.readonly'},
{'name': 'People API',
'api': PEOPLE,
'subscopes': READONLY,
'scope': PEOPLE_SCOPE},
{'name': 'Pub / Sub API',
'api': PUBSUB,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/pubsub'},
{'name': 'Reports API - Audit Reports readonly',
'api': REPORTS,
'scope': 'https://www.googleapis.com/auth/admin.reports.audit.readonly'},
{'name': 'Reports API - Usage Reports readonly',
'api': REPORTS,
'scope': 'https://www.googleapis.com/auth/admin.reports.usage.readonly'},
{'name': 'Reseller API',
'api': RESELLER,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/apps.order'},
{'name': 'Resource Manager API - Organizations readonly',
'api': CLOUDRESOURCEMANAGER,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloudplatformorganizations.readonly'},
{'name': 'Resource Manager API - Projects readonly',
'api': CLOUDRESOURCEMANAGER,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloudplatformprojects.readonly'},
{'name': 'Service Account Lookup pseudo-API',
'api': SERVICEACCOUNTLOOKUP,
'scope': ''},
{'name': 'Site Verification API',
'api': SITEVERIFICATION,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/siteverification'},
{'name': 'Vault API',
'api': VAULT,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/ediscovery'},
]
_COMMANDDATA_CLIENT_SCOPES = [
{'name': 'Drive API - commanddata_clientaccess',
'api': DRIVE3,
'scope': DRIVE_READONLY_SCOPE},
{'name': 'Sheets API - commanddata_clientaccess readonly',
'api': SHEETS,
'scope': 'https://www.googleapis.com/auth/spreadsheets.readonly'},
]
_TODRIVE_CLIENT_SCOPES = [
{'name': 'Drive API - todrive_clientaccess',
'api': DRIVE3,
'scope': DRIVE_SCOPE},
{'name': 'Drive File API - todrive_clientaccess',
'api': DRIVE3,
'scope': DRIVE_FILE_SCOPE},
{'name': 'Gmail API - todrive_clientaccess',
'api': GMAIL,
'scope': GMAIL_SEND_SCOPE},
{'name': 'Sheets API - todrive_clientaccess',
'api': SHEETS,
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
]
OAUTH2SA_SCOPES = 'us_scopes'
_SVCACCT_SCOPES = [
{'name': 'AlertCenter API',
'api': ALERTCENTER,
'scope': 'https://www.googleapis.com/auth/apps.alerts'},
{'name': 'Analytics Admin API - readonly',
'api': ANALYTICS_ADMIN,
'scope': 'https://www.googleapis.com/auth/analytics.readonly'},
{'name': 'Business Account Management API',
'api': BUSINESSACCOUNTMANAGEMENT,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/business.manage'},
{'name': 'Calendar API',
'api': CALENDAR,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/calendar'},
{'name': 'Chat API - Custom Emojis',
'api': CHAT_CUSTOM_EMOJIS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.customemojis'},
{'name': 'Chat API - Memberships',
'api': CHAT_MEMBERSHIPS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.memberships'},
{'name': 'Chat API - Memberships Admin',
'api': CHAT_MEMBERSHIPS_ADMIN,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.admin.memberships'},
{'name': 'Chat API - Messages',
'api': CHAT_MESSAGES,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.messages'},
{'name': 'Chat API - User Sections',
'api': CHAT_SECTIONS,
'offByDefault': True,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.users.sections'},
{'name': 'Chat API - Spaces',
'api': CHAT_SPACES,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.spaces'},
{'name': 'Chat API - Spaces Admin',
'api': CHAT_SPACES_ADMIN,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chat.admin.spaces'},
{'name': 'Chat API - Spaces Delete',
'api': CHAT_SPACES_DELETE,
'scope': 'https://www.googleapis.com/auth/chat.delete'},
{'name': 'Chat API - Spaces Delete Admin',
'api': CHAT_SPACES_DELETE_ADMIN,
'scope': 'https://www.googleapis.com/auth/chat.admin.delete'},
{'name': 'Classroom API - Course Announcements',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.announcements'},
{'name': 'Classroom API - Course Topics',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.topics'},
{'name': 'Classroom API - Course Work/Materials',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.courseworkmaterials'},
{'name': 'Classroom API - Course Work/Submissions',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.coursework.students'},
{'name': 'Classroom API - Profile Emails',
'api': CLASSROOM,
'scope': 'https://www.googleapis.com/auth/classroom.profile.emails'},
{'name': 'Classroom API - Profile Photos',
'api': CLASSROOM,
'scope': 'https://www.googleapis.com/auth/classroom.profile.photos'},
{'name': 'Classroom API - Rosters',
'api': CLASSROOM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/classroom.rosters'},
{'name': 'Cloud Identity Devices API',
'api': CLOUDIDENTITY_DEVICES,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/cloud-identity.devices'},
# {'name': 'Cloud Identity API - Policy',
# 'api': CLOUDIDENTITY_POLICY,
# 'subscopes': READONLY,
# 'roByDefault': True,
# 'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
# {'name': 'Cloud Identity API - Policy Beta',
# 'api': CLOUDIDENTITY_POLICY_BETA,
# 'offByDefault': True,
# 'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
# {'name': 'Cloud Identity User Invitations API',
# 'api': CLOUDIDENTITY_USERINVITATIONS,
# 'subscopes': READONLY,
# 'scope': 'https://www.googleapis.com/auth/cloud-identity'},
# {'name': 'Contacts API - Users',
# 'api': CONTACTS,
# 'scope': 'https://www.google.com/m8/feeds'},
{'name': 'Drive API',
'api': DRIVE3,
'subscopes': READONLY,
'scope': DRIVE_SCOPE},
{'name': 'Drive Activity API v2 - must pair with Drive API',
'api': DRIVEACTIVITY,
'scope': [DRIVE_READONLY_SCOPE,
'https://www.googleapis.com/auth/drive.activity']},
{'name': 'Drive Labels API - Admin',
'api': DRIVELABELS_ADMIN,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/drive.admin.labels'},
{'name': 'Drive Labels API - User',
'api': DRIVELABELS_USER,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/drive.labels'},
{'name': 'Docs API',
'api': DOCS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/documents'},
{'name': 'Forms API - must pair with Drive API',
'api': FORMS,
'scope': [DRIVE_READONLY_SCOPE,
'https://www.googleapis.com/auth/forms.body',
'https://www.googleapis.com/auth/forms.responses.readonly']},
{'name': 'Gmail API - Full Access (Labels, Messages)',
'api': GMAIL,
'scope': 'https://mail.google.com/'},
{'name': 'Gmail API - Full Access (Labels, Messages) except delete message',
'api': GMAIL,
'scope': 'https://www.googleapis.com/auth/gmail.modify'},
{'name': 'Gmail API - Basic Settings (Filters, IMAP, Language, POP, Vacation) - read/write, Sharing Settings (Delegates, Forwarding, SendAs) - read',
'api': GMAIL,
'scope': 'https://www.googleapis.com/auth/gmail.settings.basic'},
{'name': 'Gmail API - Sharing Settings (Delegates, Forwarding, SendAs) - write',
'api': GMAIL,
'scope': 'https://www.googleapis.com/auth/gmail.settings.sharing'},
# {'name': 'Identity and Access Management API',
# 'api': IAM,
# 'offByDefault': True,
# 'scope': CLOUD_PLATFORM_SCOPE},
{'name': 'Keep API',
'api': KEEP,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/keep'},
{'name': 'Looker Studio API',
'api': LOOKERSTUDIO,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/datastudio'},
{'name': 'Meet API - Manage/Display Meeting Spaces',
'api': MEET_SPACES,
'scope': ['https://www.googleapis.com/auth/meetings.space.created',
'https://www.googleapis.com/auth/meetings.space.settings']},
{'name': 'Meet API - Read Meeting Spaces metadata readonly',
'api': MEET_READONLY,
'scope': 'https://www.googleapis.com/auth/meetings.space.readonly'},
{'name': 'OAuth2 API',
'api': OAUTH2,
'scope': USERINFO_PROFILE_SCOPE},
{'name': 'People API',
'api': PEOPLE,
'subscopes': READONLY,
'scope': PEOPLE_SCOPE},
{'name': 'People Directory API - readonly',
'api': PEOPLE_DIRECTORY,
'scope': 'https://www.googleapis.com/auth/directory.readonly'},
{'name': 'People API - Other Contacts - readonly',
'api': PEOPLE_OTHERCONTACTS,
'scope': 'https://www.googleapis.com/auth/contacts.other.readonly'},
{'name': 'Search Console API - readonly',
'api': SEARCHCONSOLE,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/webmasters.readonly'},
{'name': 'Sheets API',
'api': SHEETS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
{'name': 'Site Verification API',
'api': SITEVERIFICATION,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/siteverification'},
{'name': 'Tag Manager API - Accounts, Containers, Workspaces, Tags - readonly',
'api': TAGMANAGER,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/tagmanager.readonly'},
{'name': 'Tag Manager API - Users',
'api': TAGMANAGER_USERS,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/tagmanager.manage.users'},
{'name': 'Tasks API',
'api': TASKS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/tasks'},
{'name': 'Youtube API - readonly',
'api': YOUTUBE,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/youtube.readonly'},
]
_SVCACCT_SPECIAL_SCOPES = [
{'name': 'Drive API - write todrive data - has access to all Drive',
'api': DRIVETD,
'offByDefault': True,
'scope': DRIVE_SCOPE},
{'name': 'Gmail API - Full Access - readonly',
'api': GMAIL,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/gmail.readonly'},
{'name': 'Gmail API - Send Messages - including todrive',
'api': GMAIL,
'offByDefault': True,
'scope': GMAIL_SEND_SCOPE},
{'name': 'Sheets API - write todrive data - has access to all Sheets',
'api': SHEETSTD,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
]
_USER_SVCACCT_ONLY_SCOPES = [
{'name': 'Groups Migration API',
'api': GROUPSMIGRATION,
'scope': 'https://www.googleapis.com/auth/apps.groups.migration'},
]
def getAPIName(api):
return _INFO[api]['name']
def getVersion(api):
version = _INFO[api]['version']
v2discovery = _INFO[api]['v2discovery']
api = _INFO[api].get('mappedAPI', api)
return (api, version, v2discovery)
def getAPIsList():
apisList = set()
for api, value in _INFO.items():
apisList.add(value.get('mappedAPI', api))
return apisList
def getClientScopesSet(api):
return {scope['scope'] for scope in _CLIENT_SCOPES if scope['api'] == api}
def getClientScopesList(commanddataClientAccess, todriveClientAccess):
caScopes = _CLIENT_SCOPES[:]
if commanddataClientAccess:
caScopes.extend(_COMMANDDATA_CLIENT_SCOPES)
if todriveClientAccess:
caScopes.extend(_TODRIVE_CLIENT_SCOPES)
return sorted(caScopes, key=lambda k: k['name'])
def getClientScopesURLs(commanddataClientAccess, todriveClientAccess):
caScopes = _CLIENT_SCOPES[:]
if commanddataClientAccess:
caScopes.extend(_COMMANDDATA_CLIENT_SCOPES)
if todriveClientAccess:
caScopes.extend(_TODRIVE_CLIENT_SCOPES)
return sorted({scope['scope'] for scope in _CLIENT_SCOPES})
def getSvcAcctScopeAPI(uscope):
for scope in _SVCACCT_SCOPES:
if uscope == scope['scope'] or (uscope.endswith('.readonly') and 'readonly' in scope.get('subscopes', [])):
return scope['api']
return None
def getSvcAcctScopes(userServiceAccountAccessOnly, svcAcctSpecialScopes):
saScopes = [scope['scope'] for scope in _SVCACCT_SCOPES]
if userServiceAccountAccessOnly:
saScopes.extend([scope['scope'] for scope in _USER_SVCACCT_ONLY_SCOPES])
if svcAcctSpecialScopes:
saScopes.extend([scope['scope'] for scope in _SVCACCT_SPECIAL_SCOPES])
return saScopes
def getSvcAcctScopesList(userServiceAccountAccessOnly, svcAcctSpecialScopes):
saScopes = _SVCACCT_SCOPES[:]
if userServiceAccountAccessOnly:
saScopes.extend(_USER_SVCACCT_ONLY_SCOPES)
if svcAcctSpecialScopes:
saScopes.extend(_SVCACCT_SPECIAL_SCOPES)
return sorted(saScopes, key=lambda k: k['name'])
def hasLocalJSON(api):
return _INFO[api].get('localjson', False)
def findAPIforScope(scopesList):
def checkScopeMatch(scope, cscope):
if cscope['scope'] == scope:
requiredAPIs.append(cscope['name'])
return True
if 'readonly' in cscope.get('subscopes', []) and cscope['scope']+'.readonly' == scope:
requiredAPIs.append(cscope['name']+' (supports readonly)')
return True
return False
requiredAPIs = []
for scope in scopesList:
for cscope in _CLIENT_SCOPES:
if checkScopeMatch(scope, cscope):
break
else:
for cscope in _SVCACCT_SCOPES:
if checkScopeMatch(scope, cscope):
break
if not requiredAPIs:
requiredAPIs = scopesList
return ' or '.join(requiredAPIs)

View File

@@ -1,653 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM gam.cfg variables
"""
import os
TRUE = 'true'
FALSE = 'false'
DEFAULT_CHARSET = 'utf-8'
MY_CUSTOMER = 'my_customer'
NEVER = 'Never'
TLS_CHOICE_MAP = {
'': '',
'tlsv1_2': 'TLSv1_2', 'tlsv1.2': 'TLSv1_2',
'tlsv1_3': 'TLSv1_3', 'tlsv1.3': 'TLSv1_3',
}
FN_CACERTS_PEM = 'cacerts.pem'
FN_CLIENT_SECRETS_JSON = 'client_secrets.json'
FN_EXTRA_ARGS_TXT = 'extra-args.txt'
FN_OAUTH2_TXT = 'oauth2.txt'
FN_OAUTH2SERVICE_JSON = 'oauth2service.json'
# Global variables defined in gam.cfg
# The following XXX constants are the names of the items in gam.cfg
# When retrieving lists of Google Drive activities from API, how many should be retrieved in each chunk
ACTIVITY_MAX_RESULTS = 'activity_max_results'
# Admin email address, required when enable_dasa is true, overrides oauth2.txt value otherwise
ADMIN_EMAIL = 'admin_email'
# Check if API calls rate exceeds limit
API_CALLS_RATE_CHECK = 'api_calls_rate_check'
# API calls per 100 seconds limit
API_CALLS_RATE_LIMIT = 'api_calls_rate_limit'
# API calls tries limit
API_CALLS_TRIES_LIMIT = 'api_calls_tries_limit'
# Automatically generate gam batch command if number of users specified in gam users xxx command exceeds this number
# Default: 0, do not automatically generate gam batch commands
AUTO_BATCH_MIN = 'auto_batch_min'
# When bailing on internal errors, how many total tries should be performed
BAIL_ON_INTERNAL_ERROR_TRIES = 'bail_on_internal_error_tries'
# When processing items in batches, how many should be processed in each batch
BATCH_SIZE = 'batch_size'
# Location of cacerts.pem for API calls
CACERTS_PEM = 'cacerts_pem'
# GAM cache directory
CACHE_DIR = 'cache_dir'
# GAM cache discovery only. If no_cache is False, only API discovery calls will be cached
CACHE_DISCOVERY_ONLY = 'cache_discovery_only'
# Channel custmerId from gam.cfg
CHANNEL_CUSTOMER_ID = 'channel_customer_id'
# Character set of batch, csv, data files
CHARSET = 'charset'
# When retrieving lists of Chat items from API, how many should be retrieved in each chunk
CHAT_MAX_RESULTS = 'chat_max_results'
# When retrieving lists of Google Classroom items from API, how many should be retrieved in each chunk
CLASSROOM_MAX_RESULTS = 'classroom_max_results'
# Path to client_secrets.json
CLIENT_SECRETS_JSON = 'client_secrets_json'
# Allowed clock skew in seconds
CLOCK_SKEW_IN_SECONDS = 'clock_skew_in_seconds'
# Command logging filename
CMDLOG = 'cmdlog'
# Bogus Command logging maximum number of backup log files
CMDLOG_MAX__BACKUPS = 'cmdlog_max__backups'
# Command logging maximum number of backup log files
CMDLOG_MAX_BACKUPS = 'cmdlog_max_backups'
# Command logging max kilo bytes per log file
CMDLOG_MAX_KILO_BYTES = 'cmdlog_max_kilo_bytes'
# Use client access for command data from Google Docs/Sheets
COMMANDDATA_CLIENTACCESS = 'commanddata_clientaccess'
# GAM config directory containing client_secrets.json, oauth2.txt, oauth2service.json, extra_args.txt
CONFIG_DIR = 'config_dir'
# When retrieving lists of Google Contacts from API, how many should be retrieved in each chunk
CONTACT_MAX_RESULTS = 'contact_max_results'
# Column delimiter in CSV input file
CSV_INPUT_COLUMN_DELIMITER = 'csv_input_column_delimiter'
# No escape character in CSV input file
CSV_INPUT_NO_ESCAPE_CHAR = 'csv_input_no_escape_char'
# Quote character in CSV input file
CSV_INPUT_QUOTE_CHAR = 'csv_input_quote_char'
# Filter for input column values
CSV_INPUT_ROW_FILTER = 'csv_input_row_filter'
# Mode (and|or) for input column values
CSV_INPUT_ROW_FILTER_MODE = 'csv_input_row_filter_mode'
# Filter for input column drop values
CSV_INPUT_ROW_DROP_FILTER = 'csv_input_row_drop_filter'
# Mode (and|or) for input column drop values
CSV_INPUT_ROW_DROP_FILTER_MODE = 'csv_input_row_drop_filter_mode'
# Limit number of input rows
CSV_INPUT_ROW_LIMIT = 'csv_input_row_limit'
# Convert newlines in text fields to "\n" in CSV output file
CSV_OUTPUT_CONVERT_CR_NL = 'csv_output_convert_cr_nl'
# Column delimiter in CSV output file
CSV_OUTPUT_COLUMN_DELIMITER = 'csv_output_column_delimiter'
# No escape character in CSV output file
CSV_OUTPUT_NO_ESCAPE_CHAR = 'csv_output_no_escape_char'
# Field list delimiter in CSV output file
CSV_OUTPUT_FIELD_DELIMITER = 'csv_output_field_delimiter'
# Filter for output column headers
CSV_OUTPUT_HEADER_FILTER = 'csv_output_header_filter'
# Filter for output column headers to drop
CSV_OUTPUT_HEADER_DROP_FILTER = 'csv_output_header_drop_filter'
# Force output column headers
CSV_OUTPUT_HEADER_FORCE = 'csv_output_header_force'
# Orde output column headers
CSV_OUTPUT_HEADER_ORDER = 'csv_output_header_order'
# Required output column headers
CSV_OUTPUT_HEADER_REQUIRED = 'csv_output_header_required'
# Line terminator in CSV output file
CSV_OUTPUT_LINE_TERMINATOR = 'csv_output_line_terminator'
# Quote character in CSV output file
CSV_OUTPUT_QUOTE_CHAR = 'csv_output_quote_char'
# Filter for output column values
CSV_OUTPUT_ROW_FILTER = 'csv_output_row_filter'
# Mode (and|or) for output column values
CSV_OUTPUT_ROW_FILTER_MODE = 'csv_output_row_filter_mode'
# Filter for output column drop values
CSV_OUTPUT_ROW_DROP_FILTER = 'csv_output_row_drop_filter'
# Mode (and|or) for output column drop values
CSV_OUTPUT_ROW_DROP_FILTER_MODE = 'csv_output_row_drop_filter_mode'
# Limit number of output rows
CSV_OUTPUT_ROW_LIMIT = 'csv_output_row_limit'
# Output sort headers
CSV_OUTPUT_SORT_HEADERS = 'csv_output_sort_headers'
# Column header subfield name delimiter in CSV output file
CSV_OUTPUT_SUBFIELD_DELIMITER = 'csv_output_subfield_delimiter'
# Add timestamp column to CSV output file
CSV_OUTPUT_TIMESTAMP_COLUMN = 'csv_output_timestamp_column'
# Output rows for users even if they do not have the print object (delegate, filters, ...)
CSV_OUTPUT_USERS_AUDIT = 'csv_output_users_audit'
# custmerId from gam.cfg or retrieved from Google
CUSTOMER_ID = 'customer_id'
# If debug_level > 0: extra_args['prettyPrint'] = True, httplib2.debuglevel = gam_debug_level, appsObj.debug = True
DEBUG_LEVEL = 'debug_level'
# redact sensitive credentials from debug output
DEBUG_REDACTION = 'debug_redaction'
# Developer Preview APIs
DEVELOPER_PREVIEW_APIS = 'developer_preview_apis'
# Developer Preview API Key
DEVELOPER_PREVIEW_API_KEY = 'developer_preview_api_key'
# When retrieving lists of ChromeOS devices from API, how many should be retrieved in each chunk
DEVICE_MAX_RESULTS = 'device_max_results'
# Domain obtained from gam.cfg or oauth2.txt
DOMAIN = 'domain'
# directory for file output
DRIVE_DIR = 'drive_dir'
# When retrieving lists of Drive files/folders from API, how many should be retrieved in each chunk
DRIVE_MAX_RESULTS = 'drive_max_results'
# When processing email messages in batches, how many should be processed in each batch
EMAIL_BATCH_SIZE = 'email_batch_size'
# Enable Delegated Admin Service Account
ENABLE_DASA = 'enable_dasa'
# Enable Cloud Session Reauthentication by borrowing a RAPT token from gcloud command
ENABLE_GCLOUD_REAUTH = 'enable_gcloud_reauth'
# When retrieving lists of calendar events from API, how many should be retrieved in each chunk
EVENT_MAX_RESULTS = 'event_max_results'
# Path to extra_args.txt
EXTRA_ARGS = 'extra_args'
# Google Cloud Project Organization ID
GCP_ORG_ID = 'gcp_org_id'
# Gmail CSE certificates directory
GMAIL_CSE_INCERT_DIR = 'gmail_cse_incert_dir'
# Gmail CSE KACL wrapped key files
GMAIL_CSE_INKEY_DIR = 'gmail_cse_inkey_dir'
# directory for file input
INPUT_DIR = 'input_dir'
# When processing items in batches, how many seconds should GAM wait between batches
INTER_BATCH_WAIT = 'inter_batch_wait'
# When retrieving lists of licenses from API, how many should be retrieved in each chunk
LICENSE_MAX_RESULTS = 'license_max_results'
# License SKUs to process
LICENSE_SKUS = 'license_skus'
# When retrieving lists of Google Group members from API, how many should be retrieved in each chunk
MEMBER_MAX_RESULTS = 'member_max_results'
# CI API Group members max page size when view=BASIC
MEMBER_MAX_RESULTS_CI_BASIC = 'member_max_results_ci_basic'
# CI API Group members max page size when view=FULL
MEMBER_MAX_RESULTS_CI_FULL = 'member_max_results_ci_full'
# When deleting or modifying Gmail messages, how many should be processed in each batch
MESSAGE_BATCH_SIZE = 'message_batch_size'
# When retrieving lists of Gmail messages from API, how many should be retrieved in each chunk
MESSAGE_MAX_RESULTS = 'message_max_results'
# When retrieving lists of Mobile devices from API, how many should be retrieved in each chunk
MOBILE_MAX_RESULTS = 'mobile_max_results'
# Number of parallel multiprocess pool.apply_async calls; -1: no limit, 0: NUM_THREADS, >0: specific limit
MULTIPROCESS_POOL_LIMIT = 'multiprocess_pool_limit'
# Value to substitute for NEVER_TIME
NEVER_TIME = 'never_time'
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
# and doOAuthRequest prints a link and waits for the verification code when oauth2.txt is being created
NO_BROWSER = 'no_browser'
# Disable GAM API caching
NO_CACHE = 'no_cache'
# Do noit use URL shortner for authentication URLs
NO_SHORT_URLS = 'no_short_urls'
# Disable GAM update check
NO_UPDATE_CHECK = 'no_update_check'
# Disable SSL certificate validation
NO_VERIFY_SSL = 'no_verify_ssl'
# Number of threads for gam tbatch
NUM_TBATCH_THREADS = 'num_tbatch_threads'
# Number of threads for gam batch/csv
NUM_THREADS = 'num_threads'
# Path to oauth2.txt
OAUTH2_TXT = 'oauth2_txt'
# File permissions for oauth2.txt.lock
OAUTH2_TXT_LOCK_MODE = 'oauth2_txt_lock_mode'
# Path to oauth2service.json
OAUTH2SERVICE_JSON = 'oauth2service_json'
# Output date format, empty defalts to ISOFormat
OUTPUT_DATEFORMAT = 'output_dateformat'
# Output time format, empty defalts to ISOFormat
OUTPUT_TIMEFORMAT = 'output_timeformat'
# When retrieving lists of people from API, how many should be retrieved in each chunk
PEOPLE_MAX_RESULTS = 'people_max_results'
# Domains for print alises|groups|users
PRINT_AGU_DOMAINS = 'print_agu_domains'
# OrgUnits for print cros
PRINT_CROS_OUS = 'print_cros_ous'
# OrgUnits and children for print cros
PRINT_CROS_OUS_AND_CHILDREN = 'print_cros_ous_and_children'
# Number of seconds to wait for batch/csv processes to complete
PROCESS_WAIT_LIMIT = 'process_wait_limit'
# Use quick method to move Chromebooks to OU
QUICK_CROS_MOVE = 'quick_cros_move'
# Quick info user: nogroups nolicenses noschemas
QUICK_INFO_USER = 'quick_info_user'
# resellerId from gam.cfg or retrieved from Google
RESELLER_ID = 'reseller_id'
# Retry service not available errors on API calls
RETRY_API_SERVICE_NOT_AVAILABLE = 'retry_api_service_not_available'
# Default section to use for processing
SECTION = 'section'
# Show API calls retry data
SHOW_API_CALLS_RETRY_DATA = 'show_api_calls_retry_data'
# Show commands when processing batch/csv/loop
SHOW_COMMANDS = 'show_commands'
# Convert newlines in text fields to "\n" in show commands
SHOW_CONVERT_CR_NL = 'show_convert_cr_nl'
# Add (n/m) to end of messages if number of items to be processed exceeds this number
SHOW_COUNTS_MIN = 'show_counts_min'
# Enable/disable "Getting ... " messages
SHOW_GETTINGS = 'show_gettings'
# Enable/disable NL at end of "Got ..." messages
SHOW_GETTINGS_GOT_NL = 'show_gettings_got_nl'
# Enable/disable showing multiprocess info in redirected stdout/stderr
SHOW_MULTIPROCESS_INFO = 'show_multiprocess_info'
# SMTP fqdn
SMTP_FQDN = 'smtp_fqdn'
# SMTP host
SMTP_HOST = 'smtp_host'
# SMTP username
SMTP_USERNAME = 'smtp_username'
# SMTP password
SMTP_PASSWORD = 'smtp_password'
# Time Zone
TIMEZONE = 'timezone'
## Minimum TLS Version required for HTTPS connections
TLS_MIN_VERSION = 'tls_min_version'
## Maximum TLS Version used for HTTPS connections
TLS_MAX_VERSION = 'tls_max_version'
# Clear basic filter when updating an existing sheet
TODRIVE_CLEARFILTER = 'todrive_clearfilter'
# Use client access for todrive
TODRIVE_CLIENTACCESS = 'todrive_clientaccess'
# Enable conversion to Google Sheets when uploading todrive files
TODRIVE_CONVERSION = 'todrive_conversion'
# Save local copy of CSV file
TODRIVE_LOCALCOPY = 'todrive_localcopy'
# Specify locale for Google Sheets
TODRIVE_LOCALE = 'todrive_locale'
# Suppress opening browser on todrive upload
TODRIVE_NOBROWSER = 'todrive_nobrowser'
# Suppress sending email on todrive upload
TODRIVE_NOEMAIL = 'todrive_noemail'
# No escape character in CSV output file
TODRIVE_NO_ESCAPE_CHAR = 'todrive_no_escape_char'
# ID/Name of parent folder for todrive files
TODRIVE_PARENT = 'todrive_parent'
# Append timestamp to todrive sheet name
TODRIVE_SHEET_TIMESTAMP = 'todrive_sheet_timestamp'
# Sheet timestamp format, empty defalts to ISOFormat
TODRIVE_SHEET_TIMEFORMAT = 'todrive_sheet_timeformat'
# Append timestamp to todrive file name
TODRIVE_TIMESTAMP = 'todrive_timestamp'
# Timestamp format, empty defalts to ISOFormat
TODRIVE_TIMEFORMAT = 'todrive_timeformat'
# Specify timezone for Google Sheets
TODRIVE_TIMEZONE = 'todrive_timezone'
# Upload data files with no data
TODRIVE_UPLOAD_NODATA = 'todrive_upload_nodata'
# User for todrive files
TODRIVE_USER = 'todrive_user'
# Truncate Client ID
TRUNCATE_CLIENT_ID = 'truncate_client_id'
# Update CrOS org unit with orgUnitId
UPDATE_CROS_OU_WITH_ID = 'update_cros_ou_with_id'
# Use admin access for chat where possible
USE_CHAT_ADMIN_ACCESS = 'use_chat_admin_access'
# Use course owner for course access
USE_COURSE_OWNER_ACCESS = 'use_course_owner_access'
# Use Project ID as Project Name and App Name
USE_PROJECTID_AS_NAME = 'use_projectid_as_name'
# When retrieving lists of Users from API, how many should be retrieved in each chunk
USER_MAX_RESULTS = 'user_max_results'
# User service account access only, no client access
USER_SERVICE_ACCOUNT_ACCESS_ONLY = 'user_service_account_access_only'
CSV_INPUT_ROW_FILTER_ITEMS = {CSV_INPUT_ROW_FILTER, CSV_INPUT_ROW_FILTER_MODE,
CSV_INPUT_ROW_DROP_FILTER, CSV_INPUT_ROW_DROP_FILTER_MODE,
CSV_INPUT_ROW_LIMIT}
CSV_OUTPUT_ROW_FILTER_ITEMS = {CSV_OUTPUT_HEADER_FILTER, CSV_OUTPUT_HEADER_DROP_FILTER,
CSV_OUTPUT_HEADER_FORCE, CSV_OUTPUT_HEADER_ORDER,
CSV_OUTPUT_HEADER_REQUIRED,
CSV_OUTPUT_ROW_FILTER, CSV_OUTPUT_ROW_FILTER_MODE,
CSV_OUTPUT_ROW_DROP_FILTER, CSV_OUTPUT_ROW_DROP_FILTER_MODE,
CSV_OUTPUT_ROW_LIMIT}
Defaults = {
ACTIVITY_MAX_RESULTS: '100',
ADMIN_EMAIL: '',
API_CALLS_RATE_CHECK: FALSE,
API_CALLS_RATE_LIMIT: '100',
API_CALLS_TRIES_LIMIT: '10',
AUTO_BATCH_MIN: '0',
BAIL_ON_INTERNAL_ERROR_TRIES: '2',
BATCH_SIZE: '50',
CACERTS_PEM: '',
CACHE_DIR: '',
CACHE_DISCOVERY_ONLY: TRUE,
CHARSET: DEFAULT_CHARSET,
CHANNEL_CUSTOMER_ID: '',
CHAT_MAX_RESULTS: '100',
CLASSROOM_MAX_RESULTS: '0',
CLIENT_SECRETS_JSON: FN_CLIENT_SECRETS_JSON,
CLOCK_SKEW_IN_SECONDS: '10',
CMDLOG: '',
CMDLOG_MAX_BACKUPS: 5,
CMDLOG_MAX_KILO_BYTES: 1000,
COMMANDDATA_CLIENTACCESS: FALSE,
CONFIG_DIR: '',
CONTACT_MAX_RESULTS: '100',
CSV_INPUT_COLUMN_DELIMITER: ',',
CSV_INPUT_NO_ESCAPE_CHAR: TRUE,
CSV_INPUT_QUOTE_CHAR: '\'"\'',
CSV_INPUT_ROW_FILTER: '',
CSV_INPUT_ROW_FILTER_MODE: 'allmatch',
CSV_INPUT_ROW_DROP_FILTER: '',
CSV_INPUT_ROW_DROP_FILTER_MODE: 'anymatch',
CSV_INPUT_ROW_LIMIT: '0',
CSV_OUTPUT_COLUMN_DELIMITER: ',',
CSV_OUTPUT_CONVERT_CR_NL: FALSE,
CSV_OUTPUT_NO_ESCAPE_CHAR: FALSE,
CSV_OUTPUT_FIELD_DELIMITER: "' '",
CSV_OUTPUT_HEADER_FILTER: '',
CSV_OUTPUT_HEADER_DROP_FILTER: '',
CSV_OUTPUT_HEADER_FORCE: '',
CSV_OUTPUT_HEADER_ORDER: '',
CSV_OUTPUT_HEADER_REQUIRED: '',
CSV_OUTPUT_LINE_TERMINATOR: 'lf',
CSV_OUTPUT_QUOTE_CHAR: '\'"\'',
CSV_OUTPUT_ROW_FILTER: '',
CSV_OUTPUT_ROW_FILTER_MODE: 'allmatch',
CSV_OUTPUT_ROW_DROP_FILTER: '',
CSV_OUTPUT_ROW_DROP_FILTER_MODE: 'anymatch',
CSV_OUTPUT_ROW_LIMIT: '0',
CSV_OUTPUT_SORT_HEADERS: '',
CSV_OUTPUT_SUBFIELD_DELIMITER: '.',
CSV_OUTPUT_TIMESTAMP_COLUMN: '',
CSV_OUTPUT_USERS_AUDIT: FALSE,
CUSTOMER_ID: MY_CUSTOMER,
DEBUG_LEVEL: '0',
DEBUG_REDACTION: TRUE,
DEVELOPER_PREVIEW_APIS: '',
DEVELOPER_PREVIEW_API_KEY: '',
DEVICE_MAX_RESULTS: '200',
DOMAIN: '',
DRIVE_DIR: '',
DRIVE_MAX_RESULTS: '1000',
EMAIL_BATCH_SIZE: '50',
ENABLE_DASA: FALSE,
ENABLE_GCLOUD_REAUTH: FALSE,
EVENT_MAX_RESULTS: '250',
EXTRA_ARGS: '',
GCP_ORG_ID: '',
GMAIL_CSE_INCERT_DIR: '',
GMAIL_CSE_INKEY_DIR: '',
INPUT_DIR: '.',
INTER_BATCH_WAIT: '0',
LICENSE_MAX_RESULTS: '100',
LICENSE_SKUS: '',
MEMBER_MAX_RESULTS: '200',
MEMBER_MAX_RESULTS_CI_BASIC: '1000',
MEMBER_MAX_RESULTS_CI_FULL: '500',
MESSAGE_BATCH_SIZE: '50',
MESSAGE_MAX_RESULTS: '500',
MOBILE_MAX_RESULTS: '100',
MULTIPROCESS_POOL_LIMIT: '0',
NEVER_TIME: NEVER,
NO_BROWSER: FALSE,
NO_CACHE: FALSE,
NO_SHORT_URLS: TRUE,
NO_UPDATE_CHECK: TRUE,
NO_VERIFY_SSL: FALSE,
NUM_TBATCH_THREADS: '2',
NUM_THREADS: '5',
OAUTH2_TXT: FN_OAUTH2_TXT,
OAUTH2_TXT_LOCK_MODE: '644',
OAUTH2SERVICE_JSON: FN_OAUTH2SERVICE_JSON,
OUTPUT_DATEFORMAT: '',
OUTPUT_TIMEFORMAT: '',
PEOPLE_MAX_RESULTS: '100',
PRINT_AGU_DOMAINS: '',
PRINT_CROS_OUS: '',
PRINT_CROS_OUS_AND_CHILDREN: '',
PROCESS_WAIT_LIMIT: '0',
QUICK_CROS_MOVE: FALSE,
QUICK_INFO_USER: FALSE,
RESELLER_ID: '',
RETRY_API_SERVICE_NOT_AVAILABLE: FALSE,
SECTION: '',
SHOW_API_CALLS_RETRY_DATA: FALSE,
SHOW_COMMANDS: FALSE,
SHOW_CONVERT_CR_NL: FALSE,
SHOW_COUNTS_MIN: '1',
SHOW_GETTINGS: TRUE,
SHOW_GETTINGS_GOT_NL: FALSE,
SHOW_MULTIPROCESS_INFO: FALSE,
SMTP_FQDN: '',
SMTP_HOST: '',
SMTP_USERNAME: '',
SMTP_PASSWORD: '',
TIMEZONE: 'utc',
TLS_MIN_VERSION: 'TLSv1_3',
TLS_MAX_VERSION: '',
TODRIVE_CLEARFILTER: FALSE,
TODRIVE_CLIENTACCESS: FALSE,
TODRIVE_CONVERSION: TRUE,
TODRIVE_LOCALCOPY: FALSE,
TODRIVE_LOCALE: '',
TODRIVE_NOBROWSER: '',
TODRIVE_NOEMAIL: '',
TODRIVE_NO_ESCAPE_CHAR: TRUE,
TODRIVE_PARENT: 'root',
TODRIVE_SHEET_TIMESTAMP: 'copy', # copy from TODRIVE_TIMESTAMP
TODRIVE_SHEET_TIMEFORMAT: 'copy', # copy from TODRIVE_TIMEFORMAT
TODRIVE_TIMESTAMP: FALSE,
TODRIVE_TIMEFORMAT: '',
TODRIVE_TIMEZONE: '',
TODRIVE_UPLOAD_NODATA: TRUE,
TODRIVE_USER: '',
TRUNCATE_CLIENT_ID: FALSE,
UPDATE_CROS_OU_WITH_ID: FALSE,
USE_CHAT_ADMIN_ACCESS: FALSE,
USE_COURSE_OWNER_ACCESS: FALSE,
USE_PROJECTID_AS_NAME: FALSE,
USER_MAX_RESULTS: '500',
USER_SERVICE_ACCOUNT_ACCESS_ONLY: FALSE,
}
Values = {DEBUG_LEVEL: 0}
TYPE_BOOLEAN = 'bool'
TYPE_CHARACTER = 'char'
TYPE_CHOICE = 'choi'
TYPE_CHOICE_LIST = 'chol'
TYPE_DATETIME = 'datm'
TYPE_DIRECTORY = 'dire'
TYPE_EMAIL = 'emai'
TYPE_EMAIL_OPTIONAL = 'emao'
TYPE_FILE = 'file'
TYPE_FLOAT = 'floa'
TYPE_HEADERFILTER = 'heaf'
TYPE_HEADERFORCEREQUIRED = 'hefr'
TYPE_HEADERORDER = 'heor'
TYPE_INTEGER = 'inte'
TYPE_LANGUAGE = 'lang'
TYPE_LOCALE = 'locl'
TYPE_PASSWORD = 'pass'
TYPE_ROWFILTER = 'rowf'
TYPE_STRING = 'stri'
TYPE_STRINGLIST = 'strl'
TYPE_TIMEZONE = 'tmzn'
VAR_TYPE = 'type'
VAR_ENVVAR = 'enva'
VAR_CHOICES = 'chod'
VAR_LIMITS = 'lmit'
VAR_SFFT = 'sfft'
VAR_SIGFILE = 'sigf'
VAR_ACCESS = 'aces'
VAR_INFO = {
ACTIVITY_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 500)},
ADMIN_EMAIL: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GA_ADMIN_EMAIL', VAR_LIMITS: (0, None)},
API_CALLS_RATE_CHECK: {VAR_TYPE: TYPE_BOOLEAN},
API_CALLS_RATE_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (50, None)},
API_CALLS_TRIES_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (3, 30)},
AUTO_BATCH_MIN: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_AUTOBATCH', VAR_LIMITS: (0, 100)},
BAIL_ON_INTERNAL_ERROR_TRIES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)},
BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_BATCH_SIZE', VAR_LIMITS: (1, 1000)},
CACERTS_PEM: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'GAM_CA_FILE', VAR_ACCESS: os.R_OK},
CACHE_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMCACHEDIR'},
CACHE_DISCOVERY_ONLY: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'allcache.txt', VAR_SFFT: (TRUE, FALSE)},
CHARSET: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GAM_CHARSET', VAR_LIMITS: (1, None)},
CHANNEL_CUSTOMER_ID: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
CHAT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
CLASSROOM_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 1000)},
CLIENT_SECRETS_JSON: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'CLIENTSECRETS', VAR_ACCESS: os.R_OK},
CLOCK_SKEW_IN_SECONDS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (10, 3600)},
CMDLOG: {VAR_TYPE: TYPE_FILE, VAR_ACCESS: os.W_OK},
CMDLOG_MAX_BACKUPS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)},
CMDLOG_MAX_KILO_BYTES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (100, 10000)},
COMMANDDATA_CLIENTACCESS: {VAR_TYPE: TYPE_BOOLEAN},
CONFIG_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMUSERCONFIGDIR'},
CONTACT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)},
CSV_INPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
CSV_INPUT_NO_ESCAPE_CHAR: {VAR_TYPE: TYPE_BOOLEAN},
CSV_INPUT_QUOTE_CHAR: {VAR_TYPE: TYPE_CHARACTER},
CSV_INPUT_ROW_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
CSV_INPUT_ROW_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
CSV_INPUT_ROW_DROP_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
CSV_INPUT_ROW_DROP_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
CSV_INPUT_ROW_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)},
CSV_OUTPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
CSV_OUTPUT_CONVERT_CR_NL: {VAR_TYPE: TYPE_BOOLEAN},
CSV_OUTPUT_NO_ESCAPE_CHAR: {VAR_TYPE: TYPE_BOOLEAN},
CSV_OUTPUT_FIELD_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
CSV_OUTPUT_HEADER_FILTER: {VAR_TYPE: TYPE_HEADERFILTER},
CSV_OUTPUT_HEADER_DROP_FILTER: {VAR_TYPE: TYPE_HEADERFILTER},
CSV_OUTPUT_HEADER_FORCE: {VAR_TYPE: TYPE_HEADERFORCEREQUIRED},
CSV_OUTPUT_HEADER_ORDER: {VAR_TYPE: TYPE_HEADERORDER},
CSV_OUTPUT_HEADER_REQUIRED: {VAR_TYPE: TYPE_HEADERFORCEREQUIRED},
CSV_OUTPUT_LINE_TERMINATOR: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'cr': '\r', 'lf': '\n', 'crlf': '\r\n'}},
CSV_OUTPUT_QUOTE_CHAR: {VAR_TYPE: TYPE_CHARACTER},
CSV_OUTPUT_ROW_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
CSV_OUTPUT_ROW_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
CSV_OUTPUT_ROW_DROP_FILTER: {VAR_TYPE: TYPE_ROWFILTER},
CSV_OUTPUT_ROW_DROP_FILTER_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'allmatch': True, 'anymatch': False}},
CSV_OUTPUT_ROW_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)},
CSV_OUTPUT_SORT_HEADERS: {VAR_TYPE: TYPE_STRINGLIST},
CSV_OUTPUT_SUBFIELD_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},
CSV_OUTPUT_TIMESTAMP_COLUMN: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
CSV_OUTPUT_USERS_AUDIT: {VAR_TYPE: TYPE_BOOLEAN},
CUSTOMER_ID: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'CUSTOMER_ID', VAR_LIMITS: (0, None)},
DEBUG_LEVEL: {VAR_TYPE: TYPE_INTEGER, VAR_SIGFILE: 'debug.gam', VAR_LIMITS: (0, None), VAR_SFFT: ('0', '4')},
DEBUG_REDACTION: {VAR_TYPE: TYPE_BOOLEAN},
DEVELOPER_PREVIEW_APIS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
DEVELOPER_PREVIEW_API_KEY: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
DEVICE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 200)},
DOMAIN: {VAR_TYPE: TYPE_STRING, VAR_ENVVAR: 'GA_DOMAIN', VAR_LIMITS: (0, None)},
DRIVE_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMDRIVEDIR'},
DRIVE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
EMAIL_BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 100)},
ENABLE_DASA: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'enabledasa.txt', VAR_SFFT: (FALSE, TRUE)},
ENABLE_GCLOUD_REAUTH: {VAR_TYPE: TYPE_BOOLEAN},
EVENT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 2500)},
EXTRA_ARGS: {VAR_TYPE: TYPE_FILE, VAR_SIGFILE: FN_EXTRA_ARGS_TXT, VAR_SFFT: ('', FN_EXTRA_ARGS_TXT), VAR_ACCESS: os.R_OK},
GCP_ORG_ID: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
GMAIL_CSE_INCERT_DIR: {VAR_TYPE: TYPE_DIRECTORY},
GMAIL_CSE_INKEY_DIR: {VAR_TYPE: TYPE_DIRECTORY},
INPUT_DIR: {VAR_TYPE: TYPE_DIRECTORY},
INTER_BATCH_WAIT: {VAR_TYPE: TYPE_FLOAT, VAR_LIMITS: (0.0, 60.0)},
LICENSE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (10, 1000)},
LICENSE_SKUS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
MEMBER_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 200)},
MEMBER_MAX_RESULTS_CI_BASIC: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
MEMBER_MAX_RESULTS_CI_FULL: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 500)},
MESSAGE_BATCH_SIZE: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
MESSAGE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)},
MOBILE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 100)},
MULTIPROCESS_POOL_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (-1, None)},
NEVER_TIME: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
NO_BROWSER: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'nobrowser.txt', VAR_SFFT: (FALSE, TRUE)},
NO_CACHE: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'nocache.txt', VAR_SFFT: (FALSE, TRUE)},
NO_SHORT_URLS: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'noshorturls.txt', VAR_SFFT: (FALSE, TRUE)},
NO_UPDATE_CHECK: {VAR_TYPE: TYPE_BOOLEAN},
NO_VERIFY_SSL: {VAR_TYPE: TYPE_BOOLEAN},
NUM_TBATCH_THREADS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 1000)},
NUM_THREADS: {VAR_TYPE: TYPE_INTEGER, VAR_ENVVAR: 'GAM_THREADS', VAR_LIMITS: (1, 1000)},
OAUTH2_TXT: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'OAUTHFILE', VAR_ACCESS: os.R_OK | os.W_OK},
OAUTH2_TXT_LOCK_MODE: {VAR_TYPE: TYPE_CHOICE, VAR_CHOICES: {'644': 0o644, '664': 0o664, '666': 0o666}},
OAUTH2SERVICE_JSON: {VAR_TYPE: TYPE_FILE, VAR_ENVVAR: 'OAUTHSERVICEFILE', VAR_ACCESS: os.R_OK | os.W_OK},
OUTPUT_DATEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
OUTPUT_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
PEOPLE_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 1000)},
PRINT_AGU_DOMAINS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
PRINT_CROS_OUS: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
PRINT_CROS_OUS_AND_CHILDREN: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
PROCESS_WAIT_LIMIT: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, None)},
QUICK_CROS_MOVE: {VAR_TYPE: TYPE_BOOLEAN},
QUICK_INFO_USER: {VAR_TYPE: TYPE_BOOLEAN},
RESELLER_ID: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
RETRY_API_SERVICE_NOT_AVAILABLE: {VAR_TYPE: TYPE_BOOLEAN},
SECTION: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
SHOW_API_CALLS_RETRY_DATA: {VAR_TYPE: TYPE_BOOLEAN},
SHOW_COMMANDS: {VAR_TYPE: TYPE_BOOLEAN},
SHOW_CONVERT_CR_NL: {VAR_TYPE: TYPE_BOOLEAN},
SHOW_COUNTS_MIN: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (0, 100)},
SHOW_GETTINGS: {VAR_TYPE: TYPE_BOOLEAN},
SHOW_GETTINGS_GOT_NL: {VAR_TYPE: TYPE_BOOLEAN},
SHOW_MULTIPROCESS_INFO: {VAR_TYPE: TYPE_BOOLEAN},
SMTP_FQDN: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
SMTP_HOST: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
SMTP_USERNAME: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
SMTP_PASSWORD: {VAR_TYPE: TYPE_PASSWORD, VAR_LIMITS: (0, None)},
TIMEZONE: {VAR_TYPE: TYPE_TIMEZONE},
TLS_MIN_VERSION: {VAR_TYPE: TYPE_CHOICE, VAR_ENVVAR: 'GAM_TLS_MIN_VERSION', VAR_CHOICES: TLS_CHOICE_MAP},
TLS_MAX_VERSION: {VAR_TYPE: TYPE_CHOICE, VAR_ENVVAR: 'GAM_TLS_MAX_VERSION', VAR_CHOICES: TLS_CHOICE_MAP},
TODRIVE_CLEARFILTER: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_CLIENTACCESS: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_CONVERSION: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_LOCALCOPY: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_LOCALE: {VAR_TYPE: TYPE_LOCALE},
TODRIVE_NOBROWSER: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'nobrowser.txt', VAR_SFFT: (FALSE, TRUE)},
TODRIVE_NOEMAIL: {VAR_TYPE: TYPE_BOOLEAN, VAR_SIGFILE: 'notdemail.txt', VAR_SFFT: (FALSE, TRUE)},
TODRIVE_NO_ESCAPE_CHAR: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_PARENT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
TODRIVE_SHEET_TIMESTAMP: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_SHEET_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
TODRIVE_TIMESTAMP: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_TIMEFORMAT: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
TODRIVE_TIMEZONE: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
TODRIVE_UPLOAD_NODATA: {VAR_TYPE: TYPE_BOOLEAN},
TODRIVE_USER: {VAR_TYPE: TYPE_STRING, VAR_LIMITS: (0, None)},
TRUNCATE_CLIENT_ID: {VAR_TYPE: TYPE_BOOLEAN},
UPDATE_CROS_OU_WITH_ID: {VAR_TYPE: TYPE_BOOLEAN},
USE_CHAT_ADMIN_ACCESS: {VAR_TYPE: TYPE_BOOLEAN},
USE_COURSE_OWNER_ACCESS: {VAR_TYPE: TYPE_BOOLEAN},
USE_PROJECTID_AS_NAME: {VAR_TYPE: TYPE_BOOLEAN},
USER_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 500)},
USER_SERVICE_ACCOUNT_ACCESS_ONLY: {VAR_TYPE: TYPE_BOOLEAN},
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,869 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM entity processing
"""
class GamEntity():
ROLE_MANAGER = 'MANAGER'
ROLE_MEMBER = 'MEMBER'
ROLE_OWNER = 'OWNER'
ROLE_LIST = [ROLE_MANAGER, ROLE_MEMBER, ROLE_OWNER]
ROLE_USER = 'USER'
ROLE_MANAGER_MEMBER = ','.join([ROLE_MANAGER, ROLE_MEMBER])
ROLE_MANAGER_OWNER = ','.join([ROLE_MANAGER, ROLE_OWNER])
ROLE_MEMBER_OWNER = ','.join([ROLE_MEMBER, ROLE_OWNER])
ROLE_MANAGER_MEMBER_OWNER = ','.join(ROLE_LIST)
ROLE_PUBLIC = 'PUBLIC'
ROLE_ALL = ROLE_MANAGER_MEMBER_OWNER
TYPE_CBCM_BROWSER = 'CBCM_BROWSER'
TYPE_CUSTOMER = 'CUSTOMER'
TYPE_EXTERNAL = 'EXTERNAL'
TYPE_OTHER = 'OTHER'
TYPE_GROUP = 'GROUP'
TYPE_SERVICE_ACCOUNT = 'SERVICE_ACCOUNT'
TYPE_USER = 'USER'
# Keys into NAMES; arbitrary values but must be unique
ACCESS_TOKEN = 'atok'
ACCOUNT = 'acct'
ACTION = 'actn'
ACTIVITY = 'actv'
ADMINISTRATOR = 'admi'
ADMIN_ROLE = 'adro'
ADMIN_ROLE_ASSIGNMENT = 'adra'
ALERT = 'alrt'
ALERT_ID = 'alri'
ALERT_FEEDBACK = 'alfb'
ALERT_FEEDBACK_ID = 'alfi'
ALERT_SETTINGS = 'alrs'
ALIAS = 'alia'
ALIAS_EMAIL = 'alie'
ALIAS_TARGET = 'alit'
ANALYTIC_ACCOUNT = 'anac'
ANALYTIC_ACCOUNT_SUMMARY = 'anas'
ANALYTIC_DATASTREAM = 'anad'
ANALYTIC_PROPERTY = 'anap'
API = 'api '
APP_ACCESS_SETTINGS = 'apps'
APP_ID = 'appi'
APP_NAME = 'appn'
APPLICATION_SPECIFIC_PASSWORD = 'aspa'
ARROWS_ENABLED = 'arro'
ATTACHMENT = 'atta'
ATTENDEE = 'atnd'
AUDIT_ACTIVITY_REQUEST = 'auda'
AUDIT_EXPORT_REQUEST = 'audx'
AUDIT_MONITOR_REQUEST = 'audm'
BACKUP_VERIFICATION_CODES = 'buvc'
BUILDING = 'bldg'
BUILDING_ID = 'bldi'
BUSINESS_PROFILE_ACCOUNT = 'bpac'
CAA_LEVEL = 'calv'
CALENDAR = 'cale'
CALENDAR_ACL = 'cacl'
CALENDAR_SETTINGS = 'cset'
CHANNEL_CUSTOMER = 'chcu'
CHANNEL_CUSTOMER_ENTITLEMENT = 'chce'
CHANNEL_OFFER = 'chof'
CHANNEL_PRODUCT = 'chpr'
CHANNEL_SKU = 'chsk'
CHAT_BOT = 'chbo'
CHAT_ADMIN = 'chad'
CHAT_EMOJI = 'chem'
CHAT_EVENT = 'chev'
CHAT_MANAGER_USER = 'chgu'
CHAT_MEMBER = 'chme'
CHAT_MEMBER_GROUP = 'chmg'
CHAT_MEMBER_USER = 'chmu'
CHAT_MESSAGE = 'chms'
CHAT_MESSAGE_ID = 'chmi'
CHAT_OWNER_USER = 'chou'
CHAT_SECTION = 'chse'
CHAT_SECTION_ITEM = 'chsi'
CHAT_SPACE = 'chsp'
CHAT_THREAD = 'chth'
CHILD_ORGANIZATIONAL_UNIT = 'corg'
CHROME_APP = 'capp'
CHROME_APP_DEVICE = 'capd'
CHROME_BROWSER = 'chbr'
CHROME_BROWSER_ENROLLMENT_TOKEN = 'cbet'
CHROME_CHANNEL = 'chan'
CHROME_DEVICE = 'chdv'
CHROME_DEVICE_COUNT = 'chdc'
CHROME_MODEL = 'chmo'
CHROME_NETWORK_ID = 'chni'
CHROME_NETWORK_NAME = 'chnn'
CHROME_PLATFORM = 'cpla'
CHROME_POLICY = 'cpol'
CHROME_POLICY_IMAGE = 'cpim'
CHROME_POLICY_SCHEMA = 'cpsc'
CHROME_PROFILE = 'cpro'
CHROME_PROFILE_COMMAND = 'cpcm'
CHROME_RELEASE = 'crel'
CHROME_VERSION = 'cver'
CLASSIFICATION_LABEL = 'dlab'
CLASSIFICATION_LABEL_FIELD_ID = 'dlfi'
CLASSIFICATION_LABEL_ID = 'dlid'
CLASSIFICATION_LABEL_NAME = 'dlna'
CLASSIFICATION_LABEL_PERMISSION = 'dlpe'
CLASSIFICATION_LABEL_PERMISSION_NAME = 'dlpn'
CLASSROOM_INVITATION = 'clai'
CLASSROOM_INVITATION_OWNER = 'clio'
CLASSROOM_INVITATION_STUDENT = 'clis'
CLASSROOM_INVITATION_TEACHER = 'clit'
CLASSROOM_OAUTH2_TXT_FILE = 'coa'
CLASSROOM_USER_PROFILE = 'clup'
CLIENT_ID = 'clid'
CLIENT_SECRETS_JSON_FILE = 'csjf'
CLOUD_IDENTITY_GROUP = 'cidg'
CLOUD_STORAGE_BUCKET = 'clsb'
CLOUD_STORAGE_FILE = 'clsf'
COLLABORATOR = 'cola'
COMMAND_ID = 'cmdi'
COMPANY_DEVICE = 'codv'
CONFIG_FILE = 'conf'
CONTACT = 'cont'
CONTACT_DELEGATE = 'cond'
CONTACT_GROUP = 'cogr'
CONTACT_GROUP_NAME = 'cogn'
COPYFROM_COURSE = 'cfco'
COPYFROM_GROUP = 'cfgr'
COURSE = 'cour'
COURSE_ALIAS = 'coal'
COURSE_ANNOUNCEMENT = 'cann'
COURSE_ANNOUNCEMENT_ID = 'caid'
COURSE_ANNOUNCEMENT_STATE = 'cast'
COURSE_MATERIAL_DRIVEFILE = 'comd'
COURSE_MATERIAL_FORM = 'comf'
COURSE_MATERIAL = 'cmtl'
COURSE_MATERIAL_ID = 'cmid'
COURSE_MATERIAL_STATE = 'cmst'
COURSE_NAME = 'cona'
COURSE_STATE = 'cost'
COURSE_STUDENTGROUP = 'cosg'
COURSE_STUDENTGROUP_MEMBER = 'csgm'
COURSE_SUBMISSION_ID = 'csid'
COURSE_SUBMISSION_STATE = 'csst'
COURSE_TOPIC = 'ctop'
COURSE_TOPIC_ID = 'ctoi'
COURSE_WORK = 'cwrk'
COURSE_WORK_ID = 'cwid'
COURSE_WORK_STATE = 'cwst'
CREATOR_ID = 'crid'
CREDENTIALS = 'cred'
CRITERIA = 'crit'
CROS_DEVICE = 'cros'
CROS_SERIAL_NUMBER = 'crsn'
CSE_IDENTITY = 'csei'
CSE_KEYPAIR = 'csek'
CUSTOMER_DOMAIN = 'cudo'
CUSTOMER_ID = 'cuid'
DATE = 'date'
DEFAULT_LANGUAGE = 'dfla'
DELEGATE = 'dele'
DELETED_USER = 'delu'
DELIVERY = 'deli'
DEVICE = 'devi'
DEVICE_FILE = 'devf'
DIRECTORY = 'drct'
DEVICE_USER = 'devu'
DEVICE_USER_CLIENT_STATE = 'ducs'
DISCOVERY_JSON_FILE = 'disc'
DOCUMENT = 'docu'
DOMAIN = 'doma'
DOMAIN_ALIAS = 'doal'
DOMAIN_CONTACT = 'doco'
DOMAIN_PEOPLE_CONTACT = 'dopc'
DOMAIN_PROFILE = 'dopr'
DRIVE_DISK_USAGE = 'drdu'
DRIVE_FILE = 'dfil'
DRIVE_FILE_COMMENT = 'filc'
DRIVE_FILE_ID = 'fili'
DRIVE_FILE_NAME = 'filn'
DRIVE_FILE_RENAMED = 'firn'
DRIVE_FILE_REVISION = 'filr'
DRIVE_FILE_SHORTCUT = 'fils'
DRIVE_FILE_OR_FOLDER = 'fifo'
DRIVE_FILE_OR_FOLDER_ACL = 'fiac'
DRIVE_FILE_OR_FOLDER_ID = 'fifi'
DRIVE_FOLDER = 'fold'
DRIVE_FOLDER_ID = 'foli'
DRIVE_FOLDER_NAME = 'foln'
DRIVE_FOLDER_PATH = 'folp'
DRIVE_FOLDER_RENAMED = 'forn'
DRIVE_FOLDER_SHORTCUT = 'fols'
DRIVE_ORPHAN_FILE_OR_FOLDER = 'orph'
DRIVE_PARENT_FOLDER = 'fipf'
DRIVE_PARENT_FOLDER_ID = 'fipi'
DRIVE_PARENT_FOLDER_REFERENCE = 'pfrf'
DRIVE_PATH = 'drvp'
DRIVE_SETTINGS = 'drvs'
DRIVE_SHORTCUT = 'drsc'
DRIVE_SHORTCUT_ID = 'dsci'
DRIVE_3PSHORTCUT = 'dr3s'
DRIVE_TRASH = 'drvt'
EMAIL = 'emai'
EMAIL_ALIAS = 'emal'
EMAIL_SETTINGS = 'emse'
END_TIME = 'endt'
ENTITY = 'enti'
EVENT = 'evnt'
EVENT_BIRTHDAY = 'evbd'
EVENT_FOCUSTIME = 'evft'
EVENT_OUTOFOFFICE = 'evoo'
EVENT_WORKINGLOCATION = 'evwl'
FEATURE = 'feat'
FIELD = 'fiel'
FILE = 'file'
FILE_PARENT_TREE = 'fptr'
FILTER = 'filt'
FORM = 'form'
FORM_RESPONSE = 'frmr'
FORWARD_ENABLED = 'fwde'
FORWARDING_ADDRESS = 'fwda'
GCP_FOLDER = 'gcpf'
GCP_FOLDER_NAME = 'gcpn'
GCP_ORG_ID = 'gcpo'
GMAIL_PROFILE = 'gmpr'
GROUP = 'grou'
GROUP_ALIAS = 'gali'
GROUP_EMAIL = 'gale'
GROUP_MEMBERSHIP = 'gmem'
GROUP_MEMBERSHIP_TREE = 'gmtr'
GROUP_SETTINGS = 'gset'
GROUP_TREE = 'gtre'
GUARDIAN = 'guar'
GUARDIAN_INVITATION = 'gari'
GUARDIAN_AND_INVITATION = 'gani'
GUEST_USER = 'gstu'
IAM_POLICY = 'iamp'
IMAP_ENABLED = 'imap'
INBOUND_SSO_ASSIGNMENT = 'insa'
INBOUND_SSO_CREDENTIALS = 'insc'
INBOUND_SSO_PROFILE = 'insp'
INSTANCE = 'inst'
ITEM = 'item'
ISSUER_CN = 'iscn'
KEYBOARD_SHORTCUTS_ENABLED = 'kbsc'
LABEL = 'labe'
LABEL_ID = 'labi'
LANGUAGE = 'lang'
LICENSE = 'lice'
LOCATION = 'loca'
LOOKERSTUDIO_ASSET = 'lsas'
LOOKERSTUDIO_ASSET_DATASOURCE = 'lsad'
LOOKERSTUDIO_ASSETID = 'lsai'
LOOKERSTUDIO_ASSET_REPORT = 'lsar'
LOOKERSTUDIO_PERMISSION = 'lspe'
MD5HASH = 'md5h'
MEET_SPACE = 'mesp'
MEET_CONFERENCE = 'msco'
MEET_PARTICIPANT = 'msps'
MEET_RECORDING = 'msre'
MEET_TRANSCRIPT = 'mstr'
MEMBER = 'memb'
MEMBER_NOT_ARCHIVED = 'mena'
MEMBER_ARCHIVED = 'mear'
MEMBER_NOT_SUSPENDED = 'mens'
MEMBER_SUSPENDED = 'mesu'
MEMBER_NOT_SUSPENDED_NOT_ARCHIVED = 'nsna'
MEMBER_SUSPENDED_ARCHIVED = 'suar'
MEMBER_RESTRICTION = 'memr'
MEMBER_URI = 'memu'
MEMBERSHIP_TREE = 'metr'
MESSAGE = 'mesg'
MIMETYPE = 'mime'
MOBILE_DEVICE = 'mobi'
NAME = 'name'
NONEDITABLE_ALIAS = 'neal'
NOTE = 'note'
NOTE_ACL = 'nota'
NOTES_ACLS = 'naac'
NOTIFICATION = 'noti'
OAUTH2_TXT_FILE = 'oaut'
OAUTH2SERVICE_JSON_FILE = 'oau2'
ORGANIZATIONAL_UNIT = 'orgu'
OTHER_CONTACT = 'otco'
OWNER = 'ownr'
OWNER_ID = 'owid'
PAGE_SIZE = 'page'
PARENT_ORGANIZATIONAL_UNIT = 'porg'
PARTICIPANT = 'part'
PEOPLE_CONTACT = 'peco'
PEOPLE_CONTACT_GROUP = 'pecg'
PEOPLE_PHOTO = 'peph'
PEOPLE_PROFILE = 'pepr'
PERMISSION = 'perm'
PERMISSION_ID = 'peid'
PERMITTEE = 'prmt'
PERSONAL_DEVICE = 'pedv'
PHOTO = 'phot'
POLICY = 'poli'
POP_ENABLED = 'popa'
PRESENTATION = 'pres'
PRINTER = 'prin'
PRINTER_ID = 'prid'
PRINTER_MODEL = 'prmd'
PRIVILEGE = 'priv'
PRODUCT = 'prod'
PROFILE_SHARING_ENABLED = 'prof'
PROJECT = 'proj'
PROJECT_FOLDER = 'prjf'
PROJECT_ID = 'prji'
PUBLIC_KEY = 'pubk'
QUERY = 'quer'
RECIPIENT = 'recp'
RECIPIENT_BCC = 'rebc'
RECIPIENT_CC = 'recc'
REPORT = 'rept'
REQUEST_ID = 'reqi'
RESOURCE_CALENDAR = 'resc'
RESOURCE_ID = 'resi'
ROLE = 'role'
ROW = 'row '
SCOPE = 'scop'
SECTION = 'sect'
SENDAS_ADDRESS = 'sasa'
SENDER = 'send'
SERVICE = 'serv'
SHAREDDRIVE = 'tdrv'
SHAREDDRIVE_ACL = 'tdac'
SHAREDDRIVE_FOLDER = 'tdfo'
SHAREDDRIVE_ID = 'tdid'
SHAREDDRIVE_NAME = 'tdna'
SHAREDDRIVE_THEME = 'tdth'
SHEET = 'shet'
SHEET_ID = 'shti'
SIGNATURE = 'sign'
SIZE = 'size'
SKU = 'sku '
SMIME_ID = 'smid'
SNIPPETS_ENABLED = 'snip'
SSO_KEY = 'ssok'
SSO_SETTINGS = 'ssos'
SOURCE_USER = 'srcu'
SPREADSHEET = 'sprd'
SPREADSHEET_RANGE = 'ssrn'
START_TIME = 'strt'
STATUS = 'stat'
STUDENT = 'stud'
SUBSCRIPTION = 'subs'
SVCACCT = 'svac'
SVCACCT_KEY = 'svky'
TAGMANAGER_ACCOUNT = 'tmac'
TAGMANAGER_CONTAINER = 'tmco'
TAGMANAGER_PERMISSION = 'tmpm'
TAGMANAGER_TAG = 'tmtg'
TAGMANAGER_WORKSPACE = 'tmws'
TARGET_USER = 'tgt '
TASK = 'task'
TASKLIST = 'tali'
TEACHER = 'teac'
THREAD = 'thre'
TRANSFER_APPLICATION = 'trap'
TRANSFER_ID = 'trid'
TRANSFER_REQUEST = 'trnr'
TRASHED_EVENT = 'trev'
TRUSTED_APPLICATION = 'trus'
TYPE = 'type'
UNICODE_ENCODING_ENABLED = 'unic'
UNIQUE_ID = 'uniq'
URL = 'url '
USER = 'user'
USER_ALIAS = 'uali'
USER_NOT_ARCHIVED = 'usna'
USER_ARCHIVED = 'usar'
USER_EMAIL = 'uema'
USER_INVITATION = 'uinv'
USER_NOT_SUSPENDED = 'usns'
USER_SUSPENDED = 'usup'
USER_SCHEMA = 'usch'
VACATION = 'vaca'
VACATION_ENABLED = 'vace'
VALUE = 'val'
VAULT_EXPORT = 'vlte'
VAULT_HOLD = 'vlth'
VAULT_MATTER = 'vltm'
VAULT_MATTER_ARTIFACT = 'vlma'
VAULT_MATTER_ID = 'vlmi'
VAULT_OPERATION = 'vlto'
VAULT_QUERY = 'vltq'
WEB_MASTERSITE = 'wems'
WEB_RESOURCE = 'were'
WEBCLIPS_ENABLED = 'webc'
YOUTUBE_CHANNEL = 'ytch'
# _NAMES[0] is plural, _NAMES[1] is singular unless the item name is explicitly plural (Calendar Settings)
# For items with Boolean values, both entries are singular (Forward, POP)
# These values can be translated into other languages
_NAMES = {
ACCESS_TOKEN: ['Access Tokens', 'Access Token'],
ACCOUNT: ['Google Workspace Accounts', 'Google Workspace Account'],
ACTION: ['Actions', 'Action'],
ACTIVITY: ['Activities', 'Activity'],
ADMINISTRATOR: ['Administrators', 'Administrator'],
ADMIN_ROLE: ['Admin Roles', 'Admin Role'],
ADMIN_ROLE_ASSIGNMENT: ['Admin Role Assignments', 'Admin Role Assignment'],
ALERT: ['Alerts', 'Alert'],
ALERT_ID: ['Alert IDs', 'Alert ID'],
ALERT_FEEDBACK: ['Alert Feedbacks', 'Alert Feedback'],
ALERT_FEEDBACK_ID: ['Alert Feedback IDs', 'Alert Feedback ID'],
ALERT_SETTINGS: ['Alert Settings', 'Alert Settings'],
ALIAS: ['Aliases', 'Alias'],
ALIAS_EMAIL: ['Alias Emails', 'Alias Email'],
ALIAS_TARGET: ['Alias Targets', 'Alias Target'],
ANALYTIC_ACCOUNT: ['Analytic Accounts', 'Analytic Account'],
ANALYTIC_ACCOUNT_SUMMARY: ['Analytic Account Summaries', 'Analytic Account Summary'],
ANALYTIC_DATASTREAM: ['Analytic Datastreams', 'Analytic Datastream'],
ANALYTIC_PROPERTY: ['Analytic GA4 Properties', 'Analytic GA4 Property'],
API: ['APIs', 'API'],
APP_ACCESS_SETTINGS: ['Application Access Settings', 'Application Access Settings'],
APP_ID: ['Application IDs', 'Application ID'],
APP_NAME: ['Application Names', 'Application Name'],
APPLICATION_SPECIFIC_PASSWORD: ['Application Specific Password IDs', 'Application Specific Password ID'],
ARROWS_ENABLED: ['Personal Indicator Arrows Enabled', 'Personal Indicator Arrows Enabled'],
ATTACHMENT: ['Attachments', 'Attachment'],
ATTENDEE: ['Attendees', 'Attendee'],
AUDIT_ACTIVITY_REQUEST: ['Audit Activity Requests', 'Audit Activity Request'],
AUDIT_EXPORT_REQUEST: ['Audit Export Requests', 'Audit Export Request'],
AUDIT_MONITOR_REQUEST: ['Audit Monitor Requests', 'Audit Monitor Request'],
BACKUP_VERIFICATION_CODES: ['Backup Verification Codes', 'Backup Verification Codes'],
BUILDING: ['Buildings', 'Building'],
BUILDING_ID: ['Building IDs', 'Building ID'],
BUSINESS_PROFILE_ACCOUNT: ['Business Profile Accounts', 'Business Profile Account'],
CAA_LEVEL: ['CAA Levels', 'CAA Level'],
CALENDAR: ['Calendars', 'Calendar'],
CALENDAR_ACL: ['Calendar ACLs', 'Calendar ACL'],
CALENDAR_SETTINGS: ['Calendar Settings', 'Calendar Settings'],
CHANNEL_CUSTOMER: ['Channel Customers', 'Channel Customer'],
CHANNEL_CUSTOMER_ENTITLEMENT: ['Channel Customer Entitlements', 'Channel Customer Entitlement'],
CHANNEL_OFFER: ['Channel Offers', 'Channel Offer'],
CHANNEL_PRODUCT: ['Channel Products', 'Channel Product'],
CHANNEL_SKU: ['Channel SKUs', 'Channel SKU'],
CHAT_BOT: ['Chat BOTs', 'Chat BOT'],
CHAT_ADMIN: ['Chat Admins', 'Chat Admin'],
CHAT_EMOJI: ['Chat Emojis', 'Chat Emoji'],
CHAT_EVENT: ['Chat Events', 'Chat Event'],
CHAT_MANAGER_USER: ['Chat User Managers', 'Chat User Manager'],
CHAT_MESSAGE: ['Chat Messages', 'Chat Message'],
CHAT_MESSAGE_ID: ['Chat Message IDs', 'Chat Message ID'],
CHAT_MEMBER: ['Chat Members', 'Chat Member'],
CHAT_MEMBER_GROUP: ['Chat Group Members', 'Chat Group Member'],
CHAT_MEMBER_USER: ['Chat User Members', 'Chat User Member'],
CHAT_OWNER_USER: ['Chat User Owners', 'Chat User Owner'],
CHAT_SECTION: ['Chat User Sections', 'Chat User Section'],
CHAT_SECTION_ITEM: ['Chat User Section Items', 'Chat User Section Item'],
CHAT_SPACE: ['Chat Spaces', 'Chat Space'],
CHAT_THREAD: ['Chat Threads', 'Chat Thread'],
CHILD_ORGANIZATIONAL_UNIT: ['Child Organizational Units', 'Child Organizational Unit'],
CHROME_APP: ['Chrome Applications', 'Chrome Application'],
CHROME_APP_DEVICE: ['Chrome Application Devices', 'Chrome Application Device'],
CHROME_BROWSER: ['Chrome Browsers', 'Chrome Browser'],
CHROME_BROWSER_ENROLLMENT_TOKEN: ['Chrome Browser Enrollment Tokens', 'Chrome Browser Enrollment Token'],
CHROME_CHANNEL: ['Chrome Channels', 'Chrome Channel'],
CHROME_DEVICE: ['Chrome Devices', 'Chrome Device'],
CHROME_DEVICE_COUNT: ['Chrome Device Counts', 'Chrome Device Count'],
CHROME_MODEL: ['Chrome Models', 'Chrome Model'],
CHROME_NETWORK_ID: ['Chrome Network IDs', 'Chrome Network ID'],
CHROME_NETWORK_NAME: ['Chrome Network Names', 'Chrome Network Name'],
CHROME_PLATFORM: ['Chrome Platforms', 'Chrome Platform'],
CHROME_POLICY: ['Chrome Policies', 'Chrome Policy'],
CHROME_POLICY_IMAGE: ['Chrome Policy Images', 'Chrome Policy Image'],
CHROME_POLICY_SCHEMA: ['Chrome Policy Schemas', 'Chrome Policy Schema'],
CHROME_PROFILE: ['Chrome Profiles', 'Chrome Profile'],
CHROME_PROFILE_COMMAND: ['Chrome Profile Commands', 'Chrome Profile Command'],
CHROME_RELEASE: ['Chrome Releases', 'Chrome Release'],
CHROME_VERSION: ['Chrome Versions', 'Chrome Version'],
CLASSIFICATION_LABEL: ['Classification Labels', 'Classification Label'],
CLASSIFICATION_LABEL_FIELD_ID: ['Classification Label Field IDs', 'Classification Label Field ID'],
CLASSIFICATION_LABEL_ID: ['Classification Label IDs', 'Classification Label ID'],
CLASSIFICATION_LABEL_NAME: ['Classification Label Names', 'Classification Label Name'],
CLASSIFICATION_LABEL_PERMISSION: ['Classification Label Permissions', 'Classification Label Permission'],
CLASSIFICATION_LABEL_PERMISSION_NAME: ['Classification Label Permission Names', 'Classification Label Permission Name'],
CLASSROOM_INVITATION: ['Classroom Invitations', 'Classroom Invitation'],
CLASSROOM_INVITATION_OWNER: ['Classroom Owner Invitations', 'Classroom Owner Invitation'],
CLASSROOM_INVITATION_STUDENT: ['Classroom Student Invitations', 'Classroom Student Invitation'],
CLASSROOM_INVITATION_TEACHER: ['Classroom Teacher Invitations', 'Classroom Teacher Invitation'],
CLASSROOM_OAUTH2_TXT_FILE: ['Classroom OAuth2 File', 'Classroom OAuth2 File'],
CLASSROOM_USER_PROFILE: ['Classroom User Profile', 'Classroom User Profile'],
CLIENT_ID: ['Client IDs', 'Client ID'],
CLIENT_SECRETS_JSON_FILE: ['Client Secrets File', 'Client Secrets File'],
CLOUD_IDENTITY_GROUP: ['Cloud Identity Groups', 'Cloud Identity Group'],
CLOUD_STORAGE_BUCKET: ['Cloud Storage Buckets', 'Cloud Storage Bucket'],
CLOUD_STORAGE_FILE: ['Cloud Storage Files', 'Cloud Storage File'],
COLLABORATOR: ['Collaborators', 'Collaborator'],
COMMAND_ID: ['Command IDs', 'Command ID'],
COMPANY_DEVICE: ['Company Devices', 'Company Device'],
CONFIG_FILE: ['Config File', 'Config File'],
CONTACT: ['Contacts', 'Contact'],
CONTACT_DELEGATE: ['Contact Delegates', 'Contact Delegate'],
CONTACT_GROUP: ['Contact Groups', 'Contact Group'],
CONTACT_GROUP_NAME: ['Contact Group Names', 'Contact Group Name'],
COPYFROM_COURSE: ['Copy From Courses', 'CopyFrom Course'],
COPYFROM_GROUP: ['Copy From Groups', 'CopyFrom Group'],
COURSE: ['Courses', 'Course'],
COURSE_ALIAS: ['Course Aliases', 'Course Alias'],
COURSE_ANNOUNCEMENT: ['Course Announcements', 'Course Announcement'],
COURSE_ANNOUNCEMENT_ID: ['Course Announcement IDs', 'Course Announcement ID'],
COURSE_ANNOUNCEMENT_STATE: ['Course Announcement States', 'Course Announcement State'],
COURSE_MATERIAL_DRIVEFILE: ['Course Material Drive Files', 'Course Material Drive File'],
COURSE_MATERIAL_FORM: ['Course Material Forms', 'Course Material Form'],
COURSE_MATERIAL: ['Course Materials', 'Course Material'],
COURSE_MATERIAL_ID: ['Course Material IDs', 'Course Material ID'],
COURSE_MATERIAL_STATE: ['Course Material States', 'Course Material State'],
COURSE_NAME: ['Course Names', 'Course Name'],
COURSE_STATE: ['Course States', 'Course State'],
COURSE_STUDENTGROUP: ['Course Student Groups', 'Course Student Group'],
COURSE_STUDENTGROUP_MEMBER: ['Course Student Group Members', 'Course Student Group Member'],
COURSE_SUBMISSION_ID: ['Course Submission IDs', 'Course Submission ID'],
COURSE_SUBMISSION_STATE: ['Course Submission States', 'Course Submission State'],
COURSE_TOPIC: ['Course Topics', 'Course Topic'],
COURSE_TOPIC_ID: ['Course Topic IDs', 'Course Topic ID'],
COURSE_WORK: ['Course Works', 'Course Work'],
COURSE_WORK_ID: ['Course Work IDs', 'Course Work ID'],
COURSE_WORK_STATE: ['Course Work States', 'Course Work State'],
CREATOR_ID: ['Creator IDs', 'Creator ID'],
CREDENTIALS: ['Credentials', 'Credentials'],
CRITERIA: ['Criteria', 'Criteria'],
CROS_DEVICE: ['CrOS Devices', 'CrOS Device'],
CROS_SERIAL_NUMBER: ['CrOS Serial Numbers', 'CrOS Serial Numbers'],
CSE_IDENTITY: ['CSE Identities', 'CSE Identity'],
CSE_KEYPAIR: ['CSE KeyPairs', 'CSE KeyPair'],
CUSTOMER_DOMAIN: ['Customer Domains', 'Customer Domain'],
CUSTOMER_ID: ['Customer IDs', 'Customer ID'],
DATE: ['Dates', 'Date'],
DEFAULT_LANGUAGE: ['Default Language', 'Default Language'],
DELEGATE: ['Delegates', 'Delegate'],
DELETED_USER: ['Deleted Users', 'Deleted User'],
DELIVERY: ['Delivery', 'Delivery'],
DEVICE: ['Devices', 'Device'],
DEVICE_FILE: ['Device Files', 'Device File'],
DEVICE_USER: ['Device Users', 'Device User'],
DEVICE_USER_CLIENT_STATE: ['Device Users Client States', 'Device User Client State'],
DIRECTORY: ['Directories', 'Directory'],
DISCOVERY_JSON_FILE: ['Discovery File', 'Discovery File'],
DOCUMENT: ['Documents', 'Document'],
DOMAIN: ['Domains', 'Domain'],
DOMAIN_ALIAS: ['Domain Aliases', 'Domain Alias'],
DOMAIN_CONTACT: ['Domain Contacts', 'Domain Contact'],
DOMAIN_PEOPLE_CONTACT: ['Domain People Contacts', 'Domain People Contact'],
DOMAIN_PROFILE: ['Domain Profiles', 'Domain Profile'],
DRIVE_DISK_USAGE: ['Drive Disk Usages', 'Drive Disk Usage'],
DRIVE_FILE: ['Drive Files', 'Drive File'],
DRIVE_FILE_COMMENT: ['Drive File Comments', 'Drive File Comment'],
DRIVE_FILE_ID: ['Drive File IDs', 'Drive File ID'],
DRIVE_FILE_NAME: ['Drive File Names', 'Drive File Name'],
DRIVE_FILE_REVISION: ['Drive File Revisions', 'Drive File Revision'],
DRIVE_FILE_RENAMED: ['Drive Files Renamed', 'Drive File Renamed'],
DRIVE_FILE_SHORTCUT: ['Drive File Shortcuts', 'Drive File Shortcut'],
DRIVE_FILE_OR_FOLDER: ['Drive Files/Folders', 'Drive File/Folder'],
DRIVE_FILE_OR_FOLDER_ACL: ['Drive File/Folder ACLs', 'Drive File/Folder ACL'],
DRIVE_FILE_OR_FOLDER_ID: ['Drive File/Folder IDs', 'Drive File/Folder ID'],
DRIVE_FOLDER: ['Drive Folders', 'Drive Folder'],
DRIVE_FOLDER_ID: ['Drive Folder IDs', 'Drive Folder ID'],
DRIVE_FOLDER_NAME: ['Drive Folder Names', 'Drive Folder Name'],
DRIVE_FOLDER_PATH: ['Drive Folder Paths', 'Drive Folder Path'],
DRIVE_FOLDER_RENAMED: ['Drive Folders Renamed', 'Drive Folder Renamed'],
DRIVE_FOLDER_SHORTCUT: ['Drive Folder Shortcuts', 'Drive Folder Shortcut'],
DRIVE_ORPHAN_FILE_OR_FOLDER: ['Drive Orphan Files/Folders', 'Drive Orphan File/Folder'],
DRIVE_PARENT_FOLDER: ['Drive Parent Folders', 'Drive Parent Folder'],
DRIVE_PARENT_FOLDER_ID: ['Drive Parent Folder IDs', 'Drive Parent Folder ID'],
DRIVE_PARENT_FOLDER_REFERENCE: ['Drive Parent Folder References', 'Drive Parent Folder Reference'],
DRIVE_PATH: ['Drive Paths', 'Drive Path'],
DRIVE_SETTINGS: ['Drive Settings', 'Drive Settings'],
DRIVE_SHORTCUT: ['Drive Shortcuts', 'Drive Shortcut'],
DRIVE_SHORTCUT_ID: ['Drive Shortcut IDs', 'Drive Shortcut ID'],
DRIVE_3PSHORTCUT: ['Drive 3rd Party Shortcuts', 'Drive 3rd Party Shortcut'],
DRIVE_TRASH: ['Drive Trash', 'Drive Trash'],
EMAIL: ['Email Addresses', 'Email Address'],
EMAIL_ALIAS: ['Email Aliases', 'Email Alias'],
EMAIL_SETTINGS: ['Email Settings', 'Email Settings'],
END_TIME: ['End Times', 'End Time'],
ENTITY: ['Entities', 'Entity'],
EVENT: ['Events', 'Event'],
EVENT_BIRTHDAY: ['Borthday Events', 'Birthday Event'],
EVENT_FOCUSTIME: ['Focus Time Events', 'Focus Time Event'],
EVENT_OUTOFOFFICE: ['Out of Office Events', 'Out of Office Event'],
EVENT_WORKINGLOCATION: ['Working Location Events', 'Working Location Event'],
FEATURE: ['Features', 'Feature'],
FIELD: ['Fields', 'Field'],
FILE: ['Files', 'File'],
FILE_PARENT_TREE: ['File Parent Trees', 'File Parent Tree'],
FILTER: ['Filters', 'Filter'],
FORM: ['Forms', 'Form'],
FORM_RESPONSE: ['Form Responses', 'Form Response'],
FORWARD_ENABLED: ['Forward Enabled', 'Forward Enabled'],
FORWARDING_ADDRESS: ['Forwarding Addresses', 'Forwarding Address'],
GCP_FOLDER: ['GCP Folders', 'GCP Folder'],
GCP_FOLDER_NAME: ['GCP Folder Names', 'GCP Folder Name'],
GCP_ORG_ID: ['GCP Organization ID', 'GCP Organization ID'],
GMAIL_PROFILE: ['Gmail Profile', 'Gmail Profile'],
GROUP: ['Groups', 'Group'],
GROUP_ALIAS: ['Group Aliases', 'Group Alias'],
GROUP_EMAIL: ['Group Emails', 'Group Email'],
GROUP_MEMBERSHIP: ['Group Memberships', 'Group Membership'],
GROUP_MEMBERSHIP_TREE: ['Group Membership Trees', 'Group Membership Tree'],
GROUP_SETTINGS: ['Group Settings', 'Group Settings'],
GROUP_TREE: ['Group Trees', 'Group Tree'],
GUEST_USER: ['Guest Users', 'Guest User'],
GUARDIAN: ['Guardians', 'Guardian'],
GUARDIAN_INVITATION: ['Guardian Invitations', 'Guardian Invitation'],
GUARDIAN_AND_INVITATION: ['Guardians and Invitations', 'Guardian and Invitation'],
IAM_POLICY: ['IAM Policies', 'IAM Policy'],
IMAP_ENABLED: ['IMAP Enabled', 'IMAP Enabled'],
INBOUND_SSO_ASSIGNMENT: ['Inbound SSO Assignments', 'Inbound SSO Assignment'],
INBOUND_SSO_CREDENTIALS: ['Inbound SSO Credentials', 'Inbound SSO Credential'],
INBOUND_SSO_PROFILE: ['Inbound SSO Profiles', 'Inbound SSO Profile'],
INSTANCE: ['Instances', 'Instance'],
ISSUER_CN: ['Issuer CNs', 'Issuer CN'],
ITEM: ['Items', 'Item'],
KEYBOARD_SHORTCUTS_ENABLED: ['Keyboard Shortcuts Enabled', 'Keyboard Shortcuts Enabled'],
LABEL: ['Labels', 'Label'],
LABEL_ID: ['Label IDs', 'Label ID'],
LANGUAGE: ['Languages', 'Language'],
LICENSE: ['Licenses', 'License'],
LOCATION: ['Locations', 'Location'],
LOOKERSTUDIO_ASSET: ['Looker Studio Assets', 'Looker Studio Asset'],
LOOKERSTUDIO_ASSET_DATASOURCE: ['Looker Studio DATA_SOURCE Assets', 'Looker Studio DATA_SOURCE Asset'],
LOOKERSTUDIO_ASSETID: ['Looker Studio Asset IDs', 'Looker Studio Asset ID'],
LOOKERSTUDIO_ASSET_REPORT: ['Looker Studio REPORT Assets', 'Looker Studio REPORT Asset'],
LOOKERSTUDIO_PERMISSION: ['Looker Studio Permissions', 'Looker Studio Permission'],
MD5HASH: ['MD5 hash', 'MD5 Hash'],
MEET_SPACE: ['Meet Spaces', 'Meet Space'],
MEET_CONFERENCE: ['Meet Conferences', 'Meet Conference'],
MEET_PARTICIPANT: ['Meet Participants', 'Meet Participant'],
MEET_RECORDING: ['Meet Recordings', 'Meet Recording'],
MEET_TRANSCRIPT: ['Meet Transcripts', 'Meet Transcript'],
MEMBER: ['Members', 'Member'],
MEMBER_NOT_ARCHIVED: ['Members (Not Archived)', 'Member (Not Archived)'],
MEMBER_ARCHIVED: ['Members (Archived)', 'Member (Archived)'],
MEMBER_NOT_SUSPENDED: ['Members (Not Suspended)', 'Member (Not Suspended)'],
MEMBER_SUSPENDED: ['Members (Suspended)', 'Member (Suspended)'],
MEMBER_NOT_SUSPENDED_NOT_ARCHIVED: ['Members (Not Suspended & Not Archived)', 'Member (Not Suspended & Not Archived)'],
MEMBER_SUSPENDED_ARCHIVED: ['Members (Suspended & Archived)', 'Member (Suspended & Archived)'],
MEMBER_RESTRICTION: ['Member Restrictions', 'Member Restriction'],
MEMBER_URI: ['Member URIs', 'Member URI'],
MEMBERSHIP_TREE: ['Membership Trees', 'Membership Tree'],
MESSAGE: ['Messages', 'Message'],
MIMETYPE: ['MIME Types', 'MIME Type'],
MOBILE_DEVICE: ['Mobile Devices', 'Mobile Device'],
NAME: ['Names', 'Name'],
NONEDITABLE_ALIAS: ['Non-Editable Aliases', 'Non-Editable Alias'],
NOTE: ['Notes', 'Note'],
NOTE_ACL: ['Note ACLs', 'Note ACL'],
NOTES_ACLS: ["'Note's ACLs", "Note's ACLs"],
NOTIFICATION: ['Notifications', 'Notification'],
OAUTH2_TXT_FILE: ['Client OAuth2 File', 'Client OAuth2 File'],
OAUTH2SERVICE_JSON_FILE: ['Service Account OAuth2 File', 'Service Account OAuth2 File'],
ORGANIZATIONAL_UNIT: ['Organizational Units', 'Organizational Unit'],
OTHER_CONTACT: ['Other Contacts', 'Other Contact'],
OWNER: ['Owners', 'Owner'],
OWNER_ID: ['Owner IDs', 'Owner ID'],
PAGE_SIZE: ['Page Size', 'Page Size'],
PARENT_ORGANIZATIONAL_UNIT: ['Parent Organizational Units', 'Parent Organizational Unit'],
PARTICIPANT: ['Participants', 'Participant'],
PEOPLE_CONTACT: ['People Contacts', 'Person Contact'],
PEOPLE_CONTACT_GROUP: ['People Contact Groups', 'People Contact Group'],
PEOPLE_PHOTO: ['People Photos', 'Person Photo'],
PEOPLE_PROFILE: ['People Profiles', 'People Profile'],
PERMISSION: ['Permissions', 'Permission'],
PERMISSION_ID: ['Permission IDs', 'Permission ID'],
PERMITTEE: ['Permittees', 'Permittee'],
PERSONAL_DEVICE: ['Personal Devices', 'Personal Device'],
PHOTO: ['Photos', 'Photo'],
POLICY: ['Policies', 'Policy'],
POP_ENABLED: ['POP Enabled', 'POP Enabled'],
PRESENTATION: ['Presentations', 'Presentation'],
PRINTER: ['Printers', 'Printer'],
PRINTER_ID: ['Printer IDs', 'Printer ID'],
PRINTER_MODEL: ['Printer Models', 'Printer Model'],
PRIVILEGE: ['Privileges', 'Privilege'],
PRODUCT: ['Products', 'Product'],
PROFILE_SHARING_ENABLED: ['Profile Sharing Enabled', 'Profile Sharing Enabled'],
PROJECT: ['Projects', 'Project'],
PROJECT_FOLDER: ['Project Folders', 'Project Folder'],
PROJECT_ID: ['Project IDs', 'Project ID'],
PUBLIC_KEY: ['Public Key', 'Public Key'],
QUERY: ['Queries', 'Query'],
RECIPIENT: ['Recipients', 'Recipient'],
RECIPIENT_BCC: ['Recipients (BCC)', 'Recipient (BCC)'],
RECIPIENT_CC: ['Recipients (CC)', 'Recipient (CC)'],
REPORT: ['Reports', 'Report'],
REQUEST_ID: ['Request IDs', 'Request ID'],
RESOURCE_CALENDAR: ['Resource Calendars', 'Resource Calendar'],
RESOURCE_ID: ['Resource IDs', 'Resource ID'],
ROLE: ['Roles', 'Role'],
ROW: ['Rows', 'Row'],
SCOPE: ['Scopes', 'Scope'],
SECTION: ['Sections', 'Section'],
SENDAS_ADDRESS: ['SendAs Addresses', 'SendAs Address'],
SENDER: ['Senders', 'Sender'],
SERVICE: ['Services', 'Service'],
SHAREDDRIVE: ['Shared Drives', 'Shared Drive'],
SHAREDDRIVE_ACL: ['Shared Drive ACLs', 'Shared Drive ACL'],
SHAREDDRIVE_FOLDER: ['Shared Drive Folders', 'Shared Drive Folder'],
SHAREDDRIVE_ID: ['Shared Drive IDs', 'Shared Drive ID'],
SHAREDDRIVE_NAME: ['Shared Drive Names', 'Shared Drive Name'],
SHAREDDRIVE_THEME: ['Shared Drive Themes', 'Shared Drive Theme'],
SHEET: ['Sheets', 'Sheet'],
SHEET_ID: ['Sheet IDs', 'Sheet ID'],
SIGNATURE: ['Signatures', 'Signature'],
SIZE: ['Sizes', 'Size'],
SKU: ['SKUs', 'SKU'],
SMIME_ID: ['S/MIME Certificate IDs', 'S/MIME Certificate ID'],
SNIPPETS_ENABLED: ['Preview Snippets Enabled', 'Preview Snippets Enabled'],
SSO_KEY: ['SSO Key', 'SSO Key'],
SSO_SETTINGS: ['SSO Settings', 'SSO Settings'],
SOURCE_USER: ['Source Users', 'Source User'],
SPREADSHEET: ['Spreadsheets', 'Spreadsheet'],
SPREADSHEET_RANGE: ['Spreadsheet Ranges', 'Spreadsheet Range'],
START_TIME: ['Start Times', 'Start Time'],
STATUS: ['Status', 'Status'],
STUDENT: ['Students', 'Student'],
SUBSCRIPTION: ['Subscriptions', 'Subscription'],
SVCACCT: ['Service Accounts', 'Service Account'],
SVCACCT_KEY: ['Service Account Keys', 'Service Account Key'],
TAGMANAGER_ACCOUNT: ['Tag Manager Accounts', 'Tag Manager Account'],
TAGMANAGER_CONTAINER: ['Tag Manager Containers', 'Tag Manager Container'],
TAGMANAGER_PERMISSION: ['Tag Manager Permissions', 'Tag Manager Permission'],
TAGMANAGER_TAG: ['Tag Manager Tags', 'Tag Manager Tag'],
TAGMANAGER_WORKSPACE: ['Tag Manager Workspaces', 'Tag Manager Workspace'],
TARGET_USER: ['Target Users', 'Target User'],
TASK: ['Tasks', 'Task'],
TASKLIST: ['Tasklists', 'Tasklist'],
TEACHER: ['Teachers', 'Teacher'],
THREAD: ['Threads', 'Thread'],
TRANSFER_APPLICATION: ['Transfer Applications', 'Transfer Application'],
TRANSFER_ID: ['Transfer IDs', 'Transfer ID'],
TRANSFER_REQUEST: ['Transfer Requests', 'Transfer Request'],
TRASHED_EVENT: ['Trashed Events', 'Trashed Event'],
TRUSTED_APPLICATION: ['Trusted Applications', 'Trusted Application'],
TYPE: ['Types', 'Type'],
UNICODE_ENCODING_ENABLED: ['UTF-8 Encoding Enabled', 'UTF-8 Encoding Enabled'],
UNIQUE_ID: ['Unique IDs', 'Unique ID'],
URL: ['URLs', 'URL'],
USER: ['Users', 'User'],
USER_ALIAS: ['User Aliases', 'User Alias'],
USER_NOT_ARCHIVED: ['Users (Not archived)', 'User (Not archived)'],
USER_ARCHIVED: ['Users (Archived)', 'User (Archived)'],
USER_EMAIL: ['User Emails', 'User Email'],
USER_INVITATION: ['User Invitations', 'User Invitation'],
USER_NOT_SUSPENDED: ['Users (Not suspended)', 'User (Not suspended)'],
USER_SUSPENDED: ['Users (Suspended)', 'User (Suspended)'],
USER_SCHEMA: ['Schemas', 'Schema'],
VACATION: ['Vacation', 'Vacation'],
VACATION_ENABLED: ['Vacation Enabled', 'Vacation Enabled'],
VALUE: ['Values', 'Value'],
VAULT_EXPORT: ['Vault Exports', 'Vault Export'],
VAULT_HOLD: ['Vault Holds', 'Vault Hold'],
VAULT_MATTER: ['Vault Matters', 'Vault Matter'],
VAULT_MATTER_ARTIFACT: ['Vault Matter Artifacts', 'Vault Matter Artifact'],
VAULT_MATTER_ID: ['Vault Matter IDs', 'Vault Matter ID'],
VAULT_OPERATION: ['Vault Operations', 'Vault Operation'],
VAULT_QUERY: ['Vault Queries', 'Vault Query'],
WEBCLIPS_ENABLED: ['Web Clips Enabled', 'Web Clips Enabled'],
WEB_MASTERSITE: ['Web Master Sites', 'Web Master Site'],
WEB_RESOURCE: ['Web Resources', 'Web Resource'],
YOUTUBE_CHANNEL: ['YouTube Channels', 'YouTube Channel'],
ROLE_MANAGER: ['Managers', 'Manager'],
ROLE_MEMBER: ['Members', 'Member'],
ROLE_OWNER: ['Owners', 'Owner'],
ROLE_ALL: ['Members, Managers, Owners', 'Member, Manager, Owner'],
ROLE_USER: ['Users', 'User'],
ROLE_MANAGER_MEMBER: ['Members, Managers', 'Member, Manager'],
ROLE_MANAGER_OWNER: ['Managers, Owners', 'Manager, Owner'],
ROLE_MEMBER_OWNER: ['Members, Owners', 'Member, Owner'],
ROLE_MANAGER_MEMBER_OWNER: ['Members, Managers, Owners', 'Member, Manager, Owner'],
ROLE_PUBLIC: ['Public', 'Public'],
}
def __init__(self):
self.entityType = None
self.forWhom = None
self.preQualifier = ''
self.postQualifier = ''
def SetGetting(self, entityType):
self.entityType = entityType
self.preQualifier = self.postQualifier = ''
def SetGettingQuery(self, entityType, query):
self.entityType = entityType
self.preQualifier = f' that match query ({query})'
self.postQualifier = f' that matched query ({query})'
def SetGettingQualifier(self, entityType, qualifier):
self.entityType = entityType
self.preQualifier = self.postQualifier = qualifier
def Getting(self):
return self.entityType
def GettingPreQualifier(self):
return self.preQualifier
def GettingPostQualifier(self):
return self.postQualifier
def SetGettingForWhom(self, forWhom):
self.forWhom = forWhom
def GettingForWhom(self):
return self.forWhom
def Choose(self, entityType, count):
return self._NAMES[entityType][[0, 1][count == 1]]
def ChooseGetting(self, count):
return self._NAMES[self.entityType][[0, 1][count == 1]]
def Plural(self, entityType):
return self._NAMES[entityType][0]
def PluralGetting(self):
return self._NAMES[self.entityType][0]
def Singular(self, entityType):
return self._NAMES[entityType][1]
def SingularGetting(self):
return self._NAMES[self.entityType][1]
def MayTakeTime(self, entityType):
if entityType:
return f', may take some time on a large {self.Singular(entityType)}...'
return ''
def FormatEntityValueList(self, entityValueList):
evList = []
for j in range(0, len(entityValueList), 2):
evList.append(self.Singular(entityValueList[j]))
evList.append(entityValueList[j+1])
return evList
def TypeMessage(self, entityType, message):
return f'{self.Singular(entityType)}: {message}'
def TypeName(self, entityType, entityName):
return f'{self.Singular(entityType)}: {entityName}'
def TypeNameMessage(self, entityType, entityName, message):
return f'{self.Singular(entityType)}: {entityName} {message}'

View File

@@ -1,866 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM GAPI resources
"""
# callGAPI throw reasons
ABORTED = 'aborted'
ABUSIVE_CONTENT_RESTRICTION = 'abusiveContentRestriction'
ACCESS_NOT_CONFIGURED = 'accessNotConfigured'
ADMIN_CANNOT_UNSUSPEND = 'adminCannotUnsuspend'
ALREADY_EXISTS = 'alreadyExists'
APPLY_LABEL_FORBIDDEN = 'applyLabelForbidden'
AUTH_ERROR = 'authError'
BACKEND_ERROR = 'backendError'
BAD_GATEWAY = 'badGateway'
BAD_REQUEST = 'badRequest'
CANNOT_ADD_PARENT = 'cannotAddParent'
CANNOT_CHANGE_ORGANIZER = 'cannotChangeOrganizer'
CANNOT_CHANGE_ORGANIZER_OF_INSTANCE = 'cannotChangeOrganizerOfInstance'
CANNOT_CHANGE_OWN_ACL = 'cannotChangeOwnAcl'
CANNOT_CHANGE_OWNER_ACL = 'cannotChangeOwnerAcl'
CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION = 'cannotChangeOwnPrimarySubscription'
CANNOT_COPY_FILE = 'cannotCopyFile'
CANNOT_DELETE_ONLY_REVISION = 'cannotDeleteOnlyRevision'
CANNOT_DELETE_PERMISSION = 'cannotDeletePermission'
CANNOT_DELETE_PRIMARY_CALENDAR = 'cannotDeletePrimaryCalendar'
CANNOT_DELETE_PRIMARY_SENDAS = 'cannotDeletePrimarySendAs'
CANNOT_DELETE_RESOURCE_WITH_CHILDREN = 'cannotDeleteResourceWithChildren'
CANNOT_MODIFY_ACL_OF_CALENDAR_OWNER = 'cannotModifyAclOfCalendarOwner'
CANNOT_MODIFY_INHERITED_PERMISSION = 'cannotModifyInheritedPermission'
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION = 'cannotModifyInheritedTeamDrivePermission'
CANNOT_MODIFY_RESTRICTED_LABEL = 'cannotModifyRestrictedLabel'
CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT = 'cannotModifyViewersCanCopyContent'
CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE = 'cannotMoveTrashedItemIntoTeamDrive'
CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE = 'cannotMoveTrashedItemOutOfTeamDrive'
CANNOT_REMOVE_OWNER = 'cannotRemoveOwner'
CANNOT_SET_EXPIRATION = 'cannotSetExpiration'
CANNOT_SET_EXPIRATION_ON_ANYONE_OR_DOMAIN = 'cannotSetExpirationOnAnyoneOrDomain'
CANNOT_SHARE_GROUPS_WITHLINK = 'cannotShareGroupsWithLink'
CANNOT_SHARE_USERS_WITHLINK = 'cannotShareUsersWithLink'
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS = 'cannotShareTeamDriveTopFolderWithAnyoneOrDomains'
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS = 'cannotShareTeamDriveWithNonGoogleAccounts'
CANNOT_UNSUBSCRIBE_FROM_OWNED_CALENDAR = 'cannotUnsubscribeFromOwnedCalendar'
CANNOT_UPDATE_PERMISSION = 'cannotUpdatePermission'
CONDITION_NOT_MET = 'conditionNotMet'
CONFLICT = 'conflict'
CONTENT_OWNER_ACCOUNT_NOT_FOUND = 'contentOwnerAccountNotFound'
CROSS_DOMAIN_MOVE_RESTRICTION = 'crossDomainMoveRestriction'
CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT = 'CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT'
CUSTOMER_NOT_FOUND = 'customerNotFound'
CYCLIC_MEMBERSHIPS_NOT_ALLOWED = 'cyclicMembershipsNotAllowed'
DAILY_LIMIT_EXCEEDED = 'dailyLimitExceeded'
DELETED = 'deleted'
DELETED_USER_NOT_FOUND = 'deletedUserNotFound'
DOMAIN_ALIAS_NOT_FOUND = 'domainAliasNotFound'
DOMAIN_CANNOT_USE_APIS = 'domainCannotUseApis'
DOMAIN_NOT_FOUND = 'domainNotFound'
DOMAIN_NOT_VERIFIED_SECONDARY = 'domainNotVerifiedSecondary'
DOMAIN_POLICY = 'domainPolicy'
DOWNLOAD_QUOTA_EXCEEDED = 'downloadQuotaExceeded'
DUPLICATE = 'duplicate'
EVENT_DURATION_EXCEEDS_LIMIT = 'eventDurationExceedsLimit'
EVENT_TYPE_RESTRICTION = 'eventTypeRestriction'
EXPIRATION_DATES_MUST_BE_IN_THE_FUTURE = 'expirationDatesMustBeInTheFuture'
EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS = 'expirationDateNotAllowedForSharedDriveMembers'
FAILED_PRECONDITION = 'failedPrecondition'
FIELD_IN_USE = 'fieldInUse'
FIELD_NOT_WRITABLE = 'fieldNotWritable'
FILE_NEVER_WRITABLE = 'fileNeverWritable'
FILE_NOT_FOUND = 'fileNotFound'
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE = 'fileOrganizerNotYetEnabledForThisTeamDrive'
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY = 'fileOrganizerOnFoldersInSharedDriveOnly'
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED = 'fileOrganizerOnNonTeamDriveNotSupported'
FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE = 'fileOwnerNotMemberOfTeamDrive'
FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN = 'fileOwnerNotMemberOfWriterDomain'
FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED = 'fileWriterTeamDriveMoveInDisabled'
FORBIDDEN = 'forbidden'
GATEWAY_TIMEOUT = 'gatewayTimeout'
GROUP_NOT_FOUND = 'groupNotFound'
ILLEGAL_ACCESS_ROLE_FOR_DEFAULT = 'illegalAccessRoleForDefault'
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES = 'insufficientAdministratorPrivileges'
INSUFFICIENT_ARCHIVED_USER_LICENSES = 'insufficientArchivedUserLicenses'
INSUFFICIENT_FILE_PERMISSIONS = 'insufficientFilePermissions'
INSUFFICIENT_PARENT_PERMISSIONS = 'insufficientParentPermissions'
INSUFFICIENT_PERMISSIONS = 'insufficientPermissions'
INTERNAL_ERROR = 'internalError'
INVALID = 'invalid'
INVALID_ARGUMENT = 'invalidArgument'
INVALID_ATTRIBUTE_VALUE = 'invalidAttributeValue'
INVALID_CUSTOMER_ID = 'invalidCustomerId'
INVALID_INPUT = 'invalidInput'
INVALID_LINK_VISIBILITY = 'invalidLinkVisibility'
INVALID_MEMBER = 'invalidMember'
INVALID_MESSAGE_ID = 'invalidMessageId'
INVALID_ORGUNIT = 'invalidOrgunit'
INVALID_ORGUNIT_NAME = 'invalidOrgunitName'
INVALID_OWNERSHIP_TRANSFER = 'invalidOwnershipTransfer'
INVALID_PARAMETER = 'invalidParameter'
INVALID_PARENT_ORGUNIT = 'invalidParentOrgunit'
INVALID_QUERY = 'invalidQuery'
INVALID_RESOURCE = 'invalidResource'
INVALID_SCHEMA_VALUE = 'invalidSchemaValue'
INVALID_SCOPE_VALUE = 'invalidScopeValue'
INVALID_SHARING_REQUEST = 'invalidSharingRequest'
LABEL_MULTIPLE_VALUES_FOR_SINGULAR_FIELD = 'labelMultipleValuesForSingularField'
LABEL_MUTATION_FORBIDDEN = 'labelMutationForbidden'
LABEL_MUTATION_ILLEGAL_SELECTION = 'labelMutationIllegalSelection'
LABEL_MUTATION_UNKNOWN_FIELD = 'labelMutationUnknownField'
LIMIT_EXCEEDED = 'limitExceeded'
LOGIN_REQUIRED = 'loginRequired'
MALFORMED_WORKING_LOCATION_EVENT = 'malformedWorkingLocationEvent'
MEMBER_NOT_FOUND = 'memberNotFound'
MYDRIVE_HIERARCHY_DEPTH_LIMIT_EXCEEDED = 'myDriveHierarchyDepthLimitExceeded'
NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE = 'noListTeamDrivesAdministratorPrivilege'
NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE = 'noManageTeamDriveAdministratorPrivilege'
NOT_A_CALENDAR_USER = 'notACalendarUser'
NOT_FOUND = 'notFound'
NOT_IMPLEMENTED = 'notImplemented'
NUM_CHILDREN_IN_NON_ROOT_LIMIT_EXCEEDED = 'numChildrenInNonRootLimitExceeded'
OPERATION_NOT_SUPPORTED = 'operationNotSupported'
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED = 'organizerOnNonTeamDriveNotSupported'
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED = 'organizerOnNonTeamDriveItemNotSupported'
ORGUNIT_NOT_FOUND = 'orgunitNotFound'
OUTSIDE_DOMAIN_MEMBER_CANNOT_CHANGE_TEAMDRIVE_RESTRICTIONS = 'outsideDomainMemberCannotChangeTeamDriveRestrictions'
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED = 'ownerOnTeamDriveItemNotSupported'
OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED = 'ownershipChangeAcrossDomainNotPermitted'
PARTICIPANT_IS_NEITHER_ORGANIZER_NOR_ATTENDEE = 'participantIsNeitherOrganizerNorAttendee'
PERMISSION_DENIED = 'permissionDenied'
PERMISSION_NOT_FOUND = 'permissionNotFound'
PHOTO_NOT_FOUND = 'photoNotFound'
PUBLISH_OUT_NOT_PERMITTED = 'publishOutNotPermitted'
QUERY_REQUIRES_ADMIN_CREDENTIALS = 'queryRequiresAdminCredentials'
QUOTA_EXCEEDED = 'quotaExceeded'
RATE_LIMIT_EXCEEDED = 'rateLimitExceeded'
REQUIRED = 'required'
REQUIRED_ACCESS_LEVEL = 'requiredAccessLevel'
RESOURCE_EXHAUSTED = 'resourceExhausted'
RESOURCE_ID_NOT_FOUND = 'resourceIdNotFound'
RESOURCE_NOT_FOUND = 'resourceNotFound'
RESPONSE_PREPARATION_FAILURE = 'responsePreparationFailure'
REVISION_DELETION_NOT_SUPPORTED = 'revisionDeletionNotSupported'
REVISION_NOT_FOUND = 'revisionNotFound'
REVISIONS_NOT_SUPPORTED = 'revisionsNotSupported'
SERVICE_LIMIT = 'serviceLimit'
SERVICE_NOT_AVAILABLE = 'serviceNotAvailable'
SHARE_IN_NOT_PERMITTED = 'shareInNotPermitted'
SHARE_OUT_NOT_PERMITTED = 'shareOutNotPermitted'
SHARE_OUT_NOT_PERMITTED_TO_USER = 'shareOutNotPermittedToUser'
SHARE_OUT_WARNING = 'shareOutWarning'
SHARING_RATE_LIMIT_EXCEEDED = 'sharingRateLimitExceeded'
SHORTCUT_TARGET_INVALID = 'shortcutTargetInvalid'
STORAGE_QUOTA_EXCEEDED = 'storageQuotaExceeded'
SYSTEM_ERROR = 'systemError'
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION = 'targetUserRoleLimitedByLicenseRestriction'
TEAMDRIVE_ALREADY_EXISTS = 'teamDriveAlreadyExists'
TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION = 'teamDriveDomainUsersOnlyRestriction'
TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION = 'teamDriveTeamMembersOnlyRestriction'
TEAMDRIVE_FILE_LIMIT_EXCEEDED = 'teamDriveFileLimitExceeded'
TEAMDRIVE_HIERARCHY_TOO_DEEP = 'teamDriveHierarchyTooDeep'
TEAMDRIVE_MEMBERSHIP_REQUIRED = 'teamDriveMembershipRequired'
TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED = 'teamDrivesFolderMoveInNotSupported'
TEAMDRIVES_FOLDER_SHARING_NOT_SUPPORTED = 'teamDrivesFolderSharingNotSupported'
TEAMDRIVES_PARENT_LIMIT = 'teamDrivesParentLimit'
TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED = 'teamDrivesSharingRestrictionNotAllowed'
TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED = 'teamDrivesShortcutFileNotSupported'
TIME_RANGE_EMPTY = 'timeRangeEmpty'
TRANSIENT_ERROR = 'transientError'
UNIMPLEMENTED_ERROR = 'unimplementedError'
UNKNOWN_ERROR = 'unknownError'
UNSUPPORTED_LANGUAGE_CODE = 'unsupportedLanguageCode'
UNSUPPORTED_SUPERVISED_ACCOUNT = 'unsupportedSupervisedAccount'
UPLOAD_TOO_LARGE = 'uploadTooLarge'
USER_CANNOT_CREATE_TEAMDRIVES = 'userCannotCreateTeamDrives'
USER_ACCESS = 'userAccess'
USER_NOT_FOUND = 'userNotFound'
USER_RATE_LIMIT_EXCEEDED = 'userRateLimitExceeded'
#
DEFAULT_RETRY_REASONS = [QUOTA_EXCEEDED, RATE_LIMIT_EXCEEDED, SHARING_RATE_LIMIT_EXCEEDED, USER_RATE_LIMIT_EXCEEDED,
BACKEND_ERROR, BAD_GATEWAY, GATEWAY_TIMEOUT, INTERNAL_ERROR, TRANSIENT_ERROR]
SERVICE_NOT_AVAILABLE_RETRY_REASONS = [SERVICE_NOT_AVAILABLE]
ACTIVITY_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST]
ALERT_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, PERMISSION_DENIED]
CALENDAR_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, NOT_A_CALENDAR_USER]
CIGROUP_CREATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, ALREADY_EXISTS, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_ARGUMENT, PERMISSION_DENIED, FAILED_PRECONDITION]
CIGROUP_GET_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, INVALID_ARGUMENT, SYSTEM_ERROR, PERMISSION_DENIED]
CIGROUP_LIST_THROW_REASONS = [SERVICE_NOT_AVAILABLE, RESOURCE_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, INVALID_ARGUMENT, SYSTEM_ERROR, PERMISSION_DENIED]
CIGROUP_LIST_USERKEY_THROW_REASONS = CIGROUP_LIST_THROW_REASONS+[INVALID_ARGUMENT]
CIGROUP_UPDATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS,
FORBIDDEN, BAD_REQUEST, INVALID, INVALID_INPUT, INVALID_ARGUMENT,
SYSTEM_ERROR, PERMISSION_DENIED, FAILED_PRECONDITION]
CIGROUP_RETRY_REASONS = [INVALID, SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
CIMEMBERS_THROW_REASONS = [SERVICE_NOT_AVAILABLE, MEMBER_NOT_FOUND, INVALID_MEMBER]
CISSO_CREATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, FAILED_PRECONDITION, NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_ARGUMENT, PERMISSION_DENIED, INTERNAL_ERROR]
CISSO_GET_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR, PERMISSION_DENIED, INTERNAL_ERROR]
CISSO_LIST_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR, PERMISSION_DENIED, INTERNAL_ERROR]
CISSO_UPDATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, NOT_FOUND, FAILED_PRECONDITION, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS,
FORBIDDEN, BAD_REQUEST, INVALID, INVALID_INPUT, INVALID_ARGUMENT,
SYSTEM_ERROR, PERMISSION_DENIED, INTERNAL_ERROR]
CONTACT_DELEGATE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, FAILED_PRECONDITION, PERMISSION_DENIED, FORBIDDEN, INVALID_ARGUMENT]
COURSE_ACCESS_THROW_REASONS = [NOT_FOUND, INSUFFICIENT_PERMISSIONS, PERMISSION_DENIED, FORBIDDEN, INVALID_ARGUMENT]
DRIVE_USER_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, DOMAIN_POLICY]
DRIVE_ACCESS_THROW_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, FORBIDDEN, INTERNAL_ERROR, INSUFFICIENT_FILE_PERMISSIONS, UNKNOWN_ERROR, INVALID]
DRIVE_COPY_THROW_REASONS = DRIVE_ACCESS_THROW_REASONS+[CANNOT_COPY_FILE, BAD_REQUEST, RESPONSE_PREPARATION_FAILURE, TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
FIELD_NOT_WRITABLE, RATE_LIMIT_EXCEEDED, USER_RATE_LIMIT_EXCEEDED,
STORAGE_QUOTA_EXCEEDED, TEAMDRIVE_FILE_LIMIT_EXCEEDED, TEAMDRIVE_HIERARCHY_TOO_DEEP]
DRIVE_GET_THROW_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, DOWNLOAD_QUOTA_EXCEEDED]
DRIVE3_CREATE_ACL_THROW_REASONS = [BAD_REQUEST, INVALID, INVALID_SHARING_REQUEST, OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED,
CANNOT_SET_EXPIRATION, CANNOT_SET_EXPIRATION_ON_ANYONE_OR_DOMAIN,
EXPIRATION_DATES_MUST_BE_IN_THE_FUTURE, EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS,
NOT_FOUND, TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION, TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION,
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION, INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, SHARING_RATE_LIMIT_EXCEEDED,
PUBLISH_OUT_NOT_PERMITTED, SHARE_IN_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED_TO_USER,
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS,
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS,
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE,
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY,
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
CANNOT_MODIFY_INHERITED_PERMISSION,
TEAMDRIVES_FOLDER_SHARING_NOT_SUPPORTED, INVALID_LINK_VISIBILITY, ABUSIVE_CONTENT_RESTRICTION]
DRIVE3_GET_ACL_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, FORBIDDEN, INTERNAL_ERROR,
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, INSUFFICIENT_FILE_PERMISSIONS,
UNKNOWN_ERROR, INVALID]
DRIVE3_UPDATE_ACL_THROW_REASONS = [BAD_REQUEST, INVALID_OWNERSHIP_TRANSFER, CANNOT_REMOVE_OWNER,
CANNOT_SET_EXPIRATION, CANNOT_SET_EXPIRATION_ON_ANYONE_OR_DOMAIN,
EXPIRATION_DATES_MUST_BE_IN_THE_FUTURE, EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS,
OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED,
NOT_FOUND, TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION, TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION,
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION, INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, SHARING_RATE_LIMIT_EXCEEDED,
PUBLISH_OUT_NOT_PERMITTED, SHARE_IN_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED, SHARE_OUT_NOT_PERMITTED_TO_USER,
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS,
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS,
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED,
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE,
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY,
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED,
CANNOT_UPDATE_PERMISSION,
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION, CANNOT_MODIFY_INHERITED_PERMISSION,
FIELD_NOT_WRITABLE, PERMISSION_NOT_FOUND]
DRIVE3_DELETE_ACL_THROW_REASONS = [BAD_REQUEST, NOT_FOUND, PERMISSION_NOT_FOUND, CANNOT_REMOVE_OWNER,
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION, CANNOT_MODIFY_INHERITED_PERMISSION,
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, SHARING_RATE_LIMIT_EXCEEDED,
CANNOT_DELETE_PERMISSION, FILE_NEVER_WRITABLE]
DRIVE3_MODIFY_LABEL_THROW_REASONS = DRIVE_USER_THROW_REASONS+[FILE_NOT_FOUND, NOT_FOUND, FORBIDDEN, INTERNAL_ERROR,
FILE_NEVER_WRITABLE, APPLY_LABEL_FORBIDDEN,
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES, INSUFFICIENT_FILE_PERMISSIONS,
UNKNOWN_ERROR, INVALID_INPUT, BAD_REQUEST,
LABEL_MULTIPLE_VALUES_FOR_SINGULAR_FIELD, LABEL_MUTATION_FORBIDDEN,
LABEL_MUTATION_ILLEGAL_SELECTION, LABEL_MUTATION_UNKNOWN_FIELD]
DOCS_ACCESS_THROW_REASONS = DRIVE_USER_THROW_REASONS+[NOT_FOUND, PERMISSION_DENIED, FORBIDDEN, INTERNAL_ERROR, INSUFFICIENT_FILE_PERMISSIONS,
BAD_REQUEST, INVALID, INVALID_ARGUMENT, FAILED_PRECONDITION]
GMAIL_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST]
GMAIL_LIST_THROW_REASONS = [FAILED_PRECONDITION, PERMISSION_DENIED, INVALID, INVALID_ARGUMENT]
GMAIL_SMIME_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, INVALID_ARGUMENT, FORBIDDEN, NOT_FOUND, PERMISSION_DENIED]
GROUP_GET_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, INVALID, SYSTEM_ERROR]
GROUP_GET_RETRY_REASONS = [INVALID, SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
GROUP_CREATE_THROW_REASONS = [DUPLICATE, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT, RESOURCE_NOT_FOUND]
GROUP_UPDATE_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, INVALID, INVALID_INPUT]
GROUP_SETTINGS_THROW_REASONS = [NOT_FOUND, GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, SYSTEM_ERROR, PERMISSION_DENIED,
INVALID, INVALID_ARGUMENT, INVALID_PARAMETER, INVALID_ATTRIBUTE_VALUE, INVALID_INPUT,
SERVICE_LIMIT, SERVICE_NOT_AVAILABLE, AUTH_ERROR, REQUIRED]
GROUP_SETTINGS_RETRY_REASONS = [INVALID, SERVICE_LIMIT, SERVICE_NOT_AVAILABLE]
GROUP_LIST_THROW_REASONS = [RESOURCE_NOT_FOUND, DOMAIN_NOT_FOUND, FORBIDDEN, BAD_REQUEST, PERMISSION_DENIED]
GROUP_LIST_USERKEY_THROW_REASONS = GROUP_LIST_THROW_REASONS+[INVALID_MEMBER, INVALID_INPUT]
KEEP_THROW_REASONS = [AUTH_ERROR, BAD_REQUEST, PERMISSION_DENIED, INVALID_ARGUMENT, NOT_FOUND]
LOOKERSTUDIO_THROW_REASONS = [INVALID_ARGUMENT, SERVICE_NOT_AVAILABLE, BAD_REQUEST, NOT_FOUND, PERMISSION_DENIED, INTERNAL_ERROR]
MEMBERS_THROW_REASONS = [GROUP_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, INVALID, FORBIDDEN, SERVICE_NOT_AVAILABLE, PERMISSION_DENIED]
MEMBERS_RETRY_REASONS = [SYSTEM_ERROR, SERVICE_NOT_AVAILABLE]
ORGUNIT_GET_THROW_REASONS = [INVALID_ORGUNIT, ORGUNIT_NOT_FOUND, BACKEND_ERROR, BAD_REQUEST, INVALID_CUSTOMER_ID, LOGIN_REQUIRED]
PEOPLE_ACCESS_THROW_REASONS = [SERVICE_NOT_AVAILABLE, FORBIDDEN, PERMISSION_DENIED, FAILED_PRECONDITION]
RESELLER_THROW_REASONS = [BAD_REQUEST, RESOURCE_NOT_FOUND, FORBIDDEN, INVALID]
SHEETS_ACCESS_THROW_REASONS = DRIVE_USER_THROW_REASONS+[NOT_FOUND, PERMISSION_DENIED, FORBIDDEN, INTERNAL_ERROR, INSUFFICIENT_FILE_PERMISSIONS,
BAD_REQUEST, INVALID, INVALID_ARGUMENT, FAILED_PRECONDITION]
TAGMANAGER_THROW_REASONS = [BAD_REQUEST, PERMISSION_DENIED, INVALID, NOT_FOUND, ACCESS_NOT_CONFIGURED]
TASK_THROW_REASONS = [BAD_REQUEST, PERMISSION_DENIED, INVALID, NOT_FOUND, ACCESS_NOT_CONFIGURED]
TASKLIST_THROW_REASONS = [BAD_REQUEST, PERMISSION_DENIED, INVALID, NOT_FOUND, ACCESS_NOT_CONFIGURED]
USER_GET_THROW_REASONS = [USER_NOT_FOUND, DOMAIN_NOT_FOUND, DOMAIN_CANNOT_USE_APIS, FORBIDDEN, BAD_REQUEST, SYSTEM_ERROR]
YOUTUBE_THROW_REASONS = [SERVICE_NOT_AVAILABLE, AUTH_ERROR, UNSUPPORTED_SUPERVISED_ACCOUNT, UNSUPPORTED_LANGUAGE_CODE, CONTENT_OWNER_ACCOUNT_NOT_FOUND]
REASON_MESSAGE_MAP = {
ABORTED: [
('Label name exists or conflicts', DUPLICATE),
('The operation was aborted', ABORTED),
],
CONDITION_NOT_MET: [
('Cyclic memberships not allowed', CYCLIC_MEMBERSHIPS_NOT_ALLOWED),
('undelete', DELETED_USER_NOT_FOUND),
],
FAILED_PRECONDITION: [
('Bad Request', BAD_REQUEST),
('Mail service not enabled', SERVICE_NOT_AVAILABLE),
],
INVALID: [
('userId', USER_NOT_FOUND),
('memberKey', INVALID_MEMBER),
('A system error has occurred', SYSTEM_ERROR),
('Expiration dates must be in the future', EXPIRATION_DATES_MUST_BE_IN_THE_FUTURE),
('Invalid attribute value', INVALID_ATTRIBUTE_VALUE),
('Invalid Customer Id', INVALID_CUSTOMER_ID),
('Invalid Input: INVALID_OU_ID', INVALID_ORGUNIT),
('Invalid Input: custom_schema', INVALID_SCHEMA_VALUE),
('Invalid Input: groupKey', INVALID_INPUT),
('Invalid Input: resource', INVALID_RESOURCE),
('Invalid Input:', INVALID_INPUT),
('Invalid Input', INVALID_INPUT),
('Invalid Org Unit', INVALID_ORGUNIT),
('Invalid Ou Id', INVALID_ORGUNIT),
('Invalid Ou Name', INVALID_ORGUNIT_NAME),
('Invalid Parent Orgunit Id', INVALID_PARENT_ORGUNIT),
('Invalid query', INVALID_QUERY),
('Invalid scope value', INVALID_SCOPE_VALUE),
('Invalid value', INVALID_INPUT),
('New domain name is not a verified secondary domain', DOMAIN_NOT_VERIFIED_SECONDARY),
('PermissionDenied', PERMISSION_DENIED),
],
INVALID_ARGUMENT: [
('Cannot delete primary send-as', CANNOT_DELETE_PRIMARY_SENDAS),
('Invalid id value', INVALID_MESSAGE_ID),
('Invalid ids value', INVALID_MESSAGE_ID),
],
NOT_FOUND: [
('userKey', USER_NOT_FOUND),
('groupKey', GROUP_NOT_FOUND),
('memberKey', MEMBER_NOT_FOUND),
('photo', PHOTO_NOT_FOUND),
('resource_id', RESOURCE_ID_NOT_FOUND),
('resourceId', RESOURCE_ID_NOT_FOUND),
('Customer doesn\'t exist', CUSTOMER_NOT_FOUND),
('Domain alias does not exist', DOMAIN_ALIAS_NOT_FOUND),
('Domain not found', DOMAIN_NOT_FOUND),
('domain', DOMAIN_NOT_FOUND),
('File not found', FILE_NOT_FOUND),
('Org unit not found', ORGUNIT_NOT_FOUND),
('Permission not found', PERMISSION_NOT_FOUND),
('Resource Not Found', RESOURCE_NOT_FOUND),
('Revision not found', REVISION_NOT_FOUND),
('Shared Drive not found', NOT_FOUND),
('Not Found', NOT_FOUND),
],
REQUIRED: [
('Login Required', LOGIN_REQUIRED),
('memberKey', MEMBER_NOT_FOUND),
],
RESOURCE_NOT_FOUND: [
('resourceId', RESOURCE_ID_NOT_FOUND),
],
}
class aborted(Exception):
pass
class abusiveContentRestriction(Exception):
pass
class accessNotConfigured(Exception):
pass
class adminCannotUnsuspend(Exception):
pass
class alreadyExists(Exception):
pass
class applyLabelForbidden(Exception):
pass
class authError(Exception):
pass
class backendError(Exception):
pass
class badRequest(Exception):
pass
class cannotAddParent(Exception):
pass
class cannotChangeOrganizer(Exception):
pass
class cannotChangeOrganizerOfInstance(Exception):
pass
class cannotChangeOwnAcl(Exception):
pass
class cannotChangeOwnerAcl(Exception):
pass
class cannotChangeOwnPrimarySubscription(Exception):
pass
class cannotCopyFile(Exception):
pass
class cannotDeleteOnlyRevision(Exception):
pass
class cannotDeletePermission(Exception):
pass
class cannotDeletePrimaryCalendar(Exception):
pass
class cannotDeletePrimarySendAs(Exception):
pass
class cannotDeleteResourceWithChildren(Exception):
pass
class cannotModifyAclOfCalendarOwner(Exception):
pass
class cannotModifyInheritedPermission(Exception):
pass
class cannotModifyInheritedTeamDrivePermission(Exception):
pass
class cannotModifyRestrictedLabel(Exception):
pass
class cannotModifyViewersCanCopyContent(Exception):
pass
class cannotMoveTrashedItemIntoTeamDrive(Exception):
pass
class cannotMoveTrashedItemOutOfTeamDrive(Exception):
pass
class cannotRemoveOwner(Exception):
pass
class cannotSetExpiration(Exception):
pass
class cannotSetExpirationOnAnyoneOrDomain(Exception):
pass
class cannotShareGroupsWithLink(Exception):
pass
class cannotShareUsersWithLink(Exception):
pass
class cannotShareTeamDriveTopFolderWithAnyoneOrDomains(Exception):
pass
class cannotShareTeamDriveWithNonGoogleAccounts(Exception):
pass
class cannotUnsubscribeFromOwnedCalendar(Exception):
pass
class cannotUpdatePermission(Exception):
pass
class conditionNotMet(Exception):
pass
class conflict(Exception):
pass
class contentOwnerAccountNotFound(Exception):
pass
class crossDomainMoveRestriction(Exception):
pass
class customerExceededRoleAssignmentsLimit(Exception):
pass
class customerNotFound(Exception):
pass
class cyclicMembershipsNotAllowed(Exception):
pass
class deleted(Exception):
pass
class deletedUserNotFound(Exception):
pass
class domainAliasNotFound(Exception):
pass
class domainCannotUseApis(Exception):
pass
class domainNotFound(Exception):
pass
class domainNotVerifiedSecondary(Exception):
pass
class domainPolicy(Exception):
pass
class downloadQuotaExceeded(Exception):
pass
class duplicate(Exception):
pass
class eventDurationExceedsLimit(Exception):
pass
class eventTypeRestriction(Exception):
pass
class expirationDatesMustBeInTheFuture(Exception):
pass
class expirationDateNotAllowedForSharedDriveMembers(Exception):
pass
class failedPrecondition(Exception):
pass
class fieldInUse(Exception):
pass
class fieldNotWritable(Exception):
pass
class fileNeverWritable(Exception):
pass
class fileNotFound(Exception):
pass
class fileOrganizerNotYetEnabledForThisTeamDrive(Exception):
pass
class fileOrganizerOnFoldersInSharedDriveOnly(Exception):
pass
class fileOrganizerOnNonTeamDriveNotSupported(Exception):
pass
class fileOwnerNotMemberOfTeamDrive(Exception):
pass
class fileOwnerNotMemberOfWriterDomain(Exception):
pass
class fileWriterTeamDriveMoveInDisabled(Exception):
pass
class forbidden(Exception):
pass
class groupNotFound(Exception):
pass
class illegalAccessRoleForDefault(Exception):
pass
class insufficientAdministratorPrivileges(Exception):
pass
class insufficientArchivedUserLicenses(Exception):
pass
class insufficientFilePermissions(Exception):
pass
class insufficientParentPermissions(Exception):
pass
class insufficientPermissions(Exception):
pass
class internalError(Exception):
pass
class invalid(Exception):
pass
class invalidArgument(Exception):
pass
class invalidAttributeValue(Exception):
pass
class invalidCustomerId(Exception):
pass
class invalidInput(Exception):
pass
class invalidLinkVisibility(Exception):
pass
class invalidMember(Exception):
pass
class invalidMessageId(Exception):
pass
class invalidOrgunit(Exception):
pass
class invalidOrgunitName(Exception):
pass
class invalidOwnershipTransfer(Exception):
pass
class invalidParameter(Exception):
pass
class invalidParentOrgunit(Exception):
pass
class invalidQuery(Exception):
pass
class invalidResource(Exception):
pass
class invalidSchemaValue(Exception):
pass
class invalidScopeValue(Exception):
pass
class invalidSharingRequest(Exception):
pass
class labelMultipleValuesForSingularField(Exception):
pass
class labelMutationForbidden(Exception):
pass
class labelMutationIllegalSelection(Exception):
pass
class labelMutationUnknownField(Exception):
pass
class limitExceeded(Exception):
pass
class loginRequired(Exception):
pass
class malformedWorkingLocationEvent(Exception):
pass
class memberNotFound(Exception):
pass
class noListTeamDrivesAdministratorPrivilege(Exception):
pass
class noManageTeamDriveAdministratorPrivilege(Exception):
pass
class notACalendarUser(Exception):
pass
class notFound(Exception):
pass
class notImplemented(Exception):
pass
class operationNotSupported(Exception):
pass
class organizerOnNonTeamDriveNotSupported(Exception):
pass
class organizerOnNonTeamDriveItemNotSupported(Exception):
pass
class orgunitNotFound(Exception):
pass
class outsideDomainMemberCannotChangeTeamDriveRestrictions(Exception):
pass
class ownerOnTeamDriveItemNotSupported(Exception):
pass
class ownershipChangeAcrossDomainNotPermitted(Exception):
pass
class participantIsNeitherOrganizerNorAttendee(Exception):
pass
class permissionDenied(Exception):
pass
class permissionNotFound(Exception):
pass
class photoNotFound(Exception):
pass
class publishOutNotPermitted(Exception):
pass
class queryRequiresAdminCredentials(Exception):
pass
class quotaExceeded(Exception):
pass
class rateLimitExceeded(Exception):
pass
class required(Exception):
pass
class requiredAccessLevel(Exception):
pass
class resourceExhausted(Exception):
pass
class resourceIdNotFound(Exception):
pass
class resourceNotFound(Exception):
pass
class responsePreparationFailure(Exception):
pass
class revisionDeletionNotSupported(Exception):
pass
class revisionNotFound(Exception):
pass
class revisionsNotSupported(Exception):
pass
class serviceLimit(Exception):
pass
class serviceNotAvailable(Exception):
pass
class shareInNotPermitted(Exception):
pass
class shareOutNotPermitted(Exception):
pass
class shareOutNotPermittedToUser(Exception):
pass
class shareOutWarning(Exception):
pass
class sharingRateLimitExceeded(Exception):
pass
class shortcutTargetInvalid(Exception):
pass
class storageQuotaExceeded(Exception):
pass
class systemError(Exception):
pass
class targetUserRoleLimitedByLicenseRestriction(Exception):
pass
class teamDriveAlreadyExists(Exception):
pass
class teamDriveDomainUsersOnlyRestriction(Exception):
pass
class teamDriveTeamMembersOnlyRestriction(Exception):
pass
class teamDriveFileLimitExceeded(Exception):
pass
class teamDriveHierarchyTooDeep(Exception):
pass
class teamDriveMembershipRequired(Exception):
pass
class teamDrivesFolderMoveInNotSupported(Exception):
pass
class teamDrivesFolderSharingNotSupported(Exception):
pass
class teamDrivesParentLimit(Exception):
pass
class teamDrivesSharingRestrictionNotAllowed(Exception):
pass
class teamDrivesShortcutFileNotSupported(Exception):
pass
class timeRangeEmpty(Exception):
pass
class transientError(Exception):
pass
class unimplementedError(Exception):
pass
class unknownError(Exception):
pass
class unsupportedLanguageCode(Exception):
pass
class unsupportedSupervisedAccount(Exception):
pass
class uploadTooLarge(Exception):
pass
class userCannotCreateTeamDrives(Exception):
pass
class userAccess(Exception):
pass
class userNotFound(Exception):
pass
class userRateLimitExceeded(Exception):
pass
REASON_EXCEPTION_MAP = {
ABORTED: aborted,
ABUSIVE_CONTENT_RESTRICTION: abusiveContentRestriction,
ACCESS_NOT_CONFIGURED: accessNotConfigured,
ADMIN_CANNOT_UNSUSPEND: adminCannotUnsuspend,
ALREADY_EXISTS: alreadyExists,
APPLY_LABEL_FORBIDDEN: applyLabelForbidden,
AUTH_ERROR: authError,
BACKEND_ERROR: backendError,
BAD_REQUEST: badRequest,
CANNOT_ADD_PARENT: cannotAddParent,
CANNOT_CHANGE_ORGANIZER: cannotChangeOrganizer,
CANNOT_CHANGE_ORGANIZER_OF_INSTANCE: cannotChangeOrganizerOfInstance,
CANNOT_CHANGE_OWN_ACL: cannotChangeOwnAcl,
CANNOT_CHANGE_OWNER_ACL: cannotChangeOwnerAcl,
CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION: cannotChangeOwnPrimarySubscription,
CANNOT_COPY_FILE: cannotCopyFile,
CANNOT_DELETE_ONLY_REVISION: cannotDeleteOnlyRevision,
CANNOT_DELETE_PERMISSION: cannotDeletePermission,
CANNOT_DELETE_PRIMARY_CALENDAR: cannotDeletePrimaryCalendar,
CANNOT_DELETE_PRIMARY_SENDAS: cannotDeletePrimarySendAs,
CANNOT_DELETE_RESOURCE_WITH_CHILDREN: cannotDeleteResourceWithChildren,
CANNOT_MODIFY_ACL_OF_CALENDAR_OWNER: cannotModifyAclOfCalendarOwner,
CANNOT_MODIFY_INHERITED_PERMISSION: cannotModifyInheritedPermission,
CANNOT_MODIFY_INHERITED_TEAMDRIVE_PERMISSION: cannotModifyInheritedTeamDrivePermission,
CANNOT_MODIFY_RESTRICTED_LABEL: cannotModifyRestrictedLabel,
CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT: cannotModifyViewersCanCopyContent,
CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE: cannotMoveTrashedItemIntoTeamDrive,
CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE: cannotMoveTrashedItemOutOfTeamDrive,
CANNOT_REMOVE_OWNER: cannotRemoveOwner,
CANNOT_SET_EXPIRATION: cannotSetExpiration,
CANNOT_SET_EXPIRATION_ON_ANYONE_OR_DOMAIN: cannotSetExpirationOnAnyoneOrDomain,
CANNOT_SHARE_GROUPS_WITHLINK: cannotShareGroupsWithLink,
CANNOT_SHARE_USERS_WITHLINK: cannotShareUsersWithLink,
CANNOT_SHARE_TEAMDRIVE_TOPFOLDER_WITH_ANYONEORDOMAINS: cannotShareTeamDriveTopFolderWithAnyoneOrDomains,
CANNOT_SHARE_TEAMDRIVE_WITH_NONGOOGLE_ACCOUNTS: cannotShareTeamDriveWithNonGoogleAccounts,
CANNOT_UNSUBSCRIBE_FROM_OWNED_CALENDAR: cannotUnsubscribeFromOwnedCalendar,
CANNOT_UPDATE_PERMISSION: cannotUpdatePermission,
CONDITION_NOT_MET: conditionNotMet,
CONFLICT: conflict,
CONTENT_OWNER_ACCOUNT_NOT_FOUND: contentOwnerAccountNotFound,
CROSS_DOMAIN_MOVE_RESTRICTION: crossDomainMoveRestriction,
CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT: customerExceededRoleAssignmentsLimit,
CUSTOMER_NOT_FOUND: customerNotFound,
CYCLIC_MEMBERSHIPS_NOT_ALLOWED: cyclicMembershipsNotAllowed,
DELETED: deleted,
DELETED_USER_NOT_FOUND: deletedUserNotFound,
DOMAIN_ALIAS_NOT_FOUND: domainAliasNotFound,
DOMAIN_CANNOT_USE_APIS: domainCannotUseApis,
DOMAIN_NOT_FOUND: domainNotFound,
DOMAIN_NOT_VERIFIED_SECONDARY: domainNotVerifiedSecondary,
DOMAIN_POLICY: domainPolicy,
DOWNLOAD_QUOTA_EXCEEDED: downloadQuotaExceeded,
DUPLICATE: duplicate,
EVENT_DURATION_EXCEEDS_LIMIT: eventDurationExceedsLimit,
EVENT_TYPE_RESTRICTION: eventTypeRestriction,
EXPIRATION_DATES_MUST_BE_IN_THE_FUTURE: expirationDatesMustBeInTheFuture,
EXPIRATION_DATE_NOT_ALLOWED_FOR_SHARED_DRIVE_MEMBERS: expirationDateNotAllowedForSharedDriveMembers,
FAILED_PRECONDITION: failedPrecondition,
FIELD_IN_USE: fieldInUse,
FIELD_NOT_WRITABLE: fieldNotWritable,
FILE_NEVER_WRITABLE: fileNeverWritable,
FILE_NOT_FOUND: fileNotFound,
FILE_ORGANIZER_NOT_YET_ENABLED_FOR_THIS_TEAMDRIVE: fileOrganizerNotYetEnabledForThisTeamDrive,
FILE_ORGANIZER_ON_FOLDERS_IN_SHARED_DRIVE_ONLY: fileOrganizerOnFoldersInSharedDriveOnly,
FILE_ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED: fileOrganizerOnNonTeamDriveNotSupported,
FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE: fileOwnerNotMemberOfTeamDrive,
FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN: fileOwnerNotMemberOfWriterDomain,
FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED: fileWriterTeamDriveMoveInDisabled,
FORBIDDEN: forbidden,
GROUP_NOT_FOUND: groupNotFound,
ILLEGAL_ACCESS_ROLE_FOR_DEFAULT: illegalAccessRoleForDefault,
INSUFFICIENT_ADMINISTRATOR_PRIVILEGES: insufficientAdministratorPrivileges,
INSUFFICIENT_ARCHIVED_USER_LICENSES: insufficientArchivedUserLicenses,
INSUFFICIENT_FILE_PERMISSIONS: insufficientFilePermissions,
INSUFFICIENT_PARENT_PERMISSIONS: insufficientParentPermissions,
INSUFFICIENT_PERMISSIONS: insufficientPermissions,
INTERNAL_ERROR: internalError,
INVALID: invalid,
INVALID_ARGUMENT: invalidArgument,
INVALID_ATTRIBUTE_VALUE: invalidAttributeValue,
INVALID_CUSTOMER_ID: invalidCustomerId,
INVALID_INPUT: invalidInput,
INVALID_LINK_VISIBILITY: invalidLinkVisibility,
INVALID_MEMBER: invalidMember,
INVALID_MESSAGE_ID: invalidMessageId,
INVALID_ORGUNIT: invalidOrgunit,
INVALID_ORGUNIT_NAME: invalidOrgunitName,
INVALID_OWNERSHIP_TRANSFER: invalidOwnershipTransfer,
INVALID_PARAMETER: invalidParameter,
INVALID_PARENT_ORGUNIT: invalidParentOrgunit,
INVALID_QUERY: invalidQuery,
INVALID_RESOURCE: invalidResource,
INVALID_SCHEMA_VALUE: invalidSchemaValue,
INVALID_SCOPE_VALUE: invalidScopeValue,
INVALID_SHARING_REQUEST: invalidSharingRequest,
LABEL_MULTIPLE_VALUES_FOR_SINGULAR_FIELD: labelMultipleValuesForSingularField,
LABEL_MUTATION_FORBIDDEN: labelMutationForbidden,
LABEL_MUTATION_ILLEGAL_SELECTION: labelMutationIllegalSelection,
LABEL_MUTATION_UNKNOWN_FIELD: labelMutationUnknownField,
LIMIT_EXCEEDED: limitExceeded,
LOGIN_REQUIRED: loginRequired,
MALFORMED_WORKING_LOCATION_EVENT: malformedWorkingLocationEvent,
MEMBER_NOT_FOUND: memberNotFound,
NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE: noListTeamDrivesAdministratorPrivilege,
NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE: noManageTeamDriveAdministratorPrivilege,
NOT_A_CALENDAR_USER: notACalendarUser,
NOT_FOUND: notFound,
NOT_IMPLEMENTED: notImplemented,
OPERATION_NOT_SUPPORTED: operationNotSupported,
ORGANIZER_ON_NON_TEAMDRIVE_NOT_SUPPORTED: organizerOnNonTeamDriveNotSupported,
ORGANIZER_ON_NON_TEAMDRIVE_ITEM_NOT_SUPPORTED: organizerOnNonTeamDriveItemNotSupported,
ORGUNIT_NOT_FOUND: orgunitNotFound,
OUTSIDE_DOMAIN_MEMBER_CANNOT_CHANGE_TEAMDRIVE_RESTRICTIONS: outsideDomainMemberCannotChangeTeamDriveRestrictions,
OWNER_ON_TEAMDRIVE_ITEM_NOT_SUPPORTED: ownerOnTeamDriveItemNotSupported,
OWNERSHIP_CHANGE_ACROSS_DOMAIN_NOT_PERMITTED: ownershipChangeAcrossDomainNotPermitted,
PARTICIPANT_IS_NEITHER_ORGANIZER_NOR_ATTENDEE: participantIsNeitherOrganizerNorAttendee,
PERMISSION_DENIED: permissionDenied,
PERMISSION_NOT_FOUND: permissionNotFound,
PHOTO_NOT_FOUND: photoNotFound,
PUBLISH_OUT_NOT_PERMITTED: publishOutNotPermitted,
QUERY_REQUIRES_ADMIN_CREDENTIALS: queryRequiresAdminCredentials,
QUOTA_EXCEEDED: quotaExceeded,
RATE_LIMIT_EXCEEDED: rateLimitExceeded,
REQUIRED: required,
REQUIRED_ACCESS_LEVEL: requiredAccessLevel,
RESOURCE_EXHAUSTED: resourceExhausted,
RESOURCE_ID_NOT_FOUND: resourceIdNotFound,
RESOURCE_NOT_FOUND: resourceNotFound,
RESPONSE_PREPARATION_FAILURE: responsePreparationFailure,
REVISION_DELETION_NOT_SUPPORTED: revisionDeletionNotSupported,
REVISION_NOT_FOUND: revisionNotFound,
REVISIONS_NOT_SUPPORTED: revisionsNotSupported,
SERVICE_LIMIT: serviceLimit,
SERVICE_NOT_AVAILABLE: serviceNotAvailable,
SHARE_IN_NOT_PERMITTED: shareInNotPermitted,
SHARE_OUT_NOT_PERMITTED: shareOutNotPermitted,
SHARE_OUT_NOT_PERMITTED_TO_USER: shareOutNotPermittedToUser,
SHARE_OUT_WARNING: shareOutWarning,
SHARING_RATE_LIMIT_EXCEEDED: sharingRateLimitExceeded,
SHORTCUT_TARGET_INVALID: shortcutTargetInvalid,
STORAGE_QUOTA_EXCEEDED: storageQuotaExceeded,
SYSTEM_ERROR: systemError,
TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION: targetUserRoleLimitedByLicenseRestriction,
TEAMDRIVE_ALREADY_EXISTS: teamDriveAlreadyExists,
TEAMDRIVE_DOMAIN_USERS_ONLY_RESTRICTION: teamDriveDomainUsersOnlyRestriction,
TEAMDRIVE_TEAM_MEMBERS_ONLY_RESTRICTION: teamDriveTeamMembersOnlyRestriction,
TEAMDRIVE_FILE_LIMIT_EXCEEDED: teamDriveFileLimitExceeded,
TEAMDRIVE_HIERARCHY_TOO_DEEP: teamDriveHierarchyTooDeep,
TEAMDRIVE_MEMBERSHIP_REQUIRED: teamDriveMembershipRequired,
TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED: teamDrivesFolderMoveInNotSupported,
TEAMDRIVES_FOLDER_SHARING_NOT_SUPPORTED: teamDrivesFolderSharingNotSupported,
TEAMDRIVES_PARENT_LIMIT: teamDrivesParentLimit,
TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED: teamDrivesSharingRestrictionNotAllowed,
TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED: teamDrivesShortcutFileNotSupported,
TIME_RANGE_EMPTY: timeRangeEmpty,
TRANSIENT_ERROR: transientError,
UNIMPLEMENTED_ERROR: unimplementedError,
UNKNOWN_ERROR: unknownError,
UNSUPPORTED_LANGUAGE_CODE: unsupportedLanguageCode,
UNSUPPORTED_SUPERVISED_ACCOUNT: unsupportedSupervisedAccount,
UPLOAD_TOO_LARGE: uploadTooLarge,
USER_CANNOT_CREATE_TEAMDRIVES: userCannotCreateTeamDrives,
USER_ACCESS: userAccess,
USER_NOT_FOUND: userNotFound,
USER_RATE_LIMIT_EXCEEDED: userRateLimitExceeded,
}

View File

@@ -1,98 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM GData resources
"""
API_DEPRECATED_MSG = 'Contacts API is being deprecated.'
# callGData throw errors
API_DEPRECATED = 612
BAD_GATEWAY = 601
BAD_REQUEST = 602
DOES_NOT_EXIST = 1301
ENTITY_EXISTS = 1300
FORBIDDEN = 603
GATEWAY_TIMEOUT = 612
INSUFFICIENT_PERMISSIONS = 604
INTERNAL_SERVER_ERROR = 1000
INVALID_DOMAIN = 605
INVALID_INPUT = 1317
INVALID_VALUE = 1801
NAME_NOT_VALID = 1303
NOT_FOUND = 606
NOT_IMPLEMENTED = 607
PRECONDITION_FAILED = 608
QUOTA_EXCEEDED = 609
SERVICE_NOT_APPLICABLE = 1410
SERVICE_UNAVAILABLE = 610
TOKEN_EXPIRED = 611
TOKEN_INVALID = 403
UNKNOWN_ERROR = 600
#
NON_TERMINATING_ERRORS = [API_DEPRECATED, BAD_GATEWAY, GATEWAY_TIMEOUT, QUOTA_EXCEEDED, SERVICE_UNAVAILABLE, TOKEN_EXPIRED]
EMAILSETTINGS_THROW_LIST = [INVALID_DOMAIN, DOES_NOT_EXIST, SERVICE_NOT_APPLICABLE, BAD_REQUEST, NAME_NOT_VALID, INTERNAL_SERVER_ERROR, INVALID_VALUE]
#
class apiDeprecated(Exception):
pass
class badRequest(Exception):
pass
class doesNotExist(Exception):
pass
class entityExists(Exception):
pass
class forbidden(Exception):
pass
class insufficientPermissions(Exception):
pass
class internalServerError(Exception):
pass
class invalidDomain(Exception):
pass
class invalidInput(Exception):
pass
class invalidValue(Exception):
pass
class nameNotValid(Exception):
pass
class notFound(Exception):
pass
class notImplemented(Exception):
pass
class preconditionFailed(Exception):
pass
class serviceNotApplicable(Exception):
pass
ERROR_CODE_EXCEPTION_MAP = {
API_DEPRECATED: apiDeprecated,
BAD_REQUEST: badRequest,
DOES_NOT_EXIST: doesNotExist,
ENTITY_EXISTS: entityExists,
FORBIDDEN: forbidden,
INSUFFICIENT_PERMISSIONS: insufficientPermissions,
INTERNAL_SERVER_ERROR: internalServerError,
INVALID_DOMAIN: invalidDomain,
INVALID_INPUT: invalidInput,
INVALID_VALUE: invalidValue,
NAME_NOT_VALID: nameNotValid,
NOT_FOUND: notFound,
NOT_IMPLEMENTED: notImplemented,
PRECONDITION_FAILED: preconditionFailed,
SERVICE_NOT_APPLICABLE: serviceNotApplicable,
}

View File

@@ -1,329 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM global variables
"""
# The following GM_XXX constants are arbitrary but must be unique
# Most errors print a message and bail out with a return code
# Some commands want to set a non-zero return code but not bail
# GAM admin user from oauth2.txt or oauth2service.json
ADMIN = 'admn'
# Number/length of API call retries
API_CALLS_RETRY_DATA = 'rtry'
# GAM cache directory. If no_cache is True, this variable will be set to None
CACHE_DIR = 'gacd'
# Reset GAM cache directory after discovery
CACHE_DISCOVERY_ONLY = 'gcdo'
# Classroom owner service object
CLASSROOM_OWNER_SA = 'cosa'
# Classroom service not available
CLASSROOM_SERVICE_NOT_AVAILABLE = 'csna'
# Command logging
CMDLOG_HANDLER = 'clha'
CMDLOG_LOGGER = 'cllo'
# Convert to local time
CONVERT_TO_LOCAL_TIME = 'ctlt'
# Credentials scopes
CREDENTIALS_SCOPES = 'crsc'
# csvfile keyfield <FieldName> [delimiter <Character>] (matchfield <FieldName> <MatchPattern>)* [datafield <FieldName>(:<FieldName>*) [delimiter <String>]]
CSVFILE = 'csvf'
# { key: [datafieldvalues]}
CSV_DATA_DICT = 'csdd'
CSV_KEY_FIELD = 'cskf'
CSV_SUBKEY_FIELD = 'cssk'
CSV_DATA_FIELD = 'csdf'
# Filter for input column drop values
CSV_INPUT_ROW_DROP_FILTER = 'cird'
# Mode (and|or) for input column drop values
CSV_INPUT_ROW_DROP_FILTER_MODE = 'cidm'
# Filter for input column values
CSV_INPUT_ROW_FILTER = 'cirf'
# Mode (and|or) for input column values
CSV_INPUT_ROW_FILTER_MODE = 'cirm'
# Limit number of input rows
CSV_INPUT_ROW_LIMIT = 'cirl'
# Column delimiter in CSV output file
CSV_OUTPUT_COLUMN_DELIMITER = 'codl'
# Filter for output column headers to drop
CSV_OUTPUT_HEADER_DROP_FILTER = 'cohd'
# Filter for output column headers
CSV_OUTPUT_HEADER_FILTER = 'cohf'
# Force output column headers
CSV_OUTPUT_HEADER_FORCE = 'cofh'
# Order output column headers
CSV_OUTPUT_HEADER_ORDER = 'coho'
# Required output column headers
CSV_OUTPUT_HEADER_REQUIRED = 'corh'
# No escape character in CSV output file
CSV_OUTPUT_NO_ESCAPE_CHAR = 'cone'
# Quote character in CSV output file
CSV_OUTPUT_QUOTE_CHAR = 'coqc'
# Filter for output column drop values
CSV_OUTPUT_ROW_DROP_FILTER = 'cord'
# Mode (and|or) for output column drop values
CSV_OUTPUT_ROW_DROP_FILTER_MODE = 'codm'
# Filter for output column values
CSV_OUTPUT_ROW_FILTER = 'corf'
# Mode (and|or) for output column values
CSV_OUTPUT_ROW_FILTER_MODE = 'corm'
# Limit number of output rows
CSV_OUTPUT_ROW_LIMIT = 'corl'
# Add timestamp column to CSV output file
CSV_OUTPUT_TIMESTAMP_COLUMN = 'cotc'
# Transpose output rows/columns
CSV_OUTPUT_TRANSPOSE = 'cotr'
# Output sort headers
CSV_OUTPUT_SORT_HEADERS = 'cosh'
# CSV todrive options
CSV_TODRIVE = 'todr'
# Current API services
CURRENT_API_SERVICES = 'caps'
# Current Client API
CURRENT_CLIENT_API = 'ccap'
# Current Client API scopes
CURRENT_CLIENT_API_SCOPES = 'ccas'
# Current Service Account API
CURRENT_SVCACCT_API = 'csap'
# Current Service Account API scopes
CURRENT_SVCACCT_API_SCOPES = 'csas'
# Current Service Account user
CURRENT_SVCACCT_USER = 'csa'
# datetime.datetime.now
DATETIME_NOW = 'dtno'
# If debug_level > 0: extra_args['prettyPrint'] = True, httplib2.debuglevel = gam_debug_level, appsObj.debug = True
DEBUG_LEVEL = 'dbgl'
# Whether debug output should redact sensitive credentials
DEBUG_REDACTION = 'dbrd'
# Decoded ID token
DECODED_ID_TOKEN = 'didt'
# Developer Preview APIs
DEVELOPER_PREVIEW_APIS = 'dapi'
# Index of start of <UserTypeEntity> in command line
ENTITY_CL_DELAY_START = 'ecld'
ENTITY_CL_START = 'ecls'
# Extra arguments to pass to GAPI functions
EXTRA_ARGS_LIST = 'exad'
# gam.cfg file
GAM_CFG_FILE = 'gcfi'
GAM_CFG_PATH = 'gcpa'
GAM_CFG_SECTION = 'gcse'
GAM_CFG_SECTION_NAME = 'gcsn'
# Path to gam
GAM_PATH = 'gpth'
# Python source, PyInstaller or StaticX?
GAM_TYPE = 'gtyp'
# Shared Service Account HTTP Object
HTTP_OBJECT = 'http'
# Are we on Global Compute Engine
IS_ON_GCE = 'ogce'
# Length of last Got message
LAST_GOT_MSG_LEN = 'lgml'
# License SKUs
LICENSE_SKUS = 'lsku'
# Make Building ID/Name map
MAKE_BUILDING_ID_NAME_MAP = 'mkbm'
# Dictionary mapping Building ID to Name
MAP_BUILDING_ID_TO_NAME = 'bi2n'
# Dictionary mapping Building Name to ID
MAP_BUILDING_NAME_TO_ID = 'bn2i'
# Dictionary mapping OrgUnit ID to Name
MAP_ORGUNIT_ID_TO_NAME = 'oi2n'
# Dictionary mapping Shared Drive ID to Name
MAP_SHAREDDRIVE_ID_TO_NAME = 'si2n'
# Make Role ID/Name map
MAKE_ROLE_ID_NAME_MAP = 'mkrm'
# Dictionary mapping Role ID to Name
MAP_ROLE_ID_TO_NAME = 'ri2n'
# Dictionary mapping Role Name to ID
MAP_ROLE_NAME_TO_ID = 'rn2i'
# Dictionary mapping User ID to Name
MAP_USER_ID_TO_NAME = 'ui2n'
# Multiprocess exit condition
MULTIPROCESS_EXIT_CONDITION = 'mpec'
# Multiprocess exit processing
MULTIPROCESS_EXIT_PROCESSING = 'mpep'
# Number of batch items
NUM_BATCH_ITEMS = 'nbat'
# Values retrieved from oauth2service.json
OAUTH2SERVICE_CLIENT_ID = 'osci'
OAUTH2SERVICE_JSON_DATA = 'osjd'
# Values retrieved from oauth2.txt
OAUTH2_CLIENT_ID = 'oaci'
# oauth2.txt lock file
OAUTH2_TXT_LOCK = 'oatl'
# Output date format, empty defalts to ISOFormat
OUTPUT_DATEFORMAT = 'oudf'
# Output time format, empty defalts to ISOFormat
OUTPUT_TIMEFORMAT = 'outf'
# gam.cfg parser
PARSER = 'pars'
# Process ID
PID = 'pid '
# Domains for print alises|groups|users
PRINT_AGU_DOMAINS = 'pagu'
# OrgUnits for print cros
PRINT_CROS_OUS = 'pcou'
# OrgUnits and children for print cros
PRINT_CROS_OUS_AND_CHILDREN = 'pcoc'
# Check API calls rate
RATE_CHECK_COUNT = 'rccn'
RATE_CHECK_START = 'rcst'
# Section name from outer gam, passed to inner gams
SECTION = 'sect'
# Enable/disable "Getting ... " messages
SHOW_GETTINGS = 'shog'
# Enable/disable NL at end of "Got ..." messages
SHOW_GETTINGS_GOT_NL = 'shgn'
# redirected files
SAVED_STDOUT = 'svso'
STDERR = 'stde'
STDOUT = 'stdo'
# Scopes values retrieved from oauth2service.json
SVCACCT_SCOPES = 'sasc'
# Were scopes values retrieved from oauth2service.json
SVCACCT_SCOPES_DEFINED = 'sasd'
# Most errors print a message and bail out with a return code
# Some commands want to set a non-zero return code but not bail
SYSEXITRC = 'sxrc'
# Encodings
SYS_ENCODING = 'syen'
# Shared by threadBatchWorker and threadBatchGAMCommands
TBATCH_QUEUE = 'batq'
# redirected file fields: name, mode, encoding, write header, multiproces, queue
REDIRECT_NAME = 'rdfn'
REDIRECT_MODE = 'rdmo'
REDIRECT_FD = 'rdfd'
REDIRECT_MULTI_FD = 'rdmf'
REDIRECT_STD = 'rdst'
REDIRECT_ENCODING = 'rden'
REDIRECT_WRITE_HEADER = 'rdwh'
REDIRECT_MULTIPROCESS = 'rdmp'
REDIRECT_QUEUE = 'rdq'
REDIRECT_QUEUE_NAME = 'name'
REDIRECT_QUEUE_CLEAR_ROW_FILTERS = 'clearRowFilters'
REDIRECT_QUEUE_TODRIVE = 'todrive'
REDIRECT_QUEUE_CSVPF = 'csvpf'
REDIRECT_QUEUE_DATA = 'rows'
REDIRECT_QUEUE_ARGS = 'args'
REDIRECT_QUEUE_GLOBALS = 'globals'
REDIRECT_QUEUE_VALUES = 'values'
REDIRECT_QUEUE_START = 'start'
REDIRECT_QUEUE_END = 'end'
REDIRECT_QUEUE_EOF = 'eof'
#
Globals = {
ADMIN: None,
API_CALLS_RETRY_DATA: {},
CACHE_DIR: None,
CACHE_DISCOVERY_ONLY: True,
CLASSROOM_OWNER_SA: {},
CLASSROOM_SERVICE_NOT_AVAILABLE: False,
CMDLOG_HANDLER: None,
CMDLOG_LOGGER: None,
CONVERT_TO_LOCAL_TIME: False,
CREDENTIALS_SCOPES: set(),
CSVFILE: {},
CSV_DATA_DICT: {},
CSV_KEY_FIELD: None,
CSV_SUBKEY_FIELD: None,
CSV_DATA_FIELD: None,
CSV_INPUT_ROW_DROP_FILTER: [],
CSV_INPUT_ROW_DROP_FILTER_MODE: False,
CSV_INPUT_ROW_FILTER: [],
CSV_INPUT_ROW_FILTER_MODE: True,
CSV_INPUT_ROW_LIMIT: 0,
CSV_OUTPUT_COLUMN_DELIMITER: None,
CSV_OUTPUT_HEADER_DROP_FILTER: [],
CSV_OUTPUT_HEADER_FILTER: [],
CSV_OUTPUT_HEADER_FORCE: [],
CSV_OUTPUT_HEADER_ORDER: [],
CSV_OUTPUT_HEADER_REQUIRED: [],
CSV_OUTPUT_NO_ESCAPE_CHAR: None,
CSV_OUTPUT_QUOTE_CHAR: None,
CSV_OUTPUT_ROW_DROP_FILTER: [],
CSV_OUTPUT_ROW_DROP_FILTER_MODE: False,
CSV_OUTPUT_ROW_FILTER: [],
CSV_OUTPUT_ROW_FILTER_MODE: True,
CSV_OUTPUT_ROW_LIMIT: 0,
CSV_OUTPUT_SORT_HEADERS: [],
CSV_OUTPUT_TIMESTAMP_COLUMN: None,
CSV_OUTPUT_TRANSPOSE: False,
CSV_TODRIVE: {},
CURRENT_API_SERVICES: {},
CURRENT_CLIENT_API: None,
CURRENT_CLIENT_API_SCOPES: set(),
CURRENT_SVCACCT_API: None,
CURRENT_SVCACCT_API_SCOPES: set(),
CURRENT_SVCACCT_USER: None,
DATETIME_NOW: None,
DEBUG_LEVEL: 0,
DEBUG_REDACTION: True,
DECODED_ID_TOKEN: None,
DEVELOPER_PREVIEW_APIS: set(),
ENTITY_CL_DELAY_START: 1,
ENTITY_CL_START: 1,
EXTRA_ARGS_LIST: [],
GAM_CFG_FILE: '',
GAM_CFG_PATH: '',
GAM_CFG_SECTION: '',
GAM_CFG_SECTION_NAME: '',
GAM_PATH: '.',
GAM_TYPE: '',
HTTP_OBJECT: None,
IS_ON_GCE: False,
LAST_GOT_MSG_LEN: 0,
LICENSE_SKUS: [],
MAKE_BUILDING_ID_NAME_MAP: True,
MAKE_ROLE_ID_NAME_MAP: True,
MAP_BUILDING_ID_TO_NAME: {},
MAP_BUILDING_NAME_TO_ID: {},
MAP_ORGUNIT_ID_TO_NAME: {},
MAP_SHAREDDRIVE_ID_TO_NAME: {},
MAP_ROLE_ID_TO_NAME: {},
MAP_ROLE_NAME_TO_ID: {},
MAP_USER_ID_TO_NAME: {},
MULTIPROCESS_EXIT_CONDITION: None,
MULTIPROCESS_EXIT_PROCESSING: False,
NUM_BATCH_ITEMS: 0,
OAUTH2SERVICE_CLIENT_ID: None,
OAUTH2SERVICE_JSON_DATA: {},
OAUTH2_CLIENT_ID: None,
OAUTH2_TXT_LOCK: None,
OUTPUT_DATEFORMAT: '',
OUTPUT_TIMEFORMAT: '',
PARSER: None,
PID: 0,
PRINT_AGU_DOMAINS: '',
PRINT_CROS_OUS: '',
PRINT_CROS_OUS_AND_CHILDREN: '',
RATE_CHECK_COUNT: 0,
RATE_CHECK_START: 0,
SECTION: None,
SHOW_GETTINGS: True,
SHOW_GETTINGS_GOT_NL: False,
SAVED_STDOUT: None,
STDERR: {},
STDOUT: {},
SVCACCT_SCOPES: {},
SVCACCT_SCOPES_DEFINED: False,
SYSEXITRC: 0,
SYS_ENCODING: 'utf-8',
TBATCH_QUEUE: None
}

View File

@@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM indent processing
"""
class GamIndent():
INDENT_SPACES_PER_LEVEL = ' '
def __init__(self):
self.indent = 0
def Reset(self):
self.indent = 0
def Increment(self):
self.indent += 1
def Decrement(self):
self.indent -= 1
def Spaces(self):
return self.INDENT_SPACES_PER_LEVEL*self.indent
def SpacesSub1(self):
return self.INDENT_SPACES_PER_LEVEL*(self.indent-1)
def MultiLineText(self, message, n=0):
return message.replace('\n', f'\n{self.INDENT_SPACES_PER_LEVEL*(self.indent+n)}').rstrip()

View File

@@ -1,569 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM messages
"""
# These values can be translated into other languages
# Project creation messages in order of appearance
CREATING_PROJECT = 'Creating project "{0}"...\n'
CHECK_INTERRUPTED = 'Check interrupted'
CHECKING_PROJECT_CREATION_STATUS = 'Checking project creation status...\n'
NO_RIGHTS_GOOGLE_CLOUD_ORGANIZATION = 'Looks like you have no rights to your Google Cloud Organization.\nAttempting to fix that...\n'
YOUR_ORGANIZATION_NAME_IS = 'Your organization name is {0}\n'
YOU_HAVE_NO_RIGHTS_TO_CREATE_PROJECTS_AND_YOU_ARE_NOT_A_SUPER_ADMIN = 'You have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.'
LOOKS_LIKE_NO_ONE_HAS_RIGHTS_TO_YOUR_GOOGLE_CLOUD_ORGANIZATION_ATTEMPTING_TO_GIVE_YOU_CREATE_RIGHTS = 'Looks like no one has rights to your Google Cloud Organization. Attempting to give you create rights...\n'
THE_FOLLOWING_RIGHTS_SEEM_TO_EXIST = 'The following rights seem to exist:\n'
GIVING_LOGIN_HINT_THE_CREATOR_ROLE = 'Giving {0} the role of {1}...\n'
ACCEPT_CLOUD_TOS = '''
Please go to:
https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project
sign in as {0} and accept the Terms of Service (ToS). As soon as you've accepted the ToS popup, you can return here and press enter.\n'''
PROJECT_STILL_BEING_CREATED_SLEEPING = 'Project still being created. Sleeping {0} seconds\n'
FAILED_TO_CREATE_PROJECT = 'Failed to create project: {0}\n'
SETTING_GAM_PROJECT_CONSENT_SCREEN_CREATING_CLIENT = 'Setting GAM project consent screen, creating client...\n'
CREATE_CLIENT_INSTRUCTIONS = '''
Please go to:
{0}
1. If "+ CREATE CLIENT" is on the screen, skip to step 14
2. Click "GET STARTED"
3. Under "App Information", enter {1} or another value in "App name *"
4. Under "App Information", enter {2} in "User support email *"
5. Click "NEXT"
6. Under "Audience", choose INTERNAL
7. Click "NEXT"
8. Under, "Contact Information", enter {2} or another value in "Email addresses *"
9. Click "NEXT"
10. Under "Finish", click "I agree to the Google API Services: User Data Policy."
11. Click "CONTINUE"
12. Click "CREATE"
13. Click "Clients" in the left-hand column
14. Click "+ CREATE CLIENT"
15. Choose "Desktop App" for "Application type"
16. Enter {1} or another value in "Name *"
17. Click "Create"
18. Under "Name", click your client name
19. Copy the "Client ID" value under "Additional information"
20. Paste it at the "Enter your Client ID: " prompt in your terminal
21. Press return/enter in your terminal
22. Switch back to the browser
23. Copy the "Client secret" value under "Client Secrets"
24. Paste it at the "Enter your Client Secret: " prompt in your terminal
25. Press return/enter in your terminal
26. Switch back to the browser
27. Click "OK"
28. These steps are complete
'''
ENTER_YOUR_CLIENT_ID = '\nEnter your Client ID: '
ENTER_YOUR_CLIENT_SECRET = '\nEnter your Client Secret: '
IS_NOT_A_VALID_CLIENT_ID = '''
{0}
Is not a valid Client ID.
Please make sure you are following the directions exactly and that there are no extra spaces in your Client ID.
'''
IS_NOT_A_VALID_CLIENT_SECRET = '''
{0}
Is not a valid Client Secret.
Please make sure you are following the directions exactly and that there are no extra spaces in your Client Secret.
'''
TRUST_GAM_CLIENT_ID = '''
It's important to mark the {0} Client ID as trusted by your Workspace instance.
Please go to:
https://admin.google.com/ac/owl/list?tab=configuredApps
1. Click on: Configure new app
2. Enter the following Client ID value in Search for app:
{1}
3. Press Search, select the {0} app, click
4. Keep the default scope or select a preferred scope that includes your GAM admin.
5. Press Continue
6. Select Trusted radio button, press Continue and Finish.
7. Press Confirm if Confirm parental consent pops up
8. Press enter here on the terminal once trust is complete.
'''
ENABLE_SERVICE_ACCOUNT_PRIVATE_KEY_UPLOAD = '''
Your workspace is configured to disable service account private key uploads.
Please go to:
https://github.com/GAM-team/GAM/wiki/Authorization#authorize-service-account-key-uploads
Follow the steps to allow a service account private key upload for the project ({0}) just created.
Once those steps are completed, you can continue with your project authentication.
'''
YOUR_GAM_PROJECT_IS_CREATED_AND_READY_TO_USE = '''
That\'s it! Your GAM Project is created and ready to use.
Proceed to the authentication steps.
'''
# check|update service messages in order of appearance
SYSTEM_TIME_STATUS = 'System time status'
YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE = 'Your system time differs from {0} by {1}'
PRESS_ENTER_ONCE_AUTHORIZATION_IS_COMPLETE = 'Press enter once authorization is complete.'
SERVICE_ACCOUNT_API_DISABLED = '{0} not enabled. Please run "gam update project" and "gam user user@domain.com update serviceaccount"'
SERVICE_ACCOUNT_PRIVATE_KEY_AUTHENTICATION = 'Service Account Private Key Authentication'
SERVICE_ACCOUNT_CHECK_PRIVATE_KEY_AGE = 'Service Account Private Key age; Google recommends rotating keys on a routine basis'
SERVICE_ACCOUNT_PRIVATE_KEY_AGE = 'Service Account Private Key age: {0} days'
SERVICE_ACCOUNT_SKIPPING_KEY_AGE_CHECK = 'Skipping Private Key age check: {0} rotation not necessary'
UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS = 'Please run "gam update project" to view/manage service account keys'
DOMAIN_WIDE_DELEGATION_AUTHENTICATION = 'Domain-wide Delegation authentication'
DEPRECATED_SCOPES = 'Deprecated scopes that GAM should NEVER have DwD access to'
SCOPE_AUTHORIZATION_PASSED = '''All scopes PASSED!
Service Account Client name: {0} is fully authorized.
'''
SCOPE_AUTHORIZATION_UPDATE_PASSED = '''All scopes PASSED!
To update authorization (in case some scopes were unselected), please go to the following link in your browser:
{0}
{1}
You will be directed to the Google Workspace admin console Security > API Controls > Domain-wide Delegation page
The "Add a new Client ID" box will open
Make sure that "Overwrite existing client ID" is checked
Click AUTHORIZE
When the box closes you're done
After authorizing it may take some time for this test to pass so wait a few moments and then try this command again.
'''
SCOPE_AUTHORIZATION_FAILED = '''Some scopes FAILED or should be DISABLED!
To update authorization, please go to the following link in your browser:
{0}
{1}
You will be directed to the Google Workspace admin console Security > API Controls > Domain-wide Delegation page
The "Add a new Client ID" box will open
Make sure that "Overwrite existing client ID" is checked
Click AUTHORIZE
When the box closes you're done
After authorizing it may take some time for this test to pass so wait a few moments and then try this command again.
'''
# General messages
ACCESS_FORBIDDEN = 'Access Forbidden'
ACTION_APPLIED = 'Action Applied'
ACTION_IN_PROGRESS = 'Action {0} in progress'
ACTION_MAY_BE_DELAYED = 'Action may be delayed'
ADMIN_STATUS_CHANGED_TO = 'Admin Status Changed to'
ALL = 'All'
ALL_FOLDER_NAMES_MUST_BE_NON_BLANK = 'All folder names must be non-blank: {0}'
ALL_SKU_PRODUCTIDS_MUST_MATCH = 'All SKU productIds must match, {0} != {1}'
ALREADY_WAS_OWNER = 'Already was owner'
ALREADY_EXISTS = 'Already exists'
ALREADY_EXISTS_IN_TARGET_FOLDER = 'Already exists in {0}: {1}'
ALREADY_EXISTS_USE_MERGE_ARGUMENT = 'Already exists; use the "merge" argument to merge the labels'
API_ACCESS_DENIED = 'API access Denied'
API_CALLS_RETRY_DATA = 'API calls retry data\n'
API_CHECK_CLIENT_AUTHORIZATION = 'Please make sure the Client ID: {0} is authorized for the appropriate API or scopes: {1}\n\nRun: gam oauth create\n'
API_CHECK_SVCACCT_AUTHORIZATION = 'Please make sure the Service Account Client ID: {0} is authorized for the appropriate API or scopes: {1}\n\nRun: gam user {2} update serviceaccount\n'
API_ERROR_SETTINGS = 'API error, some settings not set'
ARE_BOTH_REQUIRED = 'Arguments {0} and {1} are both required'
ARE_MUTUALLY_EXCLUSIVE = 'Arguments {0} and {1} are mutually exclusive'
AS = 'as'
ATTENDEES_ADD = 'Add Attendees'
ATTENDEES_ADD_REMOVE = 'Add/Remove Attendees'
ATTENDEES_REMOVE = 'Remove Attendees'
AUTHORIZATION_FILE_ALREADY_EXISTS = '{0} already exists. Please delete or rename it before attempting to {1} project.'
AUTHENTICATION_FLOW_COMPLETE = '\nThe authentication flow has completed.'
AUTHENTICATION_FLOW_COMPLETE_CLOSE_BROWSER = 'The authentication flow has completed. You may close this browser window and return to {0}.'
AUTHENTICATION_FLOW_FAILED = 'The authentication flow failed, reissue command'
BAD_ENTITIES_IN_SOURCE = '{0} {1} {2} in source marked >>> <<< above'
BAD_REQUEST = 'Bad Request'
BATCH = 'Batch'
BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE = '"gam {0} - ..." is not compatible with debugging. Disable debugging by setting debug_level = 0 in gam.cfg'
BATCH_CSV_PROCESSING_COMPLETE = '{0},0/{1},Processing complete\n'
BATCH_CSV_TERMINATE_N_PROCESSES = '{0},0/{1},Terminating {2} running {3}\n'
BATCH_CSV_WAIT_LIMIT = ', wait limit {0} seconds'
BATCH_CSV_WAIT_N_PROCESSES = '{0},0/{1},Waiting for {2} running {3} to finish before terminating{4}\n'
BATCH_NOT_PROCESSED_ERRORS = '{0}batch file: {1}, not processed, {2} {3}\n'
CALLING_GCLOUD_FOR_REAUTH = 'Calling gcloud for reauth credentials..."\n'
CAN_NOT_DELETE_USER_WITH_VAULT_HOLD = '{0}: The user may be (or have recently been) on Google Vault Hold and thus not eligible for deletion. You can check holds with "gam user {1} show vaultholds".'
CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE = 'Argument {0} can not be specified more than once'
CHAT_ADMIN_ACCESS_LIMITED_TO_ONE_USER = 'Chat adminaccess|asadmin limited to one user, {0} specified'
CHROME_TARGET_VERSION_FORMAT = r'^([a-z]+)-(\d+)$ or ^(\d{1,4}\.){1,4}$'
COLUMN_DOES_NOT_MATCH_ANY_INPUT_COLUMNS = '{0} column "{1}" does not match any input columns'
COLUMN_DOES_NOT_MATCH_ANY_OUTPUT_COLUMNS = '{0} column "{1}" does not match any output columns'
COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA = 'gam {0} {1} is not compatible with enable_dasa = true in gam.cfg'
COMMIT_BATCH_COMPLETE = '{0},0/{1},commit-batch - running {2} finished, proceeding\n'
COMMIT_BATCH_WAIT_N_PROCESSES = '{0},0/{1},commit-batch - waiting for {2} running {3} to finish before proceeding\n'
CONFIRM_WIPE_YUBIKEY_PIV = 'This will wipe all YubiKey PIV keys and configuration from your YubiKey. Are you sure? (y/N) '
CONTACT_ADMINISTRATOR_FOR_PASSWORD = 'Contact administrator for password'
CONTACT_PHOTO_NOT_FOUND = 'Contact photo not found'
CONTAINS_AT_LEAST_1_ITEM = 'Contains at least 1 item'
COUNT_N_EXCEEDS_MAX_TO_PROCESS_M = 'Count {0} exceeds maximum to {1} {2}'
CORRUPT_FILE = 'Corrupt file'
COULD_NOT_FIND_ANY_YUBIKEY = 'Could not find any YubiKey\n'
COULD_NOT_FIND_YUBIKEY_WITH_SERIAL = 'Could not find YubiKey with serial number {0}\n'
CREATE_DELEGATE_NOTIFY_MESSAGE = '#user# has granted you #delegate# access to read, delete and send mail on their behalf.'
CREATE_DELEGATE_NOTIFY_SUBJECT = '#user# mail delegation to #delegate#'
CREATE_USER_NOTIFY_MESSAGE = 'Hello #givenname# #familyname#,\n\nYou have a new account at #domain#\nAccount details:\nUsername: #user#\nPassword: #password#\nStart using your new account by signing in at\nhttps://www.google.com/accounts/AccountChooser?Email=#user#&continue=https://workspace.google.com/dashboard\n'
CREATE_USER_NOTIFY_SUBJECT = 'Welcome to #domain#'
CSV_DATA_ALREADY_SAVED = 'CSV data already saved'
CSV_FILE_HEADERS = 'The CSV file ({0}) has the following headers:\n'
CSV_SAMPLE_COMMANDS = 'Here are the first {0} commands {1} will run\n'
DATA_FIELD_MISMATCH = 'datafield {0} does not match saved datafield {1}'
DATA_TRANSFER_COMPLETED = 'Data Transfer completed: {0}\n'
DATA_UPLOADED_TO_DRIVE_FILE = 'Data uploaded to Drive File'
DEFAULT_SMIME = 'Default S/MIME'
DELETED = 'Deleted'
DEVELOPER_PREVIEW_REQUIRED = 'Developer Preview is required for this command\n'
DEVICE_LIST_BUG = 'GAM hit Google internal bug 237397223. Please file a Google Support ticket stating that you are encountering this bug.'
DEVICE_LIST_BUG_WORKAROUND_NOT_POSSIBLE = 'GAM workaround for this issue only works if orderby argument is not used and query does not contain \'register\'.'
DEVICE_LIST_BUG_ATTEMPTING_WORKAROUND = 'GAM is attempting to work around the bug by filtering for devices created on or after the newest we\'ve seen ({0})...\n'
DIRECTLY_IN_THE = ' directly in the {0}'
DISABLE_TLS_MIN_MAX = 'Execute: gam select default config tls_max_version "" tls_min_version "" save\n'
DISPLAYNAME_NOT_ALLOWED_WHEN_UPDATING_MULTIPLE_SCHEMAS = 'displayname not allowed when updating multiple schemas'
DOES_NOT_EXIST = 'Does not exist'
DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT = '{0}: {1}, Does not exist or has invalid format, {2}'
DOES_NOT_MATCH = 'Does not match {0}'
DOMAIN_NOT_FOUND_IN_DNS = 'Domain not found in DNS!'
DOMAIN_NOT_VERIFIED_SECONDARY = 'Domain is not a verified secondary domain'
DONE_GENERATING_PRIVATE_KEY_AND_PUBLIC_CERTIFICATE = 'Done generating private key and public certificate'
DO_NOT_EXIST = 'Do not exist'
DOWNLOADING_AGAIN_AND_OVER_WRITING = 'Downloading again and over-writing...'
DUPLICATE = 'Duplicate'
DUPLICATE_ALREADY_A_ROLE = 'Duplicate, already a {0}'
DYNAMIC_GROUP_MEMBERSHIP_CANNOT_BE_MODIFIED = 'Dynamic group membership cannot be modified'
EITHER = 'Either'
EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT = 'The email address is an unmanaged account'
ENABLE_PROJECT_APIS_AUTOMATICALLY_OR_MANUALLY = 'Do you want to enable project APIs [a]utomatically or [m]anually? (a/m): '
ENTER_GSUITE_ADMIN_EMAIL_ADDRESS = '\nEnter your Google Workspace admin email address? '
ENTER_MANAGE_GCP_PROJECT_EMAIL_ADDRESS = '\nEnter your Google Workspace admin or GCP project manager email address authorized to manage project(s): {0}? '
ENTER_VERIFICATION_CODE_OR_URL = 'Enter verification code or paste "Unable to connect" URL from other computer (only URL data up to &scope required): '
ENTITY_DOES_NOT_EXIST = '{0} does not exist'
ENTITY_NAME_NOT_VALID = 'Entity Name Not Valid'
ERROR = 'error'
ERRORS = 'errors'
EVENT_IS_CANCELED = 'Event is canceled'
EXECUTE_GAM_OAUTH_CREATE = '\nPlease run\n\ngam oauth delete\ngam oauth create\n\n'
EXISTS = 'Exists'
EXPECTED = 'Expected'
EXPORT_NOT_COMPLETE = 'Export needs to be complete before downloading, current status is: {0}'
EXTRACTING_PUBLIC_CERTIFICATE = 'Extracting public certificate'
FAILED_PRECONDITION = 'Failed precondition'
FAILED_TO_PARSE_AS_JSON = 'Failed to parse as JSON'
FAILED_TO_PARSE_AS_LIST = 'Failed to parse as list'
FIELD_NOT_FOUND_IN_SCHEMA = 'Field {0} not found in schema {1}'
FILE_NOT_FOUND = 'File {0} not found'
FINISHED = 'Finished'
FILTER_CAN_ONLY_CONTAIN_ONE_CATEGORY_LABEL = 'Filter can only contain one CATEGORY label'
FILTER_CAN_ONLY_CONTAIN_ONE_USER_LABEL = 'Filter can only contain one USER label'
FOR = 'for'
FORBIDDEN = 'Forbidden'
FORMAT_NOT_AVAILABLE = 'Format ({0}) not available'
FORMAT_NOT_DOWNLOADABLE = 'Format not downloadable'
FROM = 'From'
FROM_LC = 'from'
FULL_PATH_MUST_START_WITH_DRIVE = 'fullpath must start with {0} or {1}'
GAM_BATCH_FILE_WRITTEN = 'GAM batch file {0} written\n'
GAM_LATEST_VERSION_NOT_AVAILABLE = 'GAM Latest Version information not available'
GAM_OUT_OF_MEMORY = 'GAM has run out of memory. If this is a large Google Workspace instance, you should use a 64-bit version of GAM on Windows or a 64-bit version of Python on other systems.'
GENERATING_NEW_PRIVATE_KEY = 'Generating new private key'
GETTING = 'Getting'
GETTING_ALL = 'Getting all'
GRANTING_RIGHTS_TO_ROTATE_ITS_OWN_PRIVATE_KEY = '{0} rights to rotate its own private key'
GOOGLE_DELEGATION_ERROR = 'Google delegation error, delegator and delegate both exist and are valid for delegation'
GOT = 'Got'
GROUP_MAPS_TO_MULTIPLE_OUS = 'File: {0}, Group: {1} references multiple OUs: {2}'
GROUP_MAPS_TO_OU_INVALID_ROW = 'File: {0}, Invalid row, must contain non-blank <EmailAddress> and <OrgUnitPath>: <{1}> <{2}>'
GUARDIAN_INVITATION_STATUS_NOT_PENDING = 'Guardian invitation status is not PENDING'
HAS_CHILD_ORGS = 'Has child {0}'
HAS_INVALID_FORMAT = '{0}: {1}, Has invalid format'
HEADER_NOT_FOUND_IN_CSV_HEADERS = 'Header "{0}" not found in CSV headers of "{1}".'
HELP_SYNTAX = 'Help: Syntax in file {0}\n'
HELP_WIKI = 'Help: Documentation is at {0}\n'
IGNORED = 'Ignored'
INSTRUCTIONS_CLIENT_SECRETS_JSON = 'Please run\n\ngam create|use project\ngam oauth create\n\nto create and authorize a Client account.\n'
INSTRUCTIONS_OAUTH2SERVICE_JSON = 'Please run\n\ngam create|use project\ngam user <user> update serviceaccount\n\nto create and authorize a Service account.\n'
INSUFFICIENT_PERMISSIONS_TO_PERFORM_TASK = 'Insufficient permissions to perform this task'
INTER_BATCH_WAIT_INCREASED = 'inter_batch_wait increased to {0:.2f}'
INVALID = 'Invalid'
INVALID_ALIAS = 'Invalid Alias'
INVALID_ATTENDEE_CHANGE = 'Invalid attendee change "{0}"'
INVALID_CHARSET = 'Invalid charset "{0}"'
INVALID_DATE_TIME_RANGE = '{0} {1} must be greater than/equal to {2} {3}'
INVALID_DEVICE_QUERY = 'Invalid {0} query "{1}"; it must be if the form "field:value" and must not contain a "?"'
INVALID_EMOJI_NAME = '{0} does not match pattern :[0-9a-z_-]:'
INVALID_ENTITY = 'Invalid {0}, {1}'
INVALID_EVENT_TIMERANGE = '{0} {1} must be less than {2}'
INVALID_FILE_SELECTION_WITH_ADMIN_ACCESS = 'Invalid file selection with adminaccess|asadmin'
INVALID_GROUP = 'Invalid Group'
INVALID_HTTP_HEADER = 'Invalid http header data: {0}'
INVALID_JSON_INFORMATION = 'Google API reported Invalid JSON Information'
INVALID_JSON_SETTING = 'Invalid JSON setting'
INVALID_LIST = 'Invalid list'
INVALID_MEMBER = 'Invalid Member address'
INVALID_MESSAGE_ID = 'Invalid message id(s)'
INVALID_MIMETYPE = 'Invalid mimeType {0}, must be {1}'
INVALID_NUMBER_OF_CHAT_SPACE_MEMBERS = '{0} type {1} number of members, {2}, must be in range {3} to {4}'
INVALID_ORGUNIT = 'Invalid Organizational Unit'
INVALID_PATH = 'Invalid Path'
INVALID_PERMISSION_ATTRIBUTE_TYPE = 'permission attribute {0} not allowed with type {1}'
INVALID_REGION = 'See: https://github.com/GAM-team/GAM/wiki/Context-Aware-Access-Levels#caa-region-codes'
INVALID_QUERY = 'Invalid Query'
INVALID_RE = 'Invalid RE'
INVALID_REQUEST = 'Invalid Request'
INVALID_RESELLER_CUSTOMER_NAME = 'name must be: accounts/<ResellerID>/customers/<ChannelCustomerID>'
INVALID_ROLE = 'Invalid subkeyfield Role, must be one of: {0}'
INVALID_SCHEMA_VALUE = 'Invalid Schema Value'
INVALID_SCOPE = 'Invalid Scope'
INVALID_SITE = 'Invalid Site ({0}), must match pattern ({1})'
INVALID_TAG_SPECIFICATION = 'Invalid tag, expected field.subfield or field.subfield.subfield.string'
INVALID_TIMEOFDAY_RANGE = '{0} must be less than/equal to {1}'
IN_SKIPIDS = 'In skipids'
IN_THE = ' in the {0}'
IN_TRASH_AND_EXCLUDE_TRASHED = 'In Trash and excludeTrashed'
IS_EXPIRED_OR_REVOKED = '{0}: {1}, Is expired or has been revoked'
IS_NOT_DONE_CHECKING_IN_SECONDS = 'Is not done, checking again in {0} seconds'
IS_NOT_UNIQUE = 'Is not unique, {0}: {1}'
IS_REQD_TO_CHG_PWD_NO_DELEGATION = 'Is required to change password at next login. You must change password or clear changepassword flag for delegation.'
IS_SUSPENDED_NO_BACKUPCODES = 'User is suspended. You must unsuspend to process backupcodes'
IS_SUSPENDED_NO_DELEGATION = 'Is suspended. You must unsuspend for delegation.'
IS_YUBIKEY_INSERTED = 'Is YubiKey inserted?'
JSON_ERROR = 'JSON error "{0}" in file {1}'
JSON_KEY_NOT_FOUND = 'JSON key "{0}" not found in file {1}'
KIOSK_MODE_REQUIRED = ' This command ({0}) requires that the ChromeOS device be in Kiosk mode.'
LESS_THAN_1_SECOND = 'less than 1 second'
LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY = 'List ChromeOSdevices Invalid Input: pageToken retry'
LOGGING_INITIALIZATION_ERROR = 'Logging initialization error: {0}'
LOOKING_UP_GOOGLE_UNIQUE_ID = 'Looking up Google Unique ID'
MAP_PERMISSIONS_EMAIL_FILE_HEADERS_REQUIRED = '{0} <CSVFileInput> requires headers "sourceEmail" and "destinationEmail"'
MARKED_AS = 'Marked as'
MATCHED_THE_FOLLOWING = 'Matched the following'
MATTER_NOT_OPEN = 'Matter needs to be open, current state is: {0}'
MAXIMUM_OF = 'maximum of'
MEMBERSHIP_IS_PENDING_WILL_DELETE_ADD_TO_ACCEPT = 'Membership is pending, will delete and add to accept'
MIMETYPE_MISMATCH = 'Shortcut target mimeType {0} does not match actual target mimeType {1}'
MIMETYPE_NOT_PRESENT_IN_ATTACHMENT = 'MIME type not present in attachment'
MISMATCH_RE_SEARCH_REPLACE_SUBFIELDS = 'The subfield ({2}) in replace "{3}" exceeds the number of subfields ({0}) in search "{1}"'
MISMATCH_SEARCH_REPLACE_SUBFIELDS = 'The number of subfields ({0}) in search "{1}" does not match the number of subfields ({2}) in replace "{3}"'
MISSING_FIELDS = 'Missing fields: {0}\n'
MULTIPLE_BUILDINGS_SAME_NAME = '{0} {1} with the same (case-insensitive) name exist'
MULTIPLE_ENTITIES_FOUND = 'Multiple {0} ({1}) found, {2}'
MULTIPLE_ITEMS_SPECIFIED = 'Multiple {0} are specfied, only one is allowed'
MULTIPLE_ITEMS_MARKED_PRIMARY = 'Multiple {0} are marked primary, only one can be primary'
MULTIPLE_PARENTS_SPECIFIED = 'Multiple parents ({0}) specified, only one is allowed'
MULTIPLE_SEARCH_METHODS_SPECIFIED = 'Multiple search methods ({0}) specified, only one is allowed'
MULTIPLE_SSO_PROFILES_MATCH = 'Multiple SSO profiles match display name {0}:\n'
MULTIPLE_YUBIKEYS_CONNECTED = 'Multiple YubiKeys connected. Specify yubikey_serial_number and one of {0}\n'
MUST_BE_NUMERIC = 'Must be numeric'
NEED_READ_ACCESS = 'Need Read access'
NEED_READ_WRITE_ACCESS = 'Need Read/Write access'
NEED_WRITE_ACCESS = 'Need Write access'
NESTED_LOOP_CMD_NOT_ALLOWED = 'Command can not be nested.'
NEWUSER_REQUIREMENTS = 'newuser option requires: at least 1 recipient and givenname, familyname and password options'
NEW_OWNER_MUST_DIFFER_FROM_OLD_OWNER = 'New owner must differ from old owner'
NO_DATA = 'No data'
NON_BLANK = 'Non-blank'
NON_EMPTY = 'Non-empty'
NOT_A = 'Not a'
NOT_A_PRIMARY_EMAIL_ADDRESS = 'Not a primary email address'
NOT_A_MEMBER = 'Not a member'
NOT_ACTIVE = 'Not Active'
NOT_ALLOWED = 'Not Allowed'
NOT_AN_ENTITY = 'Not a {0}'
NOT_APPROPRIATE = 'Not Appropriate'
NOT_COMPATIBLE = 'Not Compatible'
NOT_COPYABLE = 'Not Copyable'
NOT_COPYABLE_INTO_ITSELF = 'Not copyable into itself'
NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_MERGE = 'Not copyable with same name into current folder with duplicatefolders merge'
NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE = 'Not copyable with same name into current folder with duplicatefiles overwriteall|overwriteolder'
NOT_DELETABLE = 'Not Deletable'
NOT_FOUND = 'Not Found'
NOT_MOVABLE = 'Not Movable'
NOT_MOVABLE_IN_TRASH = 'Not Movable, in Trash'
NOT_MOVABLE_INTO_ITSELF = 'Not movable into itself'
NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_MERGE = 'Not movable with same name into current folder with duplicatefolders merge'
NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE = 'Not movable with same name into current folder with duplicatefiles overwriteall|overwriteolder'
NOT_OWNED_BY = 'Not owned by {0}'
NOT_SELECTED = 'Not Selected'
NOT_WRITABLE = 'Not Writable'
NOW_THE_PRIMARY_DOMAIN = 'Now the primary domain'
NO_ACTION_SPECIFIED = 'No action specified'
NO_AVAILABLE_LICENSES = "There aren't enough available licenses for the specified product-SKU pair(s)"
NO_CHANGES = 'No changes'
NO_CLIENT_ACCESS_ALLOWED = 'No Client Access allowed'
NO_CLIENT_ACCESS_CREATE_UPDATE_ALLOWED = 'No Client Access create/update allowed'
NO_COLUMNS_SELECTED_WITH_CSV_OUTPUT_HEADER_FILTER = 'No columns selected with {0} and {1}'
NO_CREDENTIALS_REPLACEMENT = '{0}: {1} has {2} {3}. We only replace if there are 2.\n'
NO_CSV_DATA_TO_UPLOAD = 'No CSV data to upload'
NO_CSV_FILE_DATA_FOUND = 'No CSV file data found'
NO_CSV_FILE_DATA_SAVED = 'No CSV file data saved'
NO_CSV_FILE_SUBKEYS_SAVED = 'No CSV file subkeys saved'
NO_DATA_TRANSFER_APP_FOR_PARAMETER = 'No data transfer application for key {0}'
NO_ENTITIES_FOUND = 'No {0} found'
NO_ENTITIES_MATCHED = 'No {0} matched'
NO_FILTER_ACTIONS = 'No {0} actions specified'
NO_FILTER_CRITERIA = 'No {0} criteria specified'
NO_LABELS_MATCH = 'No Labels match'
NO_LABELS_TO_PROCESS = 'No Labels to process'
NO_MESSAGES_WITH_LABEL = 'No Messages with Label'
NO_PARENTS_TO_CONVERT_TO_SHORTCUTS = 'No parents to convert to shortcuts'
NO_REPORT_AVAILABLE = 'No {0} report available.'
NO_SCOPES_FOR_API = 'There are no scopes authorized for the API(s): {0}'
NO_SERIAL_NUMBERS_SPECIFIED = 'No serial numbers specified'
NO_SSO_PROFILE_MATCHES = 'No SSO profile matches display name {0}'
NO_SSO_PROFILE_ASSIGNED = 'No SSO profile assigned to {0} {1}'
NO_SVCACCT_ACCESS_ALLOWED = 'No Service Account Access allowed'
NO_TRANSFER_LACK_OF_DISK_SPACE = 'Transfer not performed due to lack of target drive space.'
NO_USAGE_PARAMETERS_DATA_AVAILABLE = 'No usage parameters data available.'
NO_USER_COUNTS_DATA_AVAILABLE = 'No User counts data available.'
NUM_SELECTED_CLIENT_SCOPES = '\n{0} scopes are selected, if more than {1} scopes are selected, Google will probably generate a "Something went wrong" error\n'
OAUTH2_GO_TO_LINK_MESSAGE = """
Go to the following link in a browser on this computer or on another computer:
{url}
If you use a browser on another computer, you will get a browser error that the site can't be reached AFTER you
click the Allow button, paste "Unable to connect" URL from other computer (only URL data up to &scope required):
"""
ON_CURRENT_PRIVATE_KEY = ' on current key'
ON_VAULT_HOLD = 'On Google Vault Hold'
ONLY_ADMINISTRATORS_CAN_PERFORM_SHARED_DRIVE_QUERIES = 'Only administrators can perform Shared Drive queries'
ONLY_ADMINISTRATORS_CAN_SPECIFY_SHARED_DRIVE_ORGUNIT = 'Only administrators can specify Shared Drive Org Unit'
ONLY_ONE_DEVICE_SELECTION_ALLOWED = 'Only one device selection allowed, filter = "{0}"'
ONLY_ONE_JSON_RANGE_ALLOWED = 'Only one range/json allowed'
ONLY_ONE_OWNER_ALLOWED = 'Only one owner allowed'
OR = 'or'
OU_AND_MOVETOOU_CANNOT_BE_IDENTICAL = 'ou {0} can not be be identical to movetoou {1}'
OU_SUBOUS_CANNOT_BE_MOVED_TO_MOVETOOU = 'ou {0} sub OUs can not be be moved to movetoou {1}'
PERMISSION_DENIED = 'The caller does not have permission'
PLEASE_CORRECT_YOUR_SYSTEM_TIME = 'Please correct your system time.'
PLEASE_ENTER_A_OR_M = 'Please enter a or m ...\n'
PLEASE_SELECT_ENTITY_TO_PROCESS = '{0} {1} found, please select the correct one to {2} and specify with {3}'
PLEASE_SPECIFY_BUILDING_EXACT_CASE_NAME_OR_ID = 'Please specify building by exact case name or ID.'
PREVIEW_ONLY = 'Preview Only'
PRIMARY_EMAIL_DID_NOT_MATCH_PATTERN = 'primaryEmail address did not match pattern: {0}'
PROCESS = 'process'
PROCESSES = 'processes'
PROCESSING_ITEM_N = '{0},0,Processing item {1}\n'
PROCESSING_ITEM_N_OF_M = '{0},0,Processing item {1}/{2}\n'
PROFILE_PHOTO_NOT_FOUND = 'Profile photo not found'
PROFILE_PHOTO_IS_DEFAULT = 'Profile photo is default'
QUOTA_EXCEEDED = 'Quota exceeded'
REASON_ONLY_VALID_WITH_CONTENTRESTRICTIONS_READONLY_TRUE = 'reason only valid with contentrestrictions readonly true'
REAUTHENTICATION_IS_NEEDED = 'Reauthentication is needed, please run\n\ngam oauth create'
RECOMMEND_RUNNING_GAM_ROTATE_SAKEY = 'Recommend running "gam rotate sakey" to get a new key\n'
REFUSING_TO_DEPROVISION_DEVICES = 'Refusing to deprovision {0} devices because acknowledge_device_touch_requirement not specified.\nDeprovisioning a device means the device will have to be physically wiped and re-enrolled to be managed by your domain again.\nThis requires physical access to the device and is very time consuming to perform for each device.\nPlease add "acknowledge_device_touch_requirement" to the GAM command if you understand this and wish to proceed with the deprovision.\nPlease also be aware that deprovisioning can have an effect on your device license count.\nSee https://support.google.com/chrome/a/answer/3523633 for full details.'
REFUSING_TO_DEPROVISION_N_DEVICES = 'Refusing to deprovision {0} devices due to maxtodepov {1}.\nSpecify "maxtodeprov 0" to deprovision all {0} devices'
REPLY_TO_CUSTOM_REQUIRES_EMAIL_ADDRESS = 'replyto REPLY_TO_CUSTOM requires customReplyTo <EmailAddress>'
REQUEST_COMPLETED_NO_FILES = 'Request completed but no results/files were returned, try requesting again'
REQUEST_NOT_COMPLETE = 'Request needs to be completed before downloading, current status is: {0}'
RERUN_THE_COMMAND_AND_SPECIFY_A_NEW_SANAME = """
Re-run the command specify a new service account name with: saname <ServiceAccountName>
See: https://github.com/GAM-team/GAM/wiki/Authorization#advanced-use
"""
RESOURCE_CAPACITY_FLOOR_REQUIRED = 'Options "capacity <Number>" (<Number> > 0) and "floor <String>" required'
RESOURCE_FLOOR_REQUIRED = 'Option "floor <String>" required'
RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET = 'Results are too large for Google Spreadsheets. Uploading as a regular CSV file.'
RETRIES_EXHAUSTED = 'Retries {0} exhausted'
RETRYING_GOOGLE_SHEET_EXPORT_SLEEPING = 'Retrying Google Sheet export {0}/{1}. Sleeping {2} seconds\n'
ROLE_MUST_BE_ORGANIZER = 'Role must be organizer'
ROLE_NOT_IN_SET = 'Role not in set: {0})'
SCHEMA_WOULD_HAVE_NO_FIELDS = '{0} would have no {1}'
SELECTED = 'Selected'
SERVICE_NOT_APPLICABLE = 'Service not applicable/Does not exist'
SERVICE_NOT_APPLICABLE_THIS_ADDRESS = 'Service not applicable for this address: {0}'
SERVICE_NOT_ENABLED = '{0} Service/App not enabled'
SHORTCUT_TARGET_CAPABILITY_IS_FALSE = '{0} capability {1} is False'
SITES_COMMAND_DEPRECATED = 'The Classic Sites API is deprecated, this command will not work:\n{0}'
SKU_HAS_NO_MATCHING_ARCHIVED_USER_SKU = 'SKU {0} has no matching Archived User SKU'
STARTING_THREAD = 'Starting thread'
STATISTICS_COPY_FILE = 'Total: {0}, Copied: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Copy Failed: {5}, Not copyable: {6}, In skipids: {7}, Permissions Failed: {8}, Protected Ranges Failed: {9}'
STATISTICS_COPY_FOLDER = 'Total: {0}, Copied: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Merged: {5}, Copy Failed: {6}, Not writable: {7}, Permissions Failed: {8}'
STATISTICS_MOVE_FILE = 'Total: {0}, Moved: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Move Failed: {5}, Not movable: {6}'
STATISTICS_MOVE_FOLDER = 'Total: {0}, Moved: {1}, Shortcut created {2}, Shortcut exists {3}, Duplicate: {4}, Merged: {5}, Move Failed: {6}, Not writable: {7}'
STATISTICS_USER_NOT_ORGANIZER = 'User not organizer: {0}'
STRING_LENGTH = 'string length'
STUDENT_NOT_IN_COURSE = 'Student not in course'
SUBKEY_FIELD_MISMATCH = 'subkeyfield {0} does not match saved subkeyfield {1}'
SUBSCRIPTION_NOT_FOUND = 'Could not find subscription'
SUFFIX_NOT_ALLOWED_WITH_CUSTOMLANGUAGE = 'Suffix {0} not allowed with customLanguage {1}'
TASKLIST_TITLE_NOT_FOUND = 'Task list title not found'
THREAD = 'thread'
THREADS = 'threads'
TO = 'To'
TO_LC = 'to'
TO_MAXIMUM_OF = 'to maximum of'
TO_SET_UP_GOOGLE_CHAT = """
To set up Google Chat for your current project, please go to:
{0}
and follow the instructions at:
https://github.com/GAM-team/GAM/wiki/Chat-Bot-Setup-Use#set-up-a-chat-bot
You'll use projects/{1}/topics/no-topic in Connection settings Cloud Pub/Sub Topic Name
"""
TOTAL_ITEMS_IN_ENTITY = 'Total {0} in {1}'
TRIMMED_MESSAGE_FROM_LENGTH_TO_MAXIMUM = 'Trimmed message of length {0} to maximum length {1}'
UNABLE_TO_GET_PERMISSION_ID = 'Unable to get Permission ID for <{0}>'
UNABLE_TO_CREATE_NOT_FOUND_USER = 'Unable to create not found user, some required field (givenName, familyName, password/notfoundpassword) not present'
UNAVAILABLE = 'Unavailable'
UNKNOWN = 'Unknown'
UNKNOWN_API_OR_VERSION = 'Unknown Google API or version: ({0}), contact {1}'
UNRECOVERABLE_ERROR = 'Unrecoverable error'
UPDATE_ATTENDEE_CHANGES = 'Update attendee changes'
UPDATE_GAM_TO_64BIT = "You're running a 32-bit version of GAM on a 64-bit version of Windows, upgrade to a windows-x86_64 version of GAM"
UPDATE_PRIMARY_EMAIL_PREVIEW = 'updateprimaryemail preview: {0}'
UPDATE_USER_PASSWORD_CHANGE_NOTIFY_MESSAGE = 'The account password for #givenname# #familyname#, #user# has been changed to: #password#\n'
UPDATE_USER_PASSWORD_CHANGE_NOTIFY_SUBJECT = 'Account #user# password has been changed'
UPLOAD_CSV_FILE_INTERNAL_ERROR = 'Google reported "{0}" but the file was probably uploaded, check that it has {1} rows'
UPLOADING_NEW_PUBLIC_CERTIFICATE_TO_GOOGLE = 'Uploading new public certificate to Google...\n'
URL_ERROR = 'URL error: {0}'
USE_MIMETYPE_TO_SPECIFY_GOOGLE_FORMAT = 'Use "mimetype <MimeType>" to specify Google file format\n'
USED = 'Used'
USER_BELONGS_TO_N_GROUPS_THAT_MAP_TO_ORGUNITS = 'User belongs to {0} groups ({1}) that map to OUs'
USER_CANCELLED = 'User cancelled'
USER_HAS_MULTIPLE_DIRECT_OR_INHERITED_MEMBERSHIPS_IN_GROUP = 'User has multiple direct or inherited memberships in group'
USER_IN_OTHER_DOMAIN = '{0}: {1} in other domain.'
USER_IS_NOT_ORGANIZER = 'User is not organizer, use anyorganizer option to override'
USER_NOT_IN_MATCHUSERS = 'User not in matchusers'
USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT = 'user substitutions not allowed in replace <Tag> <String>'
USE_DOIT_ARGUMENT_TO_PERFORM_ACTION = 'Use the "doit" argument to perform action'
USING_N_PROCESSES = '{0},0/{1},Using {2} {3}...\n'
VALUES_ARE_NOT_CONSISTENT = 'Values are not consistent'
VERSION_UPDATE_AVAILABLE = 'Version update available'
WAITING_FOR_DATA_TRANSFER_TO_COMPLETE_SLEEPING = 'Waiting for Data Transfer to complete. Sleeping {0} seconds\n'
WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING = 'Waiting for {0} creation to complete. Sleeping {1} seconds\n'
WHAT_IS_YOUR_PROJECT_ID = '\nWhat is your project ID? '
WILL_RERUN_WITH_NO_BROWSER_TRUE = 'Will re-run command with no_browser true\n'
WITH = 'with'
WOULD_MAKE_MEMBERSHIP_CYCLE = 'Would make membership cycle'
WRITER_ACCESS_REQUIRED_TO_BOTH_CALENDARS = 'Writer access required to both calendars'
WROTE_PRIVATE_KEY_DATA = 'Wrote private key data to {0}\n'
WROTE_PUBLIC_CERTIFICATE = 'Wrote public certificate to {0}\n'
YOU_CAN_ADD_DOMAIN_TO_ACCOUNT = 'You can now add: {0} or its subdomains as secondary or domain aliases of the Google Workspace Account: {1}'
YUBIKEY_GENERATING_NONEXPORTABLE_PRIVATE_KEY = 'YubiKey is generating a non-exportable private key...\n'
YUBIKEY_PIN_SET_TO = 'YubiKey PIN set to: {0}\n'

View File

@@ -1,262 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Google SKUs
"""
# Products/SKUs
_PRODUCTS = {
'101001': 'Cloud Identity Free',
'101005': 'Cloud Identity Premium',
'101031': 'Google Workspace for Education',
'101033': 'Google Voice',
'101034': 'Google Workspace Archived User',
'101035': 'Cloud Search',
'101036': 'Google Meet Global Dialing',
'101037': 'Google Workspace for Education',
'101038': 'AppSheet',
'101039': 'Assured Controls',
'101040': 'Chrome Enterprise',
'101043': 'Google Workspace Additional Storage',
'101047': 'Gemini',
'101049': 'Education Endpoint Management',
'101050': 'Colab',
'Google-Apps': 'Google Workspace',
'Google-Chrome-Device-Management': 'Google Chrome Device Management',
'Google-Drive-storage': 'Google Drive Storage',
'Google-Vault': 'Google Vault',
}
_SKUS = {
'1010010001': {
'product': '101001', 'aliases': ['identity', 'cloudidentity', 'cloudidentityfree'], 'displayName': 'Cloud Identity Free'},
'1010050001': {
'product': '101005', 'aliases': ['identitypremium', 'cloudidentitypremium'], 'displayName': 'Cloud Identity Premium'},
'1010070001': {
'product': 'Google-Apps', 'aliases': ['gwef', 'workspaceeducationfundamentals'], 'displayName': 'Google Workspace for Education Fundamentals'},
'1010070004': {
'product': 'Google-Apps', 'aliases': ['gwegmo', 'workspaceeducationgmailonly'], 'displayName': 'Google Workspace for Education Gmail Only'},
'1010310002': {
'product': '101031', 'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'], 'displayName': 'Google Workspace for Education Plus - Legacy'},
'1010310003': {
'product': '101031', 'aliases': ['gsefes', 'e4es', 'gsuiteenterpriseeducationstudent'], 'displayName': 'Google Workspace for Education Plus - Legacy (Student)'},
'1010310005': {
'product': '101031', 'aliases': ['gwes', 'workspaceeducationstandard'], 'displayName': 'Google Workspace for Education Standard'},
'1010310006': {
'product': '101031', 'aliases': ['gwesstaff', 'workspaceeducationstandardstaff'], 'displayName': 'Google Workspace for Education Standard (Staff)'},
'1010310007': {
'product': '101031', 'aliases': ['gwesstudent', 'workspaceeducationstandardstudent'], 'displayName': 'Google Workspace for Education Standard (Extra Student)'},
'1010310008': {
'product': '101031', 'aliases': ['gwep', 'workspaceeducationplus'], 'displayName': 'Google Workspace for Education Plus'},
'1010310009': {
'product': '101031', 'aliases': ['gwepstaff', 'workspaceeducationplusstaff'], 'displayName': 'Google Workspace for Education Plus (Staff)'},
'1010310010': {
'product': '101031', 'aliases': ['gwepstudent', 'workspaceeducationplusstudent'], 'displayName': 'Google Workspace for Education Plus (Extra Student)'},
'1010330002': {
'product': '101033', 'aliases': ['gvpremier', 'voicepremier', 'googlevoicepremier'], 'displayName': 'Google Voice Premier'},
'1010330003': {
'product': '101033', 'aliases': ['gvstarter', 'voicestarter', 'googlevoicestarter'], 'displayName': 'Google Voice Starter'},
'1010330004': {
'product': '101033', 'aliases': ['gvstandard', 'voicestandard', 'googlevoicestandard'], 'displayName': 'Google Voice Standard'},
'1010350001': {
'product': '101035', 'aliases': ['cloudsearch'], 'displayName': 'Cloud Search'},
'1010360001': {
'product': '101036', 'aliases': ['meetdialing','googlemeetglobaldialing'], 'displayName': 'Google Meet Global Dialing'},
'1010370001': {
'product': '101037', 'aliases': ['gwetlu', 'workspaceeducationupgrade'], 'displayName': 'Google Workspace for Education: Teaching and Learning Upgrade'},
'1010380001': {
'product': '101038', 'aliases': ['appsheetcore'], 'displayName': 'AppSheet Core'},
'1010380002': {
'product': '101038', 'aliases': ['appsheetstandard', 'appsheetenterprisestandard'], 'displayName': 'AppSheet Enterprise Standard'},
'1010380003': {
'product': '101038', 'aliases': ['appsheetplus', 'appsheetenterpriseplus'], 'displayName': 'AppSheet Enterprise Plus'},
'1010390001': {
'product': '101039', 'aliases': ['assuredcontrols'], 'displayName': 'Assured Controls'},
'1010390002': {
'product': '101039', 'aliases': ['assuredcontrolsplus'], 'displayName': 'Assured Controls Plus'},
'1010400001': {
'product': '101040', 'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce', 'cep', 'chromeenterprisepremium'], 'displayName': 'Chrome Enterprise Premium'},
'1010430001': {
'product': '101043', 'aliases': ['gwas', 'plusstorage'], 'displayName': 'Google Workspace Additional Storage'},
'1010470001': {
'product': '101047', 'aliases': ['geminient', 'duetai'], 'displayName': 'Gemini Enterprise - Legacy'},
'1010470002': {
'product': '101047', 'aliases': ['gwlabs', 'workspacelabs'], 'displayName': 'Google Workspace Labs'},
'1010470003': {
'product': '101047', 'aliases': ['geminibiz'], 'displayName': 'Gemini Business'},
'1010470004': {
'product': '101047', 'aliases': ['gaiproedu', 'geminiedu'], 'displayName': 'Google AI Pro for Education'},
'1010470005': {
'product': '101047', 'aliases': ['geminiedupremium'], 'displayName': 'Gemini Education Premium'},
'1010470006': {
'product': '101047', 'aliases': ['aisecurity'], 'displayName': 'AI Security'},
'1010470007': {
'product': '101047', 'aliases': ['aimeetingsandmessaging'], 'displayName': 'AI Meetings and Messaging'},
'1010470008': {
'product': '101047', 'aliases': ['geminiultra'], 'displayName': 'Google AI Ultra for Business'},
'1010470009': {
'product': '101047', 'aliases': ['aiexpandedaccess'], 'displayName': 'AI Expanded Access'},
'1010490001': {
'product': '101049', 'aliases': ['eeu'], 'displayName': 'Endpoint Education Upgrade'},
'1010500001': {
'product': '101050', 'aliases': ['colabpro'], 'displayName': 'Colab Pro'},
'1010500002': {
'product': '101050', 'aliases': ['colabpro+', 'colabproplus'], 'displayName': 'Colab Pro+'},
'Google-Apps': {
'product': 'Google-Apps', 'aliases': ['standard', 'free'], 'displayName': 'G Suite Legacy'},
'Google-Apps-For-Business': {
'product': 'Google-Apps', 'aliases': ['gafb', 'gafw', 'basic', 'gsuitebasic'], 'displayName': 'G Suite Basic'},
'Google-Apps-For-Education': {
'product': 'Google-Apps', 'aliases': ['gafe', 'gsuiteeducation', 'gsuiteedu'], 'displayName': 'Google Workspace for Education - Fundamentals'},
'Google-Apps-For-Government': {
'product': 'Google-Apps', 'aliases': ['gafg', 'gsuitegovernment', 'gsuitegov'], 'displayName': 'Google Workspace Government'},
'Google-Apps-For-Postini': {
'product': 'Google-Apps', 'aliases': ['gams', 'postini', 'gsuitegams', 'gsuitepostini', 'gsuitemessagesecurity'], 'displayName': 'Google Apps Message Security'},
'Google-Apps-Lite': {
'product': 'Google-Apps', 'aliases': ['gal', 'gsl', 'lite', 'gsuitelite'], 'displayName': 'G Suite Lite'},
'Google-Apps-Unlimited': {
'product': 'Google-Apps', 'aliases': ['gau', 'gsb', 'unlimited', 'gsuitebusiness'], 'displayName': 'G Suite Business'},
'1010020020': {
'product': 'Google-Apps', 'aliases': ['gae', 'gse', 'enterprise', 'gsuiteenterprise',
'wsentplus', 'workspaceenterpriseplus'], 'displayName': 'Google Workspace Enterprise Plus (formerly G Suite Enterprise)'},
'1010020025': {
'product': 'Google-Apps', 'aliases': ['wsbizplus', 'workspacebusinessplus'], 'displayName': 'Google Workspace Business Plus'},
'1010020026': {
'product': 'Google-Apps', 'aliases': ['wsentstan', 'workspaceenterprisestandard'], 'displayName': 'Google Workspace Enterprise Standard'},
'1010020027': {
'product': 'Google-Apps', 'aliases': ['wsbizstart', 'wsbizstarter', 'workspacebusinessstarter'], 'displayName': 'Google Workspace Business Starter'},
'1010020028': {
'product': 'Google-Apps', 'aliases': ['wsbizstan', 'workspacebusinessstandard'], 'displayName': 'Google Workspace Business Standard'},
'1010020029': {
'product': 'Google-Apps', 'aliases': ['wes', 'wsentstarter', 'workspaceenterprisestarter'], 'displayName': 'Workspace Enterprise Starter'},
'1010020030': {
'product': 'Google-Apps', 'aliases': ['wsflw', 'workspacefrontline', 'workspacefrontlineworker'], 'displayName': 'Google Workspace Frontline Starter'},
'1010020031': {
'product': 'Google-Apps', 'aliases': ['wsflwstan', 'workspacefrontlinestan', 'workspacefrontlineworkerstan'], 'displayName': 'Google Workspace Frontline Standard'},
'1010020034': {
'product': 'Google-Apps', 'aliases': ['wsflwplus', 'workspacefrontlineplus', 'workspacefrontlineworkerplus'], 'displayName': 'Google Workspace Frontline Plus'},
'1010340001': {
'product': '101034', 'aliases': ['gseau', 'enterprisearchived', 'gsuiteenterprisearchived'], 'displayName': 'Google Workspace Enterprise Plus - Archived User'},
'1010340002': {
'product': '101034', 'aliases': ['gsbau', 'businessarchived', 'gsuitebusinessarchived'], 'displayName': 'Google Workspace Business - Archived User'},
'1010340003': {
'product': '101034', 'aliases': ['wsbizplusarchived', 'workspacebusinessplusarchived'], 'displayName': 'Google Workspace Business Plus - Archived User'},
'1010340004': {
'product': '101034', 'aliases': ['wsentstanarchived', 'workspaceenterprisestandardarchived'], 'displayName': 'Google Workspace Enterprise Standard - Archived User'},
'1010340005': {
'product': '101034', 'aliases': ['wsbizstarterarchived', 'workspacebusinessstarterarchived'], 'displayName': 'Google Workspace Business Starter - Archived User'},
'1010340006': {
'product': '101034', 'aliases': ['wsbizstanarchived', 'workspacebusinessstanarchived'], 'displayName': 'Google Workspace Business Standard - Archived User'},
'1010340007': {
'product': '101034', 'aliases': ['gwefau', 'gwefarchived', 'workspaceeducationfundamentalsarchived'], 'displayName': 'Google Workspace for Education Fundamentals - Archived User'},
'1010060001': {
'product': '101006', 'aliases': ['gsuiteessentials', 'essentials',
'd4e', 'driveenterprise', 'drive4enterprise',
'wsess', 'workspaceesentials'], 'displayName': 'Google Workspace Essentials (formerly G Suite Essentials)'},
'1010060003': {
'product': 'Google-Apps', 'aliases': ['wsentess', 'workspaceenterpriseessentials'], 'displayName': 'Google Workspace Enterprise Essentials'},
'1010060005': {
'product': 'Google-Apps', 'aliases': ['wsessplus', 'workspaceessentialsplus'], 'displayName': 'Google Workspace Enterprise Essentials Plus'},
'Google-Drive-storage-20GB': {
'product': 'Google-Drive-storage', 'aliases': ['drive20gb', '20gb', 'googledrivestorage20gb'], 'displayName': 'Google Drive Storage 20GB'},
'Google-Drive-storage-50GB': {
'product': 'Google-Drive-storage', 'aliases': ['drive50gb', '50gb', 'googledrivestorage50gb'], 'displayName': 'Google Drive Storage 50GB'},
'Google-Drive-storage-200GB': {
'product': 'Google-Drive-storage', 'aliases': ['drive200gb', '200gb', 'googledrivestorage200gb'], 'displayName': 'Google Drive Storage 200GB'},
'Google-Drive-storage-400GB': {
'product': 'Google-Drive-storage', 'aliases': ['drive400gb', '400gb', 'googledrivestorage400gb'], 'displayName': 'Google Drive Storage 400GB'},
'Google-Drive-storage-1TB': {
'product': 'Google-Drive-storage', 'aliases': ['drive1tb', '1tb', 'googledrivestorage1tb'], 'displayName': 'Google Drive Storage 1TB'},
'Google-Drive-storage-2TB': {
'product': 'Google-Drive-storage', 'aliases': ['drive2tb', '2tb', 'googledrivestorage2tb'], 'displayName': 'Google Drive Storage 2TB'},
'Google-Drive-storage-4TB': {
'product': 'Google-Drive-storage', 'aliases': ['drive4tb', '4tb', 'googledrivestorage4tb'], 'displayName': 'Google Drive Storage 4TB'},
'Google-Drive-storage-8TB': {
'product': 'Google-Drive-storage', 'aliases': ['drive8tb', '8tb', 'googledrivestorage8tb'], 'displayName': 'Google Drive Storage 8TB'},
'Google-Drive-storage-16TB': {
'product': 'Google-Drive-storage', 'aliases': ['drive16tb', '16tb', 'googledrivestorage16tb'], 'displayName': 'Google Drive Storage 16TB'},
'Google-Vault': {
'product': 'Google-Vault', 'aliases': ['vault', 'googlevault'], 'displayName': 'Google Vault'},
'Google-Vault-Former-Employee': {
'product': 'Google-Vault', 'aliases': ['vfe', 'googlevaultformeremployee'], 'displayName': 'Google Vault - Former Employee'},
'Google-Chrome-Device-Management': {
'product': 'Google-Chrome-Device-Management', 'aliases': ['chrome', 'cdm', 'googlechromedevicemanagement'], 'displayName': 'Google Chrome Device Management'}
}
ARCHIVABLE_SKUS = {'1010020020', '1010020025', '1010020026', '1010020027', '1010020028', 'Google-Apps-Unlimited'}
def getProductAndSKU(sku):
l_sku = sku.lower().replace('-', '').replace(' ', '').replace('"', '').replace("'", '').strip()
if l_sku.startswith('nv:'):
if ':' in sku[3:]:
return sku[3:].split(':', 1)
return (None, sku)
for a_sku, sku_values in list(_SKUS.items()):
if ((l_sku == a_sku.lower().replace('-', '')) or
(l_sku in sku_values['aliases']) or
(l_sku == sku_values['displayName'].lower().replace(' ', ''))):
return (sku_values['product'], a_sku)
return (None, sku)
def productIdToDisplayName(productId):
return _PRODUCTS.get(productId, productId)
def formatProductIdDisplayName(productId):
productIdDisplay = productIdToDisplayName(productId)
if productId == productIdDisplay:
return productId
return f'{productId} ({productIdDisplay})'
def normalizeProductId(product):
l_product = product.lower().replace('-', '').replace(' ', '').strip()
if l_product.startswith('nv:'):
return (True, product[3:])
for a_sku, sku_values in list(_SKUS.items()):
if ((l_product == sku_values['product'].lower().replace('-', '')) or
(l_product == a_sku.lower().replace('-', '')) or
(l_product in sku_values['aliases']) or
(l_product == sku_values['displayName'].lower().replace(' ', ''))):
return (True, sku_values['product'])
return (False, product)
def getSortedProductList():
return sorted(_PRODUCTS)
def skuIdToDisplayName(skuId):
return _SKUS[skuId]['displayName'] if skuId in _SKUS else skuId
def formatSKUIdDisplayName(skuId):
skuIdDisplay = skuIdToDisplayName(skuId)
if skuId == skuIdDisplay:
return skuId
return f'{skuId} ({skuIdDisplay})'
def getSortedSKUList():
return sorted(_SKUS)
def convertProductListToSKUList(productList):
skuList = []
for productId in productList:
skuList += [(productId, skuId) for skuId in _SKUS if _SKUS[skuId]['product'] == productId]
return skuList
def getAllSKUs():
return convertProductListToSKUList(sorted(_PRODUCTS))
def getGSuiteSKUs():
return convertProductListToSKUList(['Google-Apps', '101031'])

View File

@@ -1,287 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM user properties
"""
# notes
# a|b|c
# getKeywordAttribute(CUSTOM_TYPE_NOCUSTOM, attrdict)
#CUSTOM_TYPE_NOCUSTOM = {
# PTKW_CL_TYPE_KEYWORD: 'type',
# PTKW_CL_CUSTOM_KEYWORD: None,
# PTKW_ATTR_TYPE_KEYWORD: 'type',
# PTKW_ATTR_TYPE_CUSTOM_VALUE: None,
# PTKW_ATTR_CUSTOMTYPE_KEYWORD: None,
# PTKW_KEYWORD_LIST: ['a', 'b', 'c']
# }
# addresses, ims
# type a|b|c|([custom] <String>)
# getChoice([CUSTOM_TYPE_CUSTOM[PTKW_CL_TYPE_KEYWORD]])
# getKeywordAttribute(CUSTOM_TYPE_CUSTOM, attrdict)
# emails, externalids, relations, websites
# [type] a|b|c|([custom] <String>)
# getChoice([CUSTOM_TYPE_CUSTOM[PTKW_CL_TYPE_KEYWORD]], defaultChoice=None)
# getKeywordAttribute(CUSTOM_TYPE_IMPLICIT, attrdict)
# locations, phones
# type a|b|c|([custom] <String>)
# if argument == CUSTOM_TYPE_CUSTOM[PTKW_CL_TYPE_KEYWORD]:
# getKeywordAttribute(CUSTOM_TYPE_CUSTOM, attrdict)
#CUSTOM_TYPE_CUSTOM = {
# PTKW_CL_TYPE_KEYWORD: 'type',
# PTKW_CL_CUSTOM_KEYWORD: 'custom',
# PTKW_ATTR_TYPE_KEYWORD: 'type',
# PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom',
# PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
# PTKW_KEYWORD_LIST: ['custom', 'a', 'b', 'c']
# }
# organizations
# (type a|b|c|([custom] <String>)) | (custom_type <String>)
# if argument == CUSTOM_TYPE_DIFFERENT_KEYWORD[PTKW_CL_TYPE_KEYWORD]:
# getKeywordAttribute(CUSTOM_TYPE_DIFFERENT_KEYWORD, attrdict)
# elif argument == CUSTOM_TYPE_DIFFERENT_KEYWORD[PTKW_CL_CUSTOM_KEYWORD]:
# attrdict[CUSTOM_TYPE_DIFFERENT_KEYWORD[PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = getValue()
#CUSTOM_TYPE_DIFFERENT_KEYWORD = {
# PTKW_CL_TYPE_KEYWORD: 'type',
# PTKW_CL_CUSTOM_KEYWORD: 'custom',
# PTKW_CL_CUSTOMTYPE_KEYWORD: 'custom_type',
# PTKW_ATTR_TYPE_KEYWORD: 'type',
# PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom',
# PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
# PTKW_KEYWORD_LIST: ['custom', 'a', 'b', 'c']
# }
# Keys into USER_PROPERTIES
CLASS = 'clas'
TITLE = 'titl'
TYPE_KEYWORDS = 'tykw'
PTKW_CL_TYPE_KEYWORD = 'ctkw'
PTKW_CL_CUSTOM_KEYWORD = 'ccuk'
PTKW_CL_CUSTOMTYPE_KEYWORD = 'cctk'
PTKW_ATTR_TYPE_KEYWORD = 'atkw'
PTKW_ATTR_TYPE_CUSTOM_VALUE = 'atcv'
PTKW_ATTR_CUSTOMTYPE_KEYWORD = 'actk'
PTKW_KEYWORD_LIST = 'kwli'
#
PC_ADDRESSES = 'addr'
PC_ALIASES = 'alia'
PC_ARRAY = 'arry'
PC_BOOLEAN = 'bool'
PC_EMAILS = 'emai'
PC_GENDER = 'gndr'
PC_IMS = 'ims '
PC_LANGUAGES = 'lang'
PC_LOCATIONS = 'loca'
PC_NAME = 'name'
PC_NOTES = 'note'
PC_ORGANIZATIONS = 'orga'
PC_POSIX = 'posi'
PC_SCHEMAS = 'schm'
PC_SSH = 'ssh '
PC_STRING = 'stri'
PC_TIME = 'time'
PROPERTIES = {
'primaryEmail':
{CLASS: PC_STRING, TITLE: 'User',},
'name':
{CLASS: PC_NAME, TITLE: 'Name',},
'givenName':
{CLASS: PC_STRING, TITLE: 'First Name',},
'familyName':
{CLASS: PC_STRING, TITLE: 'Last Name',},
'fullName':
{CLASS: PC_STRING, TITLE: 'Full Name',},
'displayName':
{CLASS: PC_STRING, TITLE: 'Display Name',},
'primaryGuestEmail':
{CLASS: PC_STRING, TITLE: 'Primary Guest Email',},
'languages':
{CLASS: PC_LANGUAGES, TITLE: 'Languages',},
'languageCode':
{CLASS: PC_LANGUAGES, TITLE: 'Languages',},
'customLanguage':
{CLASS: PC_LANGUAGES, TITLE: 'Custom Languages',},
'password':
{CLASS: PC_STRING, TITLE: 'Password',},
'hashFunction':
{CLASS: PC_STRING, TITLE: 'Hash Function',},
'isAdmin':
{CLASS: PC_BOOLEAN, TITLE: 'Is a Super Admin',},
'isDelegatedAdmin':
{CLASS: PC_BOOLEAN, TITLE: 'Is Delegated Admin',},
'isGuestUser':
{CLASS: PC_BOOLEAN, TITLE: 'Is a Guest User',},
'isEnrolledIn2Sv':
{CLASS: PC_BOOLEAN, TITLE: '2-step enrolled',},
'isEnforcedIn2Sv':
{CLASS: PC_BOOLEAN, TITLE: '2-step enforced',},
'agreedToTerms':
{CLASS: PC_BOOLEAN, TITLE: 'Has Agreed to Terms',},
'ipWhitelisted':
{CLASS: PC_BOOLEAN, TITLE: 'IP Whitelisted',},
'archived':
{CLASS: PC_BOOLEAN, TITLE: 'Is Archived',},
'archivalTime':
{CLASS: PC_TIME, TITLE: 'Archival Time',},
'suspended':
{CLASS: PC_BOOLEAN, TITLE: 'Account Suspended',},
'suspensionReason':
{CLASS: PC_STRING, TITLE: 'Suspension Reason',},
'suspensionTime':
{CLASS: PC_TIME, TITLE: 'Suspension Time',},
'changePasswordAtNextLogin':
{CLASS: PC_BOOLEAN, TITLE: 'Must Change Password',},
'recoveryEmail':
{CLASS: PC_STRING, TITLE: 'Recovery Email',},
'recoveryPhone':
{CLASS: PC_STRING, TITLE: 'Recovery Phone',},
'id':
{CLASS: PC_STRING, TITLE: 'Google Unique ID',},
'customerId':
{CLASS: PC_STRING, TITLE: 'Customer ID',},
'isMailboxSetup':
{CLASS: PC_BOOLEAN, TITLE: 'Mailbox is setup',},
'includeInGlobalAddressList':
{CLASS: PC_BOOLEAN, TITLE: 'Included in GAL',},
'creationTime':
{CLASS: PC_TIME, TITLE: 'Creation Time',},
'lastLoginTime':
{CLASS: PC_TIME, TITLE: 'Last login time',},
'deletionTime':
{CLASS: PC_TIME, TITLE: 'Deletion Time',},
'orgUnitPath':
{CLASS: PC_STRING, TITLE: 'Google Org Unit Path',},
'thumbnailPhotoUrl':
{CLASS: PC_STRING, TITLE: 'Photo URL',},
'addresses':
{CLASS: PC_ADDRESSES, TITLE: 'Addresses',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
'emails':
{CLASS: PC_EMAILS, TITLE: 'Other Emails',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
'externalIds':
{CLASS: PC_ARRAY, TITLE: 'External IDs',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'account', 'customer', 'login_id', 'network', 'organization'],},},
'gender':
{CLASS: PC_GENDER, TITLE: 'Gender',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'other',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'other', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customGender',
PTKW_KEYWORD_LIST: ['male', 'female', 'other', 'unknown'],},},
'ims':
{CLASS: PC_IMS, TITLE: 'IMs',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
'keywords':
{CLASS: PC_ARRAY, TITLE: 'Keywords',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'mission', 'occupation', 'outlook'],},},
'locations':
{CLASS: PC_LOCATIONS, TITLE: 'Locations',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'default', 'desk'],},},
'notes':
{CLASS: PC_NOTES, TITLE: 'Notes',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: None,
PTKW_ATTR_TYPE_KEYWORD: 'contentType', PTKW_ATTR_TYPE_CUSTOM_VALUE: None, PTKW_ATTR_CUSTOMTYPE_KEYWORD: None,
PTKW_KEYWORD_LIST: ['text_plain', 'text_html'],},},
'organizations':
{CLASS: PC_ORGANIZATIONS, TITLE: 'Organizations',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom', PTKW_CL_CUSTOMTYPE_KEYWORD: 'customtype',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: None, PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'domain_only', 'school', 'unknown', 'work'],},},
'phones':
{CLASS: PC_ARRAY, TITLE: 'Phones',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'home', 'work', 'other',
'home_fax', 'work_fax', 'other_fax',
'mobile', 'pager',
'company_main', 'assistant',
'car', 'radio', 'isdn', 'callback',
'telex', 'tty_tdd', 'work_mobile',
'work_pager', 'main', 'grand_central'],},},
'posixAccounts':
{CLASS: PC_POSIX, TITLE: 'Posix Accounts',},
'relations':
{CLASS: PC_ARRAY, TITLE: 'Relations',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'spouse', 'child', 'mother',
'father', 'parent', 'brother',
'sister', 'friend', 'relative',
'domestic_partner', 'partner',
'manager', 'dotted_line_manager',
'assistant', 'admin_assistant', 'exec_assistant',
'referred_by'],},},
'sshPublicKeys':
{CLASS: PC_SSH, TITLE: 'SSH Public Keys',},
'websites':
{CLASS: PC_ARRAY, TITLE: 'Websites',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'home', 'work',
'home_page', 'ftp', 'blog',
'profile', 'other', 'reservations',
'app_install_page', 'resume'],},},
'customSchemas':
{CLASS: PC_SCHEMAS, TITLE: 'Custom Schemas',
TYPE_KEYWORDS:
{PTKW_CL_TYPE_KEYWORD: 'type', PTKW_CL_CUSTOM_KEYWORD: 'custom',
PTKW_ATTR_TYPE_KEYWORD: 'type', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customType',
PTKW_KEYWORD_LIST: ['custom', 'home', 'other', 'work'],},},
'aliases': {
CLASS: PC_ALIASES, TITLE: 'Email Aliases',},
'nonEditableAliases': {
CLASS: PC_ALIASES, TITLE: 'NonEditable Aliases',},
}
#
IM_PROTOCOLS = {
PTKW_CL_TYPE_KEYWORD: 'protocol', PTKW_CL_CUSTOM_KEYWORD: 'custom_protocol',
PTKW_ATTR_TYPE_KEYWORD: 'protocol', PTKW_ATTR_TYPE_CUSTOM_VALUE: 'custom_protocol', PTKW_ATTR_CUSTOMTYPE_KEYWORD: 'customProtocol',
PTKW_KEYWORD_LIST: ['custom_protocol', 'aim', 'gtalk', 'icq', 'jabber', 'msn', 'net_meeting', 'qq', 'skype', 'xmpp', 'yahoo']
}

View File

@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2025 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""GAM extended library versions
"""
GAM_VER_LIBS = [
'arrow',
'chardet',
'cryptography',
'filelock',
'google-api-python-client',
'google-auth-httplib2',
'google-auth-oauthlib',
'google-auth',
'lxml',
'httplib2',
'passlib',
'pathvalidate',
'pyscard',
'yubikey-manager',
]

View File

@@ -1,201 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2025 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""YubiKey"""
import base64
from datetime import datetime, timedelta
from secrets import SystemRandom
import string
import sys
from gam import mplock
from gam import systemErrorExit
from gam import readStdin
from gam import writeStdout
from gam.gamlib import glmsgs as Msg
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from smartcard.Exceptions import CardConnectionException
from ykman.device import list_all_devices
from ykman.piv import generate_self_signed_certificate, generate_chuid
from yubikit.piv import DEFAULT_MANAGEMENT_KEY, \
InvalidPinError, \
KEY_TYPE, \
PIN_POLICY, \
PivSession, \
OBJECT_ID, \
SLOT, \
TOUCH_POLICY
from yubikit.core.smartcard import ApduError, SmartCardConnection
YUBIKEY_CONNECTION_ERROR_RC = 80
YUBIKEY_INVALID_KEY_TYPE_RC = 81
YUBIKEY_INVALID_SLOT_RC = 82
YUBIKEY_INVALID_PIN_RC = 83
YUBIKEY_APDU_ERROR_RC = 84
YUBIKEY_VALUE_ERROR_RC = 85
YUBIKEY_MULTIPLE_CONNECTED_RC = 86
YUBIKEY_NOT_FOUND_RC = 87
PIN_PUK_CHARS = string.ascii_letters+string.digits+string.punctuation
class YubiKey():
def __init__(self, service_account_info=None):
self.key_type = None
self.slot = None
self.serial_number = None
self.pin = None
self.key_id = None
if service_account_info:
key_type = service_account_info.get('yubikey_key_type', 'RSA2048')
try:
self.key_type = getattr(KEY_TYPE, key_type.upper())
except AttributeError:
systemErrorExit(YUBIKEY_INVALID_KEY_TYPE_RC, f'{key_type} is not a valid value for yubikey_key_type')
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
try:
self.slot = getattr(SLOT, slot.upper())
except AttributeError:
systemErrorExit(YUBIKEY_INVALID_SLOT_RC, f'{slot} is not a valid value for yubikey_slot')
self.serial_number = service_account_info.get('yubikey_serial_number')
self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id')
def _connect(self):
try:
devices = list_all_devices()
for (device, info) in devices:
if info.serial == self.serial_number:
return device.open_connection(SmartCardConnection)
except CardConnectionException as err:
systemErrorExit(YUBIKEY_CONNECTION_ERROR_RC, f'YubiKey - {err}')
def get_certificate(self):
try:
conn = self._connect()
with conn:
session = PivSession(conn)
if self.pin:
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
systemErrorExit(YUBIKEY_INVALID_PIN_RC, f'YubiKey - {err}')
try:
cert = session.get_certificate(self.slot)
except ApduError as err:
systemErrorExit(YUBIKEY_APDU_ERROR_RC, f'YubiKey - {err}')
cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode()
publicKeyData = base64.b64encode(cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
return publicKeyData
except ValueError as err:
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
except TypeError as err:
systemErrorExit(YUBIKEY_NOT_FOUND_RC, f'YubiKey - {err} - {Msg.IS_YUBIKEY_INSERTED}')
def get_serial_number(self):
try:
devices = list_all_devices()
if not devices:
systemErrorExit(YUBIKEY_NOT_FOUND_RC, Msg.COULD_NOT_FIND_ANY_YUBIKEY)
if self.serial_number:
for (_, info) in devices:
if info.serial == self.serial_number:
return info.serial
systemErrorExit(YUBIKEY_NOT_FOUND_RC, Msg.COULD_NOT_FIND_YUBIKEY_WITH_SERIAL.format(self.serial_number))
if len(devices) > 1:
serials = ', '.join([str(info.serial) for (_, info) in devices])
systemErrorExit(YUBIKEY_MULTIPLE_CONNECTED_RC, Msg.MULTIPLE_YUBIKEYS_CONNECTED.format(serials))
return devices[0][1].serial
except ValueError as err:
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
def reset_piv(self):
'''Resets YubiKey PIV app and generates new key for GAM to use.'''
reply = str(readStdin(Msg.CONFIRM_WIPE_YUBIKEY_PIV).lower().strip())
if reply != 'y':
sys.exit(1)
try:
conn = self._connect()
with conn:
piv = PivSession(conn)
piv.reset()
rnd = SystemRandom()
new_puk = ''.join(rnd.choice(PIN_PUK_CHARS) for _ in range(8))
new_pin = ''.join(rnd.choice(PIN_PUK_CHARS) for _ in range(8))
piv.change_puk('12345678', new_puk)
piv.change_pin('123456', new_pin)
writeStdout(Msg.YUBIKEY_PIN_SET_TO.format(new_pin))
piv.authenticate(piv.management_key_type, DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
writeStdout(Msg.YUBIKEY_GENERATING_NONEXPORTABLE_PRIVATE_KEY)
pubkey = piv.generate_key(SLOT.AUTHENTICATION,
KEY_TYPE.RSA2048,
PIN_POLICY.ALWAYS,
TOUCH_POLICY.NEVER)
now = datetime.utcnow()
valid_to = now + timedelta(days=3650)
subject = 'CN=GAM Created Key'
piv.authenticate(piv.management_key_type, DEFAULT_MANAGEMENT_KEY)
piv.verify_pin(new_pin)
cert = generate_self_signed_certificate(piv,
SLOT.AUTHENTICATION,
pubkey,
subject,
now,
valid_to)
piv.put_certificate(SLOT.AUTHENTICATION, cert)
piv.put_object(OBJECT_ID.CHUID, generate_chuid())
except ValueError as err:
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
except TypeError as err:
systemErrorExit(YUBIKEY_NOT_FOUND_RC, f'YubiKey - {err} - {Msg.IS_YUBIKEY_INSERTED}')
def sign(self, message):
if mplock is not None:
mplock.acquire()
try:
conn = self._connect()
with conn:
session = PivSession(conn)
if self.pin:
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
systemErrorExit(YUBIKEY_INVALID_PIN_RC, f'YubiKey - {err}')
try:
signed = session.sign(slot=self.slot,
key_type=self.key_type,
message=message,
hash_algorithm=hashes.SHA256(),
padding=padding.PKCS1v15())
except ApduError as err:
systemErrorExit(YUBIKEY_APDU_ERROR_RC, f'YubiKey - {err}')
except ValueError as err:
systemErrorExit(YUBIKEY_VALUE_ERROR_RC, f'YubiKey - {err}')
except TypeError as err:
systemErrorExit(YUBIKEY_NOT_FOUND_RC, f'YubiKey - {err} - {Msg.IS_YUBIKEY_INSERTED}')
if mplock is not None:
mplock.release()
return signed

View File

@@ -1,825 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2006 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains classes representing Google Data elements.
Extends Atom classes to add Google Data specific elements.
"""
__author__ = 'j.s@google.com (Jeffrey Scudder)'
import os
import atom
import lxml.etree as ElementTree
# XML namespaces which are often used in GData entities.
GDATA_NAMESPACE = 'http://schemas.google.com/g/2005'
GDATA_TEMPLATE = '{http://schemas.google.com/g/2005}%s'
OPENSEARCH_NAMESPACE = 'http://a9.com/-/spec/opensearchrss/1.0/'
OPENSEARCH_TEMPLATE = '{http://a9.com/-/spec/opensearchrss/1.0/}%s'
BATCH_NAMESPACE = 'http://schemas.google.com/gdata/batch'
GACL_NAMESPACE = 'http://schemas.google.com/acl/2007'
GACL_TEMPLATE = '{http://schemas.google.com/acl/2007}%s'
# Labels used in batch request entries to specify the desired CRUD operation.
BATCH_INSERT = 'insert'
BATCH_UPDATE = 'update'
BATCH_DELETE = 'delete'
BATCH_QUERY = 'query'
class Error(Exception):
pass
class MissingRequiredParameters(Error):
pass
class MediaSource(object):
"""GData Entries can refer to media sources, so this class provides a
place to store references to these objects along with some metadata.
"""
def __init__(self, file_handle=None, content_type=None, content_length=None,
file_path=None, file_name=None):
"""Creates an object of type MediaSource.
Args:
file_handle: A file handle pointing to the file to be encapsulated in the
MediaSource
content_type: string The MIME type of the file. Required if a file_handle
is given.
content_length: int The size of the file. Required if a file_handle is
given.
file_path: string (optional) A full path name to the file. Used in
place of a file_handle.
file_name: string The name of the file without any path information.
Required if a file_handle is given.
"""
self.file_handle = file_handle
self.content_type = content_type
self.content_length = content_length
self.file_name = file_name
if (file_handle is None and content_type is not None and
file_path is not None):
self.setFile(file_path, content_type)
def setFile(self, file_name, content_type):
"""A helper function which can create a file handle from a given filename
and set the content type and length all at once.
Args:
file_name: string The path and file name to the file containing the media
content_type: string A MIME type representing the type of the media
"""
self.file_handle = open(file_name, 'rb')
self.content_type = content_type
self.content_length = os.path.getsize(file_name)
self.file_name = os.path.basename(file_name)
class LinkFinder(atom.LinkFinder):
"""An "interface" providing methods to find link elements
GData Entry elements often contain multiple links which differ in the rel
attribute or content type. Often, developers are interested in a specific
type of link so this class provides methods to find specific classes of
links.
This class is used as a mixin in GData entries.
"""
def GetSelfLink(self):
"""Find the first link with rel set to 'self'
Returns:
An atom.Link or none if none of the links had rel equal to 'self'
"""
for a_link in self.link:
if a_link.rel == 'self':
return a_link
return None
def GetEditLink(self):
for a_link in self.link:
if a_link.rel == 'edit':
return a_link
return None
def GetEditMediaLink(self):
"""The Picasa API mistakenly returns media-edit rather than edit-media, but
this may change soon.
"""
for a_link in self.link:
if a_link.rel == 'edit-media':
return a_link
if a_link.rel == 'media-edit':
return a_link
return None
def GetHtmlLink(self):
"""Find the first link with rel of alternate and type of text/html
Returns:
An atom.Link or None if no links matched
"""
for a_link in self.link:
if a_link.rel == 'alternate' and a_link.type == 'text/html':
return a_link
return None
def GetPostLink(self):
"""Get a link containing the POST target URL.
The POST target URL is used to insert new entries.
Returns:
A link object with a rel matching the POST type.
"""
for a_link in self.link:
if a_link.rel == 'http://schemas.google.com/g/2005#post':
return a_link
return None
def GetAclLink(self):
for a_link in self.link:
if a_link.rel == 'http://schemas.google.com/acl/2007#accessControlList':
return a_link
return None
def GetFeedLink(self):
for a_link in self.link:
if a_link.rel == 'http://schemas.google.com/g/2005#feed':
return a_link
return None
def GetNextLink(self):
for a_link in self.link:
if a_link.rel == 'next':
return a_link
return None
def GetPrevLink(self):
for a_link in self.link:
if a_link.rel == 'previous':
return a_link
return None
class TotalResults(atom.AtomBase):
"""opensearch:TotalResults for a GData feed"""
_tag = 'totalResults'
_namespace = OPENSEARCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
def __init__(self, extension_elements=None,
extension_attributes=None, text=None):
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def TotalResultsFromString(xml_string):
return atom.CreateClassFromXMLString(TotalResults, xml_string)
class StartIndex(atom.AtomBase):
"""The opensearch:startIndex element in GData feed"""
_tag = 'startIndex'
_namespace = OPENSEARCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
def __init__(self, extension_elements=None,
extension_attributes=None, text=None):
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def StartIndexFromString(xml_string):
return atom.CreateClassFromXMLString(StartIndex, xml_string)
class ItemsPerPage(atom.AtomBase):
"""The opensearch:itemsPerPage element in GData feed"""
_tag = 'itemsPerPage'
_namespace = OPENSEARCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
def __init__(self, extension_elements=None,
extension_attributes=None, text=None):
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def ItemsPerPageFromString(xml_string):
return atom.CreateClassFromXMLString(ItemsPerPage, xml_string)
class ExtendedProperty(atom.AtomBase):
"""The Google Data extendedProperty element.
Used to store arbitrary key-value information specific to your
application. The value can either be a text string stored as an XML
attribute (.value), or an XML node (XmlBlob) as a child element.
This element is used in the Google Calendar data API and the Google
Contacts data API.
"""
_tag = 'extendedProperty'
_namespace = GDATA_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['name'] = 'name'
_attributes['value'] = 'value'
def __init__(self, name=None, value=None, extension_elements=None,
extension_attributes=None, text=None):
self.name = name
self.value = value
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def GetXmlBlobExtensionElement(self):
"""Returns the XML blob as an atom.ExtensionElement.
Returns:
An atom.ExtensionElement representing the blob's XML, or None if no
blob was set.
"""
if len(self.extension_elements) < 1:
return None
else:
return self.extension_elements[0]
def GetXmlBlobString(self):
"""Returns the XML blob as a string.
Returns:
A string containing the blob's XML, or None if no blob was set.
"""
blob = self.GetXmlBlobExtensionElement()
if blob:
return blob.ToString()
return None
def SetXmlBlob(self, blob):
"""Sets the contents of the extendedProperty to XML as a child node.
Since the extendedProperty is only allowed one child element as an XML
blob, setting the XML blob will erase any preexisting extension elements
in this object.
Args:
blob: str, ElementTree Element or atom.ExtensionElement representing
the XML blob stored in the extendedProperty.
"""
# Erase any existing extension_elements, clears the child nodes from the
# extendedProperty.
self.extension_elements = []
if isinstance(blob, atom.ExtensionElement):
self.extension_elements.append(blob)
elif ElementTree.iselement(blob):
self.extension_elements.append(atom._ExtensionElementFromElementTree(
blob))
else:
self.extension_elements.append(atom.ExtensionElementFromString(blob))
def ExtendedPropertyFromString(xml_string):
return atom.CreateClassFromXMLString(ExtendedProperty, xml_string)
class GDataEntry(atom.Entry, LinkFinder):
"""Extends Atom Entry to provide data processing"""
_tag = atom.Entry._tag
_namespace = atom.Entry._namespace
_children = atom.Entry._children.copy()
_attributes = atom.Entry._attributes.copy()
def __GetId(self):
return self.__id
# This method was created to strip the unwanted whitespace from the id's
# text node.
def __SetId(self, id):
self.__id = id
if id is not None and id.text is not None:
self.__id.text = id.text.strip()
id = property(__GetId, __SetId)
def IsMedia(self):
"""Determines whether or not an entry is a GData Media entry.
"""
if (self.GetEditMediaLink()):
return True
else:
return False
def GetMediaURL(self):
"""Returns the URL to the media content, if the entry is a media entry.
Otherwise returns None.
"""
if not self.IsMedia():
return None
else:
return self.content.src
def GDataEntryFromString(xml_string):
"""Creates a new GDataEntry instance given a string of XML."""
return atom.CreateClassFromXMLString(GDataEntry, xml_string)
class GDataFeed(atom.Feed, LinkFinder):
"""A Feed from a GData service"""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = atom.Feed._children.copy()
_attributes = atom.Feed._attributes.copy()
_children['{%s}totalResults' % OPENSEARCH_NAMESPACE] = ('total_results',
TotalResults)
_children['{%s}startIndex' % OPENSEARCH_NAMESPACE] = ('start_index',
StartIndex)
_children['{%s}itemsPerPage' % OPENSEARCH_NAMESPACE] = ('items_per_page',
ItemsPerPage)
# Add a conversion rule for atom:entry to make it into a GData
# Entry.
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GDataEntry])
def __GetId(self):
return self.__id
def __SetId(self, id):
self.__id = id
if id is not None and id.text is not None:
self.__id.text = id.text.strip()
id = property(__GetId, __SetId)
def __GetGenerator(self):
return self.__generator
def __SetGenerator(self, generator):
self.__generator = generator
if generator is not None:
self.__generator.text = generator.text.strip()
generator = property(__GetGenerator, __SetGenerator)
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None, entry=None,
total_results=None, start_index=None, items_per_page=None,
extension_elements=None, extension_attributes=None, text=None):
"""Constructor for Source
Args:
author: list (optional) A list of Author instances which belong to this
class.
category: list (optional) A list of Category instances
contributor: list (optional) A list on Contributor instances
generator: Generator (optional)
icon: Icon (optional)
id: Id (optional) The entry's Id element
link: list (optional) A list of Link instances
logo: Logo (optional)
rights: Rights (optional) The entry's Rights element
subtitle: Subtitle (optional) The entry's subtitle element
title: Title (optional) the entry's title element
updated: Updated (optional) the entry's updated element
entry: list (optional) A list of the Entry instances contained in the
feed.
text: String (optional) The text contents of the element. This is the
contents of the Entry's XML text node.
(Example: <foo>This is the text</foo>)
extension_elements: list (optional) A list of ExtensionElement instances
which are children of this element.
extension_attributes: dict (optional) A dictionary of strings which are
the values for additional XML attributes of this element.
"""
self.author = author or []
self.category = category or []
self.contributor = contributor or []
self.generator = generator
self.icon = icon
self.id = atom_id
self.link = link or []
self.logo = logo
self.rights = rights
self.subtitle = subtitle
self.title = title
self.updated = updated
self.entry = entry or []
self.total_results = total_results
self.start_index = start_index
self.items_per_page = items_per_page
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def GDataFeedFromString(xml_string):
return atom.CreateClassFromXMLString(GDataFeed, xml_string)
class BatchId(atom.AtomBase):
_tag = 'id'
_namespace = BATCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
def BatchIdFromString(xml_string):
return atom.CreateClassFromXMLString(BatchId, xml_string)
class BatchOperation(atom.AtomBase):
_tag = 'operation'
_namespace = BATCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['type'] = 'type'
def __init__(self, op_type=None, extension_elements=None,
extension_attributes=None,
text=None):
self.type = op_type
atom.AtomBase.__init__(self,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def BatchOperationFromString(xml_string):
return atom.CreateClassFromXMLString(BatchOperation, xml_string)
class BatchStatus(atom.AtomBase):
"""The batch:status element present in a batch response entry.
A status element contains the code (HTTP response code) and
reason as elements. In a single request these fields would
be part of the HTTP response, but in a batch request each
Entry operation has a corresponding Entry in the response
feed which includes status information.
See http://code.google.com/apis/gdata/batch.html#Handling_Errors
"""
_tag = 'status'
_namespace = BATCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['code'] = 'code'
_attributes['reason'] = 'reason'
_attributes['content-type'] = 'content_type'
def __init__(self, code=None, reason=None, content_type=None,
extension_elements=None, extension_attributes=None, text=None):
self.code = code
self.reason = reason
self.content_type = content_type
atom.AtomBase.__init__(self, extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def BatchStatusFromString(xml_string):
return atom.CreateClassFromXMLString(BatchStatus, xml_string)
class BatchEntry(GDataEntry):
"""An atom:entry for use in batch requests.
The BatchEntry contains additional members to specify the operation to be
performed on this entry and a batch ID so that the server can reference
individual operations in the response feed. For more information, see:
http://code.google.com/apis/gdata/batch.html
"""
_tag = GDataEntry._tag
_namespace = GDataEntry._namespace
_children = GDataEntry._children.copy()
_children['{%s}operation' % BATCH_NAMESPACE] = ('batch_operation', BatchOperation)
_children['{%s}id' % BATCH_NAMESPACE] = ('batch_id', BatchId)
_children['{%s}status' % BATCH_NAMESPACE] = ('batch_status', BatchStatus)
_attributes = GDataEntry._attributes.copy()
def __init__(self, author=None, category=None, content=None,
contributor=None, atom_id=None, link=None, published=None, rights=None,
source=None, summary=None, control=None, title=None, updated=None,
batch_operation=None, batch_id=None, batch_status=None,
extension_elements=None, extension_attributes=None, text=None):
self.batch_operation = batch_operation
self.batch_id = batch_id
self.batch_status = batch_status
GDataEntry.__init__(self, author=author, category=category,
content=content, contributor=contributor, atom_id=atom_id, link=link,
published=published, rights=rights, source=source, summary=summary,
control=control, title=title, updated=updated,
extension_elements=extension_elements,
extension_attributes=extension_attributes, text=text)
def BatchEntryFromString(xml_string):
return atom.CreateClassFromXMLString(BatchEntry, xml_string)
class BatchInterrupted(atom.AtomBase):
"""The batch:interrupted element sent if batch request was interrupted.
Only appears in a feed if some of the batch entries could not be processed.
See: http://code.google.com/apis/gdata/batch.html#Handling_Errors
"""
_tag = 'interrupted'
_namespace = BATCH_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['reason'] = 'reason'
_attributes['success'] = 'success'
_attributes['failures'] = 'failures'
_attributes['parsed'] = 'parsed'
def __init__(self, reason=None, success=None, failures=None, parsed=None,
extension_elements=None, extension_attributes=None, text=None):
self.reason = reason
self.success = success
self.failures = failures
self.parsed = parsed
atom.AtomBase.__init__(self, extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def BatchInterruptedFromString(xml_string):
return atom.CreateClassFromXMLString(BatchInterrupted, xml_string)
class BatchFeed(GDataFeed):
"""A feed containing a list of batch request entries."""
_tag = GDataFeed._tag
_namespace = GDataFeed._namespace
_children = GDataFeed._children.copy()
_attributes = GDataFeed._attributes.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [BatchEntry])
_children['{%s}interrupted' % BATCH_NAMESPACE] = ('interrupted', BatchInterrupted)
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None, entry=None,
total_results=None, start_index=None, items_per_page=None,
interrupted=None,
extension_elements=None, extension_attributes=None, text=None):
self.interrupted = interrupted
GDataFeed.__init__(self, author=author, category=category,
contributor=contributor, generator=generator,
icon=icon, atom_id=atom_id, link=link,
logo=logo, rights=rights, subtitle=subtitle,
title=title, updated=updated, entry=entry,
total_results=total_results, start_index=start_index,
items_per_page=items_per_page,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def AddBatchEntry(self, entry=None, id_url_string=None,
batch_id_string=None, operation_string=None):
"""Logic for populating members of a BatchEntry and adding to the feed.
If the entry is not a BatchEntry, it is converted to a BatchEntry so
that the batch specific members will be present.
The id_url_string can be used in place of an entry if the batch operation
applies to a URL. For example query and delete operations require just
the URL of an entry, no body is sent in the HTTP request. If an
id_url_string is sent instead of an entry, a BatchEntry is created and
added to the feed.
This method also assigns the desired batch id to the entry so that it
can be referenced in the server's response. If the batch_id_string is
None, this method will assign a batch_id to be the index at which this
entry will be in the feed's entry list.
Args:
entry: BatchEntry, atom.Entry, or another Entry flavor (optional) The
entry which will be sent to the server as part of the batch request.
The item must have a valid atom id so that the server knows which
entry this request references.
id_url_string: str (optional) The URL of the entry to be acted on. You
can find this URL in the text member of the atom id for an entry.
If an entry is not sent, this id will be used to construct a new
BatchEntry which will be added to the request feed.
batch_id_string: str (optional) The batch ID to be used to reference
this batch operation in the results feed. If this parameter is None,
the current length of the feed's entry array will be used as a
count. Note that batch_ids should either always be specified or
never, mixing could potentially result in duplicate batch ids.
operation_string: str (optional) The desired batch operation which will
set the batch_operation.type member of the entry. Options are
'insert', 'update', 'delete', and 'query'
Raises:
MissingRequiredParameters: Raised if neither an id_ url_string nor an
entry are provided in the request.
Returns:
The added entry.
"""
if entry is None and id_url_string is None:
raise MissingRequiredParameters('supply either an entry or URL string')
if entry is None and id_url_string is not None:
entry = BatchEntry(atom_id=atom.Id(text=id_url_string))
# TODO: handle cases in which the entry lacks batch_... members.
#if not isinstance(entry, BatchEntry):
# Convert the entry to a batch entry.
if batch_id_string is not None:
entry.batch_id = BatchId(text=batch_id_string)
elif entry.batch_id is None or entry.batch_id.text is None:
entry.batch_id = BatchId(text=str(len(self.entry)))
if operation_string is not None:
entry.batch_operation = BatchOperation(op_type=operation_string)
self.entry.append(entry)
return entry
def AddInsert(self, entry, batch_id_string=None):
"""Add an insert request to the operations in this batch request feed.
If the entry doesn't yet have an operation or a batch id, these will
be set to the insert operation and a batch_id specified as a parameter.
Args:
entry: BatchEntry The entry which will be sent in the batch feed as an
insert request.
batch_id_string: str (optional) The batch ID to be used to reference
this batch operation in the results feed. If this parameter is None,
the current length of the feed's entry array will be used as a
count. Note that batch_ids should either always be specified or
never, mixing could potentially result in duplicate batch ids.
"""
entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string,
operation_string=BATCH_INSERT)
def AddUpdate(self, entry, batch_id_string=None):
"""Add an update request to the list of batch operations in this feed.
Sets the operation type of the entry to insert if it is not already set
and assigns the desired batch id to the entry so that it can be
referenced in the server's response.
Args:
entry: BatchEntry The entry which will be sent to the server as an
update (HTTP PUT) request. The item must have a valid atom id
so that the server knows which entry to replace.
batch_id_string: str (optional) The batch ID to be used to reference
this batch operation in the results feed. If this parameter is None,
the current length of the feed's entry array will be used as a
count. See also comments for AddInsert.
"""
entry = self.AddBatchEntry(entry=entry, batch_id_string=batch_id_string,
operation_string=BATCH_UPDATE)
def AddDelete(self, url_string=None, entry=None, batch_id_string=None):
"""Adds a delete request to the batch request feed.
This method takes either the url_string which is the atom id of the item
to be deleted, or the entry itself. The atom id of the entry must be
present so that the server knows which entry should be deleted.
Args:
url_string: str (optional) The URL of the entry to be deleted. You can
find this URL in the text member of the atom id for an entry.
entry: BatchEntry (optional) The entry to be deleted.
batch_id_string: str (optional)
Raises:
MissingRequiredParameters: Raised if neither a url_string nor an entry
are provided in the request.
"""
entry = self.AddBatchEntry(entry=entry, id_url_string=url_string,
batch_id_string=batch_id_string,
operation_string=BATCH_DELETE)
def AddQuery(self, url_string=None, entry=None, batch_id_string=None):
"""Adds a query request to the batch request feed.
This method takes either the url_string which is the query URL
whose results will be added to the result feed. The query URL will
be encapsulated in a BatchEntry, and you may pass in the BatchEntry
with a query URL instead of sending a url_string.
Args:
url_string: str (optional)
entry: BatchEntry (optional)
batch_id_string: str (optional)
Raises:
MissingRequiredParameters
"""
entry = self.AddBatchEntry(entry=entry, id_url_string=url_string,
batch_id_string=batch_id_string,
operation_string=BATCH_QUERY)
def GetBatchLink(self):
for link in self.link:
if link.rel == 'http://schemas.google.com/g/2005#batch':
return link
return None
def BatchFeedFromString(xml_string):
return atom.CreateClassFromXMLString(BatchFeed, xml_string)
class EntryLink(atom.AtomBase):
"""The gd:entryLink element"""
_tag = 'entryLink'
_namespace = GDATA_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
# The entry used to be an atom.Entry, now it is a GDataEntry.
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', GDataEntry)
_attributes['rel'] = 'rel'
_attributes['readOnly'] = 'read_only'
_attributes['href'] = 'href'
def __init__(self, href=None, read_only=None, rel=None,
entry=None, extension_elements=None,
extension_attributes=None, text=None):
self.href = href
self.read_only = read_only
self.rel = rel
self.entry = entry
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def EntryLinkFromString(xml_string):
return atom.CreateClassFromXMLString(EntryLink, xml_string)
class FeedLink(atom.AtomBase):
"""The gd:feedLink element"""
_tag = 'feedLink'
_namespace = GDATA_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_children['{%s}feed' % atom.ATOM_NAMESPACE] = ('feed', GDataFeed)
_attributes['rel'] = 'rel'
_attributes['readOnly'] = 'read_only'
_attributes['countHint'] = 'count_hint'
_attributes['href'] = 'href'
def __init__(self, count_hint=None, href=None, read_only=None, rel=None,
feed=None, extension_elements=None, extension_attributes=None,
text=None):
self.count_hint = count_hint
self.href = href
self.read_only = read_only
self.rel = rel
self.feed = feed
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def FeedLinkFromString(xml_string):
return atom.CreateClassFromXMLString(FeedLink, xml_string)

View File

@@ -1,20 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This package's modules adapt the gdata library to run in other environments
The first example is the appengine module which contains functions and
classes which modify a GDataService object to run on Google App Engine.
"""

View File

@@ -1,101 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2009 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides functions to persist serialized auth tokens in the datastore.
The get_token and set_token functions should be used in conjunction with
gdata.gauth's token_from_blob and token_to_blob to allow auth token objects
to be reused across requests. It is up to your own code to ensure that the
token key's are unique.
"""
__author__ = 'j.s@google.com (Jeff Scudder)'
from google.appengine.ext import db
from google.appengine.api import memcache
class Token(db.Model):
"""Datastore Model which stores a serialized auth token."""
t = db.BlobProperty()
def get_token(unique_key):
"""Searches for a stored token with the desired key.
Checks memcache and then the datastore if required.
Args:
unique_key: str which uniquely identifies the desired auth token.
Returns:
A string encoding the auth token data. Use gdata.gauth.token_from_blob to
convert back into a usable token object. None if the token was not found
in memcache or the datastore.
"""
token_string = memcache.get(unique_key)
if token_string is None:
# The token wasn't in memcache, so look in the datastore.
token = Token.get_by_key_name(unique_key)
if token is None:
return None
return token.t
return token_string
def set_token(unique_key, token_str):
"""Saves the serialized auth token in the datastore.
The token is also stored in memcache to speed up retrieval on a cache hit.
Args:
unique_key: The unique name for this token as a string. It is up to your
code to ensure that this token value is unique in your application.
Previous values will be silently overwitten.
token_str: A serialized auth token as a string. I expect that this string
will be generated by gdata.gauth.token_to_blob.
Returns:
True if the token was stored sucessfully, False if the token could not be
safely cached (if an old value could not be cleared). If the token was
set in memcache, but not in the datastore, this function will return None.
However, in that situation an exception will likely be raised.
Raises:
Datastore exceptions may be raised from the App Engine SDK in the event of
failure.
"""
# First try to save in memcache.
result = memcache.set(unique_key, token_str)
# If memcache fails to save the value, clear the cached value.
if not result:
result = memcache.delete(unique_key)
# If we could not clear the cached value for this token, refuse to save.
if result == 0:
return False
# Save to the datastore.
if Token(key_name=unique_key, t=token_str).put():
return True
return None
def delete_token(unique_key):
# Clear from memcache.
memcache.delete(unique_key)
# Clear from the datastore.
Token(key_name=unique_key).delete()

View File

@@ -1,321 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides HTTP functions for gdata.service to use on Google App Engine
AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
urlfetch API. Set the http_client member of a GDataService object to an
instance of an AppEngineHttpClient to allow the gdata library to run on
Google App Engine.
run_on_appengine: Function which will modify an existing GDataService object
to allow it to run on App Engine. It works by creating a new instance of
the AppEngineHttpClient and replacing the GDataService object's
http_client.
"""
__author__ = 'api.jscudder (Jeff Scudder)'
import StringIO
import pickle
import atom.http_interface
import atom.token_store
from google.appengine.api import urlfetch
from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.api import memcache
def run_on_appengine(gdata_service, store_tokens=True,
single_user_mode=False, deadline=None):
"""Modifies a GDataService object to allow it to run on App Engine.
Args:
gdata_service: An instance of AtomService, GDataService, or any
of their subclasses which has an http_client member and a
token_store member.
store_tokens: Boolean, defaults to True. If True, the gdata_service
will attempt to add each token to it's token_store when
SetClientLoginToken or SetAuthSubToken is called. If False
the tokens will not automatically be added to the
token_store.
single_user_mode: Boolean, defaults to False. If True, the current_token
member of gdata_service will be set when
SetClientLoginToken or SetAuthTubToken is called. If set
to True, the current_token is set in the gdata_service
and anyone who accesses the object will use the same
token.
Note: If store_tokens is set to False and
single_user_mode is set to False, all tokens will be
ignored, since the library assumes: the tokens should not
be stored in the datastore and they should not be stored
in the gdata_service object. This will make it
impossible to make requests which require authorization.
deadline: int (optional) The number of seconds to wait for a response
before timing out on the HTTP request. If no deadline is
specified, the deafault deadline for HTTP requests from App
Engine is used. The maximum is currently 10 (for 10 seconds).
The default deadline for App Engine is 5 seconds.
"""
gdata_service.http_client = AppEngineHttpClient(deadline=deadline)
gdata_service.token_store = AppEngineTokenStore()
gdata_service.auto_store_tokens = store_tokens
gdata_service.auto_set_current_token = single_user_mode
return gdata_service
class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
def __init__(self, headers=None, deadline=None):
self.debug = False
self.headers = headers or {}
self.deadline = deadline
def request(self, operation, url, data=None, headers=None):
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
DELETE.
Usage example, perform and HTTP GET on http://www.google.com/:
import atom.http
client = atom.http.HttpClient()
http_response = client.request('GET', 'http://www.google.com/')
Args:
operation: str The HTTP operation to be performed. This is usually one
of 'GET', 'POST', 'PUT', or 'DELETE'
data: filestream, list of parts, or other object which can be converted
to a string. Should be set to None when performing a GET or DELETE.
If data is a file-like object which can be read, this method will
read a chunk of 100K bytes at a time and send them.
If the data is a list of parts to be sent, each part will be
evaluated and sent.
url: The full URL to which the request should be sent. Can be a string
or atom.url.Url.
headers: dict of strings. HTTP headers which should be sent
in the request.
"""
all_headers = self.headers.copy()
if headers:
all_headers.update(headers)
# Construct the full payload.
# Assume that data is None or a string.
data_str = data
if data:
if isinstance(data, list):
# If data is a list of different objects, convert them all to strings
# and join them together.
converted_parts = [_convert_data_part(x) for x in data]
data_str = ''.join(converted_parts)
else:
data_str = _convert_data_part(data)
# If the list of headers does not include a Content-Length, attempt to
# calculate it based on the data object.
if data and 'Content-Length' not in all_headers:
all_headers['Content-Length'] = str(len(data_str))
# Set the content type to the default value if none was set.
if 'Content-Type' not in all_headers:
all_headers['Content-Type'] = 'application/atom+xml'
# Lookup the urlfetch operation which corresponds to the desired HTTP verb.
if operation == 'GET':
method = urlfetch.GET
elif operation == 'POST':
method = urlfetch.POST
elif operation == 'PUT':
method = urlfetch.PUT
elif operation == 'DELETE':
method = urlfetch.DELETE
else:
method = None
if self.deadline is None:
return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
method=method, headers=all_headers, follow_redirects=False))
return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
method=method, headers=all_headers, follow_redirects=False,
deadline=self.deadline))
def _convert_data_part(data):
if not data or isinstance(data, str):
return data
elif hasattr(data, 'read'):
# data is a file like object, so read it completely.
return data.read()
# The data object was not a file.
# Try to convert to a string and send the data.
return str(data)
class HttpResponse(object):
"""Translates a urlfetch resoinse to look like an hhtplib resoinse.
Used to allow the resoinse from HttpRequest to be usable by gdata.service
methods.
"""
def __init__(self, urlfetch_response):
self.body = StringIO.StringIO(urlfetch_response.content)
self.headers = urlfetch_response.headers
self.status = urlfetch_response.status_code
self.reason = ''
def read(self, length=None):
if not length:
return self.body.read()
else:
return self.body.read(length)
def getheader(self, name):
if not self.headers.has_key(name):
return self.headers[name.lower()]
return self.headers[name]
class TokenCollection(db.Model):
"""Datastore Model which associates auth tokens with the current user."""
user = db.UserProperty()
pickled_tokens = db.BlobProperty()
class AppEngineTokenStore(atom.token_store.TokenStore):
"""Stores the user's auth tokens in the App Engine datastore.
Tokens are only written to the datastore if a user is signed in (if
users.get_current_user() returns a user object).
"""
def __init__(self):
self.user = None
def add_token(self, token):
"""Associates the token with the current user and stores it.
If there is no current user, the token will not be stored.
Returns:
False if the token was not stored.
"""
tokens = load_auth_tokens(self.user)
if not hasattr(token, 'scopes') or not token.scopes:
return False
for scope in token.scopes:
tokens[str(scope)] = token
key = save_auth_tokens(tokens, self.user)
if key:
return True
return False
def find_token(self, url):
"""Searches the current user's collection of token for a token which can
be used for a request to the url.
Returns:
The stored token which belongs to the current user and is valid for the
desired URL. If there is no current user, or there is no valid user
token in the datastore, a atom.http_interface.GenericToken is returned.
"""
if url is None:
return None
if isinstance(url, (str, unicode)):
url = atom.url.parse_url(url)
tokens = load_auth_tokens(self.user)
if url in tokens:
token = tokens[url]
if token.valid_for_scope(url):
return token
else:
del tokens[url]
save_auth_tokens(tokens, self.user)
for scope, token in tokens.iteritems():
if token.valid_for_scope(url):
return token
return atom.http_interface.GenericToken()
def remove_token(self, token):
"""Removes the token from the current user's collection in the datastore.
Returns:
False if the token was not removed, this could be because the token was
not in the datastore, or because there is no current user.
"""
token_found = False
scopes_to_delete = []
tokens = load_auth_tokens(self.user)
for scope, stored_token in tokens.iteritems():
if stored_token == token:
scopes_to_delete.append(scope)
token_found = True
for scope in scopes_to_delete:
del tokens[scope]
if token_found:
save_auth_tokens(tokens, self.user)
return token_found
def remove_all_tokens(self):
"""Removes all of the current user's tokens from the datastore."""
save_auth_tokens({}, self.user)
def save_auth_tokens(token_dict, user=None):
"""Associates the tokens with the current user and writes to the datastore.
If there us no current user, the tokens are not written and this function
returns None.
Returns:
The key of the datastore entity containing the user's tokens, or None if
there was no current user.
"""
if user is None:
user = users.get_current_user()
if user is None:
return None
memcache.set('gdata_pickled_tokens:%s' % user, pickle.dumps(token_dict))
user_tokens = TokenCollection.all().filter('user =', user).get()
if user_tokens:
user_tokens.pickled_tokens = pickle.dumps(token_dict)
return user_tokens.put()
else:
user_tokens = TokenCollection(
user=user,
pickled_tokens=pickle.dumps(token_dict))
return user_tokens.put()
def load_auth_tokens(user=None):
"""Reads a dictionary of the current user's tokens from the datastore.
If there is no current user (a user is not signed in to the app) or the user
does not have any tokens, an empty dictionary is returned.
"""
if user is None:
user = users.get_current_user()
if user is None:
return {}
pickled_tokens = memcache.get('gdata_pickled_tokens:%s' % user)
if pickled_tokens:
return pickle.loads(pickled_tokens)
user_tokens = TokenCollection.all().filter('user =', user).get()
if user_tokens:
memcache.set('gdata_pickled_tokens:%s' % user, user_tokens.pickled_tokens)
return pickle.loads(user_tokens.pickled_tokens)
return {}

View File

@@ -1,526 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2007 SIOS Technology, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains objects used with Google Apps."""
__author__ = 'tmatsuo@sios.com (Takashi MATSUO)'
import atom
import gdata
# XML namespaces which are often used in Google Apps entity.
APPS_NAMESPACE = 'http://schemas.google.com/apps/2006'
APPS_TEMPLATE = '{http://schemas.google.com/apps/2006}%s'
class EmailList(atom.AtomBase):
"""The Google Apps EmailList element"""
_tag = 'emailList'
_namespace = APPS_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['name'] = 'name'
def __init__(self, name=None, extension_elements=None,
extension_attributes=None, text=None):
self.name = name
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def EmailListFromString(xml_string):
return atom.CreateClassFromXMLString(EmailList, xml_string)
class Who(atom.AtomBase):
"""The Google Apps Who element"""
_tag = 'who'
_namespace = gdata.GDATA_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['rel'] = 'rel'
_attributes['email'] = 'email'
def __init__(self, rel=None, email=None, extension_elements=None,
extension_attributes=None, text=None):
self.rel = rel
self.email = email
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def WhoFromString(xml_string):
return atom.CreateClassFromXMLString(Who, xml_string)
class Login(atom.AtomBase):
"""The Google Apps Login element"""
_tag = 'login'
_namespace = APPS_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['userName'] = 'user_name'
_attributes['password'] = 'password'
_attributes['suspended'] = 'suspended'
_attributes['admin'] = 'admin'
_attributes['changePasswordAtNextLogin'] = 'change_password'
_attributes['agreedToTerms'] = 'agreed_to_terms'
_attributes['ipWhitelisted'] = 'ip_whitelisted'
_attributes['hashFunctionName'] = 'hash_function_name'
def __init__(self, user_name=None, password=None, suspended=None,
ip_whitelisted=None, hash_function_name=None,
admin=None, change_password=None, agreed_to_terms=None,
extension_elements=None, extension_attributes=None,
text=None):
self.user_name = user_name
self.password = password
self.suspended = suspended
self.admin = admin
self.change_password = change_password
self.agreed_to_terms = agreed_to_terms
self.ip_whitelisted = ip_whitelisted
self.hash_function_name = hash_function_name
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def LoginFromString(xml_string):
return atom.CreateClassFromXMLString(Login, xml_string)
class Quota(atom.AtomBase):
"""The Google Apps Quota element"""
_tag = 'quota'
_namespace = APPS_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['limit'] = 'limit'
def __init__(self, limit=None, extension_elements=None,
extension_attributes=None, text=None):
self.limit = limit
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def QuotaFromString(xml_string):
return atom.CreateClassFromXMLString(Quota, xml_string)
class Name(atom.AtomBase):
"""The Google Apps Name element"""
_tag = 'name'
_namespace = APPS_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['familyName'] = 'family_name'
_attributes['givenName'] = 'given_name'
def __init__(self, family_name=None, given_name=None,
extension_elements=None, extension_attributes=None, text=None):
self.family_name = family_name
self.given_name = given_name
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def NameFromString(xml_string):
return atom.CreateClassFromXMLString(Name, xml_string)
class Nickname(atom.AtomBase):
"""The Google Apps Nickname element"""
_tag = 'nickname'
_namespace = APPS_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['name'] = 'name'
def __init__(self, name=None,
extension_elements=None, extension_attributes=None, text=None):
self.name = name
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def NicknameFromString(xml_string):
return atom.CreateClassFromXMLString(Nickname, xml_string)
class NicknameEntry(gdata.GDataEntry):
"""A Google Apps flavor of an Atom Entry for Nickname"""
_tag = 'entry'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataEntry._children.copy()
_attributes = gdata.GDataEntry._attributes.copy()
_children['{%s}login' % APPS_NAMESPACE] = ('login', Login)
_children['{%s}nickname' % APPS_NAMESPACE] = ('nickname', Nickname)
def __init__(self, author=None, category=None, content=None,
atom_id=None, link=None, published=None,
title=None, updated=None,
login=None, nickname=None,
extended_property=None,
extension_elements=None, extension_attributes=None, text=None):
gdata.GDataEntry.__init__(self, author=author, category=category,
content=content,
atom_id=atom_id, link=link, published=published,
title=title, updated=updated)
self.login = login
self.nickname = nickname
self.extended_property = extended_property or []
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def NicknameEntryFromString(xml_string):
return atom.CreateClassFromXMLString(NicknameEntry, xml_string)
class NicknameFeed(gdata.GDataFeed, gdata.LinkFinder):
"""A Google Apps Nickname feed flavor of an Atom Feed"""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataFeed._children.copy()
_attributes = gdata.GDataFeed._attributes.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [NicknameEntry])
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None,
entry=None, total_results=None, start_index=None,
items_per_page=None, extension_elements=None,
extension_attributes=None, text=None):
gdata.GDataFeed.__init__(self, author=author, category=category,
contributor=contributor, generator=generator,
icon=icon, atom_id=atom_id, link=link,
logo=logo, rights=rights, subtitle=subtitle,
title=title, updated=updated, entry=entry,
total_results=total_results,
start_index=start_index,
items_per_page=items_per_page,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def NicknameFeedFromString(xml_string):
return atom.CreateClassFromXMLString(NicknameFeed, xml_string)
class UserEntry(gdata.GDataEntry):
"""A Google Apps flavor of an Atom Entry"""
_tag = 'entry'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataEntry._children.copy()
_attributes = gdata.GDataEntry._attributes.copy()
_children['{%s}login' % APPS_NAMESPACE] = ('login', Login)
_children['{%s}name' % APPS_NAMESPACE] = ('name', Name)
_children['{%s}quota' % APPS_NAMESPACE] = ('quota', Quota)
# This child may already be defined in GDataEntry, confirm before removing.
_children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
[gdata.FeedLink])
_children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who)
def __init__(self, author=None, category=None, content=None,
atom_id=None, link=None, published=None,
title=None, updated=None,
login=None, name=None, quota=None, who=None, feed_link=None,
extended_property=None,
extension_elements=None, extension_attributes=None, text=None):
gdata.GDataEntry.__init__(self, author=author, category=category,
content=content,
atom_id=atom_id, link=link, published=published,
title=title, updated=updated)
self.login = login
self.name = name
self.quota = quota
self.who = who
self.feed_link = feed_link or []
self.extended_property = extended_property or []
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def UserEntryFromString(xml_string):
return atom.CreateClassFromXMLString(UserEntry, xml_string)
class UserFeed(gdata.GDataFeed, gdata.LinkFinder):
"""A Google Apps User feed flavor of an Atom Feed"""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataFeed._children.copy()
_attributes = gdata.GDataFeed._attributes.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [UserEntry])
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None,
entry=None, total_results=None, start_index=None,
items_per_page=None, extension_elements=None,
extension_attributes=None, text=None):
gdata.GDataFeed.__init__(self, author=author, category=category,
contributor=contributor, generator=generator,
icon=icon, atom_id=atom_id, link=link,
logo=logo, rights=rights, subtitle=subtitle,
title=title, updated=updated, entry=entry,
total_results=total_results,
start_index=start_index,
items_per_page=items_per_page,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def UserFeedFromString(xml_string):
return atom.CreateClassFromXMLString(UserFeed, xml_string)
class EmailListEntry(gdata.GDataEntry):
"""A Google Apps EmailList flavor of an Atom Entry"""
_tag = 'entry'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataEntry._children.copy()
_attributes = gdata.GDataEntry._attributes.copy()
_children['{%s}emailList' % APPS_NAMESPACE] = ('email_list', EmailList)
# Might be able to remove this _children entry.
_children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
[gdata.FeedLink])
def __init__(self, author=None, category=None, content=None,
atom_id=None, link=None, published=None,
title=None, updated=None,
email_list=None, feed_link=None,
extended_property=None,
extension_elements=None, extension_attributes=None, text=None):
gdata.GDataEntry.__init__(self, author=author, category=category,
content=content,
atom_id=atom_id, link=link, published=published,
title=title, updated=updated)
self.email_list = email_list
self.feed_link = feed_link or []
self.extended_property = extended_property or []
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def EmailListEntryFromString(xml_string):
return atom.CreateClassFromXMLString(EmailListEntry, xml_string)
class EmailListFeed(gdata.GDataFeed, gdata.LinkFinder):
"""A Google Apps EmailList feed flavor of an Atom Feed"""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataFeed._children.copy()
_attributes = gdata.GDataFeed._attributes.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [EmailListEntry])
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None,
entry=None, total_results=None, start_index=None,
items_per_page=None, extension_elements=None,
extension_attributes=None, text=None):
gdata.GDataFeed.__init__(self, author=author, category=category,
contributor=contributor, generator=generator,
icon=icon, atom_id=atom_id, link=link,
logo=logo, rights=rights, subtitle=subtitle,
title=title, updated=updated, entry=entry,
total_results=total_results,
start_index=start_index,
items_per_page=items_per_page,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def EmailListFeedFromString(xml_string):
return atom.CreateClassFromXMLString(EmailListFeed, xml_string)
class EmailListRecipientEntry(gdata.GDataEntry):
"""A Google Apps EmailListRecipient flavor of an Atom Entry"""
_tag = 'entry'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataEntry._children.copy()
_attributes = gdata.GDataEntry._attributes.copy()
_children['{%s}who' % gdata.GDATA_NAMESPACE] = ('who', Who)
def __init__(self, author=None, category=None, content=None,
atom_id=None, link=None, published=None,
title=None, updated=None,
who=None,
extended_property=None,
extension_elements=None, extension_attributes=None, text=None):
gdata.GDataEntry.__init__(self, author=author, category=category,
content=content,
atom_id=atom_id, link=link, published=published,
title=title, updated=updated)
self.who = who
self.extended_property = extended_property or []
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def EmailListRecipientEntryFromString(xml_string):
return atom.CreateClassFromXMLString(EmailListRecipientEntry, xml_string)
class EmailListRecipientFeed(gdata.GDataFeed, gdata.LinkFinder):
"""A Google Apps EmailListRecipient feed flavor of an Atom Feed"""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataFeed._children.copy()
_attributes = gdata.GDataFeed._attributes.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
[EmailListRecipientEntry])
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None,
entry=None, total_results=None, start_index=None,
items_per_page=None, extension_elements=None,
extension_attributes=None, text=None):
gdata.GDataFeed.__init__(self, author=author, category=category,
contributor=contributor, generator=generator,
icon=icon, atom_id=atom_id, link=link,
logo=logo, rights=rights, subtitle=subtitle,
title=title, updated=updated, entry=entry,
total_results=total_results,
start_index=start_index,
items_per_page=items_per_page,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def EmailListRecipientFeedFromString(xml_string):
return atom.CreateClassFromXMLString(EmailListRecipientFeed, xml_string)
class Property(atom.AtomBase):
"""The Google Apps Property element"""
_tag = 'property'
_namespace = APPS_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
_attributes['name'] = 'name'
_attributes['value'] = 'value'
def __init__(self, name=None, value=None, extension_elements=None,
extension_attributes=None, text=None):
self.name = name
self.value = value
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def PropertyFromString(xml_string):
return atom.CreateClassFromXMLString(Property, xml_string)
class PropertyEntry(gdata.GDataEntry):
"""A Google Apps Property flavor of an Atom Entry"""
_tag = 'entry'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataEntry._children.copy()
_attributes = gdata.GDataEntry._attributes.copy()
_children['{%s}property' % APPS_NAMESPACE] = ('property', [Property])
def __init__(self, author=None, category=None, content=None,
atom_id=None, link=None, published=None,
title=None, updated=None,
property=None,
extended_property=None,
extension_elements=None, extension_attributes=None, text=None):
gdata.GDataEntry.__init__(self, author=author, category=category,
content=content,
atom_id=atom_id, link=link, published=published,
title=title, updated=updated)
self.property = property
self.extended_property = extended_property or []
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
def PropertyEntryFromString(xml_string):
return atom.CreateClassFromXMLString(PropertyEntry, xml_string)
class PropertyFeed(gdata.GDataFeed, gdata.LinkFinder):
"""A Google Apps Property feed flavor of an Atom Feed"""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.GDataFeed._children.copy()
_attributes = gdata.GDataFeed._attributes.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [PropertyEntry])
def __init__(self, author=None, category=None, contributor=None,
generator=None, icon=None, atom_id=None, link=None, logo=None,
rights=None, subtitle=None, title=None, updated=None,
entry=None, total_results=None, start_index=None,
items_per_page=None, extension_elements=None,
extension_attributes=None, text=None):
gdata.GDataFeed.__init__(self, author=author, category=category,
contributor=contributor, generator=generator,
icon=icon, atom_id=atom_id, link=link,
logo=logo, rights=rights, subtitle=subtitle,
title=title, updated=updated, entry=entry,
total_results=total_results,
start_index=start_index,
items_per_page=items_per_page,
extension_elements=extension_elements,
extension_attributes=extension_attributes,
text=text)
def PropertyFeedFromString(xml_string):
return atom.CreateClassFromXMLString(PropertyFeed, xml_string)

View File

@@ -1 +0,0 @@

View File

@@ -1,278 +0,0 @@
#!/usr/bin/env python
# Copyright (C) 2008 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Allow Google Apps domain administrators to audit user data.
AuditService: Set auditing."""
__author__ = 'jlee@pbu.edu'
from base64 import b64encode
import gdata.apps
import gdata.apps.service
import gdata.service
class AuditService(gdata.apps.service.PropertyService):
"""Client for the Google Apps Audit service."""
def _serviceUrl(self, setting_id, domain=None, user=None):
if domain is None:
domain = self.domain
if user is None:
return '/a/feeds/compliance/audit/%s/%s' % (setting_id, domain)
else:
return '/a/feeds/compliance/audit/%s/%s/%s' % (setting_id, domain, user)
def updatePGPKey(self, pgpkey):
"""Updates Public PGP Key Google uses to encrypt audit data
Args:
pgpkey: string, ASCII text of PGP Public Key to be used
Returns:
A dict containing the result of the POST operation."""
uri = self._serviceUrl('publickey')
b64pgpkey = b64encode(pgpkey)
properties = {}
properties['publicKey'] = b64pgpkey
return self._PostProperties(uri, properties)
def createEmailMonitor(self, source_user, destination_user, end_date,
begin_date=None, incoming_headers_only=False,
outgoing_headers_only=False, drafts=False,
drafts_headers_only=False, chats=False,
chats_headers_only=False):
"""Creates a email monitor, forwarding the source_users emails/chats
Args:
source_user: string, the user whose email will be audited
destination_user: string, the user to receive the audited email
end_date: string, the date the audit will end in
"yyyy-MM-dd HH:mm" format, required
begin_date: string, the date the audit will start in
"yyyy-MM-dd HH:mm" format, leave blank to use current time
incoming_headers_only: boolean, whether to audit only the headers of
mail delivered to source user
outgoing_headers_only: boolean, whether to audit only the headers of
mail sent from the source user
drafts: boolean, whether to audit draft messages of the source user
drafts_headers_only: boolean, whether to audit only the headers of
mail drafts saved by the user
chats: boolean, whether to audit archived chats of the source user
chats_headers_only: boolean, whether to audit only the headers of
archived chats of the source user
Returns:
A dict containing the result of the POST operation."""
uri = self._serviceUrl('mail/monitor', user=source_user)
properties = {}
properties['destUserName'] = destination_user
if begin_date is not None:
properties['beginDate'] = begin_date
properties['endDate'] = end_date
if incoming_headers_only:
properties['incomingEmailMonitorLevel'] = 'HEADER_ONLY'
else:
properties['incomingEmailMonitorLevel'] = 'FULL_MESSAGE'
if outgoing_headers_only:
properties['outgoingEmailMonitorLevel'] = 'HEADER_ONLY'
else:
properties['outgoingEmailMonitorLevel'] = 'FULL_MESSAGE'
if drafts:
if drafts_headers_only:
properties['draftMonitorLevel'] = 'HEADER_ONLY'
else:
properties['draftMonitorLevel'] = 'FULL_MESSAGE'
if chats:
if chats_headers_only:
properties['chatMonitorLevel'] = 'HEADER_ONLY'
else:
properties['chatMonitorLevel'] = 'FULL_MESSAGE'
return self._PostProperties(uri, properties)
def getEmailMonitors(self, user):
""""Gets the email monitors for the given user
Args:
user: string, the user to retrieve email monitors for
Returns:
list results of the POST operation
"""
uri = self._serviceUrl('mail/monitor', user=user)
return self._GetPropertiesList(uri)
def deleteEmailMonitor(self, source_user, destination_user):
"""Deletes the email monitor for the given user
Args:
source_user: string, the user who is being monitored
destination_user: string, theuser who recieves the monitored emails
Returns:
Nothing
"""
uri = self._serviceUrl('mail/monitor', user=source_user+'/'+destination_user)
try:
return self._DeleteProperties(uri)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def createAccountInformationRequest(self, user):
"""Creates a request for account auditing details
Args:
user: string, the user to request account information for
Returns:
A dict containing the result of the post operation."""
uri = self._serviceUrl('account', user=user)
properties = {}
#XML Body is left empty
try:
return self._PostProperties(uri, properties)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def getAccountInformationRequestStatus(self, user, request_id):
"""Gets the status of an account auditing request
Args:
user: string, the user whose account auditing details were requested
request_id: string, the request_id
Returns:
A dict containing the result of the get operation."""
uri = self._serviceUrl('account', user=user+'/'+request_id)
try:
return self._GetProperties(uri)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def getAllAccountInformationRequestsStatus(self):
"""Gets the status of all account auditing requests for the domain
Args:
None
Returns:
list results of the POST operation
"""
uri = self._serviceUrl('account')
return self._GetPropertiesList(uri)
def deleteAccountInformationRequest(self, user, request_id):
"""Deletes the request for account auditing information
Args:
user: string, the user whose account auditing details were requested
request_id: string, the request_id
Returns:
Nothing
"""
uri = self._serviceUrl('account', user=user+'/'+request_id)
try:
return self._DeleteProperties(uri)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def createMailboxExportRequest(self, user, begin_date=None, end_date=None, include_deleted=False, search_query=None, headers_only=False):
"""Creates a mailbox export request
Args:
user: string, the user whose mailbox export is being requested
begin_date: string, date of earliest emails to export, optional, defaults to date of account creation
format is 'yyyy-MM-dd HH:mm'
end_date: string, date of latest emails to export, optional, defaults to current date
format is 'yyyy-MM-dd HH:mm'
include_deleted: boolean, whether to include deleted emails in export, mutually exclusive with search_query
search_query: string, gmail style search query, matched emails will be exported, mutually exclusive with include_deleted
Returns:
A dict containing the result of the post operation."""
uri = self._serviceUrl('mail/export', user=user)
properties = {}
if begin_date is not None:
properties['beginDate'] = begin_date
if end_date is not None:
properties['endDate'] = end_date
if include_deleted is not None:
properties['includeDeleted'] = gdata.apps.service._bool2str(include_deleted)
if search_query is not None:
properties['searchQuery'] = search_query
if headers_only is True:
properties['packageContent'] = 'HEADER_ONLY'
else:
properties['packageContent'] = 'FULL_MESSAGE'
return self._PostProperties(uri, properties)
def getMailboxExportRequestStatus(self, user, request_id):
"""Gets the status of an mailbox export request
Args:
user: string, the user whose mailbox were requested
request_id: string, the request_id
Returns:
A dict containing the result of the get operation."""
uri = self._serviceUrl('mail/export', user=user+'/'+request_id)
try:
return self._GetProperties(uri)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def getAllMailboxExportRequestsStatus(self):
"""Gets the status of all mailbox export requests for the domain
Args:
None
Returns:
list results of the POST operation
"""
uri = self._serviceUrl('mail/export')
return self._GetPropertiesList(uri)
def deleteMailboxExportRequest(self, user, request_id):
"""Deletes the request for mailbox export
Args:
user: string, the user whose mailbox were requested
request_id: string, the request_id
Returns:
Nothing
"""
uri = self._serviceUrl('mail/export', user=user+'/'+request_id)
try:
return self._DeleteProperties(uri)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])

View File

@@ -1,874 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2009 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Data model classes for parsing and generating XML for the Contacts API."""
import atom
import gdata
## Constants from http://code.google.com/apis/gdata/elements.html ##
REL_HOME = 'http://schemas.google.com/g/2005#home'
REL_WORK = 'http://schemas.google.com/g/2005#work'
REL_OTHER = 'http://schemas.google.com/g/2005#other'
IM_AIM = 'http://schemas.google.com/g/2005#AIM' # AOL Instant Messenger protocol
IM_MSN = 'http://schemas.google.com/g/2005#MSN' # MSN Messenger protocol
IM_YAHOO = 'http://schemas.google.com/g/2005#YAHOO' # Yahoo Messenger protocol
IM_SKYPE = 'http://schemas.google.com/g/2005#SKYPE' # Skype protocol
IM_QQ = 'http://schemas.google.com/g/2005#QQ' # QQ protocol
IM_GOOGLE_TALK = 'http://schemas.google.com/g/2005#GOOGLE_TALK' # Google Talk protocol
IM_ICQ = 'http://schemas.google.com/g/2005#ICQ' # ICQ protocol
IM_JABBER = 'http://schemas.google.com/g/2005#JABBER' # Jabber protocol
IM_NETMEETING = 'http://schemas.google.com/g/2005#NETMEETING' # NetMeeting
PHOTO_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#photo'
PHOTO_EDIT_LINK_REL = 'http://schemas.google.com/contacts/2008/rel#edit-photo'
# Different phone types, for more info see:
# http://code.google.com/apis/gdata/docs/2.0/elements.html#gdPhoneNumber
PHONE_ASSISTANT = 'http://schemas.google.com/g/2005#assistant'
PHONE_CALLBACK = 'http://schemas.google.com/g/2005#callback'
PHONE_CAR = 'http://schemas.google.com/g/2005#car'
PHONE_COMPANY_MAIN = 'http://schemas.google.com/g/2005#company_main'
PHONE_FAX = 'http://schemas.google.com/g/2005#fax'
PHONE_GENERAL = 'http://schemas.google.com/g/2005#general'
PHONE_HOME = REL_HOME
PHONE_HOME_FAX = 'http://schemas.google.com/g/2005#home_fax'
PHONE_INTERNAL = 'http://schemas.google.com/g/2005#internal-extension'
PHONE_ISDN = 'http://schemas.google.com/g/2005#isdn'
PHONE_MAIN = 'http://schemas.google.com/g/2005#main'
PHONE_MOBILE = 'http://schemas.google.com/g/2005#mobile'
PHONE_OTHER = REL_OTHER
PHONE_OTHER_FAX = 'http://schemas.google.com/g/2005#other_fax'
PHONE_PAGER = 'http://schemas.google.com/g/2005#pager'
PHONE_RADIO = 'http://schemas.google.com/g/2005#radio'
PHONE_SATELLITE = 'http://schemas.google.com/g/2005#satellite'
PHONE_TELEX = 'http://schemas.google.com/g/2005#telex'
PHONE_TTY_TDD = 'http://schemas.google.com/g/2005#tty_tdd'
PHONE_VOIP = 'http://schemas.google.com/g/2005#voip'
PHONE_WORK = REL_WORK
PHONE_WORK_FAX = 'http://schemas.google.com/g/2005#work_fax'
PHONE_WORK_MOBILE = 'http://schemas.google.com/g/2005#work_mobile'
PHONE_WORK_PAGER = 'http://schemas.google.com/g/2005#work_pager'
MAIL_BOTH = 'http://schemas.google.com/g/2005#both'
MAIL_LETTERS = 'http://schemas.google.com/g/2005#letters'
MAIL_PARCELS = 'http://schemas.google.com/g/2005#parcels'
MAIL_NEITHER = 'http://schemas.google.com/g/2005#neither'
GENERAL_ADDRESS = 'http://schemas.google.com/g/2005#general'
LOCAL_ADDRESS = 'http://schemas.google.com/g/2005#local'
EXTERNAL_ID_ORGANIZATION = 'organization'
RELATION_MANAGER = 'manager'
CONTACTS_NAMESPACE = 'http://schemas.google.com/contact/2008'
class GDataBase(atom.AtomBase):
"""The Google Contacts intermediate class from atom.AtomBase."""
_namespace = gdata.GDATA_NAMESPACE
_children = atom.AtomBase._children.copy()
_attributes = atom.AtomBase._attributes.copy()
def __init__(self, text=None):
atom.AtomBase.__init__(self, text=text)
class ContactsBase(GDataBase):
"""The Google Contacts intermediate class for Contacts namespace."""
_namespace = CONTACTS_NAMESPACE
class BillingInformation(ContactsBase):
"""The gContact:billingInformation element."""
_tag = 'billingInformation'
class Birthday(ContactsBase):
"""The gContact:birthday element."""
_tag = 'birthday'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['when'] = 'when'
def __init__(self, when=None):
ContactsBase.__init__(self)
self.when = when
class CalendarLink(ContactsBase):
"""The gContact:calendarLink element."""
_tag = 'calendarLink'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['href'] = 'href'
_attributes['label'] = 'label'
_attributes['primary'] = 'primary'
_attributes['rel'] = 'rel'
def __init__(self, href=None, label=None, primary='false', rel=None):
ContactsBase.__init__(self)
self.href = href
self.label = label
self.primary = primary
self.rel = rel
class Content(atom.AtomBase):
"""The Google Contacts Content element."""
_tag = 'content'
_namespace = atom.ATOM_NAMESPACE
def __init__(self, text=None):
atom.AtomBase.__init__(self, text=text)
class DirectoryServer(ContactsBase):
"""The gContact:directoryServer element."""
_tag = 'directoryServer'
class Email(GDataBase):
"""The gd:email element."""
_tag = 'email'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['address'] = 'address'
_attributes['primary'] = 'primary'
_attributes['rel'] = 'rel'
_attributes['label'] = 'label'
def __init__(self, label=None, rel=None, address=None, primary='false'):
GDataBase.__init__(self)
self.label = label
self.rel = rel
self.address = address
self.primary = primary
class When(GDataBase):
"""The Google Contacts when element."""
_tag = 'when'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['startTime'] = 'startTime'
_attributes['label'] = 'label'
def __init__(self, startTime=None, label=None):
GDataBase.__init__(self)
self.startTime = startTime
self.label = label
class Event(ContactsBase):
"""The gContact:event element."""
_tag = 'event'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
_children['{%s}when' % GDataBase._namespace] = ('when', When)
def __init__(self, label=None, rel=None, when=None):
ContactsBase.__init__(self)
self.label = label
self.rel = rel
self.when = when
def EventFromString(xml_string):
return atom.CreateClassFromXMLString(Event, xml_string)
class ExternalId(ContactsBase):
"""The gContact:externalId element."""
_tag = 'externalId'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
_attributes['value'] = 'value'
def __init__(self, label=None, rel=None, value=None):
ContactsBase.__init__(self)
self.label = label
self.rel = rel
self.value = value
def ExternalIdFromString(xml_string):
return atom.CreateClassFromXMLString(ExternalId, xml_string)
class Gender(ContactsBase):
"""The gContact:gender element."""
_tag = 'gender'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['value'] = 'value'
def __init__(self, value=None):
ContactsBase.__init__(self)
self.value = value
class Hobby(ContactsBase):
"""The gContact:hobby element."""
_tag = 'hobby'
class IM(GDataBase):
"""The gd:im element."""
_tag = 'im'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['address'] = 'address'
_attributes['primary'] = 'primary'
_attributes['protocol'] = 'protocol'
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
def __init__(self, primary='false', rel=None, address=None, protocol=None, label=None):
GDataBase.__init__(self)
self.protocol = protocol
self.address = address
self.primary = primary
self.rel = rel
self.label = label
class Initials(ContactsBase):
"""The gContact:initials element."""
_tag = 'initials'
class Jot(ContactsBase):
"""The gContact:jot element."""
_tag = 'jot'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['rel'] = 'rel'
def __init__(self, rel=None, text=None):
ContactsBase.__init__(self, text=text)
self.rel = rel
class Language(ContactsBase):
"""The gContact:language element."""
_tag = 'language'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['code'] = 'code'
_attributes['label'] = 'label'
def __init__(self, code=None, label=None):
ContactsBase.__init__(self)
self.code = code
self.label = label
class MaidenName(ContactsBase):
"""The gContact:maidenName element."""
_tag = 'maidenName'
class Mileage(ContactsBase):
"""The gContact:mileage element."""
_tag = 'mileage'
class NamePrefix(GDataBase):
"""The gd:namePrefix element."""
_tag = 'namePrefix'
class GivenName(GDataBase):
"""The gd:givenName element."""
_tag = 'givenName'
class AdditionalName(GDataBase):
"""The gd:additionalName element."""
_tag = 'additionalName'
class FamilyName(GDataBase):
"""The gd:familyName element."""
_tag = 'familyName'
class NameSuffix(GDataBase):
"""The gd:nameSuffix element."""
_tag = 'nameSuffix'
class FullName(GDataBase):
"""The gd:fullName element."""
_tag = 'fullName'
class Name(GDataBase):
"""The gd:name element."""
_tag = 'name'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_children['{%s}namePrefix' % GDataBase._namespace] = ('name_prefix', NamePrefix)
_children['{%s}givenName' % GDataBase._namespace] = ('given_name', GivenName)
_children['{%s}additionalName' % GDataBase._namespace] = ('additional_name', AdditionalName)
_children['{%s}familyName' % GDataBase._namespace] = ('family_name', FamilyName)
_children['{%s}nameSuffix' % GDataBase._namespace] = ('name_suffix', NameSuffix)
_children['{%s}fullName' % GDataBase._namespace] = ('full_name', FullName)
def __init__(self, given_name=None, additional_name=None, family_name=None,
name_prefix=None, name_suffix=None, full_name=None,):
GDataBase.__init__(self)
self.given_name = given_name
self.additional_name = additional_name
self.family_name = family_name
self.name_prefix = name_prefix
self.name_suffix = name_suffix
self.full_name = full_name
class Nickname(ContactsBase):
"""The gContact:nickname element."""
_tag = 'nickname'
class Occupation(ContactsBase):
"""The gContact:occupation element."""
_tag = 'occupation'
class PhoneNumber(GDataBase):
"""The gd:phoneNumber element."""
_tag = 'phoneNumber'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
_attributes['uri'] = 'uri'
_attributes['primary'] = 'primary'
def __init__(self, label=None, rel=None, uri=None, primary='false', text=None):
GDataBase.__init__(self, text=text)
self.label = label
self.rel = rel
self.uri = uri
self.primary = primary
class OrgName(GDataBase):
"""The gd:orgName element."""
_tag = 'orgName'
class OrgTitle(GDataBase):
"""The gd:orgTitle element."""
_tag = 'orgTitle'
class OrgSymbol(GDataBase):
"""The gd:orgSymbol element."""
_tag = 'orgSymbol'
class OrgDepartment(GDataBase):
"""The gd:orgDepartment element."""
_tag = 'orgDepartment'
class OrgJobDescription(GDataBase):
"""The gd:orgJobDescription element."""
_tag = 'orgJobDescription'
class Where(GDataBase):
"""The gd:where element."""
_tag = 'where'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['valueString'] = 'value_string'
def __init__(self, value_string=None):
GDataBase.__init__(self)
self.value_string = value_string
class Organization(GDataBase):
"""The gd:organization element."""
_tag = 'organization'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
_attributes['primary'] = 'primary'
_children['{%s}orgName' % GDataBase._namespace] = ('name', OrgName)
_children['{%s}orgSymbol' % GDataBase._namespace] = ('symbol', OrgSymbol)
_children['{%s}orgTitle' % GDataBase._namespace] = ('title', OrgTitle)
_children['{%s}orgDepartment' % GDataBase._namespace] = ('department', OrgDepartment)
_children['{%s}orgJobDescription' % GDataBase._namespace] = ('job_description', OrgJobDescription)
_children['{%s}where' % GDataBase._namespace] = ('where', Where)
def __init__(self, label=None, rel=None, primary='false', name=None,
title=None, symbol=None, department=None, job_description=None, where=None,):
GDataBase.__init__(self)
self.label = label
self.rel = rel
self.primary = primary
self.name = name
self.symbol = symbol
self.title = title
self.department = department
self.job_description = job_description
self.where = where
class PostalAddress(GDataBase):
"""The gd:postalAddress element."""
_tag = 'postalAddress'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
_attributes['primary'] = 'primary'
def __init__(self, primary=None, rel=None, label=None, text=None):
GDataBase.__init__(self, text=text)
self.label = label
self.rel = rel
self.primary = primary
class Priority(ContactsBase):
"""The gContact:priority element."""
_tag = 'priority'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['rel'] = 'rel'
def __init__(self, rel=None):
ContactsBase.__init__(self)
self.rel = rel
class Relation(ContactsBase):
"""The gContact:relation element."""
_tag = 'relation'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
def __init__(self, label=None, rel=None, text=None):
ContactsBase.__init__(self, text=text)
self.label = label
self.rel = rel
def RelationFromString(xml_string):
return atom.CreateClassFromXMLString(Relation, xml_string)
class Sensitivity(ContactsBase):
"""The gContact:sensitivity element."""
_tag = 'sensitivity'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['rel'] = 'rel'
def __init__(self, rel=None):
ContactsBase.__init__(self)
self.rel = rel
class ShortName(ContactsBase):
"""The gContact:shortName element."""
_tag = 'shortName'
class Street(GDataBase):
"""The gd:street element.
Can be street, avenue, road, etc. This element also includes the
house number and room/apartment/flat/floor number.
"""
_tag = 'street'
class PoBox(GDataBase):
"""The gd:pobox element.
Covers actual P.O. boxes, drawers, locked bags, etc. This is usually
but not always mutually exclusive with street.
"""
_tag = 'pobox'
class Neighborhood(GDataBase):
"""The gd:neighborhood element.
This is used to disambiguate a street address when a city contains more
than one street with the same name, or to specify a small place whose
mail is routed through a larger postal town. In China it could be a
county or a minor city.
"""
_tag = 'neighborhood'
class City(GDataBase):
"""The gd:city element.
Can be city, village, town, borough, etc. This is the postal town and
not necessarily the place of residence or place of business.
"""
_tag = 'city'
class Region(GDataBase):
"""The gd:region element.
A state, province, county (in Ireland), Land (in Germany),
departement (in France), etc.
"""
_tag = 'region'
class Postcode(GDataBase):
"""The gd:postcode element.
Postal code. Usually country-wide, but sometimes specific to the
city (e.g. "2" in "Dublin 2, Ireland" addresses).
"""
_tag = 'postcode'
class Country(GDataBase):
"""The gd:country element.
The name or code of the country.
"""
_tag = 'country'
class FormattedAddress(GDataBase):
"""The gd:formattedAddress element."""
_tag = 'formattedAddress'
class StructuredPostalAddress(GDataBase):
"""The gd:structuredPostalAddress element."""
_tag = 'structuredPostalAddress'
_children = GDataBase._children.copy()
_attributes = GDataBase._attributes.copy()
_attributes['label'] = 'label'
_attributes['rel'] = 'rel'
_attributes['primary'] = 'primary'
_children['{%s}street' % GDataBase._namespace] = ('street', Street)
_children['{%s}pobox' % GDataBase._namespace] = ('pobox', PoBox)
_children['{%s}neighborhood' % GDataBase._namespace] = ('neighborhood', Neighborhood)
_children['{%s}city' % GDataBase._namespace] = ('city', City)
_children['{%s}region' % GDataBase._namespace] = ('region', Region)
_children['{%s}postcode' % GDataBase._namespace] = ('postcode', Postcode)
_children['{%s}country' % GDataBase._namespace] = ('country', Country)
_children['{%s}formattedAddress' % GDataBase._namespace] = ('formatted_address', FormattedAddress)
def __init__(self, rel=None, label=None, primary='false',
street=None,
pobox=None,
neighborhood=None,
city=None,
region=None,
postcode=None,
country=None,
formatted_address=None):
GDataBase.__init__(self)
self.label = label
self.rel = rel
self.primary = primary
self.street = street
self.pobox = pobox
self.neighborhood = neighborhood
self.city = city
self.region = region
self.postcode = postcode
self.country = country
self.formatted_address = formatted_address
class Subject(ContactsBase):
"""The gContact:Subject element."""
_tag = 'subject'
class UserDefinedField(ContactsBase):
"""The gContact:userDefinedField element."""
_tag = 'userDefinedField'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['key'] = 'key'
_attributes['value'] = 'value'
def __init__(self, key=None, value=None):
ContactsBase.__init__(self)
self.key = key
self.value = value
def UserDefinedFieldFromString(xml_string):
return atom.CreateClassFromXMLString(UserDefinedField, xml_string)
class Website(ContactsBase):
"""The gContact:Website element."""
_tag = 'website'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['href'] = 'href'
_attributes['label'] = 'label'
_attributes['primary'] = 'primary'
_attributes['rel'] = 'rel'
def __init__(self, href=None, label=None, primary='false', rel=None):
ContactsBase.__init__(self)
self.href = href
self.label = label
self.primary = primary
self.rel = rel
def WebsiteFromString(xml_string):
return atom.CreateClassFromXMLString(Website, xml_string)
class PersonEntry(gdata.BatchEntry):
"""Base class for ContactEntry and ProfileEntry."""
_children = gdata.BatchEntry._children.copy()
_children['{%s}billingInformation' % CONTACTS_NAMESPACE] = ('billingInformation', BillingInformation)
_children['{%s}birthday' % CONTACTS_NAMESPACE] = ('birthday', Birthday)
_children['{%s}calendarLink' % CONTACTS_NAMESPACE] = ('calendarLink', [CalendarLink])
_children['{%s}content' % atom.ATOM_NAMESPACE] = ('content', Content)
_children['{%s}directoryServer' % CONTACTS_NAMESPACE] = ('directoryServer', DirectoryServer)
_children['{%s}email' % gdata.GDATA_NAMESPACE] = ('email', [Email])
_children['{%s}event' % CONTACTS_NAMESPACE] = ('event', [Event])
_children['{%s}externalId' % CONTACTS_NAMESPACE] = ('externalId', [ExternalId])
_children['{%s}gender' % CONTACTS_NAMESPACE] = ('gender', Gender)
_children['{%s}hobby' % CONTACTS_NAMESPACE] = ('hobby', [Hobby])
_children['{%s}im' % gdata.GDATA_NAMESPACE] = ('im', [IM])
_children['{%s}initials' % CONTACTS_NAMESPACE] = ('initials', Initials)
_children['{%s}jot' % CONTACTS_NAMESPACE] = ('jot', [Jot])
_children['{%s}language' % CONTACTS_NAMESPACE] = ('language', Language)
_children['{%s}maidenName' % CONTACTS_NAMESPACE] = ('maidenName', MaidenName)
_children['{%s}mileage' % CONTACTS_NAMESPACE] = ('mileage', Mileage)
_children['{%s}name' % gdata.GDATA_NAMESPACE] = ('name', Name)
_children['{%s}nickname' % CONTACTS_NAMESPACE] = ('nickname', Nickname)
_children['{%s}occupation' % CONTACTS_NAMESPACE] = ('occupation', Occupation)
_children['{%s}organization' % gdata.GDATA_NAMESPACE] = ('organization', [Organization])
_children['{%s}phoneNumber' % gdata.GDATA_NAMESPACE] = ('phoneNumber', [PhoneNumber])
_children['{%s}postalAddress' % gdata.GDATA_NAMESPACE] = ('postalAddress', [PostalAddress])
_children['{%s}priority' % CONTACTS_NAMESPACE] = ('priority', Priority)
_children['{%s}relation' % CONTACTS_NAMESPACE] = ('relation', [Relation])
_children['{%s}sensitivity' % CONTACTS_NAMESPACE] = ('sensitivity', Sensitivity)
_children['{%s}shortName' % CONTACTS_NAMESPACE] = ('shortName', ShortName)
_children['{%s}structuredPostalAddress' % gdata.GDATA_NAMESPACE] = ('structuredPostalAddress', [StructuredPostalAddress])
_children['{%s}subject' % CONTACTS_NAMESPACE] = ('subject', Subject)
_children['{%s}userDefinedField' % CONTACTS_NAMESPACE] = ('userDefinedField', [UserDefinedField])
_children['{%s}website' % CONTACTS_NAMESPACE] = ('website', [Website])
_children['{%s}where' % gdata.GDATA_NAMESPACE] = ('where', Where)
_attributes = gdata.BatchEntry._attributes.copy()
_attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag'
def __init__(self,
billingInformation=None,
birthday=None,
calendarLink=None,
content=None,
directoryServer=None,
email=None,
event=None,
externalId=None,
gender=None,
hobby=None,
im=None,
initials=None,
jot=None,
language=None,
maidenName=None,
mileage=None,
name=None,
nickname=None,
occupation=None,
organization=None,
phoneNumber=None,
postalAddress=None,
priority=None,
relation=None,
sensitivity=None,
shortName=None,
structuredPostalAddress=None,
subject=None,
text=None,
title=None,
userDefinedField=None,
website=None,
where=None,
etag=None):
gdata.BatchEntry.__init__(self)
self.billingInformation = billingInformation
self.birthday = birthday
self.calendarLink = calendarLink or []
self.content = content
self.directoryServer = directoryServer
self.email = email or []
self.event = event or []
self.externalId = externalId or []
self.gender = gender
self.hobby = hobby or []
self.im = im or []
self.initials = initials
self.jot = jot or []
self.language = language
self.maidenName = maidenName
self.mileage = mileage
self.name = name
self.nickname = nickname
self.occupation = occupation
self.organization = organization or []
self.phoneNumber = phoneNumber or []
self.postalAddress = postalAddress or []
self.priority = priority
self.relation = relation or []
self.sensitivity = sensitivity
self.shortName = shortName
self.structuredPostalAddress = structuredPostalAddress or []
self.subject = subject
self.text = text
self.userDefinedField = userDefinedField or []
self.website = website or []
self.where = where
self.extension_attributes = {}
self.extension_elements = []
self.etag = etag
class Deleted(GDataBase):
"""The gd:Deleted element."""
_tag = 'deleted'
class GroupMembershipInfo(ContactsBase):
"""The Google Contacts GroupMembershipInfo element."""
_tag = 'groupMembershipInfo'
_children = ContactsBase._children.copy()
_attributes = ContactsBase._attributes.copy()
_attributes['deleted'] = 'deleted'
_attributes['href'] = 'href'
def __init__(self, deleted=None, href=None, text=None):
ContactsBase.__init__(self, text=text)
self.deleted = deleted
self.href = href
class ContactEntry(PersonEntry):
"""Represents a contact."""
_tag = 'entry'
_namespace = atom.ATOM_NAMESPACE
_children = PersonEntry._children.copy()
_children['{%s}deleted' % gdata.GDATA_NAMESPACE] = ('deleted', Deleted)
_children['{%s}groupMembershipInfo' % CONTACTS_NAMESPACE] = ('groupMembershipInfo', [GroupMembershipInfo])
_children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ('extended_property', [gdata.ExtendedProperty])
# Overwrite the organization rule in PersonEntry so that a ContactEntry
# may only contain one <gd:organization> element.
#_children['{%s}organization' % gdata.GDATA_NAMESPACE] = ('organization', Organization)
def __init__(self,
billingInformation=None,
birthday=None,
calendarLink=None,
content=None,
deleted=None,
directoryServer=None,
email=None,
event=None,
extended_property=None,
externalId=None,
gender=None,
groupMembershipInfo=None,
hobby=None,
im=None,
initials=None,
jot=None,
language=None,
maidenName=None,
mileage=None,
name=None,
nickname=None,
occupation=None,
organization=None,
phoneNumber=None,
postalAddress=None,
priority=None,
relation=None,
sensitivity=None,
shortName=None,
structuredPostalAddress=None,
subject=None,
text=None,
title=None,
userDefinedField=None,
website=None,
where=None,
etag=None):
PersonEntry.__init__(self,
billingInformation=billingInformation,
birthday=birthday,
calendarLink=calendarLink,
content=content,
directoryServer=directoryServer,
email=email,
event=event,
externalId=externalId,
gender=gender,
hobby=hobby,
im=im,
initials=initials,
jot=jot,
language=language,
maidenName=maidenName,
mileage=mileage,
name=name,
nickname=nickname,
occupation=occupation,
organization=organization,
phoneNumber=phoneNumber,
postalAddress=postalAddress,
priority=priority,
relation=relation,
sensitivity=sensitivity,
shortName=shortName,
structuredPostalAddress=structuredPostalAddress,
subject=subject,
text=text,
title=title,
userDefinedField=userDefinedField,
website=website,
where=where,
etag=etag)
self.deleted = deleted
self.extended_property = extended_property or []
self.groupMembershipInfo = groupMembershipInfo or []
def GetPhotoLink(self):
for a_link in self.link:
if a_link.rel == PHOTO_LINK_REL:
return a_link
return None
def GetPhotoEditLink(self):
for a_link in self.link:
if a_link.rel == PHOTO_EDIT_LINK_REL:
return a_link
return None
def ContactEntryFromString(xml_string):
return atom.CreateClassFromXMLString(ContactEntry, xml_string)
class ContactsFeed(gdata.BatchFeed, gdata.LinkFinder):
"""A Google contacts feed flavor of an Atom Feed."""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.BatchFeed._children.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [ContactEntry])
def __init__(self):
gdata.BatchFeed.__init__(self)
def ContactsFeedFromString(xml_string):
return atom.CreateClassFromXMLString(ContactsFeed, xml_string)
class GroupEntry(gdata.BatchEntry):
"""Represents a contact group."""
_children = gdata.BatchEntry._children.copy()
_children['{%s}deleted' % gdata.GDATA_NAMESPACE] = ('deleted', Deleted)
_children['{%s}extendedProperty' % gdata.GDATA_NAMESPACE] = ('extended_property', [gdata.ExtendedProperty])
_attributes = gdata.BatchEntry._attributes.copy()
_attributes['{%s}etag' % gdata.GDATA_NAMESPACE] = 'etag'
def __init__(self,
title=None,
deleted=None,
extended_property=None,
etag=None):
gdata.BatchEntry.__init__(self)
self.title = title
self.deleted = deleted
self.extended_property = extended_property or []
self.etag = etag
def GroupEntryFromString(xml_string):
return atom.CreateClassFromXMLString(GroupEntry, xml_string)
class GroupsFeed(gdata.BatchFeed):
"""A Google contact groups feed flavor of an Atom Feed."""
_tag = 'feed'
_namespace = atom.ATOM_NAMESPACE
_children = gdata.BatchFeed._children.copy()
_children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [GroupEntry])
def __init__(self):
gdata.BatchFeed.__init__(self)
def GroupsFeedFromString(xml_string):
return atom.CreateClassFromXMLString(GroupsFeed, xml_string)

View File

@@ -1,355 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2009 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""ContactsService extends the GDataService for Google Contacts operations.
ContactsService: Provides methods to query feeds and manipulate items.
Extends GDataService.
"""
import gdata.apps
import gdata.apps.service
import gdata.service
class ContactsService(gdata.service.GDataService):
"""Client for the Google Contacts service."""
def __init__(self, email=None, password=None, source=None,
server='www.google.com', additional_headers=None,
contact_list='default', contactFeed=True, **kwargs):
"""Creates a client for the Contacts service.
Args:
email: string (optional) The user's email address, used for
authentication.
password: string (optional) The user's password.
source: string (optional) The name of the user's application.
server: string (optional) The name of the server to which a connection
will be opened. Default value: 'www.google.com'.
contact_list: string (optional) The name of the default contact list to
use when no URI is specified to the methods of the service.
Default value: 'default' (the logged in user's contact list).
contactFeed: Boolean (optional) Is this contacts feed or a gal feed
Default value: True (the logged in user's contact list).
**kwargs: The other parameters to pass to gdata.service.GDataService
constructor.
"""
self.contact_list = contact_list
self.feed_type = ['gal', 'contacts'][contactFeed]
if additional_headers == None:
additional_headers = {}
additional_headers['GData-Version'] = ['1.1', '3.1'][contactFeed]
gdata.service.GDataService.__init__(self,
email=email, password=password, service='cp', source=source,
server=server, additional_headers=additional_headers, **kwargs)
self.ssl = True
self.port = 443
def _CleanUri(self, uri):
"""Sanitizes a feed URI.
Args:
uri: The URI to sanitize, can be relative or absolute.
Returns:
The given URI without its https://server prefix, if any.
Keeps the leading slash of the URI.
"""
url_prefix = 'https://%s' % self.server
if uri.startswith(url_prefix):
uri = uri[len(url_prefix):]
return uri
def GetContactFeedUri(self, contact_list=None, projection='full', contactId=None):
"""Builds a contact feed URI. """
contact_list = contact_list or self.contact_list
uri = 'https://{0}/m8/feeds/{1}/{2}/{3}'.format(self.server, self.feed_type, contact_list, projection)
if contactId:
uri += '/{0}'.format(contactId)
return uri
def GetContactsFeed(self, uri=None,
extra_headers=None, url_params=None, escape_params=True):
uri = uri or self.GetContactFeedUri()
try:
return self.Get(uri,
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
converter=gdata.apps.contacts.ContactsFeedFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def GetContact(self, uri):
try:
return self.Get(uri, converter=gdata.apps.contacts.ContactEntryFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def CreateContact(self, new_contact, insert_uri=None, url_params=None,
escape_params=True):
"""Adds an new contact to Google Contacts.
Args:
new_contact: atom.Entry or subclass A new contact which is to be added to
Google Contacts.
insert_uri: the URL to post new contacts to the feed
url_params: dict (optional) Additional URL parameters to be included
in the insertion request.
escape_params: boolean (optional) If true, the url_parameters will be
escaped before they are included in the request.
Returns:
On successful insert, an entry containing the contact created
On failure, a RequestError is raised of the form:
{'status': HTTP status code from server,
'reason': HTTP reason from the server,
'body': HTTP body of the server's response}
"""
insert_uri = insert_uri or self.GetContactFeedUri()
try:
return self.Post(new_contact, insert_uri, url_params=url_params,
escape_params=escape_params,
converter=gdata.apps.contacts.ContactEntryFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def UpdateContact(self, edit_uri, updated_contact, extra_headers=None, url_params=None,
escape_params=True):
"""Updates an existing contact.
Args:
edit_uri: string The edit link URI for the element being updated
updated_contact: string, atom.Entry or subclass containing
the Atom Entry which will replace the contact which is
stored at the edit_url
url_params: dict (optional) Additional URL parameters to be included
in the update request.
escape_params: boolean (optional) If true, the url_parameters will be
escaped before they are included in the request.
Returns:
On successful update, a httplib.HTTPResponse containing the server's
response to the PUT request.
On failure, a RequestError is raised of the form:
{'status': HTTP status code from server,
'reason': HTTP reason from the server,
'body': HTTP body of the server's response}
"""
try:
return self.Put(updated_contact, self._CleanUri(edit_uri),
url_params=url_params, extra_headers=extra_headers,
escape_params=escape_params,
converter=gdata.apps.contacts.ContactEntryFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def DeleteContact(self, edit_uri, extra_headers=None,
url_params=None, escape_params=True):
"""Removes an contact with the specified ID from Google Contacts.
Args:
edit_uri: string The edit URL of the entry to be deleted. Example:
'/m8/feeds/contacts/default/full/xxx/yyy'
extra_headers: dict (optional)
url_params: dict (optional) Additional URL parameters to be included
in the deletion request.
escape_params: boolean (optional) If true, the url_parameters will be
escaped before they are included in the request.
Returns:
On successful delete, a httplib.HTTPResponse containing the server's
response to the DELETE request.
On failure, a RequestError is raised of the form:
{'status': HTTP status code from server,
'reason': HTTP reason from the server,
'body': HTTP body of the server's response}
"""
try:
return self.Delete(self._CleanUri(edit_uri),
url_params=url_params, escape_params=escape_params, extra_headers=extra_headers)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def ChangePhoto(self, media, contact_entry_or_url, content_type=None,
content_length=None, extra_headers=None):
"""Change the photo for the contact by uploading a new photo.
Performs a PUT against the photo edit URL to send the binary data for the
photo.
Args:
media: filename, file-like-object, or a gdata.MediaSource object to send.
contact_entry_or_url: ContactEntry or str If it is a ContactEntry, this
method will search for an edit photo link URL and
perform a PUT to the URL.
content_type: str (optional) the mime type for the photo data. This is
necessary if media is a file or file name, but if media
is a MediaSource object then the media object can contain
the mime type. If media_type is set, it will override the
mime type in the media object.
content_length: int or str (optional) Specifying the content length is
only required if media is a file-like object. If media
is a filename, the length is determined using
os.path.getsize. If media is a MediaSource object, it is
assumed that it already contains the content length.
extra_headers: dict (optional)
"""
if isinstance(contact_entry_or_url, gdata.apps.contacts.ContactEntry):
# url = contact_entry_or_url.GetPhotoEditLink().href
url = contact_entry_or_url.GetPhotoLink().href
else:
url = contact_entry_or_url
if isinstance(media, gdata.MediaSource):
payload = media
# If the media object is a file-like object, then use it as the file
# handle in the in the MediaSource.
elif hasattr(media, 'read'):
payload = gdata.MediaSource(file_handle=media,
content_type=content_type, content_length=content_length)
# Assume that the media object is a file name.
else:
payload = gdata.MediaSource(content_type=content_type,
content_length=content_length, file_path=media)
return self.Put(payload, url, extra_headers=extra_headers)
def GetPhoto(self, contact_entry_or_url):
"""Retrives the binary data for the contact's profile photo as a string.
Args:
contact_entry_or_url: a gdata.apps.contacts.ContactEntry object or a string
containing the photo link's URL. If the contact entry does not
contain a photo link, the image will not be fetched and this method
will return None.
"""
# TODO: add the ability to write out the binary image data to a file,
# reading and writing a chunk at a time to avoid potentially using up
# large amounts of memory.
url = None
if isinstance(contact_entry_or_url, gdata.apps.contacts.ContactEntry):
photo_link = contact_entry_or_url.GetPhotoLink()
if photo_link:
url = photo_link.href
else:
url = contact_entry_or_url
if url:
try:
return self.Get(url, converter=str)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
else:
return None
def DeletePhoto(self, contact_entry_or_url, extra_headers=None):
"""Deletes the contact's profile photo.
Args:
contact_entry_or_url: a gdata.apps.contacts.ContactEntry object or a string
containing the photo link's URL.
will return None.
extra_headers: dict (optional)
"""
url = None
if isinstance(contact_entry_or_url, gdata.apps.contacts.ContactEntry):
# url = contact_entry_or_url.GetPhotoEditLink().href
url = contact_entry_or_url.GetPhotoLink().href
else:
url = contact_entry_or_url
if url:
self.Delete(url, extra_headers=extra_headers)
def ExecuteBatch(self, batch_feed, url,
converter=gdata.apps.contacts.ContactsFeedFromString):
"""Sends a batch request feed to the server.
Args:
batch_feed: gdata.apps.contacts.ContactFeed A feed containing batch
request entries. Each entry contains the operation to be performed
on the data contained in the entry. For example an entry with an
operation type of insert will be used as if the individual entry
had been inserted.
url: str The batch URL to which these operations should be applied.
converter: Function (optional) The function used to convert the server's
response to an object. The default value is ContactsFeedFromString.
Returns:
The results of the batch request's execution on the server. If the
default converter is used, this is stored in a ContactsFeed.
"""
return self.Post(batch_feed, url, converter=converter)
def GetContactGroupFeedUri(self, contact_list=None, projection='full', groupId=None):
"""Builds a contact feed URI. """
contact_list = contact_list or self.contact_list
uri = 'https://{0}/m8/feeds/groups/{1}/{2}'.format(self.server, contact_list, projection)
if groupId:
uri += '/{0}'.format(groupId)
return uri
def GetGroupsFeed(self, uri=None,
extra_headers=None, url_params=None, escape_params=True):
uri = uri or self.GetContactGroupFeedUri()
try:
return self.Get(uri,
url_params=url_params, extra_headers=extra_headers, escape_params=escape_params,
converter=gdata.apps.contacts.GroupsFeedFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def GetGroup(self, uri):
try:
return self.Get(uri, converter=gdata.apps.contacts.GroupEntryFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def CreateGroup(self, new_group, insert_uri=None, url_params=None,
escape_params=True):
insert_uri = insert_uri or self.GetContactGroupFeedUri()
try:
return self.Post(new_group, insert_uri, url_params=url_params,
escape_params=escape_params,
converter=gdata.apps.contacts.GroupEntryFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def UpdateGroup(self, edit_uri, updated_group, extra_headers=None, url_params=None,
escape_params=True):
try:
return self.Put(updated_group, self._CleanUri(edit_uri),
url_params=url_params, extra_headers=extra_headers,
escape_params=escape_params,
converter=gdata.apps.contacts.GroupEntryFromString)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def DeleteGroup(self, edit_uri, extra_headers=None,
url_params=None, escape_params=True):
try:
return self.Delete(self._CleanUri(edit_uri),
url_params=url_params, escape_params=escape_params, extra_headers=extra_headers)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
class ContactsQuery(gdata.service.Query):
def __init__(self, feed=None, text_query=None, params=None,
categories=None, group=None):
self.feed = feed or '/m8/feeds/contacts/default/full'
if group:
self['group'] = group
gdata.service.Query.__init__(self, feed=self.feed, text_query=text_query,
params=params, categories=categories)

View File

@@ -1,544 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2007 SIOS Technology, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'tmatsuo@sios.com (Takashi MATSUO)'
import lxml.etree as ElementTree
import urllib.request, urllib.parse, urllib.error
import gdata
import atom.service
import gdata.service
import gdata.apps
import atom
API_VER="2.0"
HTTP_OK=200
UNKOWN_ERROR=1000
USER_DELETED_RECENTLY=1100
USER_SUSPENDED=1101
DOMAIN_USER_LIMIT_EXCEEDED=1200
DOMAIN_ALIAS_LIMIT_EXCEEDED=1201
DOMAIN_SUSPENDED=1202
DOMAIN_FEATURE_UNAVAILABLE=1203
ENTITY_EXISTS=1300
ENTITY_DOES_NOT_EXIST=1301
ENTITY_NAME_IS_RESERVED=1302
ENTITY_NAME_NOT_VALID=1303
INVALID_GIVEN_NAME=1400
INVALID_FAMILY_NAME=1401
INVALID_PASSWORD=1402
INVALID_USERNAME=1403
INVALID_HASH_FUNCTION_NAME=1404
INVALID_HASH_DIGGEST_LENGTH=1405
INVALID_EMAIL_ADDRESS=1406
INVALID_QUERY_PARAMETER_VALUE=1407
TOO_MANY_RECIPIENTS_ON_EMAIL_LIST=1500
DEFAULT_QUOTA_LIMIT='2048'
class Error(Exception):
pass
class AppsForYourDomainException(Error):
def __init__(self, response):
Error.__init__(self, response)
try:
self.element_tree = ElementTree.fromstring(response['body'])
self.error_code = int(self.element_tree[0].attrib['errorCode'])
self.reason = self.element_tree[0].attrib['reason']
self.invalidInput = self.element_tree[0].attrib['invalidInput']
except:
self.error_code = 600
class AppsService(gdata.service.GDataService):
"""Client for the Google Apps Provisioning service."""
def __init__(self, email=None, password=None, domain=None, source=None,
server='apps-apis.google.com', additional_headers=None,
**kwargs):
"""Creates a client for the Google Apps Provisioning service.
Args:
email: string (optional) The user's email address, used for
authentication.
password: string (optional) The user's password.
domain: string (optional) The Google Apps domain name.
source: string (optional) The name of the user's application.
server: string (optional) The name of the server to which a connection
will be opened. Default value: 'apps-apis.google.com'.
**kwargs: The other parameters to pass to gdata.service.GDataService
constructor.
"""
gdata.service.GDataService.__init__(
self, email=email, password=password, service='apps', source=source,
server=server, additional_headers=additional_headers, **kwargs)
self.ssl = True
self.port = 443
self.domain = domain
def _baseURL(self):
return "/a/feeds/%s" % self.domain
def AddAllElementsFromAllPages(self, link_finder, func):
"""retrieve all pages and add all elements"""
next = link_finder.GetNextLink()
while next is not None:
next_feed = self.Get(next.href, converter=func)
for a_entry in next_feed.entry:
link_finder.entry.append(a_entry)
next = next_feed.GetNextLink()
return link_finder
def RetrievePageOfEmailLists(self, start_email_list_name=None,
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY,
backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve one page of email list"""
uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
if start_email_list_name is not None:
uri += "?startEmailListName=%s" % start_email_list_name
try:
return gdata.apps.EmailListFeedFromString(str(self.GetWithRetries(
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def GetGeneratorForAllEmailLists(
self, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve a generator for all emaillists in this domain."""
first_page = self.RetrievePageOfEmailLists(num_retries=num_retries,
delay=delay,
backoff=backoff)
return self.GetGeneratorFromLinkFinder(
first_page, gdata.apps.EmailListRecipientFeedFromString,
num_retries=num_retries, delay=delay, backoff=backoff)
def RetrieveAllEmailLists(self):
"""Retrieve all email list of a domain."""
ret = self.RetrievePageOfEmailLists()
# pagination
return self.AddAllElementsFromAllPages(
ret, gdata.apps.EmailListFeedFromString)
def RetrieveEmailList(self, list_name):
"""Retreive a single email list by the list's name."""
uri = "%s/emailList/%s/%s" % (
self._baseURL(), API_VER, list_name)
try:
return self.Get(uri, converter=gdata.apps.EmailListEntryFromString)
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def RetrieveEmailLists(self, recipient):
"""Retrieve All Email List Subscriptions for an Email Address."""
uri = "%s/emailList/%s?recipient=%s" % (
self._baseURL(), API_VER, recipient)
try:
ret = gdata.apps.EmailListFeedFromString(str(self.Get(uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
# pagination
return self.AddAllElementsFromAllPages(
ret, gdata.apps.EmailListFeedFromString)
def RemoveRecipientFromEmailList(self, recipient, list_name):
"""Remove recipient from email list."""
uri = "%s/emailList/%s/%s/recipient/%s" % (
self._baseURL(), API_VER, list_name, recipient)
try:
self.Delete(uri)
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def RetrievePageOfRecipients(self, list_name, start_recipient=None,
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY,
backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve one page of recipient of an email list. """
uri = "%s/emailList/%s/%s/recipient" % (
self._baseURL(), API_VER, list_name)
if start_recipient is not None:
uri += "?startRecipient=%s" % start_recipient
try:
return gdata.apps.EmailListRecipientFeedFromString(str(
self.GetWithRetries(
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def GetGeneratorForAllRecipients(
self, list_name, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve a generator for all recipients of a particular emaillist."""
first_page = self.RetrievePageOfRecipients(list_name,
num_retries=num_retries,
delay=delay,
backoff=backoff)
return self.GetGeneratorFromLinkFinder(
first_page, gdata.apps.EmailListRecipientFeedFromString,
num_retries=num_retries, delay=delay, backoff=backoff)
def RetrieveAllRecipients(self, list_name):
"""Retrieve all recipient of an email list."""
ret = self.RetrievePageOfRecipients(list_name)
# pagination
return self.AddAllElementsFromAllPages(
ret, gdata.apps.EmailListRecipientFeedFromString)
def AddRecipientToEmailList(self, recipient, list_name):
"""Add a recipient to a email list."""
uri = "%s/emailList/%s/%s/recipient" % (
self._baseURL(), API_VER, list_name)
recipient_entry = gdata.apps.EmailListRecipientEntry()
recipient_entry.who = gdata.apps.Who(email=recipient)
try:
return gdata.apps.EmailListRecipientEntryFromString(
str(self.Post(recipient_entry, uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def DeleteEmailList(self, list_name):
"""Delete a email list"""
uri = "%s/emailList/%s/%s" % (self._baseURL(), API_VER, list_name)
try:
self.Delete(uri)
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def CreateEmailList(self, list_name):
"""Create a email list. """
uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
email_list_entry = gdata.apps.EmailListEntry()
email_list_entry.email_list = gdata.apps.EmailList(name=list_name)
try:
return gdata.apps.EmailListEntryFromString(
str(self.Post(email_list_entry, uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def DeleteNickname(self, nickname):
"""Delete a nickname"""
uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname)
try:
self.Delete(uri)
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def RetrievePageOfNicknames(self, start_nickname=None,
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY,
backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve one page of nicknames in the domain"""
uri = "%s/nickname/%s" % (self._baseURL(), API_VER)
if start_nickname is not None:
uri += "?startNickname=%s" % start_nickname
try:
return gdata.apps.NicknameFeedFromString(str(self.GetWithRetries(
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def GetGeneratorForAllNicknames(
self, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve a generator for all nicknames in this domain."""
first_page = self.RetrievePageOfNicknames(num_retries=num_retries,
delay=delay,
backoff=backoff)
return self.GetGeneratorFromLinkFinder(
first_page, gdata.apps.NicknameFeedFromString, num_retries=num_retries,
delay=delay, backoff=backoff)
def RetrieveAllNicknames(self):
"""Retrieve all nicknames in the domain"""
ret = self.RetrievePageOfNicknames()
# pagination
return self.AddAllElementsFromAllPages(
ret, gdata.apps.NicknameFeedFromString)
def GetGeneratorForAllNicknamesOfAUser(
self, user_name, num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY, backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve a generator for all nicknames of a particular user."""
uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name)
try:
first_page = gdata.apps.NicknameFeedFromString(str(self.GetWithRetries(
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
return self.GetGeneratorFromLinkFinder(
first_page, gdata.apps.NicknameFeedFromString, num_retries=num_retries,
delay=delay, backoff=backoff)
def RetrieveNicknames(self, user_name):
"""Retrieve nicknames of the user"""
uri = "%s/nickname/%s?username=%s" % (self._baseURL(), API_VER, user_name)
try:
ret = gdata.apps.NicknameFeedFromString(str(self.Get(uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
# pagination
return self.AddAllElementsFromAllPages(
ret, gdata.apps.NicknameFeedFromString)
def RetrieveNickname(self, nickname):
"""Retrieve a nickname.
Args:
nickname: string The nickname to retrieve
Returns:
gdata.apps.NicknameEntry
"""
uri = "%s/nickname/%s/%s" % (self._baseURL(), API_VER, nickname)
try:
return gdata.apps.NicknameEntryFromString(str(self.Get(uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def CreateNickname(self, user_name, nickname):
"""Create a nickname"""
uri = "%s/nickname/%s" % (self._baseURL(), API_VER)
nickname_entry = gdata.apps.NicknameEntry()
nickname_entry.login = gdata.apps.Login(user_name=user_name)
nickname_entry.nickname = gdata.apps.Nickname(name=nickname)
try:
return gdata.apps.NicknameEntryFromString(
str(self.Post(nickname_entry, uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def DeleteUser(self, user_name):
"""Delete a user account"""
uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
try:
return self.Delete(uri)
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def UpdateUser(self, user_name, user_entry):
"""Update a user account."""
uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
try:
return gdata.apps.UserEntryFromString(str(self.Put(user_entry, uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def CreateUser(self, user_name, family_name, given_name, password,
suspended='false', quota_limit=None,
password_hash_function=None,
change_password=None):
"""Create a user account. """
uri = "%s/user/%s" % (self._baseURL(), API_VER)
user_entry = gdata.apps.UserEntry()
user_entry.login = gdata.apps.Login(
user_name=user_name, password=password, suspended=suspended,
hash_function_name=password_hash_function,
change_password=change_password)
user_entry.name = gdata.apps.Name(family_name=family_name,
given_name=given_name)
if quota_limit is not None:
user_entry.quota = gdata.apps.Quota(limit=str(quota_limit))
try:
return gdata.apps.UserEntryFromString(str(self.Post(user_entry, uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def SuspendUser(self, user_name):
user_entry = self.RetrieveUser(user_name)
if user_entry.login.suspended != 'true':
user_entry.login.suspended = 'true'
user_entry = self.UpdateUser(user_name, user_entry)
return user_entry
def RestoreUser(self, user_name):
user_entry = self.RetrieveUser(user_name)
if user_entry.login.suspended != 'false':
user_entry.login.suspended = 'false'
user_entry = self.UpdateUser(user_name, user_entry)
return user_entry
def RetrieveUser(self, user_name):
"""Retrieve an user account.
Args:
user_name: string The user name to retrieve
Returns:
gdata.apps.UserEntry
"""
uri = "%s/user/%s/%s" % (self._baseURL(), API_VER, user_name)
try:
return gdata.apps.UserEntryFromString(str(self.Get(uri)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def RetrievePageOfUsers(self, start_username=None,
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY,
backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve one page of users in this domain."""
uri = "%s/user/%s" % (self._baseURL(), API_VER)
if start_username is not None:
uri += "?startUsername=%s" % start_username
try:
return gdata.apps.UserFeedFromString(str(self.GetWithRetries(
uri, num_retries=num_retries, delay=delay, backoff=backoff)))
except gdata.service.RequestError as e:
raise AppsForYourDomainException(e.args[0])
def GetGeneratorForAllUsers(self,
num_retries=gdata.service.DEFAULT_NUM_RETRIES,
delay=gdata.service.DEFAULT_DELAY,
backoff=gdata.service.DEFAULT_BACKOFF):
"""Retrieve a generator for all users in this domain."""
first_page = self.RetrievePageOfUsers(num_retries=num_retries, delay=delay,
backoff=backoff)
return self.GetGeneratorFromLinkFinder(
first_page, gdata.apps.UserFeedFromString, num_retries=num_retries,
delay=delay, backoff=backoff)
def RetrieveAllUsers(self):
"""Retrieve all users in this domain. OBSOLETE"""
ret = self.RetrievePageOfUsers()
# pagination
return self.AddAllElementsFromAllPages(
ret, gdata.apps.UserFeedFromString)
class PropertyService(gdata.service.GDataService):
"""Client for the Google Apps Property service."""
def __init__(self, email=None, password=None, domain=None, source=None,
server='apps-apis.google.com', additional_headers=None):
gdata.service.GDataService.__init__(self, email=email, password=password,
service='apps', source=source,
server=server,
additional_headers=additional_headers)
self.ssl = True
self.port = 443
self.domain = domain
def AddAllElementsFromAllPages(self, link_finder, func):
"""retrieve all pages and add all elements"""
next = link_finder.GetNextLink()
count = 0
while next is not None:
next_feed = self.Get(next.href, converter=func)
count = count + len(next_feed.entry)
for a_entry in next_feed.entry:
link_finder.entry.append(a_entry)
next = next_feed.GetNextLink()
return link_finder
def _GetPropertyEntry(self, properties):
property_entry = gdata.apps.PropertyEntry()
property = []
for name, value in properties.items():
if name is not None and value is not None:
property.append(gdata.apps.Property(name=name, value=value))
property_entry.property = property
return property_entry
def _PropertyEntry2Dict(self, property_entry):
properties = {}
for i, property in enumerate(property_entry.property):
properties[property.name] = property.value
return properties
def _GetPropertyFeed(self, uri):
try:
return gdata.apps.PropertyFeedFromString(str(self.Get(uri)))
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def _GetPropertiesList(self, uri):
property_feed = self._GetPropertyFeed(uri)
# pagination
property_feed = self.AddAllElementsFromAllPages(
property_feed, gdata.apps.PropertyFeedFromString)
properties_list = []
for property_entry in property_feed.entry:
properties_list.append(self._PropertyEntry2Dict(property_entry))
return properties_list
def _GetProperties(self, uri):
try:
return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString(
str(self.Get(uri))))
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def _PostProperties(self, uri, properties):
property_entry = self._GetPropertyEntry(properties)
try:
return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString(
str(self.Post(property_entry, uri))))
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def _PutProperties(self, uri, properties):
property_entry = self._GetPropertyEntry(properties)
try:
return self._PropertyEntry2Dict(gdata.apps.PropertyEntryFromString(
str(self.Put(property_entry, uri))))
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def _DeleteProperties(self, uri):
try:
self.Delete(uri)
except gdata.service.RequestError as e:
raise gdata.apps.service.AppsForYourDomainException(e.args[0])
def _bool2str(b):
if b is None:
return None
return str(b is True).lower()

File diff suppressed because it is too large Load Diff

View File

@@ -1,247 +0,0 @@
#!/usr/bin/python
#
# Copyright (C) 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provides HTTP functions for gdata.service to use on Google App Engine
AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
urlfetch API. Set the http_client member of a GDataService object to an
instance of an AppEngineHttpClient to allow the gdata library to run on
Google App Engine.
run_on_appengine: Function which will modify an existing GDataService object
to allow it to run on App Engine. It works by creating a new instance of
the AppEngineHttpClient and replacing the GDataService object's
http_client.
HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a
common interface which is used by gdata.service.GDataService. In other
words, this module can be used as the gdata service request handler so
that all HTTP requests will be performed by the hosting Google App Engine
server.
"""
__author__ = 'api.jscudder (Jeff Scudder)'
import io
import atom.service
import atom.http_interface
from google.appengine.api import urlfetch
def run_on_appengine(gdata_service):
"""Modifies a GDataService object to allow it to run on App Engine.
Args:
gdata_service: An instance of AtomService, GDataService, or any
of their subclasses which has an http_client member.
"""
gdata_service.http_client = AppEngineHttpClient()
class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
def __init__(self, headers=None):
self.debug = False
self.headers = headers or {}
def request(self, operation, url, data=None, headers=None):
"""Performs an HTTP call to the server, supports GET, POST, PUT, and
DELETE.
Usage example, perform and HTTP GET on http://www.google.com/:
import atom.http
client = atom.http.HttpClient()
http_response = client.request('GET', 'http://www.google.com/')
Args:
operation: str The HTTP operation to be performed. This is usually one
of 'GET', 'POST', 'PUT', or 'DELETE'
data: filestream, list of parts, or other object which can be converted
to a string. Should be set to None when performing a GET or DELETE.
If data is a file-like object which can be read, this method will
read a chunk of 100K bytes at a time and send them.
If the data is a list of parts to be sent, each part will be
evaluated and sent.
url: The full URL to which the request should be sent. Can be a string
or atom.url.Url.
headers: dict of strings. HTTP headers which should be sent
in the request.
"""
all_headers = self.headers.copy()
if headers:
all_headers.update(headers)
# Construct the full payload.
# Assume that data is None or a string.
data_str = data
if data:
if isinstance(data, list):
# If data is a list of different objects, convert them all to strings
# and join them together.
converted_parts = [__ConvertDataPart(x) for x in data]
data_str = ''.join(converted_parts)
else:
data_str = __ConvertDataPart(data)
# If the list of headers does not include a Content-Length, attempt to
# calculate it based on the data object.
if data and 'Content-Length' not in all_headers:
all_headers['Content-Length'] = len(data_str)
# Set the content type to the default value if none was set.
if 'Content-Type' not in all_headers:
all_headers['Content-Type'] = 'application/atom+xml'
# Lookup the urlfetch operation which corresponds to the desired HTTP verb.
if operation == 'GET':
method = urlfetch.GET
elif operation == 'POST':
method = urlfetch.POST
elif operation == 'PUT':
method = urlfetch.PUT
elif operation == 'DELETE':
method = urlfetch.DELETE
else:
method = None
return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
method=method, headers=all_headers))
def HttpRequest(service, operation, data, uri, extra_headers=None,
url_params=None, escape_params=True, content_type='application/atom+xml'):
"""Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.
This function is deprecated, use AppEngineHttpClient.request instead.
To use this module with gdata.service, you can set this module to be the
http_request_handler so that HTTP requests use Google App Engine's urlfetch.
import gdata.service
import gdata.urlfetch
gdata.service.http_request_handler = gdata.urlfetch
Args:
service: atom.AtomService object which contains some of the parameters
needed to make the request. The following members are used to
construct the HTTP call: server (str), additional_headers (dict),
port (int), and ssl (bool).
operation: str The HTTP operation to be performed. This is usually one of
'GET', 'POST', 'PUT', or 'DELETE'
data: filestream, list of parts, or other object which can be
converted to a string.
Should be set to None when performing a GET or PUT.
If data is a file-like object which can be read, this method will read
a chunk of 100K bytes at a time and send them.
If the data is a list of parts to be sent, each part will be evaluated
and sent.
uri: The beginning of the URL to which the request should be sent.
Examples: '/', '/base/feeds/snippets',
'/m8/feeds/contacts/default/base'
extra_headers: dict of strings. HTTP headers which should be sent
in the request. These headers are in addition to those stored in
service.additional_headers.
url_params: dict of strings. Key value pairs to be added to the URL as
URL parameters. For example {'foo':'bar', 'test':'param'} will
become ?foo=bar&test=param.
escape_params: bool default True. If true, the keys and values in
url_params will be URL escaped when the form is constructed
(Special characters converted to %XX form.)
content_type: str The MIME type for the data being sent. Defaults to
'application/atom+xml', this is only used if data is set.
"""
full_uri = atom.service.BuildUri(uri, url_params, escape_params)
(server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri)
# Construct the full URL for the request.
if ssl:
full_url = 'https://%s%s' % (server, partial_uri)
else:
full_url = 'http://%s%s' % (server, partial_uri)
# Construct the full payload.
# Assume that data is None or a string.
data_str = data
if data:
if isinstance(data, list):
# If data is a list of different objects, convert them all to strings
# and join them together.
converted_parts = [__ConvertDataPart(x) for x in data]
data_str = ''.join(converted_parts)
else:
data_str = __ConvertDataPart(data)
# Construct the dictionary of HTTP headers.
headers = {}
if isinstance(service.additional_headers, dict):
headers = service.additional_headers.copy()
if isinstance(extra_headers, dict):
for header, value in extra_headers.items():
headers[header] = value
# Add the content type header (we don't need to calculate content length,
# since urlfetch.Fetch will calculate for us).
if content_type:
headers['Content-Type'] = content_type
# Lookup the urlfetch operation which corresponds to the desired HTTP verb.
if operation == 'GET':
method = urlfetch.GET
elif operation == 'POST':
method = urlfetch.POST
elif operation == 'PUT':
method = urlfetch.PUT
elif operation == 'DELETE':
method = urlfetch.DELETE
else:
method = None
return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str,
method=method, headers=headers))
def __ConvertDataPart(data):
if not data or isinstance(data, str):
return data
elif hasattr(data, 'read'):
# data is a file like object, so read it completely.
return data.read()
# The data object was not a file.
# Try to convert to a string and send the data.
return str(data)
class HttpResponse(object):
"""Translates a urlfetch resoinse to look like an hhtplib resoinse.
Used to allow the resoinse from HttpRequest to be usable by gdata.service
methods.
"""
def __init__(self, urlfetch_response):
self.body = io.StringIO(urlfetch_response.content)
self.headers = urlfetch_response.headers
self.status = urlfetch_response.status_code
self.reason = ''
def read(self, length=None):
if not length:
return self.body.read()
else:
return self.body.read(length)
def getheader(self, name):
if name not in self.headers:
return self.headers[name.lower()]
return self.headers[name]

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +0,0 @@
{
"basePath": "",
"baseUrl": "https://www.googleapis.com/service_accounts/v1",
"canonicalName": "serviceaccountlookup",
"description": "Pseudo-API to lookup public certificates for a service account anonymously",
"discoveryVersion": "v1",
"documentationLink": "https://example.com/",
"fullyEncodeReservedExpansion": true,
"icons": {
"x16": "http://www.google.com/images/icons/product/search-16.gif",
"x32": "http://www.google.com/images/icons/product/search-32.gif"
},
"id": "serviceaccountlookup:v1",
"kind": "discovery#restDescription",
"name": "serviceaccountlookup",
"ownerDomain": "google.com",
"ownerName": "Google",
"packagePath": "admin",
"parameters": {
"$.xgafv": {
"description": "V1 error format.",
"enum": [
"1",
"2"
],
"enumDescriptions": [
"v1 error format",
"v2 error format"
],
"location": "query",
"type": "string"
},
"access_token": {
"description": "OAuth access token.",
"location": "query",
"type": "string"
},
"alt": {
"default": "json",
"description": "Data format for response.",
"enum": [
"json",
"media",
"proto"
],
"enumDescriptions": [
"Responses with Content-Type of application/json",
"Media download with context-dependent Content-Type",
"Responses with Content-Type of application/x-protobuf"
],
"location": "query",
"type": "string"
},
"callback": {
"description": "JSONP",
"location": "query",
"type": "string"
},
"fields": {
"description": "Selector specifying which fields to include in a partial response.",
"location": "query",
"type": "string"
},
"key": {
"description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
"location": "query",
"type": "string"
},
"oauth_token": {
"description": "OAuth 2.0 token for the current user.",
"location": "query",
"type": "string"
},
"prettyPrint": {
"default": "true",
"description": "Returns response with indentations and line breaks.",
"location": "query",
"type": "boolean"
},
"quotaUser": {
"description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.",
"location": "query",
"type": "string"
},
"uploadType": {
"description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").",
"location": "query",
"type": "string"
},
"upload_protocol": {
"description": "Upload protocol for media (e.g. \"raw\", \"multipart\").",
"location": "query",
"type": "string"
}
},
"protocol": "rest",
"resources": {
"serviceaccounts": {
"methods": {
"lookup": {
"description": "Lookup",
"flatPath": "metadata/x509/{account}",
"httpMethod": "GET",
"id": "serviceaccountslookup.lookup",
"parameterOrder": [
"account"
],
"parameters": {
"account": {
"description": "Email or ID of the service account.",
"location": "path",
"required": true,
"type": "string"
}
},
"path": "metadata/x509/{account}",
"response": {
"$ref": "Certificates"
}
}
}
}
},
"rootUrl": "https://www.googleapis.com/service_accounts/v1",
"schemas": {
"Certificates": {
"description": "JSON template for certificates.",
"id": "Certificates",
"properties": {
"email": { "description": "Email of the delegate.",
"type": "string"
}
},
"type": "object"
}
},
"servicePath": "",
"title": "Service Account Lookup Pseudo-API",
"version": "v1",
"version_module": true
}

View File

@@ -1,25 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf2822
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fswiss\fcharset0 Helvetica-Bold;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\vieww12000\viewh15840\viewkind0
\deftab720
\pard\pardeftab720\sl276\slmult1\sa200\qc\partightenfactor0
\f0\fs22 \cf0 Copyright 2026 Jay Lee\
\pard\pardeftab720\sa200\qc\partightenfactor0
\f1\b \cf0 Licensed under the Apache License, Version 2.0 (the "License");\
\pard\pardeftab720\sl276\slmult1\sa200\qc\partightenfactor0
\f0\b0 \cf0 you may not use this file except in compliance with the License.\
You may obtain a copy of the License at\
\
{\field{\*\fldinst{HYPERLINK "http://www.apache.org/licenses/LICENSE-2.0"}}{\fldrslt http://www.apache.org/licenses/LICENSE-2.0}}\
\
Unless required by applicable law or agreed to in writing, software\
distributed under the License is distributed on an "AS IS" BASIS,\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
See the License for the specific language governing permissions and\
limitations under the License.\
}

View File

@@ -1,115 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM">
<Configuration>Debug</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGInstrument|ARM">
<Configuration>PGInstrument</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGInstrument|ARM64">
<Configuration>PGInstrument</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGInstrument|Win32">
<Configuration>PGInstrument</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGInstrument|x64">
<Configuration>PGInstrument</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGUpdate|ARM">
<Configuration>PGUpdate</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGUpdate|ARM64">
<Configuration>PGUpdate</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGUpdate|Win32">
<Configuration>PGUpdate</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="PGUpdate|x64">
<Configuration>PGUpdate</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM">
<Configuration>Release</Configuration>
<Platform>ARM</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{447F05A8-F581-4CAC-A466-5AC7936E207E}</ProjectGuid>
<RootNamespace>_hashlib</RootNamespace>
<Keyword>Win32Proj</Keyword>
</PropertyGroup>
<Import Project="python.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<CharacterSet>NotSet</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup>
<TargetExt>$(PyStdlibPydExt)</TargetExt>
</PropertyGroup>
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="pyproject.props" />
<Import Project="openssl.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
</PropertyGroup>
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>ws2_32.lib;crypt32.lib;advapi32.lib;user32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\Modules\_hashopenssl.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\PC\python_nt.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="pythoncore.vcxproj">
<Project>{cf7ac3d1-e2df-41d2-bea6-1e2556cdea26}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env python3
from packaging import version
import sys
a = sys.argv[1]
b = sys.argv[2]
result = version.parse(a) >= version.parse(b)
if result:
print('OK: %s is equal or newer than %s' % (a, b))
else:
print('ERROR: %s is older than %s' % (a, b))
sys.exit(not result)

View File

@@ -1,23 +0,0 @@
import uuid
from lxml import etree
import sys
# Hacky solution to create a Guid for all files
# so Wix is happy and Guid is stable every time.
# uuid5 is used for the Guid and the input is the
# source filename so the Guid will be the same
# every time as long as the source file name is
# the same.
rewrite_file = sys.argv[1]
with open(rewrite_file, 'rb') as f:
input_xml = f.read()
root = etree.fromstring(input_xml)
for elem in root.getiterator():
if 'Guid' in elem.attrib:
source = elem.getchildren()[0].attrib['Source']
stable_uuid = str(uuid.uuid5(uuid.NAMESPACE_URL, source))
elem.attrib['Guid'] = stable_uuid
with open(rewrite_file, 'w') as f:
f.write(etree.tostring(root).decode())

View File

@@ -1,21 +0,0 @@
# ------------------------------------------------------------------
# Copyright (c) 2021 PyInstaller Development Team.
#
# This file is distributed under the terms of the GNU General Public
# License (version 2.0 or later).
#
# The full license is available in LICENSE, distributed with
# this software.
#
# SPDX-License-Identifier: GPL-2.0-or-later
# ------------------------------------------------------------------
from PyInstaller.utils.hooks import copy_metadata
from PyInstaller.utils.hooks import collect_data_files
# googleapiclient.model queries the library version via
# pkg_resources.get_distribution("google-api-python-client").version,
# so we need to collect that package's metadata
datas = copy_metadata('google_api_python_client')
# we don't want these cached discovery files and they make the binary HUUUGEEEE
#datas += collect_data_files('googleapiclient.discovery_cache', excludes=['*.txt', '**/__pycache__'])

View File

@@ -1,19 +0,0 @@
# ------------------------------------------------------------------
# Copyright (c) 2020 PyInstaller Development Team.
#
# This file is distributed under the terms of the GNU General Public
# License (version 2.0 or later).
#
# The full license is available in LICENSE, distributed with
# this software.
#
# SPDX-License-Identifier: GPL-2.0-or-later
# ------------------------------------------------------------------
# This is needed to bundle cacerts.txt that comes with httplib2 module
# WE DON'T NEED httplib2/cacerts.txt since we get our own
#from PyInstaller.utils.hooks import collect_data_files
#datas = collect_data_files('httplib2')

View File

@@ -1,246 +0,0 @@
#!/usr/bin/env python3
import re
import sys
from collections import OrderedDict
# A simple, quick & dirty script to parse GamCommands.txt and
# produce HTML with parameter references in commands linked to their
# definitions.
#
# We expect the following grammar:
#
# ^gam.* Beginning of a command
# <(.*?)>\s+::= Beginning of a parameter BNF production
#
# Input lines immediately following a command or parameter are
# accumulated until encountering a blank line or the next command or
# parameter. This is implemented as a state machine.
#
# In the output HTML, every parameter definition is given an id=
# attribute (__x where x is the parameter name) and references in
# commands and RHS of parameter productions are links pointing to the
# definition.
#
# Input is syntax-checked to verify matching angle-brackets. If
# an entry fails this test it is discarded and a message is written
# to stderr.
#
# There are also 3 special cases handled separately since they don't
# fit this grammar. See 'specialCases' below.
#
# If a parameter (BNF production) duplicates one we've already seen, we
# ignore it, and if it isn't identical to the existing one we write a
# message to stderr.
#
# Written with python 3.7, explaining non-use of lookahead/lookbehind
#
# Typical usage:
#
# python mkGamRef.py GamCommands.txt > GamCommands.html
#
reCommand = re.compile(r'gam ') # Start of command
reProduction = re.compile(r'<(.*?)>\s+::=') # Start of parameter BNF production
reBlankLine = re.compile(r'\s*') # Totally blank line
reWhitespace = re.compile(r'\s+') # To detect continuations for HTML <br/>
reProdRef = re.compile(r'<(.*?)>') # Cross-reference to a parameter
reBrackets = re.compile(r'[<>]') # For counting brackets
reBadLink1 = re.compile(r'[/a-zA-Z]+>') # Pin down missing opening bracket
reBadLink2 = re.compile(r'<[/a-zA-Z]+') # Pin down missing closing bracket
# Special cases of input that get "fixed" on input because
# they don't follow the grammar used everywhere else
specialCases = [
[re.compile(r'<(Number in range )(\d+)-(\d+)>'), r'&lt;\1\2-\3&gt;'],
['<|<=|>=|>|=|!=', '&lt; | &lt;= | &gt;= | &gt; | = | !='],
['<RRULE, EXRULE, RDATE and EXDATE line>', '&lt;RRULE, EXRULE, RDATE and EXDATE line&gt;'] ]
# List of gam commands. Each entry is a list of input lines
commands = []
# OrderedDict of parameter BNF productions
# key: non-terminal name
# value: List of input lines
productions = OrderedDict()
# Simple HTML wrapper
prefix = '''\
<html>
<head></head>
<body style="font-size:large; font-family:monospace">
<p><a href="#_commands">Commands</a>
<p><a href="#_parameters">Parameters</a>
'''
suffix = '''\
</body>
</html>
'''
# Examine an input line and return a token type, extracted
# production key (if this is the first line of a production)
# and the line text, all as a tuple
# Token Types:
# 0 - Blank line
# 1 - First line of gam command
# 2 - First line of BNF parameter production
# 3 - Everything else
def examine(line):
if reBlankLine.fullmatch(line):
return (0, '', '')
m = reCommand.match(line)
if m:
return (1, '', line)
m = reProduction.match(line)
if m:
return (2, m.group(1), line)
return (3, '', line)
# Save the current command or production
# Detect duplicate BNF productions and write an error message
# if not identical to the one already on file
def save(state,key,data):
global commands, productions
if state==1:
commands += [data]
elif state ==2:
if key in productions:
if data == productions[key]:
#sys.stderr.write(f"Identical duplicate production {key} ignored\n")
pass
else:
sys.stderr.write(f"Conflicting duplicate production {key} ignored\n")
else:
productions[key] = data
# Detect and fix special cases
def fixSpecialCases(line):
for c in specialCases:
# c is a tuple (search, replacement)
# search can be a re.Pattern, in which case replacement is a regex sub with backreferences
# search can be a string, in which case replacement is a direct substitution
if isinstance(c[0], re.Pattern):
if c[0].search(line):
line = c[0].sub(c[1],line)
else:
pos = line.find(c[0])
if pos >= 0:
line = line[:pos] + c[1] + line[pos+len(c[1]):]
return line
# Write the HTML for one complete command or production,
# generating the cross-references from command parameters to
# the parameter definitions (the BNF productions)
def resolve(data,id=None):
if id:
result = f'<div id="__{id}" style="padding:12px; padding-left:100px; text-indent:-90px">'
else:
result = f'<div style="padding:12px; padding-left:100px; text-indent:-90px">'
for line in data:
if reWhitespace.match(line):
result += "<br/>"
result += reProdRef.sub(r'<a href="#__\1">&lt;\1&gt;</a>', line)
result += "</div>"
return result
# Syntax check an input line
def validate(line):
# Check bracket matching/nesting (no nesting allowed)
blist = reBrackets.findall(line)
depth = 0
nnest = False
for c in blist:
if c == '<': depth += 1
elif c == '>': depth -= 1
if depth > 1: nnest = True
if depth != 0 or nnest:
return False
# Look for bracket syntax errors
for m in reBadLink1.finditer(line):
if m.start() == 0 or line[m.start()-1] != '<':
return False
for m in reBadLink2.finditer(line):
if m.end() >= len(line) or line[m.end()] != '>':
return False
return True
# Simple Finite State Machine to parse GamCommands.txt
#
# C u r r e n t S t a t e
# 0 1 2
# Input Token Start gam <BNF>
# ------------ ----- --- -----
# 0 empty Line - s0 s0
# 1 gam 1+ s1+ s1+
# 2 <BNF> 2+ s2+ s2+
# 3 all other - 1+ 2+
#
# Key:
# digit = next state
# s = current item done, save it
# + = Add current line to buffer
# - = no-op
#
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.stderr.write("Single argument must be the input file name\n")
sys.exit(1)
fname = sys.argv[1]
state = 0 # Current FSM State
buffer = [] # Work are in which we build an item
lnum = 0
key = ''
with open(fname,"r") as infile:
for line in infile:
lnum += 1
line = fixSpecialCases(line.rstrip())
if not validate(line):
sys.stderr.write(f"Unbalanced angle-brackets in line {lnum}\n")
else:
token,k,line = examine(line)
if state == 0:
if token==1 or token==2:
buffer = [line]
if token==2: key = k
state = token
else:
if token < 3:
save(state,key,buffer)
buffer = [line]
if token==2: key = k
state = token
else:
buffer.append(line)
# On EOF, close out the last unclosed item if there is one
if state > 0:
save(state,key,buffer)
# Write out the HTML
print(prefix)
print('<h1 id="_commands">Commands</h1>')
for v in sorted(commands):
print(resolve(v))
print('<h1 id="_parameters">Parameters</h1>')
for k,v in productions.items():
print(resolve(v,k))
print(suffix)

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(opensslIncludeDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(opensslOutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>ws2_32.lib;libcrypto.lib;libssl.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<PropertyGroup>
<_DLLSuffix>-3</_DLLSuffix>
<_DLLSuffix Condition="$(Platform) == 'ARM'">$(_DLLSuffix)-arm</_DLLSuffix>
<_DLLSuffix Condition="$(Platform) == 'ARM64'">$(_DLLSuffix)-arm64</_DLLSuffix>
<_DLLSuffix Condition="$(Platform) == 'x64'">$(_DLLSuffix)-x64</_DLLSuffix>
</PropertyGroup>
<!-- GAM Static Build: Disable missing DLL/PDB copy
<ItemGroup>
<_SSLDLL Include="$(opensslOutDir)\libcrypto$(_DLLSuffix).dll" />
<_SSLDLL Include="$(opensslOutDir)\libcrypto$(_DLLSuffix).pdb" />
<_SSLDLL Include="$(opensslOutDir)\libssl$(_DLLSuffix).dll" />
<_SSLDLL Include="$(opensslOutDir)\libssl$(_DLLSuffix).pdb" />
</ItemGroup>
-->
<Target Name="_CopySSLDLL" Inputs="@(_SSLDLL)" Outputs="@(_SSLDLL->'$(OutDir)%(Filename)%(Extension)')" AfterTargets="Build">
<Copy SourceFiles="@(_SSLDLL)" DestinationFolder="$(OutDir)" />
</Target>
<Target Name="_CleanSSLDLL" BeforeTargets="Clean">
<Delete Files="@(_SSLDLL->'$(OutDir)%(Filename)%(Extension)')" TreatErrorsAsWarnings="true" />
</Target>
</Project>

View File

@@ -1,67 +0,0 @@
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
index 1235eff72f7..f68e34b7560 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -134,6 +134,17 @@ static void _PySSLFixErrno(void) {
#error Unsupported OpenSSL version
#endif
+#if (OPENSSL_VERSION_NUMBER >= 0x40000000L)
+# define OPENSSL_NO_SSL3
+# define OPENSSL_NO_TLS1
+# define OPENSSL_NO_TLS1_1
+# define OPENSSL_NO_TLS1_2
+# define OPENSSL_NO_SSL3_METHOD
+# define OPENSSL_NO_TLS1_METHOD
+# define OPENSSL_NO_TLS1_1_METHOD
+# define OPENSSL_NO_TLS1_2_METHOD
+#endif
+
/* OpenSSL API 1.1.0+ does not include version methods */
#ifndef OPENSSL_NO_SSL3_METHOD
extern const SSL_METHOD *SSLv3_method(void);
@@ -1133,7 +1144,7 @@ _asn1obj2py(_sslmodulestate *state, const ASN1_OBJECT *name, int no_name)
static PyObject *
_create_tuple_for_attribute(_sslmodulestate *state,
- ASN1_OBJECT *name, ASN1_STRING *value)
+ const ASN1_OBJECT *name, const ASN1_STRING *value)
{
Py_ssize_t buflen;
PyObject *pyattr;
@@ -1162,16 +1173,16 @@ _create_tuple_for_attribute(_sslmodulestate *state,
}
static PyObject *
-_create_tuple_for_X509_NAME (_sslmodulestate *state, X509_NAME *xname)
+_create_tuple_for_X509_NAME (_sslmodulestate *state, const X509_NAME *xname)
{
PyObject *dn = NULL; /* tuple which represents the "distinguished name" */
PyObject *rdn = NULL; /* tuple to hold a "relative distinguished name" */
PyObject *rdnt;
PyObject *attr = NULL; /* tuple to hold an attribute */
int entry_count = X509_NAME_entry_count(xname);
- X509_NAME_ENTRY *entry;
- ASN1_OBJECT *name;
- ASN1_STRING *value;
+ const X509_NAME_ENTRY *entry;
+ const ASN1_OBJECT *name;
+ const ASN1_STRING *value;
int index_counter;
int rdn_level = -1;
int retcode;
@@ -6506,9 +6517,15 @@ sslmodule_init_constants(PyObject *m)
ADD_INT_CONST("PROTOCOL_TLS", PY_SSL_VERSION_TLS);
ADD_INT_CONST("PROTOCOL_TLS_CLIENT", PY_SSL_VERSION_TLS_CLIENT);
ADD_INT_CONST("PROTOCOL_TLS_SERVER", PY_SSL_VERSION_TLS_SERVER);
+#ifndef OPENSSL_NO_TLS1
ADD_INT_CONST("PROTOCOL_TLSv1", PY_SSL_VERSION_TLS1);
+#endif
+#ifndef OPENSSL_NO_TLS1_1
ADD_INT_CONST("PROTOCOL_TLSv1_1", PY_SSL_VERSION_TLS1_1);
+#endif
+#ifndef OPENSSL_NO_TLS1_2
ADD_INT_CONST("PROTOCOL_TLSv1_2", PY_SSL_VERSION_TLS1_2);
+#endif
#define ADD_OPTION(NAME, VALUE) if (sslmodule_add_option(m, NAME, (VALUE)) < 0) return -1

View File

@@ -1,204 +0,0 @@
// Node.js script to launch Simply Sign Desktop app and log a user in
// using native Windows keystrokes and screenshot-desktop for reliable CI imaging.
import { execSync, spawn } from 'child_process';
import { TOTP } from 'totp-generator';
import path from 'path';
import fs from 'fs';
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Native PowerShell Keystroke Sender
function sendKeys(keys) {
const script = `$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('${keys}')`;
execSync(`powershell -Command "${script}"`);
}
// Native PowerShell Desktop Clear
function minimizeAllWindows() {
console.log('Minimizing all rogue background windows...');
const script = `$shell = New-Object -ComObject "Shell.Application"; $shell.MinimizeAll()`;
try {
execSync(`powershell -Command "${script}"`);
} catch (err) {
console.log('Minimize command failed silently.');
}
}
async function takeScreenshot(filename) {
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const fullPath = path.join(workspace, filename);
// Create a temporary script file path
const scriptPath = path.join(workspace, `screenshot_${Date.now()}.ps1`);
const psScript = `
Add-Type -AssemblyName System.Windows.Forms;
Add-Type -AssemblyName System.Drawing;
$Screen = [System.Windows.Forms.SystemInformation]::VirtualScreen;
if ($Screen.Width -eq 0 -or $Screen.Height -eq 0) {
Write-Error "Screen dimensions are 0x0. Desktop not fully initialized.";
exit 1;
}
$bitmap = New-Object System.Drawing.Bitmap $Screen.Width, $Screen.Height;
$graphic = [System.Drawing.Graphics]::FromImage($bitmap);
$graphic.CopyFromScreen($Screen.Left, $Screen.Top, 0, 0, $bitmap.Size);
# Save the file (using single quotes safely now)
$bitmap.Save('${fullPath}');
Write-Output "Wrote ${fullPath}";
# Specify ItemType to prevent older PS versions from prompting interactively
New-Item -Path "${fullPath}.written" -ItemType File | Out-Null;
`;
try {
// 1. Write the script to disk
fs.writeFileSync(scriptPath, psScript);
// 2. Execute the file directly, piping stdout/stderr to the Node console
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`, {
stdio: 'inherit'
});
console.log(`Saved screenshot: ${fullPath}`);
} catch (err) {
console.error(`Failed to save screenshot ${fullPath}:`, err.message);
} finally {
// 3. Clean up the temp file so it doesn't clutter your CI artifacts
if (fs.existsSync(scriptPath)) {
fs.unlinkSync(scriptPath);
}
}
}
// Fire and forget application launcher with logging
function launchSSD() {
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const outLogPath = path.join(workspace, 'ssd_out.log');
const errLogPath = path.join(workspace, 'ssd_err.log');
// Open file descriptors for logging
const out = fs.openSync(outLogPath, 'a');
const err = fs.openSync(errLogPath, 'a');
console.log(`Launching SSD... Logging stdout to ${outLogPath} and stderr to ${errLogPath}`);
const child = spawn('C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe', [], {
detached: true,
// stdio array: [stdin, stdout, stderr]
// We ignore stdin, and pipe stdout/stderr to our files
stdio: ['ignore', out, err]
});
// Catch immediate errors (e.g., file not found, permission denied)
child.on('error', (error) => {
console.error('Failed to spawn SimplySign Desktop:', error.message);
});
// Unreference the child so the parent script can exit
child.unref();
}
async function runSSD() {
const runner_arch = process.env.RUNNER_ARCH;
if ( runner_arch === "ARM64" ) {
console.log('Running on ARM64. Tabbing through OOBE...');
await sleep(3000);
await takeScreenshot('oob1.png');
// Page 1: Tab through the toggles to reach the "Next" button
for (let i = 0; i < 7; i++) {
sendKeys('{TAB}');
await sleep(200);
}
sendKeys('{ENTER}');
console.log('Clicked Next');
await sleep(3000);
await takeScreenshot('ooob2.png');
// Page 2: Tab through the remaining toggles to reach the "Accept" button
for (let i = 0; i < 7; i++) {
sendKeys('{TAB}');
await sleep(200);
}
sendKeys('{ENTER}');
console.log('Clicked Accept');
await sleep(3000);
await takeScreenshot('oob3.png');
sendKeys('{ESC}');
console.log('Escaped start menu');
await sleep(3000);
await takeScreenshot('oob4.png');
} else {
console.log('NOT running on ARM64');
}
// Re-execute SSD to open login dialog
launchSSD();
await sleep(3000);
await takeScreenshot('008.png');
launchSSD();
await sleep(3000);
await takeScreenshot('009.png');
// 2. Login Flow
console.log('Typing credentials...');
// Type Email
sendKeys('jay0lee@gmail.com');
await sleep(500);
await takeScreenshot('010.png');
// Tab to next field
sendKeys('{TAB}');
await sleep(500);
// Generate and type TOTP
console.log(`Our secret is ${process.env.TOTP_SECRET.length} characters.`);
const { otp } = await TOTP.generate(process.env.TOTP_SECRET, {algorithm: 'SHA-256'});
console.log(`Our token is ${otp.length} characters.`);
sendKeys(otp);
await sleep(500);
await takeScreenshot('011.png');
// Submit
sendKeys('{ENTER}');
console.log('Login sequence complete.');
// Screenshot cascade to monitor the window closing
await takeScreenshot('012.png');
await sleep(500);
await takeScreenshot('013.png');
await sleep(500);
await takeScreenshot('014.png');
await sleep(500);
console.log('Exiting script, leaving SimplySign running in background.');
// Verification block to list all PNGs in the workspace
console.log('\n--- Screenshot Verification ---');
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
try {
const files = fs.readdirSync(workspace);
const pngFiles = files.filter(f => f.endsWith('.png'));
console.log(`Target Directory: ${workspace}`);
console.log(`Found ${pngFiles.length} .png files:`);
pngFiles.forEach(f => console.log(` - ${f}`));
} catch (err) {
console.error(`Error reading directory ${workspace}:`, err.message);
}
console.log('-------------------------------\n');
}
runSSD();

View File

@@ -1,42 +0,0 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers={VERSION_TUPLE},
prodvers={VERSION_TUPLE},
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x4,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x2,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'040904b0',
[StringStruct('CompanyName', 'GAM-team'),
StringStruct('FileDescription', 'CLI for Google Workspace admins'),
StringStruct('FileVersion', '{VERSION}'),
StringStruct('LegalCopyright', 'Copyright (c) 2024 GAM team'),
StringStruct('OriginalFilename', 'gam.exe'),
StringStruct('ProductName', 'GAM'),
StringStruct('ProductVersion', '{VERSION}')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]
)

View File

@@ -1,5 +0,0 @@
# Scratch Me Wiki
editing these files shouldn't trigger a regular GitHub Action build.
# Counter
00010

View File

@@ -1,30 +0,0 @@
# Addresses
- [API documentation](#api-documentation)
- [Display addresses](#display-addresses)
## API documentation
* [Directory API - Domains](https://developers.google.com/admin-sdk/directory/reference/rest/v1/domains)
* [Directory API - Groups](https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups)
* [Directory API - Resources Calendars](https://developers.google.com/admin-sdk/directory/reference/rest/v1/resources.calendars)
* [Directory API - Users](https://developers.google.com/admin-sdk/directory/reference/rest/v1/users)
## Display addresses
Produces a three column CSV file (headers Type, Email, Target) that displays all group and user primary
email addresses and aliases; resource calendar addresses and domain names.
The types are:
```
DomainPrimary, DomainSecondary, DomainAlias
Group, GroupAlias, GroupNEAlias
Resource
SuspendedUser, SuspendedUserAlias, SuspendedUserNEAlias
User, UserAlias, UserNEAlias
```
'NE' is an abbreviation for NonEditable.
```
gam print addresses [todrive <ToDriveAttribute>*]
[domain <DomainName>]
```
By default, groups and users in all domains in the account are selected; this options allows selection of subsets of groups and users:
* `domain <DomainName>` - Limit groups and users to those in `<DomainName>`

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +0,0 @@
# Alert Center
- [API documentation](#api-documentation)
- [Query documentation](#query-documentation)
- [Definitions](#definitions)
- [Introduction](#introduction)
- [Manage alerts](#manage-alerts)
- [Display alerts](#display-alerts)
- [Manage alert feedback](#manage-alert-feedback)
- [Display alert feedback](#display-alert-feedback)
- [Configuring settings](#configuring-settings)
## API documentation
* [Alert Center API](https://developers.google.com/admin-sdk/alertcenter/reference/rest/)
## Query documentation
* [Query Filters](https://developers.google.com/admin-sdk/alertcenter/guides/query-filters)
* [Query Fields](https://developers.google.com/admin-sdk/alertcenter/reference/filter-fields)
## Definitions
```
<AlertID> ::= <String>
<PubSubTopicName> ::= <String>
<QueryAlert> ::= <String> See: https://developers.google.com/admin-sdk/alertcenter/guides/query-filters
```
## Introduction
For an introduction, start here: https://support.google.com/a/answer/9105393
This API is in beta, most things seem to work although the filter queries don't all work, in particular those that
select alertId and feedbackId.
To use these commands you must update your gam project and service account authorization.
```
gam update project
gam user user@domain.com update serviceaccount
```
## Manage alerts
```
gam delete alert <AlertID>
gam undelete alert <AlertID>
```
## Display alerts
```
gam info alert <AlertID> [formatjson]
gam show alerts [filter <QueryAlert>] [orderby createtime [ascending|descending]]
[formatjson]
```
By default, Gam displays the information as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format.
```
gam print alerts [todrive <ToDriveAttributes>*] [filter <QueryAlert>] [orderby createtime [ascending|descending]]
[formatjson [quotechar <Character>]]
```
By default, Gam displays the information as columns of fields; the following option causes the output to be in JSON format,
* `formatjson` - Display the fields in JSON format.
By default, when writing CSV files, Gam uses a quote character of double quote `"`. The quote character is used to enclose columns that contain
the quote character itself, the column delimiter (comma by default) and new-line characters. Any quote characters within the column are doubled.
When using the `formatjson` option, double quotes are used extensively in the data resulting in hard to read/process output.
The `quotechar <Character>` option allows you to choose an alternate quote character, single quote for instance, that makes for readable/processable output.
`quotechar` defaults to `gam.cfg/csv_output_quote_char`. When uploading CSV files to Google, double quote `"` should be used.
### Eliminate unwanted fields
You can use [CSV Print Filtering](CSV-Print-Filtering) to reduce the amount of output.
This command will drop all of the data.messages columns.
```
gam config csv_output_header_drop_filter "^data.messages" redirect csv alerts.csv print alerts
```
## Manage alert feedback
```
gam create alertfeedback <AlertID> not_useful|somewhat_useful|very_useful
```
## Display alert feedback
```
gam show alertfeedback [alert <AlertID>] [filter <QueryAlert>] [orderby createtime [ascending|descending]]
[formatjson]
```
By default, Gam displays feedback for all alerts.
* `alert <AlertID>` - Display feedback for the selected alert
* `filter <QueryAlert>` - Display feebback for the filtered alerts
By default, Gam displays the information as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format.
```
gam print alertfeedback [todrive <ToDriveAttributes>*] [alert <AlertID>] [filter <QueryAlert>] [orderby createtime [ascending|descending]]
[formatjson [quotechar <Character>]]
```
By default, Gam displays feedback for all alerts.
* `alert <AlertID>` - Display feedback for the selected alert
* `filter <QueryAlert>` - Display feebback for the filtered alerts
By default, Gam displays the information as columns of fields; the following option causes the output to be in JSON format,
* `formatjson` - Display the fields in JSON format.
By default, when writing CSV files, Gam uses a quote character of double quote `"`. The quote character is used to enclose columns that contain
the quote character itself, the column delimiter (comma by default) and new-line characters. Any quote characters within the column are doubled.
When using the `formatjson` option, double quotes are used extensively in the data resulting in hard to read/process output.
The `quotechar <Character>` option allows you to choose an alternate quote character, single quote for instance, that makes for readable/processable output.
`quotechar` defaults to `gam.cfg/csv_output_quote_char`. When uploading CSV files to Google, double quote `"` should be used.
## Configuring settings
Alert Center can be configured to send notifications to a Google Cloud Pub/Sub topic, but it first requires configuration.
* See https://developers.google.com/workspace/admin/alertcenter/guides/notifications for information.
Gam can be used to display or modify the settings:
```
gam show alertsettings
gam update alertsettings <PubSubTopicName>
gam clear alertsettings
```

View File

@@ -1,201 +0,0 @@
# Aliases
- [API documentation](#api-documentation)
- [Query documentation](#query-documentation)
- [Python Regular Expressions](Python-Regular-Expressions) Match function
- [Definitions](#definitions)
- [Create an alias for a target](#create-an-alias-for-a-target)
- [Update an alias to point to a new target](#update-an-alias-to-point-to-a-new-target)
- [Delete an alias regardless of the target](#delete-an-alias-regardless-of-the-target)
- [Remove aliases from a specified target](#remove-aliases-from-a-specified-target)
- [Delete all of a user's aliases](#delete-all-of-a-users-aliases)
- [Display aliases](#display-aliases)
- [Bulk delete aliases](#bulk-delete-aliases)
- [Bulk reassign aliases](#bulk-reassign-aliases)
- [Determine if an address is a user, user alias, group or group alias](#determine-if-an-address-is-a-user-user-alias-group-or-group-alias)
## API documentation
* [Directory API - User Aliases](https://developers.google.com/admin-sdk/directory/reference/rest/v1/users.aliases)
* [Directory API - Group Aliases](https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups.aliases)
## Query documentation
* [Search Users](https://developers.google.com/admin-sdk/directory/v1/guides/search-users)
## Definitions
See [Collections of Items](Collections-of-Items)
```
<DomainName> ::= <String>(.<String>)+
<DomainNameList> ::= "<DomainName>(,<DomainName>)*"
<DomainNameEntity> ::=
<DomainNameList> | <FileSelector> | <CSVFileSelector>
<EmailAddress> ::= <String>@<DomainName>
<EmailAddressList> ::= "<EmailAddress>(,<EmailAddress>)*"
<EmailAddressEntity> ::= <EmailAddressList> | <FileSelector> | <CSVkmdSelector> | <CSVDataSelector>
See: https://github.com/GAM-team/GAM/wiki/Collections-of-Items
<UniqueID> ::= id:<String>
<RegularExpression> ::= <String>
See: https://docs.python.org/3/library/re.html
<REMatchPattern> ::= <RegularExpression>
<RESearchPattern> ::= <RegularExpression>
<RESubstitution> ::= <String>>
```
## Create an alias for a target
```
gam create alias|aliases <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
[verifynotinvitable]
```
`<EmailAddressEntity>` are the aliases, `<EmailAddress>` is the target.
The `verifynotinvitable` option causes GAM to verify that the alias email address being created is not that of an unmanaged account;
if it is, the command is not performed.
### Example
To allow Robert to also receive mail as Bob:
```
gam create alias bob[@yourdomain.com] user robert[@yourdomain.com]
```
## Update an alias to point to a new target
The existing alias is deleted and a new alias is created.
```
gam update alias|aliases <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
[notargetverify] [waitafterdelete <Integer>]
```
`<EmailAddressEntity>` are the aliases, `<EmailAddress>` is the target.
By default, GAM makes additional API calls to verify that the target email address exists before updating the alias;
if you know that the target exists, you can suppress the verification with `notargetverify.
GAM updates an alias to point to a new target by deleting the alias and then recreates the alias pointing to the new target.
Unfortunately, if these commands are executed back-to-back; Google generates the `Update Failed: Duplicate` error.
Now, GAM waits 2 seconds between the delete and the insert which seems to eliminate the problem. If the problem persists,
use the option `waitafterdelete <Integer>` to increase the wait time to a maximum of 10 seconds.
## Delete an alias regardless of the target
```
gam delete alias|aliases [user|group|target] <EmailAddressEntity>
```
`<EmailAddressEntity>` are the aliases.
## Remove aliases from a specified target
```
gam remove alias|aliases <EmailAddress> user|group <EmailAddressEntity>
```
`<EmailAddress>` is the target, `<EmailAddressEntity>` are the aliases.
## Delete all of a user's aliases
```
gam <UserTypeEntity> delete aliases
```
## Display aliases
Display a specific alias.
```
gam info alias|aliases <EmailAddressEntity>
```
Display selected aliases.
```
gam print aliases [todrive <ToDriveAttribute>*]
([domain|domains <DomainNameEntity>] [(query <QueryUser>)|(queries <QueryUserList>)]
[limittoou <OrgUnitItem>])
[user|users <EmailAddressList>] [group|groups <EmailAddressList>]
[select <UserTypeEntity>]
[issuspended <Boolean>] [isarchived <Boolean>] [aliasmatchpattern <REMatchPattern>]
[shownoneditable] [nogroups] [nousers]
[onerowpertarget] [delimiter <Character>]
[suppressnoaliasrows]
(addcsvdata <FieldName> <String>)*
```
By default, group and user aliases in all domains in the account are selected; these options allow selection of subsets of aliases:
* `domain|domains <DomainNameEntity>` - Limit aliases to those in the domains specified by `<DomainNameEntity>`
* You can predefine this list with the `print_agu_domains` variable in `gam.cfg`.
* `(query <QueryUser>)|(queries <QueryUserList>)` - Print aliases for users/groups that match a query; each query is run against each domain
* `limittoou <OrgUnitItem>` - Print aliases for users in the specified `<OrgUnitItem>`
* `user|users <EmailAddressList>` - Print aliases for users in `<EmailAddressList`
* `select <UserTypeEntity>` - Print aliases for users in `<UserTypeEntity>`
* `group|groups <EmailAddressList>` - Print aliases for groups in `<EmailAddressList`
* `issuspended <Boolean>` - Limit users based on their status
* `isarchived <Boolean>` - Limit users based on their status
* `aliasmatchpattern <REMatchPattern>` - Print aliases that match a pattern
* `nogroups` - Print only user aliases
* `nousers` - Print only group aliases
By default, the CSV output has three columns: `Alias,Target,TargetType`; if a target
has multiple aliases, there will be multiple rows, one per alias.
Use `shownoneditable` to list non-editable alias email addresses; these are typically outside of the account's primary domain or subdomains.
This adds the column `NonEditableAlias`.
Specifying `onerowpertarget` changes the three columns to: `Target,TargetType,Aliases`; all aliases for the target are listed in the
`Aliases` column. If `shownoneditable` is specified, there will be a fourth column `NonEditableAliases` with a list of non-editable aliases.
By default, the aliases in a list are separated by the `csv_output_field_delimiter' from `gam.cfg`.
* `delimiter <Character>` - Separate aliases in a list with `<Character>`
Specifying both `onerowpertarget` and `suppressnoaliasrows` causes GAM to not display any targets that have no aliases.
Add additional columns of data from the command line to the output
* `addcsvdata <FieldName> <String>`
When multiple domains are specified and a query/queries are specified, an API call is made for each domain/query combination.
```
$ gam print aliases domains school.org,students.school.org queries "'email:admin*','email:test*'"
Getting all Users that match query (domain=school.org, query="email:admin*"), may take some time on a large Google Workspace Account...
Got 3 Users: admin@school.org - admindirector@school.org
Getting all Users that match query (domain=school.org, query="email:test*"), may take some time on a large Google Workspace Account...
Got 20 Users: testusera@school.org - testuserx@school.org
Getting all Users that match query (domain=students.school.org, query="email:admin*"), may take some time on a large Google Workspace Account...
Got 1 User: admin@students.school.org - admin@students.school.org
Getting all Users that match query (domain=students.school.org, query="email:test*"), may take some time on a large Google Workspace Account...
Got 1 User: testuser1@students.school.org - testuser1@students.school.org
Alias,Target,TargetType
...
```
## Bulk delete aliases
You can bulk delete aliases as follows; use `(query <QueryUser>)|(queries <QueryUserList>)` and
`aliasmatchpattern <REMatchPattern>` as desired.
```
gam redirect csv ./OldDomainAliases.csv print aliases aliasmatchpattern ".*@olddomain.com" onerowpertarget suppressnoaliasrows
gam redirect stdout ./DeleteAliases.txt multiprocess redirect stderr stdout csv ./OldDomainAliases.csv gam remove aliases "~Target" "~TargetType" "~Aliases"
```
## Bulk reassign aliases
You can bulk reassign aliases as follows. Make a CSV file ReassignAliases.csv with two columns: OldTarget,NewTarget.
From this CSV file, all of the aliases for the users in the OldTarget column will be listed with an additional column showing the NewTarget.
```
gam redirect stdout ./GetAliases.txt multiprocess redirect stderr stdout redirect csv ./ReassignAliases.csv gam print aliases user "~OldTarget" addcsvdata NewTarget "~NewTarget"
```
If an OldTarget's aliases are to be reassigned to more than the one NewTarget, edit ReassignAliases.csv and make changes as required.
```
gam redirect stdout ./ReassignAliases.txt multiprocess redirect stderr stdout csv ReassignAliases.csv gam update alias "~Alias" user "~NewTarget"
```
## Determine if an address is a user, user alias, group or group alias
```
gam whatis <EmailItem> [noinfo] [noinvitablecheck]
```
The first line of output is: `<TypeOfEmailItem>: <EmailItem>`
There is additional output based on `<TypeOfEmailItem>`:
* User - `gam info user <EmailItem>`
* Group - `gam info group <EmailItem>`
* User Alias - `gam info alias <EmailItem>`
* Group Alias - `gam info alias <EmailItem>`
* User Invitation - `gam info userinvitation <EmailItem>`
The `noinfo` argument suppresses the additional output.
The `noinvitablecheck` argument suppresses the user invitation check
to avoid exceeding quota limits when checking a large number of addresses.
The return code is set based on `<TypeOfEmailItem>`:
* User - 20
* User Alias - 21
* Group - 22
* Group Alias - 23
* User Invitation - 24
* Unknown - 59

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
# Syntax
## BNF Syntax
This Wiki describes the GAM7 command line syntax in modified BNF.
* https://en.wikipedia.org/wiki/Backus-Naur_Form
Skip the History section and start reading at Introduction.
Items on the command line are space separated, when an actual space character is required, it will be indicated by ```<Space>```.
If an item contains spaces, it should be surrounded by ".
Metasyntactic symbols
```
[] optional item
() group items
* item may appear zero or more times
+ item may appear one or more times
| separates alternative items
```
## Items
- [Basic](Basic-Items)
- [Lists](List-Items)
## Collections
- [ChromeOS Devices](Collections-of-ChromeOS-Devices)
- [Users](Collections-of-Users)
- [Items](Collections-of-Items)
- [Verify Collections](List)
## Python Regular Expressions
- [Python Regular Expressions](Python-Regular-Expressions)

View File

@@ -1,594 +0,0 @@
# Basic Items
- [Primitives](#primitives)
- [Items built from primitives](#items-built-from-primitives)
- [Named items](#named-items)
- [List Items](List-Items)
## Primitives
```
<Character> ::= a single character
<Digit> ::= 0|1|2|3|4|5|6|7|8|9
<Number> ::= <Digit>+
<Float> ::= <Digit>*.<Digit>+
<Hex> ::= <Digit>|a|b|c|d|e|f|A|B|C|D|E|F
<Space> ::= an actual space character
<String> ::= a string of characters, surrounded by " if it contains spaces
<FalseValues>= false|off|no|disabled|0
<TrueValues> ::= true|on|yes|enabled|1
<BCP47LanguageCode> ::=
ar-sa| # Arabic Saudi Arabia
cs-cz| # Czech Czech Republic
da-dk| # Danish Denmark
de-de| # German Germany
el-gr| # Modern Greek Greece
en-au| # English Australia
en-gb| # English United Kingdom
en-ie| # English Ireland
en-us| # English United States
en-za| # English South Africa
es-es| # Spanish Spain
es-mx| # Spanish Mexico
fi-fi| # Finnish Finland
fr-ca| # French Canada
fr-fr| # French France
he-il| # Hebrew Israel
hi-in| # Hindi India
hu-hu| # Hungarian Hungary
id-id| # Indonesian Indonesia
it-it| # Italian Italy
ja-jp| # Japanese Japan
ko-kr| # Korean Republic of Korea
nl-be| # Dutch Belgium
nl-nl| # Dutch Netherlands
no-no| # Norwegian Norway
pl-pl| # Polish Poland
pt-br| # Portuguese Brazil
pt-pt| # Portuguese Portugal
ro-ro| # Romanian Romania
ru-ru| # Russian Russian Federation
sk-sk| # Slovak Slovakia
sv-se| # Swedish Sweden
th-th| # Thai Thailand
tr-tr| # Turkish Turkey
zh-cn| # Chinese China
zh-hk| # Chinese Hong Kong
zh-tw # Chinese Taiwan
<Charset> ::= ascii|latin1|mbcs|utf-8|utf-8-sig|utf-16|<String>
<CalendarColorIndex> ::= <Number in range 1-24>
<CalendarColorName> ::=
amethyst|avocado|banana|basil|birch|blueberry|
cherryblossom|citron|cobalt|cocoa|eucalyptus|flamingo|
grape|graphite|lavender|mango|peacock|pistachio|
pumpkin|radicchio|sage|tangerine|tomato|wisteria|
<ColorHex> ::= "#<Hex><Hex><Hex><Hex><Hex><Hex>"
<ColorNameGoogle> ::=
asparagus|bluevelvet|bubblegum|cardinal|chocolateicecream|denim|desertsand|
earthworm|macaroni|marsorange|mountaingray|mountaingrey|mouse|oldbrickred|
pool|purpledino|purplerain|rainysky|seafoam|slimegreen|spearmint|
toyeggplant|vernfern|wildstrawberries|yellowcab
<ColorNameWeb> ::=
aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|
blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|
cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|
darkgrey|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|
darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|
darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|
firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|
gray|grey|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|
lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|
lightgoldenrodyellow|lightgray|lightgrey|lightgreen|lightpink|lightsalmon|
lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|
lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|
mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|
mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|
navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|
palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|
peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|
sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|
slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|
wheat|white|whitesmoke|yellow|yellowgreen
<ColorName> ::= <ColorNameGoogle>|<ColorNameWeb>
<ColorValue> ::= <ColorName>|<ColorHex>
<DayOfWeek> ::= mon|tue|wed|thu|fri|sat|sun
<EventColorIndex> ::= <Number in range 1-11>
<EventColorName> ::=
banana|basil|blueberry|flamingo|graphite|grape|
lavender|peacock|sage|tangerine|tomato
<FileFormat> ::=
csv|doc|dot|docx|dotx|epub|html|jpeg|jpg|json|mht|odp|ods|odt|
pdf|png|ppt|pot|potx|pptx|rtf|svg|tsv|txt|xls|xlt|xlsx|xltx|zip|
ms|microsoft|openoffice|
<LabelColorHex> ::=
#000000|#076239|#0b804b|#149e60|#16a766|#1a764d|#1c4587|#285bac|
#2a9c68|#3c78d8|#3dc789|#41236d|#434343|#43d692|#44b984|#4a86e8|
#653e9b|#666666|#68dfa9|#6d9eeb|#822111|#83334c|#89d3b2|#8e63ce|
#999999|#a0eac9|#a46a21|#a479e2|#a4c2f4|#aa8831|#ac2b16|#b65775|
#b694e8|#b9e4d0|#c6f3de|#c9daf8|#cc3a21|#cccccc|#cf8933|#d0bcf1|
#d5ae49|#e07798|#e4d7f5|#e66550|#eaa041|#efa093|#efefef|#f2c960|
#f3f3f3|#f691b3|#f6c5be|#f7a7c0|#fad165|#fb4c2f|#fbc8d9|#fcda83|
#fcdee8|#fce8b3|#fef1d1|#ffad47|#ffbc6b|#ffd6a2|#ffe6c7|#ffffff
<LabelBackgroundColorHex> ::=
#16a765|#2da2bb|#42d692|#4986e7|#98d7e4|#a2dcc1|
#b3efd3|#b6cff5|#b99aff|#c2c2c2|#cca6ac|#e3d7ff|
#e7e7e7|#ebdbde|#f2b2a8|#f691b2|#fb4c2f|#fbd3e0|
#fbe983|#fdedc1|#ff7537|#ffad46|#ffc8af|#ffdeb5
<LabelTextColorHex> ::=
#04502e|#094228|#0b4f30|#0d3472|#0d3b44|#3d188e|
#464646|#594c05|#662e37|#684e07|#711a36|#7a2e0b|
#7a4706|#8a1c0a|#994a64|#ffffff
<LanguageCode> ::=
ach|af|ag|ak|am|ar|az|be|bem|bg|bn|br|bs|ca|chr|ckb|co|crs|cs|cy|da|de|
ee|el|en|en-ca|en-gb|en-us|eo|es|es-419|et|eu|fa|fi|fil|fo|fr|fr-ca|fy|
ga|gaa|gd|gl|gn|gu|ha|haw|he|hi|hr|ht|hu|hy|ia|id|ig|in|is|it|iw|ja|jw|
ka|kg|kk|km|kn|ko|kri|ku|ky|la|lg|ln|lo|loz|lt|lua|lv|
mfe|mg|mi|mk|ml|mn|mo|mr|ms|mt|my|ne|nl|nn|no|nso|ny|nyn|oc|om|or|
pa|pcm|pl|ps|pt-br|pt-pt|qu|rm|rn|ro|ru|rw|
sd|sh|si|sk|sl|sn|so|sq|sr|sr-me|st|su|sv|sw|
ta|te|tg|th|ti|tk|tl|tn|to|tr|tt|tum|tw|
ug|uk|ur|uz|vi|wo|xh|yi|yo|zh-cn|zh-hk|zh-tw|zu
<Language> ::=
<LanguageCode>[+|-]|
<String>
<Locale> ::=
''| #Not defined
ar-eg| #Arabic, Egypt
az-az| #Azerbaijani, Azerbaijan
be-by| #Belarusian, Belarus
bg-bg| #Bulgarian, Bulgaria
bn-in| #Bengali, India
ca-es| #Catalan, Spain
cs-cz| #Czech, Czech Republic
cy-gb| #Welsh, United Kingdom
da-dk| #Danish, Denmark
de-ch| #German, Switzerland
de-de| #German, Germany
el-gr| #Greek, Greece
en-au| #English, Australia
en-ca| #English, Canada
en-gb| #English, United Kingdom
en-ie| #English, Ireland
en-us| #English, U.S.A.
es-ar| #Spanish, Argentina
es-bo| #Spanish, Bolivia
es-cl| #Spanish, Chile
es-co| #Spanish, Colombia
es-ec| #Spanish, Ecuador
es-es| #Spanish, Spain
es-mx| #Spanish, Mexico
es-py| #Spanish, Paraguay
es-uy| #Spanish, Uruguay
es-ve| #Spanish, Venezuela
fi-fi| #Finnish, Finland
fil-ph| #Filipino, Philippines
fr-ca| #French, Canada
fr-fr| #French, France
gu-in| #Gujarati, India
hi-in| #Hindi, India
hr-hr| #Croatian, Croatia
hu-hu| #Hungarian, Hungary
hy-am| #Armenian, Armenia
in-id| #Indonesian, Indonesia
it-it| #Italian, Italy
iw-il| #Hebrew, Israel
ja-jp| #Japanese, Japan
ka-ge| #Georgian, Georgia
kk-kz| #Kazakh, Kazakhstan
kn-in| #Kannada, India
ko-kr| #Korean, Korea
lt-lt| #Lithuanian, Lithuania
lv-lv| #Latvian, Latvia
ml-in| #Malayalam, India
mn-mn| #Mongolian, Mongolia
mr-in| #Marathi, India
my-mn| #Burmese, Myanmar
nl-nl| #Dutch, Netherlands
nn-no| #Nynorsk, Norway
no-no| #Bokmal, Norway
pa-in| #Punjabi, India
pl-pl| #Polish, Poland
pt-br| #Portuguese, Brazil
pt-pt| #Portuguese, Portugal
ro-ro| #Romanian, Romania
ru-ru| #Russian, Russia
sk-sk| #Slovak, Slovakia
sl-si| #Slovenian, Slovenia
sr-rs| #Serbian, Serbia
sv-se| #Swedish, Sweden
ta-in| #Tamil, India
te-in| #Telugu, India
th-th| #Thai, Thailand
tr-tr| #Turkish, Turkey
uk-ua| #Ukrainian, Ukraine
vi-vn| #Vietnamese, Vietnam
zh-cn| #Simplified Chinese, China
zh-hk| #Traditional Chinese, Hong Kong SAR China
zh-tw #Traditional Chinese, Taiwan
<MimeTypeShortcut> ::=
gdoc|gdocument|
gdrawing|
gfile|
gfolder|gdirectory|
gform|
gfusion|
gjam|
gmap|
gpresentation|
gscript|
gsheet|gspreadsheet|
gshortcut|
g3pshortcut|
gsite|
shortcut
<MimeTypeName> ::= application|audio|font|image|message|model|multipart|text|video
<MimeType> ::= <MimeTypeShortcut>|(<MimeTypeName>/<String>)
```
## Items built from primitives
```
<Boolean> ::= <TrueValues>|<FalseValues>
<ByteCount> ::= <Number>[m|k|b]
<CIDRnetmask> ::= <Number>.<Number>.<Number>.<Number>/<Number>
<Year> ::= <Digit><Digit><Digit><Digit>
<Month> ::= <Digit><Digit>
<Day> ::= <Digit><Digit>
<Hour> ::= <Digit><Digit>
<Minute> ::= <Digit><Digit>
<Second> ::= <Digit><Digit>
<MilliSeconds> ::= <Digit><Digit><Digit>
<Date> ::=
<Year>-<Month>-<Day> |
(+|-)<Number>(d|w|y) |
never|
today
<DateTime> ::=
<Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute> |
(+|-)<Number>(m|h|d|w|y) |
never|
now|today
<Time> ::=
<Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute>:<Second>[.<MilliSeconds>](Z|(+|-(<Hour>:<Minute>))) |
(+|-)<Number>(m|h|d|w|y) |
never|
now|today
<RegularExpression> ::= <String>
See: https://docs.python.org/3/library/re.html
<REMatchPattern> ::= <RegularExpression>
<RESearchPattern> ::= <RegularExpression>
<RESubstitution> ::= <String>>
<ProjectID> ::= <String>
Must match this Python Regular Expression: [a-z][a-z0-9-]{4,28}[a-z0-9]
<ServiceAccountName> ::= <String>
Must match this Python Regular Expression: [a-z][a-z0-9-]{4,28}[a-z0-9]
<SiteName> ::= [a-z,0-9,-]+
<UniqueID> ::= id:<String>|uid:<String>
```
## Named items
```
<AccessToken> ::= <String>
<AdminAssigneeType> ::= group|user|serviceaccount|unknown
<AlertID> ::= <String>
<APIScopeURL> ::= <String>
<APPID> ::= <String>
<ASPID> ::= <String>
<AssetTag> ::= <String>
<BrowserTokenPermanentID> ::= <String>
<BuildingID> ::= <String>|id:<String>
<CAALevelName> ::= <String>
<CalendarACLScope> ::=
<EmailAddress>|user:<EmailAddress>|group:<EmailAddress>|
domain:<DomainName>|domain|default
<CalendarItem> ::= <EmailAddress>
<ChannelCustomerID> ::= <String>
<ChatEmojiName> ::= :[0-9a-z_-]+:
<ChatEmoji> ::= emojiname <ChatEmojiName> | customemojis/<String>
<ChatMember> ::= spaces/<String>/members/<String>
<ChatMessage> ::= spaces/<String>/messages/<String>
<ChatSection> ::= users/<String>/sections/<String> | sections/<String> | section <String>
<ChatSectionItem> ::= users/<String>/sections/<String>/items/<String> | sections/<String>/items/<String>
<ChatSpace> ::= spaces/<String> | space <String> | space spaces/<String>
<ChatThread> ::= spaces/<String>/threads/<String>
<ChromeProfilePermanentID> ::= <String>
<ChromeProfileName> ::= customers/<CustomerID>/profiles/<ChromeProfilePermanentID> | <ChromeProfilePermanentID>
<ChromeProfileCommandName> ::= <ChomeProfileName>/commands/<String>
<GIGroupAlias> ::= <EmailAddress>
<GIGroupItem> ::= <EmailAddress>|<UniqueID>|groups/<String>
<CIGroupMemberType> ::= cbcmbrowser|chromeosdevice|customer|group|other|serviceaccount|user
<CIPolicyName> ::= policies/<String>|settings/<String>|<String>
<ClassificationLabelID> ::= <String>
<ClassificationLabelFieldID> ::= <String>
<ClassificationLabelSelectionID> ::= <String>
<ClassificationLabelName> ::= labels/<ClassificationLabelID>[@latest|@published|@<Number>]
<ClassificationLabelPermissionName> ::= labels/<ClassificationLabelID>[@latest|@published|@<Number>]/permissions/(audiences|groups|people)/<String>
<ClassroomInvitationID> ::= <String>
<ClientID> ::= <String>
<CommandID> ::= <String>
<ContactID> ::= <String>
<ContactGroupID> ::= id:<String>
<ContactGroupName> ::= <String>
<ContactGroupItem> ::= <ContactGroupID>|<ContactGroupName>
<CorporaAttribute> ::= alldrives|allteamdrives|domain|onlyteamdrives|user
<CourseAlias> ::= <String>
<CourseAnnouncementContent> ::=
((text <String>)|
(textfile <FileName> [charset <Charset>])|
(gdoc <UserGoogleDoc>)|
(gcsdoc <StorageBucketObjectName>))
<CourseAnnouncementID> ::= <Number>
<CourseAnnouncementState> ::= draft|published|deleted
<CourseID> ::= <Number>|d:<CourseAlias>
<CourseMaterialID> ::= <Number>
<CourseMaterialState> ::= draft|published|deleted
<CourseParticipantType> ::= teacher|teachers|student|students
<CourseState> ::= active|archived|provisioned|declined|suspended
<CourseSubmissionID> ::= <Number>
<CourseSubmissionState> ::= new|created|turned_in|returned|reclaimed_by_student
<CourseTopic> ::= <String>
<CourseTopicID> ::= <Number>
<CourseWorkID> ::= <Number>
<CourseWorkState> ::= draft|published|deleted
<CrOSID> ::= <String>
<CustomerID> ::= <String>
<DateTimeFormat> ::= <String>
See: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
<DeliverySetting> ::=
allmail|
abridged|daily|
digest|
disabled|
none|nomail
<DeviceID> ::= devices/<String>
<DeviceType> ::= android|chrome_os|google_sync|ios|linux|mac_os|windows
<DeviceUserID> ::= devices/<String>/deviceUsers/<String>
<DomainAlias> ::= <String>
<DomainName> ::= <String>(.<String>)+
<DriveFileACLRole> ::=
commenter|
contentmanager|fileorganizer|
contributor|editor|writer|
manager|organizer|owner|
reader|viewer
<DriveFileACLType> ::= anyone|domain|group|user
<DriveFileID> ::= <String>
<DriveFileURL> ::=
https://drive.google.com/open?id=<DriveFileID>
https://drive.google.com/drive/files/<DriveFileID>
https://drive.google.com/drive/folders/<DriveFileID>
https://drive.google.com/drive/folders/<DriveFileID>?resourcekey=<String>
https://drive.google.com/file/d/<DriveFileID>/<String>
https://docs.google.com/document/d/<DriveFileID>/<String>
https://docs.google.com/drawings/d/<DriveFileID>/<String>
https://docs.google.com/forms/d/<DriveFileID>/<String>
https://docs.google.com/presentation/d/<DriveFileID>/<String>
https://docs.google.com/spreadsheets/d/<DriveFileID>/<String>
<DriveFileItem> ::= <DriveFileID>|<DriveFileURL>
<DriveFolderID> ::= <String>
<DriveFileName> ::= <String>
<DriveFolderName> ::= <String>
<DriveFolderPath> ::= <String>(/<String>)*
<DriveFilePermission> ::=
anyone;<DriveFileACLRole>|
anyonewithlink;<DriveFileACLRole>|
domain:<DomainName>;<DriveFileACLRole>|
domainwithlink:<DomainName>;<DriveFileACLRole>|
group:<EmailAddress>;<DriveFileACLRole>|
user:<EmailAddress>;<DriveFileACLRole>
<DriveFilePermissionID> ::= anyone|anyonewithlink|id:<String>
<DriveFilePermissionIDorEmail> ::= <DriveFilePermissionID>|<EmailAddress>
<DriveFileRevisionID> ::= <String>
<EmailAddress> ::= <String>@<DomainName>
<EmailItem> ::= <EmailAddress>|<UniqueID>|<String>
<EmailReplacement> ::= <String>
<EventID> ::= <String>
<EventName> ::= <String>
<ExportItem> ::= <UniqueID>|<String>
<ExportStatus> ::= completed|failed|inprogrsss
<FeatureName> ::= <String>
<FieldName> ::= <String>
<FileName> ::= <String>
<FileNamePattern> ::= <String>
<FilterID> ::= <String>
<FloorName> ::= <String>
<GroupItem> ::= <EmailAddress>|<UniqueID>|<String>
<GroupRole> ::= owner|manager|member
<GroupMemberType> ::= customer|group|user
<GuardianItem> ::= <EmailAddress>|<UniqueID>|<String>
<GuardianInvitationID> ::= <String>
<HoldItem> ::= <UniqueID>|<String>
<HostName> ::= <String>
<iCalUID> ::= <String>
<JSONData> ::= (json [charset <Charset>] <String>) | (json file <FileName> [charset <Charset>]) |
<Key> ::= <String>
<LabelID> ::= Label_<String>
<LabelName> ::= <String>
<LabelReplacement> ::= <String>
<LookerStudioAssetID> ::= <String>
<LookerStudioPermission> ::=
user:<EmailAddress>|
group:<EmailAddress>|
domain:<DomainName>|
serviceAccount:<EmailAddress>
<Marker> ::= <String>
<MatterItem> ::= <UniqueID>|<String>
<MatterState> ::= open|closed|deleted
<MeetConferenceName> ::= conferenceRecords/<String>
<MeetID> ::= <String>
Must match this Python Regular Expression: [a-z]{3}-[a-z]{4}-[a-z]{3}
<MeetSpaceName> ::= spaces/<String> | <String>
<MessageContent> ::=
(message|textmessage|htmlmessage <String>)|
(file|textfile|htmlfile <FileName> [charset <Charset>])|
(gdoc|ghtml <UserGoogleDoc>)|
(gcsdoc|gcshtml <StorageBucketObjectName>)
<MessageID> ::= <String>
<Namespace> ::= <String>
<NotesName> ::= notes/<String>
<NotifyMessageContent> ::=
(message|textmessage|htmlmessage <String>)|
(file|textfile|htmlfile <FileName> [charset <Charset>])|
(gdoc|ghtml <UserGoogleDoc>)|
(gcsdoc|gcshtml <StorageBucketObjectName>)
<NumberOfSeats> ::= <Number>
<NumberRange> ::= <Number>|(<Number>/<Number>)
<OrgUnitID> ::= id:<String>
<OrgUnitPath> ::= /|(/<String>)+
<OrgUnitItem> ::= <OrgUnitID>|<OrgUnitPath>
<OtherContactsResourceName> ::= otherContacts/<String>
<ParameterKey> ::= <String>
<ParameterValue> ::= <String>
<Password> ::= <String>
<PeopleResourceName> ::= people/<String>
<PrinterID> ::= <String>
<ProjectID> ::= <String>
Must match this Python Regular Expression: [a-z][a-z0-9-]{4,28}[a-z0-9]
<ProjectName> ::= <String>
Must match this Python Regular Expression: [a-zA-Z0-9 '"!-]{4,30}
<PropertyKey> ::= <String>
<PropertyValue> ::= <String>
<PubSubTopicName> ::= <String>
<QueryAlert> ::= <String>
See: https://developers.google.com/admin-sdk/alertcenter/guides/query-filters
<QueryBrowser> ::= <String>
See: https://support.google.com/chrome/a/answer/9681204#retrieve_all_chrome_devices_for_an_account
<QueryBrowserToken> ::= <String>
See: https://support.google.com/chrome/a/answer/9949706?ref_topic=9301744
<QueryCalendar> ::= <String>
<QueryCEL> ::= <String>
See: https://cloud.google.com/access-context-manager/docs/custom-access-level-spec
<QueryContact> ::= <String>
See: https://developers.google.com/google-apps/contacts/v3/reference#contacts-query-parameters-reference
<QueryCrOS> ::= <String>:<String>
See: https://support.google.com/chrome/a/answer/1698333
<QueryDevice> ::= <String>:<String>
See: https://support.google.com/a/answer/7549103
<QueryDriveFile> ::= <String>
See: https://developers.google.com/drive/api/v3/search-files
<QueryDynamicGroup> ::= <String>
See: https://cloud.google.com/identity/docs/reference/rest/v1/groups#dynamicgroupquery
<QueryGmail> ::= <String>
See: https://support.google.com/mail/answer/7190
<QueryGroup> ::= <String>
See: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
<QueryItem> ::= <UniqueID>|<String>
<QueryMemberRestrictions> ::= <String>
See: https://cloud.google.com/identity/docs/reference/rest/v1beta1/SecuritySettings#MemberRestriction
<QueryMobile> ::= <String>:<String>
See: https://support.google.com/a/answer/7549103
<QueryTeamDrive> ::= <String>
See: https://developers.google.com/drive/api/v3/search-parameters
<QueryUser> ::= <String>
See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
<QueryVaultCorpus> ::= <String>
See: https://developers.google.com/vault/reference/rest/v1/matters.holds#CorpusQuery
<RequestID> ::= <String>
<ResellerID> ::= <String>
<ResourceID> ::= <String>
<SchemaName> ::= <String>
<SchemaNameField> ::= <SchemaName>.<FieldName>
<Section> ::= <String>
<SendAsContent> ::=
(sig|signature|htmlsig <String>)|
(file|htmlfile <FileName> [charset <Charset>])|
(gdoc|ghtml <UserGoogleDoc>)|
(gcsdoc|gcshtml <StorageBucketObjectName>)
<SerialNumber> ::= <String>
<ServiceAccountName> ::= <String>
Must match this Python Regular Expression: [a-z][a-z0-9-]{4,28}[a-z0-9]
<ServiceAccountDisplayName> ::= <String>
Maximum of 100 characters
<ServiceAccountDescrition> ::= <String>
Maximum of 256 chcracters
<ServiceAccountEmail> ::= <ServiceAccountName>@<ProjectID>.iam.gserviceaccount.com
<ServiceAccountUniqueID> ::= <Number>
<ServiceAccountKey> ::= <String>
<SheetEntity> ::= <String>|id:<Number>
<SignatureContent> ::=
(<String>)|
(file|htmlfile <FileName> [charset <Charset>])|
(gdoc|ghtml <UserGoogleDoc>)|
(gcsdoc|gcshtml <StorageBucketObjectName>)
<SiteACLScope> ::=
<EmailAddress>|user:<EmailAddress>|group:<EmailAddress>|
domain:<DomainName>|domain|default
<SiteItem> ::= [<DomainName>/]<SiteName>
<S/MIMEID> ::= <String>
<SMTPHostName> ::= <String>
<StudentGroupID> ::= <Number>
<StudentItem> ::= <EmailAddress>|<UniqueID>|<String>
<SharedDriveACLRole> ::=
commenter|
contentmanager|fileorganizer|
contributor|editor|writer|
manager|organizer|owner|
reader|viewer
<SharedDriveID> ::= <String>
<SharedDriveName> ::= <String>
<StorageBucketName> ::= <String>
<StorageObjectName> ::= <String>
<StorageBucketObjectName> ::=
https://storage.cloud.google.com/<StorageBucketName>/<StorageObjectName>|
https://storage.googleapis.com/<StorageBucketName>/<StorageObjectName>|
gs://<StorageBucketName>/<StorageObjectName>|
<StorageBucketName>/<StorageObjectName>
<Tag> ::= <String>
<TagManagerAccountID> ::= <String>
<TagManagerAccountPath> ::= accounts/<TagManagerAccountID>
<TagManagerContainerID> ::= <String>
<TagManagerContainerPath> ::= accounts/<TagManagerAccountID>/containers/<TagManagerContainerID>
<TagManagerWorkspaceID> ::= <String>
<TagManagerWorkspacePath> ::= accounts/<TagManagerAccountID>/containers/<TagManagerContainerID>/workspaces/<TagManagerWorkspaceID>
<TagManagerTagID> ::= <String>
<TagManagerTagPath> ::= accounts/<TagManagerAccountID>/containers/<TagManagerContainerID>/workspaces/<TagManagerWorkspaceID>/tags/<TagManagerTagID>
<TakeoutBucketName> ::= takeout-export-[a-f,0-9,-]*
<TaskID> ::= <String>
<TaskListID> ::= <String>
<TaskListTitle> ::= tltitle:<String>
<TasklistIDTaskID> ::= <TasklistID>/<TaskID>
<ThreadID> ::= <String>
<TimeZone> ::= <String>
See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
<Title> ::= <String>
<ToDriveAttribute> ::=
(tdaddsheet [<Boolean>])|
(tdalert <EmailAddress>)*|
(tdbackupsheet (id:<Number>)|<String>)|
(tdcellnumberformat text|number)|
(tdcellwrap clip|overflow|wrap)|
(tdclearfilter [<Boolean>])|
(tdcopysheet (id:<Number>)|<String>)|
(tddescription <String>)|
(tdfileid <DriveFileID>)|
(tdfrom <EmailAddress>)|
(tdlocalcopy [<Boolean>])|
(tdlocale <Locale>)|
(tdnobrowser [<Boolean>])|
(tdnoemail [<Boolean>])|
(tdnoescapechar [<Boolean>])|
(tdnotify [<Boolean>])|
(tdparent (id:<DriveFolderID>)|<DriveFolderName>)|
(tdretaintitle [<Boolean>])|
(tdreturnidonly [<Boolean>])|
(tdshare <EmailAddress> commenter|reader|writer)*|
(tdsheet (id:<Number>)|<String>)|
(tdsheettimestamp [<Boolean>] [tdsheettimeformat <DateTimeFormat>])
(tdsheettitle <String>)|
(tdsubject <String>)|
([tdsheetdaysoffset <Number>] [tdsheethoursoffset <Number>])|
(tdtimestamp [<Boolean>] [tdtimeformat <DateTimeFormat>]
[tddaysoffset <Number>] [tdhoursoffset <Number>])|
(tdtimezone <TimeZone>)|
(tdtitle <String>)|
(tdupdatesheet [<Boolean>])|
(tduploadnodata [<Boolean>])|
(tduser <EmailAddress>)
<TransferID> ::= <String>
<URI> ::= <String>
<URL> ::= <String>
<UserItem> ::= <EmailAddress>|<UniqueID>|<String>
<UserName> ::= <String>
<VacationMessageContent> ::=
(message|textmessage|htmlmessage <String>)|
(file|textfile|htmlfile <FileName> [charset <Charset>])|
(gdoc|ghtml <UserGoogleDoc>)|
(gcsdoc|gcshtml <StorageBucketObjectName>)
<YouTubeChannelID> ::= <String>
```

View File

@@ -1,181 +0,0 @@
# Bulk Processing
- [Introduction](#introduction)
- [Python Regular Expressions](Python-Regular-Expressions)
- [GAM Configuration](gam.cfg)
- [Meta Commands and File Redirection](Meta-Commands-and-File-Redirection)
- [Definitions](#definitions)
- [Batch files](#batch-files)
- [CSV files](#csv-files)
- [CSV files with redirection and select](#csv-files-with-redirection-and-select)
- [Automatic batch processing](#automatic-batch-processing)
- [Process Google Sheet commands and save results](#process-google-sheet-commands-and-save-results)
## Introduction
Batch and CSV file processing can improve performance by executing Gam commands in parallel.
The variables `num_threads`, `num_tbatch_threads` and `auto_batch_min` in `gam.cfg` control parallelism.
## Definitions
* [Command data from Google Docs/Sheets/Storage](Command-Data-From-Google-Docs-Sheets-Storage)
`gdoc <UserGoogleDoc>` and `gsheet <UserGoogleSheet>`
<DateTimeFormat> ::= <String>
See: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
## Batch files
There are two types of batch processing, one that uses processes and one that uses threads. Using processes is higher performance but `gam csv` commands are not supported.
* `gam batch` - gam commands are run as processes, gam csv commands are not allowed in the batch file
* `gam tbatch` - gam commands are run as threads, gam csv commands are allowed in the batch file
```
gam batch <FileName>|-|(gdoc <UserGoogleDoc>) [charset <Charset>] [showcmds [<Boolean>]]
gam tbatch <FileName>|-|(gdoc <UserGoogleDoc>) [charset <Charset>] [showcmds [<Boolean>]]
```
* `<FileName>` - A flat file containing Gam commands
* `-` - Gam commands coming from stdin
* `gdoc <UserGoogleDoc>` - A Google Doc containing Gam commands
* `showcmds` - Write `timestamp,command number/number of commands,command` to stderr when each command starts; write `timestamp, command number/numberof commands,complete` to stderr when command completes
Batch files can contain the following types of lines:
* Blank lines - Ignored
* \# Comment line - Ignored
* gam \<GAMArgumentList\> - Execute a GAM command
* File path arguments in \<GAMArgumentList\> should be enclosed in \"
* commit-batch
* GAM waits for all running GAM commands to complete
* GAM continues
* commit-batch \<String\>
* GAM waits for all running GAM commands to complete
* GAM prints \<String\> and waits for the user to press any key
* GAM continues
* sleep \<Integer\> - Batch processing will suspend for \<Integer\> seconds before the next command line is processed
* To be effective, this should immediately follow commit-batch
* print \<String\> - Print \<String\> on stderr
* datetime \<DateTimeFormat\>
* The current time is formatted with \<DateTimeFormat\> and subsequent lines will have `%datetime%` replaced with the formatted time value.
* See: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
* set \<KeywordString\> \<ValueString\>
* Subsequent lines will have %\<KeywordString\>% replaced with \<ValueString\>
* clear \<KeywordString\>
* Subsequent lines will not be scanned for %\<KeywordString\>%
Tbatch files can also contain the following line:
* execute \<Program\> \<ArgumentList\> - Execute an arbitrary command; use the full path to specify \<Program\>
### Example
* You need to create accounts for your new students and assign them to groups based on their graduation year.
* You have a CSV file NewStudents.csv with columns: Email,First,Last,GradYear,Password
* You have a batch file NewStudents.bat containing these commands:
```
gam csv NewStudents.csv gam create user "~Email" firstname "~First" lastname "~Last" org "/Students/~~GradYear~~" password "~Password"
commit-batch
gam update group seniors sync members ou /Students/2020
gam update group juniors sync members ou /Students/2021
gam update group sophomores sync members ou /Students/2022
gam update group highschool sync members ous "'/Students/2020','/Students/2021','/Students/2022'"
```
* Execute the batch file
```
gam redirect stdout ./NewStudents.out redirect stderr ./NewStudents.err tbatch NewStudents.bat showcmds
```
## CSV files
```
gam csv <FileName>|-|(gsheet <UserGoogleSheet>)|(gdoc <UserGoogleDoc>) [charset <Charset>] [warnifnodata]
[columndelimiter <Character>] [noescapechar <Boolean>] [quotechar <Character>] [fields <FieldNameList>]
(matchfield|skipfield <FieldName> <RESearchPattern>)* [showcmds [<Boolean>]]
[skiprows <Integer>] [maxrows <Integer>]
gam <GAMArgumentList>
gam loop <FileName>|-|(gsheet <UserGoogleSheet>)|(gdoc <UserGoogleDoc>) [charset <Charset>] [warnifnodata]
[columndelimiter <Character>] [noescapechar <Boolean>] [quotechar <Character>] [fields <FieldNameList>]
(matchfield|skipfield <FieldName> <RESearchPattern>)* [showcmds [<Boolean>]]
[skiprows <Integer>] [maxrows <Integer>]
gam <GAMArgumentList>
```
* `gam csv` - Use parallel processing
* `gam loop` - Use serial processing
* `<FileName>` - A CSV file and the one or more columns that contain data
* `-` - The one or more columns that contain data from stdin
* `gsheet <UserGoogleSheet>` - A Google Sheet and the one or more columns that contain data
* `gdoc <UserGoogleDoc>` - A Google Doc and the one or more columns that contain data
* `columndelimiter <Character>` - Columns are separated by `<Character>`; if not specified, the value of `csv_input_column_delimiter` from `gam.cfg` will be used
* `noescapechar <Boolean>` - Should `\` be ignored as an escape character; if not specified, the value of `csv_input_no_escape_char` from `gam.cfg` will be used
* `quotechar <Character>` - The column quote characer is `<Character>`; if not specified, the value of `csv_input_quote_char` from `gam.cfg` will be used
* `fields <FieldNameList>` - The column headings of a CSV file that does not contain column headings.
* `(matchfield|skipfield <FieldName> <RESearchPattern>)*` - The criteria to select rows from the CSV file; can be used multiple times; if not specified, all rows are selected
* `showcmds` - Write `timestamp,command number/number of commands,command` to stderr when each command starts; write `timestamp, command number/numberof commands,complete` to stderr when command completes
* `skiprows <Integer>` - Skip filtered rows from the CSV file/Google Sheet.
* `skiprows 0` - All rows are processed, this is the default
* `skiprows N` - The first N filtered rows are skipped
* `maxrows <Integer>` - Limit the number of filtered rows processed from the CSV file/Google Sheet after any skipped rows.
* `maxrows 0` - All rows are processed, this is the default
* `maxrows N` - N filtered rows are processed
### Use CSV file values in command line
You can make substitutions in `<GAMArgumentList>` with values from the CSV file.
- Reference the field xxx with `~xxx` if the argument contains no other text
- Reference the field xxx with `~~xxx~~` if the argument contains other text
- An argument containing exactly `~xxx` is replaced by the value of field xxx
- An argument containing instances of `~~xxx~~` has `~~xxx~~` replaced by the value of field xxx
- An argument containing instances of `~~xxx~!~pattern~!~replacement~~` has `~~xxx~!~pattern~!~replacement~~` replaced by re.sub(pattern, replacement, value of field xxx) See: https://docs.python.org/3/library/re.html
If an argument is specifying a file path and it starts with a `~`, e.g., `targetfolder "~/Documents/GamWork"`, GAM will flag it as an error:
```
ERROR: Header "/Documents/GamWork/" not found in CSV headers of "Owner,id,title".
```
Put a space in front of the `~`: `targetfolder " ~/Documents/GamWork"` to avoid the error.
### Example
* You need to update the work addresses of a set of users
* You want a note field that shows their email address as name AT domain.com
* You have a CSV file Users.csv with columns: primaryEmail,Street,City,State,ZIP
```
gam csv Users.csv gam update user "~primaryEmail" address type work unstructured "~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~" primary note text_plain "~~primaryEmail~!~^(.+)@(.+)$~!~\1 AT \2~~"
```
* You want to do the above using a Google Sheet
```
gam csv gsheet <user> <fileID> "<sheetName>" gam update user "~primaryEmail" address type work unstructured "~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~" primary note text_plain "~~primaryEmail~!~^(.+)@(.+)$~!~\1 AT \2~~"
```
## CSV files with redirection and select
You should use the `multiprocess` option on any redirected files: `csv`, `stdout`, `stderr`.
```
gam redirect csv ./filelistperms.csv multiprocess csv Users.csv gam user "~primaryEmail" print filelist fields id,name,mimetype,basicpermissions
gam redirect csv - multiprocess todrive csv Users.csv gam user "~primaryEmail" print filelist fields id,name,mimetype,basicpermissions
```
If you want to select a `gam.cfg` section for the command, you can select the section at the outer `gam` and save it
or select the section at the inner `gam`.
```
gam select <Section> save redirect csv ./filelistperms.csv multiprocess csv Users.csv gam user "~primaryEmail" print filelist fields id,name,mimetype,basicpermissions
gam redirect csv ./filelistperms.csv multiprocess csv Users.csv gam select <Section> user "~primaryEmail" print filelist fields id,name,mimetype,basicpermissions
gam select <Section> save redirect csv - multiprocess todrive csv Users.csv gam user "~primaryEmail" print filelist fields id,name,mimetype,basicpermissions
gam redirect csv - multiprocess todrive csv Users.csv gam select <Section> user "~primaryEmail" print filelist fields id,name,mimetype,basicpermissions
```
## Automatic batch processing
You can enable automatic batch (parallel) processing when issuing commands of the form `gam <UserTypeEntity> ...`.
In the following example, if the number of users in group sales@domain.com exceeds 1, then the `print filelist` command will be processed in parallel.
```
gam config auto_batch_min 1 redirect csv ./filelistperms.csv multiprocess group sales@domain.com print filelist fields id,name,mimetype,basicpermissions
gam config auto_batch_min 1 redirect csv - multiprocess todrive group sales@domain.com print filelist fields id,name,mimetype,basicpermissions
```
With automatic batch processing, you should use the `multiprocess` option on any redirected files: `csv`, `stdout`, `stderr`.
If you want to select a `gam.cfg` section for the command, you must select and save it for it to be processed correctly.
```
gam select <Section> save config auto_batch_min 1 redirect csv ./filelistperms.csv multiprocess group sales@domain.com print filelist fields id,name,mimetype,basicpermissions
```
## Process Google Sheet commands and save results
You want to process data from a Google Sheet tab and save the results to another tab in the same sheet.
Make a Google sheet with two tabs: Commands, Results; get the File ID and the two tab IDs.
Put your command data in the Commands tab.
Run your command, write the results to Results.txt
```
gam redirect stdout ./Results.txt multiprocess redirect stderr stdout csv gsheet user@domain.com <FileID> id:<CommandsTabID> gam ... Command
```
Upload Results.txt to the Results tab of the sheet.
```
gam user user@domain.com update drivefile <FileID> localfile Results.txt retainname gsheet id:<ResultsTabID>
```

Some files were not shown because too many files have changed in this diff Show More