Compare commits

...

51 Commits

Author SHA1 Message Date
Ross Scroggs
a971ea37ae Update guest user support #1874 2026-04-28 16:53:26 -07:00
Ross Scroggs
c2192674ff Update guest user support #1874 2026-04-28 16:39:14 -07:00
Ross Scroggs
1b7e736caf Update guest user support #1874 2026-04-28 15:27:41 -07:00
Ross Scroggs
2a49e1ebe2 Merge branch 'main' of https://github.com/GAM-team/GAM
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-04-27 19:47:52 -07:00
Ross Scroggs
4075bef468 Update guest user support #1874 2026-04-27 19:47:49 -07:00
Jay Lee
00cccbe920 Comment out artifact archiving in build.yml
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
Comment out the artifact archiving step in the build workflow.
2026-04-27 14:41:00 -04:00
Ross Scroggs
0f50ce18b3 Update redirect csv <FileName> 2026-04-27 11:14:54 -07:00
Ross Scroggs
1475e7a1d2 Update redirect csv <FileName> 2026-04-27 09:47:58 -07:00
Ross Scroggs
573a0dc6f1 Fixed bug in <RowValueFilter> update
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
2026-04-26 20:23:53 -07:00
Ross Scroggs
e56b56612f Fixed bug in <RowValueFilter> update 2026-04-26 19:11:06 -07:00
Ross Scroggs
550dd6b947 Merge branch 'main' of https://github.com/GAM-team/GAM 2026-04-26 13:43:53 -07:00
Ross Scroggs
38f63188e3 RowValueFilter updates 2026-04-26 13:43:49 -07:00
Jay Lee
8d0865f692 Update Certum MSI installation URL and rename step
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-04-26 12:47:04 -04:00
Jay Lee
2ba6ad10c2 Modify Windows workflow to run SSD directly
Removed the Windows Desktop Shell initialization step and added a step to try running SSD directly.
2026-04-26 12:30:02 -04:00
Jay Lee
9e44511872 Modify artifact archiving to include log files
Updated the artifact archiving step to include log files.
2026-04-26 08:09:44 -04:00
Jay Lee
1ee31b15b2 [no ci] Add logging to launchSSD function
Enhanced the launchSSD function to include logging for stdout and stderr, directing output to specified log files.
2026-04-26 08:08:37 -04:00
Jay Lee
dfb1dd860f Update build.yml 2026-04-26 07:55:56 -04:00
Ross Scroggs
7aaf8cdaa6 Update Certum MSI download URL in build.yml
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-04-25 18:59:12 -07:00
Ross Scroggs
66a65a380e Certum URL fix 2026-04-25 18:33:14 -07:00
Jay Lee
471bd4f924 Update Certum MSI download URL in build.yml 2026-04-25 20:31:49 -04:00
Jay Lee
5525c79f5b Refactor ARM64 check in ssd.mjs
Removed redundant console log for ARM64 check.
2026-04-25 20:21:06 -04:00
Jay Lee
03d79ec62e Enhance logging and screenshot functionality
Added logging for escaping the start menu and taking screenshots.
2026-04-25 20:15:20 -04:00
Jay Lee
6f72c9844b Replace driver.sendKeys with sendKeys for Enter 2026-04-25 20:03:48 -04:00
Jay Lee
5b1d876101 Replace driver.sendKeys with sendKeys function 2026-04-25 19:55:22 -04:00
Jay Lee
1cb3223637 Fix syntax error in sendKeys call 2026-04-25 19:49:15 -04:00
Jay Lee
0330e315d2 Refactor key sending to use sendKeys function 2026-04-25 19:42:13 -04:00
Jay Lee
b8f894fddb Refactor SSD run function for ARM64 handling 2026-04-25 19:34:59 -04:00
Jay Lee
0b1fee6bc5 Refactor runSSD function to handle ARM64 OOBE
Removed commented-out screenshot logic and added ARM64 handling for OOBE dialogs.
2026-04-25 19:25:41 -04:00
Jay Lee
cc61691f28 Refactor screenshot function to use temporary script
Refactor screenshot capture to use a temporary PowerShell script file for execution. This improves the handling of the screenshot process and ensures cleanup of temporary files.
2026-04-25 18:56:42 -04:00
Jay Lee
0ece44575d Add item creation after bitmap save
Added a new item creation after saving the bitmap.
2026-04-25 18:40:57 -04:00
Jay Lee
e476ad93ba Log output after saving bitmap
Add output message after saving bitmap in PowerShell script.
2026-04-25 18:28:26 -04:00
Ross Scroggs
21ba5e3ac0 Update Certum MSI installer URL to version 9.4.3.90 2026-04-25 14:39:43 -07:00
Ross Scroggs
453f3faf62 Handle ModuleNotFoundError: No module named 'typing_extensions' 2026-04-25 14:14:23 -07:00
Ross Scroggs
296e629b69 Merge branch 'main' of https://github.com/GAM-team/GAM 2026-04-25 13:55:05 -07:00
Ross Scroggs
a319eb665d Add number/numberrange to <RowValueFilter> 2026-04-25 13:54:58 -07:00
Jay Lee
c3bf865497 List files in GitHub workspace during login
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
Added command to list files in the GitHub workspace.
2026-04-24 21:04:49 -04:00
Jay Lee
c8914ddb03 Update Certum MSI installer URL to version 9.4.2.86
Updated the URL for the Certum MSI installer to version 9.4.2.86.
2026-04-24 20:54:48 -04:00
Jay Lee
e24e127055 Fix image path in build.yml for archiving
Update image path in build workflow to use GitHub workspace.
2026-04-24 20:46:44 -04:00
Jay Lee
ed2801d612 Update image path in build workflow
Change image path to include parent directory for PNG files.
2026-04-24 20:40:21 -04:00
Ross Scroggs
245c9ca9d7 Display course updates, create group updates 2026-04-23 19:24:02 -07:00
Ross Scroggs
e4352129db Display course updates, create group updates
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-04-23 19:23:41 -07:00
Jay Lee
726f061b16 show debug output for requests just like httplib2
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-04-22 18:27:14 +00:00
Jay Lee
75598e5eb8 remove unnecessary fallback to TLS 1.2 for GitHub Actions
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-04-22 14:51:48 +00:00
Jay Lee
6e57ae33e8 see if Github Actions still needs TLS 1.2 2026-04-22 14:47:09 +00:00
Jay Lee
22bc5457a7 missing v in function name 2026-04-22 14:08:38 +00:00
Jay Lee
1ba183e8a5 Special case GCE ADC. #1901 2026-04-22 13:56:32 +00:00
Ross Scroggs
26f121b120 Fixed bug in gam print cigroups
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-04-20 15:56:29 -07:00
Ross Scroggs
c3dc91d61d Fixed bug in gam print cigroups 2026-04-20 15:56:16 -07:00
Ross Scroggs
5a43aac095 Update Upgrade-Benefits.md 2026-04-20 15:34:00 -07:00
Ross Scroggs
0e335322dd Document switch from MSI to Windows
Some checks failed
Check for Google Root CA Updates / check-certs (push) Has been cancelled
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
2026-04-16 13:09:45 -07:00
Ross Scroggs
c5215392d6 Upgraded to Python 3.14.4 and OpenSSL 4.0.0. #1899 2026-04-16 09:46:43 -07:00
23 changed files with 743 additions and 229 deletions

View File

@@ -643,6 +643,7 @@ jobs:
# arm64 needs to build a wheel and needs scons to build # arm64 needs to build a wheel and needs scons to build
sudo apt-get -qq --yes install scons sudo apt-get -qq --yes install scons
"${PYTHON}" -m pip install --upgrade patchelf-wrapper "${PYTHON}" -m pip install --upgrade patchelf-wrapper
"${PYTHON}" -m pip install --upgrade typing_extensions
# "${PYTHON}" -m pip install --upgrade staticx # "${PYTHON}" -m pip install --upgrade staticx
# install latest github src for staticx # install latest github src for staticx
"${PYTHON}" -m pip install --upgrade "git+https://github.com/JonathonReinhart/staticx" "${PYTHON}" -m pip install --upgrade "git+https://github.com/JonathonReinhart/staticx"
@@ -689,20 +690,6 @@ jobs:
echo "GAM Version ${GAMVERSION}" echo "GAM Version ${GAMVERSION}"
echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV echo "GAMVERSION=${GAMVERSION}" >> $GITHUB_ENV
- name: Initialize Windows Desktop Shell
if: runner.os == 'Windows'
shell: pwsh
run: |
Write-Host "Checking for Windows Explorer shell..."
if (-not (Get-Process -Name explorer -ErrorAction SilentlyContinue)) {
Write-Host "Explorer not found. Booting the desktop shell..."
Start-Process explorer.exe
# Give the desktop a few seconds to fully render the taskbar
Start-Sleep -Seconds 10
} else {
Write-Host "Explorer is already running."
}
- name: Install NPM deps - name: Install NPM deps
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: | run: |
@@ -715,8 +702,8 @@ jobs:
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: pwsh shell: pwsh
run: | run: |
#$url = "https://files.certum.eu/software/SimplySignDesktop/Windows/9.3.4.72/SimplySignDesktop-9.3.4.72-64-bit-en.msi" #$url = "https://files.certum.eu/software/SimplySignDesktop/Windows/9.4.2.86/SimplySignDesktop-9.4.2.86-64-bit-en.msi"
$url = "https://www.files.certum.eu/software/SimplySignDesktop/Windows/9.4.0.84/SimplySignDesktop-9.4.0.84-64-bit-en.msi" $url = "https://files.certum.eu/software/SimplySignDesktop/Windows/9.4.3.90/SimplySignDesktop-9.4.3.90-64-bit-en.msi"
$file = "SimplySignDesktop.msi" $file = "SimplySignDesktop.msi"
Invoke-WebRequest $url -OutFile $file Invoke-WebRequest $url -OutFile $file
$log = "install.log" $log = "install.log"
@@ -725,6 +712,15 @@ jobs:
$procMain.WaitForExit() $procMain.WaitForExit()
$procLog.Kill() $procLog.Kill()
- name: Run SSD directly
if: runner.os == 'Windows'
shell: pwsh
run: |
Write-Host "Running SSD..."
& "C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe"
Start-Sleep -Seconds 10
& "C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe"
- name: Login to Certum - name: Login to Certum
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: pwsh shell: pwsh
@@ -733,18 +729,20 @@ jobs:
run: | run: |
write-host "running SimplySignDesktop login..." write-host "running SimplySignDesktop login..."
node tools/ssd.mjs --log-level warn node tools/ssd.mjs --log-level warn
Get-ChildItem -Path $env:GITHUB_WORKSPACE
write-host "sleeping during login..." write-host "sleeping during login..."
Start-Sleep 10 Start-Sleep 10
- name: Archive png artifacts # - name: Archive artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0 # uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0
if: runner.os == 'Windows' # if: runner.os == 'Windows'
with: # with:
archive: true # archive: true
name: images-${{ matrix.os }} # name: images-${{ matrix.os }}
if-no-files-found: ignore # if-no-files-found: ignore
path: | # path: |
*.png # ${{ github.workspace }}/*.png
# ${{ github.workspace }}/*.log
- name: Sign gam.exe - name: Sign gam.exe
if: runner.os == 'Windows' if: runner.os == 'Windows'

View File

@@ -16,7 +16,7 @@ this will download GAM, install it and start setup.
## Windows ## Windows
Download the MSI Installer from the [GitHub Releases] page. Install the MSI and you'll be prompted to setup GAM. Download the EXE Installer from the [GitHub Releases] page. Run it and you'll be prompted to setup GAM.
## Use your own Python ## Use your own Python
If you'd prefer to install GAM as a Python package you can install with pip: If you'd prefer to install GAM as a Python package you can install with pip:

View File

@@ -916,6 +916,8 @@ Specify a collection of Users by directly specifying them or by specifying items
<UserTypeEntity> ::= <UserTypeEntity> ::=
(all users|users_na|users_arch|users_ns|users_susp|users_arch_or_susp|users_na_ns|users_ns_susp)| (all users|users_na|users_arch|users_ns|users_susp|users_arch_or_susp|users_na_ns|users_ns_susp)|
(all guests|guests_ns|guests_susp|guests_ns_susp)|
(all users_and_guests|users_and_guests_ns|users_and_guests_susp|users_and_guests_ns_susp)|
(user <UserItem>)| (user <UserItem>)|
(users <UserList>)| (users <UserList>)|
(oauthuser) (oauthuser)
@@ -1351,9 +1353,12 @@ verify
## File Redirection ## File Redirection
If the pattern {{Section}} appears in <FileName>, it will be replaced with the name of the current section. If the pattern {{Section}} appears in <FileName>, it will be replaced with the name of the current section.
For redirect csv, the optional arguments must appear in the order shown.
For redirect csv, the optional arguments can be specfified in any order but `todrive <ToDriveAttribute>*`
must be last.
<Redirect> ::= <Redirect> ::=
redirect csv <FileName> [multiprocess] [append] [noheader] [charset <Charset>] redirect csv <FileName> [delayopen] multiprocess] [append] [noheader] [charset <Charset>]
[columndelimiter <Character>] [quotechar <Character>] [noescapechar [<Boolean>]] [columndelimiter <Character>] [quotechar <Character>] [noescapechar [<Boolean>]]
[sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Bopolean>]] [sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Bopolean>]]
[todrive <ToDriveAttribute>*] | [todrive <ToDriveAttribute>*] |
@@ -3162,6 +3167,7 @@ gam <UserTypeEntity> show contactdelegates [shownames] [csv]
name| name|
owneremail| owneremail|
ownerid| ownerid|
ownername|
room| room|
section| section|
students| students|
@@ -3275,16 +3281,16 @@ gam courses <CourseEntity> sync teachers [addonly|removeonly] [makefirstteachero
gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity> gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
gam info course <CourseID> [owneraccess] gam info course <CourseID> [owneraccess]
[owneremail] [alias|aliases] [show all|students|teachers] [countsonly] [owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
[formatjson] [formatjson]
gam info courses <CourseEntity> [owneraccess] gam info courses <CourseEntity> [owneraccess]
[owneremail] [alias|aliases] [show all|students|teachers] [countsonly] [owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
[formatjson] [formatjson]
gam print courses [todrive <ToDriveAttribute>*] gam print courses [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[owneremail] [owneremailmatchpattern <REMatchPattern>] [owneremail] [owneremailmatchpattern <REMatchPattern>] [ownername]
[alias|aliases|aliasesincolumns [delimiter <Character>]] [alias|aliases|aliasesincolumns [delimiter <Character>]]
[show all|students|teachers] [countsonly] [show all|students|teachers] [countsonly]
[timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
@@ -3398,14 +3404,16 @@ gam print course-announcements [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
(announcementids <CourseAnnouncementIDEntity>)|((announcementstates <CourseAnnouncementStateList>)* (announcementids <CourseAnnouncementIDEntity>)|((announcementstates <CourseAnnouncementStateList>)*
(orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*) (orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
[showcreatoremails|creatoremail] [fields <CourseAnnouncementFieldNameList>] [showcreatoremails|creatoremail] [showcreatornames|creatorname]
[fields <CourseAnnouncementFieldNameList>]
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
[countsonly] [formatjson [quotechar <Character>]] [countsonly] [formatjson [quotechar <Character>]]
gam print course-materials [todrive <ToDriveAttribute>*] gam print course-materials [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
(materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)* (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
(orderby <CourseMaterialOrderByFieldName> [ascending|descending])*) (orderby <CourseMaterialOrderByFieldName> [ascending|descending])*)
[showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>] [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
[fields <CourseMaterialFieldNameList>]
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
[oneitemperrow] [oneitemperrow]
[countsonly] [formatjson [quotechar <Character>]] [countsonly] [formatjson [quotechar <Character>]]
@@ -3426,7 +3434,8 @@ gam print course-works [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
(workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)* (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
(orderby <CourseWorkOrderByFieldName> [ascending|descending])*) (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
[showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>] [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
[fields <CourseWorkFieldNameList>]
[showstudentsaslist [<Boolean>]] [delimiter <Character>] [showstudentsaslist [<Boolean>]] [delimiter <Character>]
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
[oneitemperrow] [oneitemperrow]
@@ -3884,6 +3893,8 @@ gam audit monitor list <EmailAddress>
gam create|add group <EmailAddress> gam create|add group <EmailAddress>
[copyfrom <GroupItem>] <GroupAttribute>* [copyfrom <GroupItem>] <GroupAttribute>*
[verifynotinvitable] [verifynotinvitable]
[recentdeleteretries <Integer>] [recentdeleteretrydelay <Integer>]
[verifycreationretries <Integer>] [verifycreationinitialdelay <Integer>] [verifycreationretrydelay <Integer>]
gam update group|groups <GroupEntity> [email <EmailAddress>] gam update group|groups <GroupEntity> [email <EmailAddress>]
[updateprimaryemail <RESearchPattern> <RESubstitution>] [updateprimaryemail <RESearchPattern> <RESubstitution>]
[copyfrom <GroupItem>] <GroupAttribute>* [copyfrom <GroupItem>] <GroupAttribute>*

View File

@@ -1,3 +1,71 @@
7.43.01
Updated `gam info user` and `gam print users` to display guest user attributes: `isGuestUser, guestAccountInfo`
Expanded `<UserTypeEntity>` to allow specification of guest users.
* See [Collections of Users](Collections-of-Users)
7.42.00
In versions prior to 7.42.00, when `redirect csv <FileName>` was used, GAM did not open and write `<FileName>`
until all processing was complete; if `<FileName>` was not accessible, an error was generated
and no results were saved. Now, `<FileName>` is opened initially to verify accessiblity
and then written when processing is complete.
In the unlikely event that this causes issues, you can do `redirect csv <FileName> delayopen`
to get the previous behavior.
7.41.03
Fixed bug in the following:
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
synonyms for `count` and `countrange`.
```
[(any|all):]number<Operator><Number>|
[(any|all):]numberrange!=<Number>/<Number>|
[(any|all):]numberrange=<Number>/<Number>|
```
7.41.02
Added option `ownername` to `gam info|print courses` to have GAM display the course owners full name;
there is an extra API call per course to get the name.
Added option `creatorname` to `gam print course-announcements|course-materials|course-works` to have
GAM display the item creators full name; there is an extra API call per course to get the name.
After creating a group, it may be sometime, e.g. 30-45 seconds, before members can
successfully be added to the group even though the API reported that the group was created.
The following options can be used with `gam create group` to verify that the group is actually ready to be updated.
This will be most useful in scripts that are used to create and then populate groups.
```
verifycreationretries <Integer> - Verify group creation, defaults to 0, no verification performed, range 0-20
verifycreationinitialdelay <Integer> - Number of seconds to delay before first verification performed, defaults to 5, range 0-60
verifycreationretrydelay <Integer> - Number of seconds to delay between verificaton retries, defaults to 5, range 1-60
```
If you have a script that deletes a group and then immediately tries to create a new group with the same email address,
you may run into issues. There seems to be a 30-45 second window after the deletion in which a couple
of strange errors can occur on the creation: `Resource not found` and `Duplicate`.
The following options can be used with `gam create group` to handle these errors. This will be most useful
in scripts that are used to delete and then immediately recreate groups.
```
recentdeleteretries <Integer> - Handle group delete/create errors, defaults to 0, no errors handled, range 0-20
recentdeleteretrydelay <Integer> - Number of seconds to delay between retries, defaults to 5, range 1-60
```
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
synonyms for `count` and `countrange`.
```
[(any|all):]number<Operator><Number>|
[(any|all):]numberrange!=<Number>/<Number>|
[(any|all):]numberrange=<Number>/<Number>|
```
7.41.01
Fixed bug in `gam print cigroups members managers owners countsonly totalcount internal external` that caused a trap.
7.41.00 7.41.00
Upgraded to Python 3.14.4 and OpenSSL 4.0.0. Upgraded to Python 3.14.4 and OpenSSL 4.0.0.

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
""" """
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>' __author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.41.00' __version__ = '7.43.01'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
# pylint: disable=wrong-import-position # pylint: disable=wrong-import-position
@@ -114,6 +114,8 @@ from pathvalidate import sanitize_filename, sanitize_filepath
import google.oauth2.credentials import google.oauth2.credentials
import google.oauth2.id_token import google.oauth2.id_token
import google.auth import google.auth
import google.auth.transport.requests
from google.auth.compute_engine import _metadata as gce_metadata
from google.auth.jwt import Credentials as JWTCredentials from google.auth.jwt import Credentials as JWTCredentials
import google.oauth2.service_account import google.oauth2.service_account
import google_auth_oauthlib.flow import google_auth_oauthlib.flow
@@ -3579,8 +3581,8 @@ def SetGlobalVariables():
return headerFilters return headerFilters
ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE) ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE)
ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE) ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length|number)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE)
ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE) ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange|numberrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE) ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE)
ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE) ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE)
ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE) ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE)
@@ -3646,7 +3648,7 @@ def SetGlobalVariables():
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue)) rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue))
else: else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue}') _printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue}')
else: # filterType in {'count', 'length'}: else: # filterType in {'count', 'length', 'number'}:
if mg.group(3).isdigit(): if mg.group(3).isdigit():
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)))) rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3))))
else: else:
@@ -3676,7 +3678,7 @@ def SetGlobalVariables():
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue1, filterValue2)) rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue1, filterValue2))
else: else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue1}/{filterValue2}') _printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue1}/{filterValue2}')
else: #countrange|lengthrange else: #countrange|lengthrange|numberrange
if mg.group(3).isdigit() and mg.group(4).isdigit(): if mg.group(3).isdigit() and mg.group(4).isdigit():
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)), int(mg.group(4)))) rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)), int(mg.group(4))))
else: else:
@@ -3951,7 +3953,7 @@ def SetGlobalVariables():
'\n')) '\n'))
status['errors'] = True status['errors'] = True
def _setCSVFile(fileName, mode, encoding, writeHeader, multi): def _setCSVFile(fileName, mode, encoding, writeHeader, multi, delayOpen):
if fileName != '-': if fileName != '-':
fileName = setFilePath(fileName, GC.DRIVE_DIR) fileName = setFilePath(fileName, GC.DRIVE_DIR)
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = fileName GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = fileName
@@ -3960,6 +3962,9 @@ def SetGlobalVariables():
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = writeHeader GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = writeHeader
GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] = multi GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] = multi
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None
if not delayOpen and fileName != '-':
GM.Globals[GM.CSVFILE][GM.REDIRECT_FD] = openFile(fileName, mode, newline='',
encoding=encoding, errors='backslashreplace')
def _setSTDFile(stdtype, fileName, mode, multi): def _setSTDFile(stdtype, fileName, mode, multi):
if stdtype == GM.STDOUT: if stdtype == GM.STDOUT:
@@ -4221,6 +4226,9 @@ def SetGlobalVariables():
GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock' GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
# Override httplib2 settings # Override httplib2 settings
httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL] httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL]
# Override requests debuglevel also. Requests is used with
# SignJWT/WIF/GCE and a few other places.
http.client.HTTPConnection.debuglevel = GC.Values[GC.DEBUG_LEVEL]
# Use our own print function for http.client so we can redact and cleanup # Use our own print function for http.client so we can redact and cleanup
http.client.print = redactable_debug_print http.client.print = redactable_debug_print
# Reset global variables if required # Reset global variables if required
@@ -4238,7 +4246,7 @@ def SetGlobalVariables():
# multiprocessexit (rc<Operator><Number>)|(rcrange=<Number>/<Number>)|(rcrange!=<Number>/<Number>) # multiprocessexit (rc<Operator><Number>)|(rcrange=<Number>/<Number>)|(rcrange!=<Number>/<Number>)
if checkArgumentPresent(Cmd.MULTIPROCESSEXIT_CMD): if checkArgumentPresent(Cmd.MULTIPROCESSEXIT_CMD):
_setMultiprocessExit() _setMultiprocessExit()
# redirect csv <FileName> [multiprocess] [append] [noheader] [charset <CharSet>] # redirect csv <FileName> [delayopen] [multiprocess] [append] [noheader] [charset <CharSet>]
# [columndelimiter <Character>] [quotechar <Character>]] [noescapechar [<Boolean>]] # [columndelimiter <Character>] [quotechar <Character>]] [noescapechar [<Boolean>]]
# [sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Boolean>]] # [sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Boolean>]]
# [todrive <ToDriveAttribute>*] # [todrive <ToDriveAttribute>*]
@@ -4251,23 +4259,39 @@ def SetGlobalVariables():
myarg = getChoice(['csv', 'stdout', 'stderr']) myarg = getChoice(['csv', 'stdout', 'stderr'])
filename = re.sub(r'{{Section}}', sectionName, getString(Cmd.OB_FILE_NAME, checkBlank=True)) filename = re.sub(r'{{Section}}', sectionName, getString(Cmd.OB_FILE_NAME, checkBlank=True))
if myarg == 'csv': if myarg == 'csv':
multi = checkArgumentPresent('multiprocess') multi = False
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE mode = DEFAULT_FILE_WRITE_MODE
writeHeader = not checkArgumentPresent('noheader') writeHeader = True
encoding = getCharSet() encoding = GC.Values[GC.CHARSET]
if checkArgumentPresent('columndelimiter'): delayOpen = False
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER] = getCharacter() while Cmd.ArgumentsRemaining():
if checkArgumentPresent('quotechar'): myarg = getArgument()
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR] = getCharacter() if myarg == 'multiprocess':
if checkArgumentPresent('noescapechar'): multi = True
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR] = getBoolean() elif myarg == 'append':
if checkArgumentPresent('sortheaders'): mode = DEFAULT_FILE_APPEND_MODE
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = GC.Values[GC.CSV_OUTPUT_SORT_HEADERS] = getString(Cmd.OB_STRING_LIST, minLen=0).replace(',', ' ').split() elif myarg == 'noheader':
if checkArgumentPresent('timestampcolumn'): writeHeader = False
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN] = getString(Cmd.OB_STRING, minLen=0) elif myarg == 'charset':
if checkArgumentPresent('transpose'): encoding = getString(Cmd.OB_CHAR_SET)
GM.Globals[GM.CSV_OUTPUT_TRANSPOSE] = getBoolean() elif myarg == 'delayopen':
_setCSVFile(filename, mode, encoding, writeHeader, multi) delayOpen = True
elif myarg == 'columndelimiter':
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER] = getCharacter()
elif myarg == 'quotechar':
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR] = getCharacter()
elif myarg == 'noescapechar':
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR] = getBoolean()
elif myarg == 'sortheaders':
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = GC.Values[GC.CSV_OUTPUT_SORT_HEADERS] = getString(Cmd.OB_STRING_LIST, minLen=0).replace(',', ' ').split()
elif myarg == 'timestampcolumn':
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN] = getString(Cmd.OB_STRING, minLen=0)
elif myarg == 'transpose':
GM.Globals[GM.CSV_OUTPUT_TRANSPOSE] = getBoolean()
else:
Cmd.Backup()
break
_setCSVFile(filename, mode, encoding, writeHeader, multi, delayOpen)
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF] = CSVPrintFile() GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF] = CSVPrintFile()
if checkArgumentPresent('todrive'): if checkArgumentPresent('todrive'):
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].GetTodriveParameters() GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].GetTodriveParameters()
@@ -4304,9 +4328,9 @@ def SetGlobalVariables():
GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] == GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS] and GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] == GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS] and
GM.Globals[GM.CSVFILE].get(GM.REDIRECT_QUEUE_CSVPF) and not GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].todrive): GM.Globals[GM.CSVFILE].get(GM.REDIRECT_QUEUE_CSVPF) and not GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].todrive):
_setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET], _setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET],
GM.Globals[GM.CSVFILE].get(GM.REDIRECT_WRITE_HEADER, True), GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS]) GM.Globals[GM.CSVFILE].get(GM.REDIRECT_WRITE_HEADER, True), GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS], False)
elif not GM.Globals[GM.CSVFILE]: elif not GM.Globals[GM.CSVFILE]:
_setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET], True, False) _setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET], True, False, False)
initAPICallsRateCheck() initAPICallsRateCheck()
# Main process # Main process
# Clear input row filters/limit from parser, children can define but shouldn't inherit global value # Clear input row filters/limit from parser, children can define but shouldn't inherit global value
@@ -4516,13 +4540,6 @@ class signjwtJWTCredentials(google.auth.jwt.Credentials):
jwt = self._signer.sign(payload) jwt = self._signer.sign(payload)
return jwt, expiry.naive return jwt, expiry.naive
# Some Workforce Identity Federation endpoints such as GitHub Actions
# only allow TLS 1.2 as of April 2023.
def getTLSv1_2Request():
httpc = getHttpObj(override_min_tls='TLSv1_2')
return transportCreateRequest(httpc)
class signjwtCredentials(google.oauth2.service_account.Credentials): class signjwtCredentials(google.oauth2.service_account.Credentials):
''' Class used for DwD ''' ''' Class used for DwD '''
@@ -4543,6 +4560,15 @@ class signjwtCredentials(google.oauth2.service_account.Credentials):
token = self._signer(payload) token = self._signer(payload)
return token return token
def get_adc_request():
request = google.auth.transport.requests.Request()
if GM.Globals[GM.IS_ON_GCE]:
return request
if gce_metadata.is_on_gce(request):
GM.Globals[GM.IS_ON_GCE] = True
return request
return transportCreateRequest()
class signjwtSignJwt(google.auth.crypt.Signer): class signjwtSignJwt(google.auth.crypt.Signer):
''' Signer class for SignJWT ''' ''' Signer class for SignJWT '''
def __init__(self, service_account_info): def __init__(self, service_account_info):
@@ -4556,12 +4582,15 @@ class signjwtSignJwt(google.auth.crypt.Signer):
def sign(self, message): def sign(self, message):
''' Call IAM Credentials SignJWT API to get our signed JWT ''' ''' Call IAM Credentials SignJWT API to get our signed JWT '''
request = get_adc_request()
try: try:
credentials, _ = google.auth.default(scopes=[API.IAM_SCOPE], credentials, _ = google.auth.default(scopes=[API.IAM_SCOPE],
request=getTLSv1_2Request()) request=request)
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e: except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
systemErrorExit(API_ACCESS_DENIED_RC, str(e)) systemErrorExit(API_ACCESS_DENIED_RC, str(e))
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(override_min_tls='TLSv1_2')) httpObj = transportAuthorizedHttp(credentials, http=getHttpObj())
# refresh here so we can use the proper request from above
httpObj.credentials.refresh(request)
iamc = getService(API.IAM_CREDENTIALS, httpObj) iamc = getService(API.IAM_CREDENTIALS, httpObj)
response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt', response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt',
name=self.name, body={'payload': json.dumps(message)}) name=self.name, body={'payload': json.dumps(message)})
@@ -7827,10 +7856,10 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter,
elif filterVal[2] == 'timeofdayrange': elif filterVal[2] == 'timeofdayrange':
if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]): if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True return True
elif filterVal[2] == 'count': elif filterVal[2] in {'count', 'number'}:
if rowCountFilterMatch(filterVal[3], filterVal[4]): if rowCountFilterMatch(filterVal[3], filterVal[4]):
return True return True
elif filterVal[2] == 'countrange': elif filterVal[2] in {'countrange', 'numberrange'}:
if rowCountRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]): if rowCountRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True return True
elif filterVal[2] == 'length': elif filterVal[2] == 'length':
@@ -8772,9 +8801,11 @@ class CSVPrintFile():
closeFile(csvFile) closeFile(csvFile)
def writeCSVToFile(): def writeCSVToFile():
csvFile = openFile(GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME], GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE], newline='', csvFile = GM.Globals[GM.CSVFILE].get(GM.REDIRECT_FD, None)
encoding=GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING], errors='backslashreplace', if not csvFile:
continueOnError=True) csvFile = openFile(GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME], GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE], newline='',
encoding=GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING], errors='backslashreplace',
continueOnError=True)
if csvFile: if csvFile:
writerDialect = setDialect(str(GC.Values[GC.CSV_OUTPUT_LINE_TERMINATOR]), self.noEscapeChar) writerDialect = setDialect(str(GC.Values[GC.CSV_OUTPUT_LINE_TERMINATOR]), self.noEscapeChar)
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect) writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
@@ -9731,18 +9762,15 @@ def initializeLogging():
logging.getLogger().addHandler(nh) logging.getLogger().addHandler(nh)
def saveNonPickleableValues(): def saveNonPickleableValues():
savedValues = {GM.STDOUT: {}, GM.STDERR: {}, GM.SAVED_STDOUT: None, savedValues = {GM.CSVFILE: {}, GM.STDOUT: {}, GM.STDERR: {},
GM.CMDLOG_HANDLER: None, GM.CMDLOG_LOGGER: None} GM.SAVED_STDOUT: None, GM.CMDLOG_HANDLER: None, GM.CMDLOG_LOGGER: None}
savedValues[GM.CSVFILE][GM.REDIRECT_FD] = GM.Globals[GM.CSVFILE].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDOUT][GM.REDIRECT_FD] = GM.Globals[GM.STDOUT].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT].pop(GM.REDIRECT_MULTI_FD, None)
savedValues[GM.STDERR][GM.REDIRECT_FD] = GM.Globals[GM.STDERR].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDERR].pop(GM.REDIRECT_MULTI_FD, None)
savedValues[GM.SAVED_STDOUT] = GM.Globals[GM.SAVED_STDOUT] savedValues[GM.SAVED_STDOUT] = GM.Globals[GM.SAVED_STDOUT]
GM.Globals[GM.SAVED_STDOUT] = None GM.Globals[GM.SAVED_STDOUT] = None
savedValues[GM.STDOUT][GM.REDIRECT_FD] = GM.Globals[GM.STDOUT].get(GM.REDIRECT_FD, None)
GM.Globals[GM.STDOUT].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDERR][GM.REDIRECT_FD] = GM.Globals[GM.STDERR].get(GM.REDIRECT_FD, None)
GM.Globals[GM.STDERR].pop(GM.REDIRECT_FD, None)
savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, None)
GM.Globals[GM.STDOUT].pop(GM.REDIRECT_MULTI_FD, None)
savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, None)
GM.Globals[GM.STDERR].pop(GM.REDIRECT_MULTI_FD, None)
savedValues[GM.CMDLOG_HANDLER] = GM.Globals[GM.CMDLOG_HANDLER] savedValues[GM.CMDLOG_HANDLER] = GM.Globals[GM.CMDLOG_HANDLER]
GM.Globals[GM.CMDLOG_HANDLER] = None GM.Globals[GM.CMDLOG_HANDLER] = None
savedValues[GM.CMDLOG_LOGGER] = GM.Globals[GM.CMDLOG_LOGGER] savedValues[GM.CMDLOG_LOGGER] = GM.Globals[GM.CMDLOG_LOGGER]
@@ -9750,11 +9778,12 @@ def saveNonPickleableValues():
return savedValues return savedValues
def restoreNonPickleableValues(savedValues): def restoreNonPickleableValues(savedValues):
GM.Globals[GM.SAVED_STDOUT] = savedValues[GM.SAVED_STDOUT] GM.Globals[GM.CSVFILE][GM.REDIRECT_FD] = savedValues[GM.CSVFILE][GM.REDIRECT_FD]
GM.Globals[GM.STDOUT][GM.REDIRECT_FD] = savedValues[GM.STDOUT][GM.REDIRECT_FD] GM.Globals[GM.STDOUT][GM.REDIRECT_FD] = savedValues[GM.STDOUT][GM.REDIRECT_FD]
GM.Globals[GM.STDERR][GM.REDIRECT_FD] = savedValues[GM.STDERR][GM.REDIRECT_FD]
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD] GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD]
GM.Globals[GM.STDERR][GM.REDIRECT_FD] = savedValues[GM.STDERR][GM.REDIRECT_FD]
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD] GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD]
GM.Globals[GM.SAVED_STDOUT] = savedValues[GM.SAVED_STDOUT]
GM.Globals[GM.CMDLOG_HANDLER] = savedValues[GM.CMDLOG_HANDLER] GM.Globals[GM.CMDLOG_HANDLER] = savedValues[GM.CMDLOG_HANDLER]
GM.Globals[GM.CMDLOG_LOGGER] = savedValues[GM.CMDLOG_LOGGER] GM.Globals[GM.CMDLOG_LOGGER] = savedValues[GM.CMDLOG_LOGGER]
@@ -11615,7 +11644,7 @@ def doEnableAPIs():
automatic = False automatic = False
else: else:
unknownArgumentExit() unknownArgumentExit()
request = getTLSv1_2Request() request = get_adc_request()
try: try:
_, projectId = google.auth.default(scopes=[API.IAM_SCOPE], request=request) _, projectId = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError): except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError):
@@ -13200,7 +13229,7 @@ def doCreateGCPServiceAccount():
checkForExtraneousArguments() checkForExtraneousArguments()
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]]) _checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
sa_info = {'key_type': 'signjwt', 'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'type': 'service_account'} sa_info = {'key_type': 'signjwt', 'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'type': 'service_account'}
request = getTLSv1_2Request() request = get_adc_request()
try: try:
credentials, sa_info['project_id'] = google.auth.default(scopes=[API.IAM_SCOPE], request=request) credentials, sa_info['project_id'] = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e: except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
@@ -34008,9 +34037,20 @@ GROUP_ACCESS_TYPE_CHOICE_MAP = {
# gam create group <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>* # gam create group <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>*
# [verifynotinvitable] # [verifynotinvitable]
# [verifyduplicateretries <Integer>] [verifyduplicateretrydelay <Integer>]
# [verifycreationretries <Integer>] [verifycreationinitialdelay <Integer>] [verifycreationretrydelay <Integer>]
def doCreateGroup(ciGroupsAPI=False): def doCreateGroup(ciGroupsAPI=False):
def waitingForCreationToComplete(sleep_time):
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.GROUP), sleep_time))
time.sleep(sleep_time)
cd = buildGAPIObject(API.DIRECTORY) cd = buildGAPIObject(API.DIRECTORY)
verifyNotInvitable = getBeforeUpdate = False verifyNotInvitable = getBeforeUpdate = False
recentDeleteRetries = 0
recentDeleteRetryDelay = 5
verifyCreationRetries = 0
verifyCreationInitialDelay = 5
verifyCreationRetryDelay = 5
groupEmail = getEmailAddress(noUid=True) groupEmail = getEmailAddress(noUid=True)
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI] entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
if not ciGroupsAPI: if not ciGroupsAPI:
@@ -34050,6 +34090,16 @@ def doCreateGroup(ciGroupsAPI=False):
body['labels'][CIGROUP_LOCKED_LABEL] = '' body['labels'][CIGROUP_LOCKED_LABEL] = ''
elif myarg == 'verifynotinvitable': elif myarg == 'verifynotinvitable':
verifyNotInvitable = True verifyNotInvitable = True
elif myarg == 'recentdeleteretries':
recentDeleteRetries = getInteger(minVal=0, maxVal=20)
elif myarg == 'recentdeleteretrydelay':
recentDeleteRetryDelay = getInteger(minVal=1, maxVal=60)
elif myarg == 'verifycreationretries':
verifyCreationRetries = getInteger(minVal=0, maxVal=20)
elif myarg == 'verifycreationinitialdelay':
verifyCreationInitialDelay = getInteger(minVal=0, maxVal=60)
elif myarg == 'verifycreationretrydelay':
verifyCreationRetryDelay = getInteger(minVal=1, maxVal=60)
else: else:
getGroupAttrValue(myarg, gs_body) getGroupAttrValue(myarg, gs_body)
if verifyNotInvitable: if verifyNotInvitable:
@@ -34070,38 +34120,89 @@ def doCreateGroup(ciGroupsAPI=False):
return return
if not getBeforeUpdate: if not getBeforeUpdate:
settings = gs_body settings = gs_body
try: duplicateRetries = 0
if not ciGroupsAPI: while True:
callGAPI(cd.groups(), 'insert', try:
throwReasons=GAPI.GROUP_CREATE_THROW_REASONS, if not ciGroupsAPI:
body=body, fields='') callGAPI(cd.groups(), 'insert',
else: throwReasons=GAPI.GROUP_CREATE_THROW_REASONS,
callGAPI(ci.groups(), 'create', body=body, fields='')
throwReasons=GAPI.CIGROUP_CREATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, else:
initialGroupConfig=initialGroupConfig, body=body, fields='') callGAPI(ci.groups(), 'create',
if gs_body and not GroupIsAbuseOrPostmaster(groupEmail): throwReasons=GAPI.CIGROUP_CREATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
if getBeforeUpdate: initialGroupConfig=initialGroupConfig, body=body, fields='')
settings = callGAPI(gs.groups(), 'get', if gs_body and not GroupIsAbuseOrPostmaster(groupEmail):
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, groupUniqueId = mapGroupEmailForSettings(groupEmail)
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND], if getBeforeUpdate:
groupUniqueId=mapGroupEmailForSettings(groupEmail), fields='*') settings = callGAPI(gs.groups(), 'get',
settings.update(gs_body) throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
callGAPI(gs.groups(), 'update', retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND],
bailOnInvalidError='messageModerationLevel' in settings, groupUniqueId=groupUniqueId, fields='*')
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, settings.update(gs_body)
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT], callGAPI(gs.groups(), 'update',
groupUniqueId=mapGroupEmailForSettings(groupEmail), body=settings, fields='') bailOnInvalidError='messageModerationLevel' in settings,
entityActionPerformed([entityType, groupEmail]) throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
except (GAPI.alreadyExists, GAPI.duplicate): retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
duplicateAliasGroupUserWarning(cd, [entityType, groupEmail]) groupUniqueId=groupUniqueId, body=settings, fields='')
except GAPI.notFound: entityActionPerformed([entityType, groupEmail])
entityActionFailedWarning([entityType, groupEmail], Msg.DOES_NOT_EXIST) break
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError, except GAPI.resourceNotFound as e:
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.invalidArgument, GAPI.failedPrecondition, # If group we're trying to create was just deleted, Google gets confused; sleep and retry
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e: duplicateRetries += 1
entityActionFailedWarning([entityType, groupEmail], str(e)) if ciGroupsAPI or duplicateRetries > recentDeleteRetries:
except GAPI.required: entityActionFailedWarning([entityType, groupEmail], str(e))
entityActionFailedWarning([entityType, groupEmail], Msg.INVALID_JSON_SETTING) break
time.sleep(recentDeleteRetryDelay)
continue
except (GAPI.alreadyExists, GAPI.duplicate):
duplicateRetries += 1
if ciGroupsAPI or duplicateRetries > recentDeleteRetries:
duplicateAliasGroupUserWarning(cd, [entityType, groupEmail])
break
time.sleep(recentDeleteRetryDelay)
continue
# except GAPI.notFound:
# entityActionFailedWarning([entityType, groupEmail], Msg.DOES_NOT_EXIST)
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.invalidArgument, GAPI.failedPrecondition,
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([entityType, groupEmail], str(e))
break
except GAPI.required:
entityActionFailedWarning([entityType, groupEmail], Msg.INVALID_JSON_SETTING)
break
if ciGroupsAPI or not verifyCreationRetries:
return
Act.Set(Act.VERIFYITEMEXISTS)
action = Act.Get()
performAction(Ent.GROUP, groupEmail)
Ind.Increment()
waitingForCreationToComplete(verifyCreationInitialDelay)
retries = 0
while True:
try:
callGAPI(cd.groups(), 'get',
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
groupKey=groupEmail, fields='name')
entityActionPerformed([Ent.GROUP, groupEmail])
break
except GAPI.groupNotFound:
retries += 1
kvList = [Act.PerformedName(action), False, 'Retry', f'{retries}/{verifyCreationRetries}']
printEntityKVList([Ent.GROUP, groupEmail], kvList)
if retries >= verifyCreationRetries:
entityActionFailedWarning([Ent.GROUP, groupEmail], Msg.RETRIES_EXHAUSTED.format(verifyCreationRetries))
break
waitingForCreationToComplete(verifyCreationRetryDelay)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden,
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
entityActionFailedWarning([Ent.GROUP, groupEmail], str(e))
break
except KeyboardInterrupt:
entityActionFailedWarning([Ent.GROUP, groupEmail], Msg.CHECK_INTERRUPTED)
break
Ind.Decrement()
# [addonly|removeonly] # [addonly|removeonly]
def getSyncOperation(): def getSyncOperation():
@@ -35649,7 +35750,7 @@ def setMemberDisplayTitles(memberDisplayOptions, csvPF):
def setMemberDisplaySortTitles(memberDisplayOptions, sortTitles): def setMemberDisplaySortTitles(memberDisplayOptions, sortTitles):
if memberDisplayOptions['totalCount']: if memberDisplayOptions['totalCount']:
sortTitles.append(MEMBERS_TITLES['total'][0]) sortTitles.append(MEMBERS_TITLES['combined']['total'][0])
for category in memberDisplayOptions['categories']: for category in memberDisplayOptions['categories']:
if memberDisplayOptions['totalCount'] and category != 'combined': if memberDisplayOptions['totalCount'] and category != 'combined':
sortTitles.append(MEMBERS_TITLES[category]['total'][0]) sortTitles.append(MEMBERS_TITLES[category]['total'][0])
@@ -45861,6 +45962,8 @@ def getUserAttributes(cd, updateCmd, noUid=False):
elif updateCmd and myarg == 'updateprimaryemail': elif updateCmd and myarg == 'updateprimaryemail':
updatePrimaryEmail = list(getREPatternSubstitution(re.IGNORECASE)) updatePrimaryEmail = list(getREPatternSubstitution(re.IGNORECASE))
updatePrimaryEmail.append(checkArgumentPresent(['preview'])) updatePrimaryEmail.append(checkArgumentPresent(['preview']))
elif updateCmd and myarg == 'primaryguestemail':
body['guestAccountInfo'] = {'primaryGuestEmail': getEmailAddress(noUid=True)}
elif myarg == 'json': elif myarg == 'json':
body.update(getJSON(USER_JSON_SKIP_FIELDS)) body.update(getJSON(USER_JSON_SKIP_FIELDS))
if 'name' in body and 'fullName' in body['name']: if 'name' in body and 'fullName' in body['name']:
@@ -46828,10 +46931,15 @@ USER_NAME_PROPERTY_PRINT_ORDER = [
'fullName', 'fullName',
'displayName', 'displayName',
] ]
USER_GUEST_PROPERTY_PRINT_ORDER = [
'primaryGuestEmail',
]
USER_LANGUAGE_PROPERTY_PRINT_ORDER = [ USER_LANGUAGE_PROPERTY_PRINT_ORDER = [
'languages', 'languages',
] ]
USER_SCALAR_PROPERTY_PRINT_ORDER = [ USER_SCALAR_PROPERTY_PRINT_ORDER = [
'isGuestUser',
'guestAccountInfo',
'isAdmin', 'isAdmin',
'isDelegatedAdmin', 'isDelegatedAdmin',
'isEnrolledIn2Sv', 'isEnrolledIn2Sv',
@@ -46949,7 +47057,7 @@ USER_FIELDS_CHOICE_MAP = {
'gal': 'includeInGlobalAddressList', 'gal': 'includeInGlobalAddressList',
'gender': ['gender.type', 'gender.customGender', 'gender.addressMeAs'], 'gender': ['gender.type', 'gender.customGender', 'gender.addressMeAs'],
'givenname': 'name.givenName', 'givenname': 'name.givenName',
'guestaccountinfo': 'guestAccountInfo', 'guestaccountinfo': ['guestAccountInfo.primaryGuestEmail'],
'id': 'id', 'id': 'id',
'im': 'ims', 'im': 'ims',
'ims': 'ims', 'ims': 'ims',
@@ -47147,7 +47255,7 @@ def infoUsers(entityList):
ci = None ci = None
setTrueCustomerId(cd) setTrueCustomerId(cd)
getAliases = getBuildingNames = getCIGroupsTree = getGroups = getLicenses = getSchemas = not GC.Values[GC.QUICK_INFO_USER] getAliases = getBuildingNames = getCIGroupsTree = getGroups = getLicenses = getSchemas = not GC.Values[GC.QUICK_INFO_USER]
getGroupsTree = False getGroupsTree = getIsGuestUser = False
FJQC = FormatJSONQuoteChar() FJQC = FormatJSONQuoteChar()
schemaParms = _initSchemaParms('full') schemaParms = _initSchemaParms('full')
viewType = 'admin_view' viewType = 'admin_view'
@@ -47202,6 +47310,7 @@ def infoUsers(entityList):
fieldsList.append('customSchemas') fieldsList.append('customSchemas')
if getAliases: if getAliases:
fieldsList.extend(['aliases', 'nonEditableAliases']) fieldsList.extend(['aliases', 'nonEditableAliases'])
getIsGuestUser = not fieldsList or 'isGuestUser' in fieldsList
fields = getFieldsFromFieldsList(fieldsList) fields = getFieldsFromFieldsList(fieldsList)
if getLicenses: if getLicenses:
lic = buildGAPIObject(API.LICENSING) lic = buildGAPIObject(API.LICENSING)
@@ -47218,6 +47327,8 @@ def infoUsers(entityList):
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND], throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND],
userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'], userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
viewType=viewType, fields=fields) viewType=viewType, fields=fields)
if getIsGuestUser and 'isGuestUser' not in user:
user['isGuestUser'] = False
if userMultiAttributeFilters: if userMultiAttributeFilters:
_filterUserMultiAttributes(user, userMultiAttributeFilters) _filterUserMultiAttributes(user, userMultiAttributeFilters)
groups = [] groups = []
@@ -47267,17 +47378,23 @@ def infoUsers(entityList):
Ind.Increment() Ind.Increment()
printKeyValueList(['Settings', None]) printKeyValueList(['Settings', None])
Ind.Increment() Ind.Increment()
if 'name' in user: up = 'name'
for up in USER_NAME_PROPERTY_PRINT_ORDER: if up in user:
if up in user['name']: for nup in USER_NAME_PROPERTY_PRINT_ORDER:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user['name'][up]]) if nup in user[up]:
printKeyValueList([UProp.PROPERTIES[nup][UProp.TITLE], user[up][nup]])
up = 'languages' up = 'languages'
if up in user: if up in user:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], _formatLanguagesList(user[up], ',')]) printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], _formatLanguagesList(user[up], ',')])
for up in USER_SCALAR_PROPERTY_PRINT_ORDER: for up in USER_SCALAR_PROPERTY_PRINT_ORDER:
if up in user: if up in user:
if up not in USER_TIME_OBJECTS: if up not in USER_TIME_OBJECTS:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user[up]]) if up != 'guestAccountInfo':
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user[up]])
else:
for gup in USER_GUEST_PROPERTY_PRINT_ORDER:
if gup in user[up]:
printKeyValueList([UProp.PROPERTIES[gup][UProp.TITLE], user[up][gup]])
else: else:
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], formatLocalTime(user[up])]) printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], formatLocalTime(user[up])])
Ind.Decrement() Ind.Decrement()
@@ -47655,6 +47772,8 @@ def doPrintUsers(entityList=None):
(isArchived == userEntity.get('archived', isArchived))) (isArchived == userEntity.get('archived', isArchived)))
if not showUser: if not showUser:
return return
if getIsGuestUser and 'isGuestUser' not in userEntity:
userEntity['isGuestUser'] = False
if showValidColumn: if showValidColumn:
userEntity[showValidColumn] = True userEntity[showValidColumn] = True
if userMultiAttributeFilters: if userMultiAttributeFilters:
@@ -47793,7 +47912,7 @@ def doPrintUsers(entityList=None):
maxResults = GC.Values[GC.USER_MAX_RESULTS] maxResults = GC.Values[GC.USER_MAX_RESULTS]
schemaParms = _initSchemaParms('basic') schemaParms = _initSchemaParms('basic')
projectionSet = False projectionSet = False
oneLicensePerRow = quotePlusPhoneNumbers = showDeleted = False getIsGuestUser = oneLicensePerRow = quotePlusPhoneNumbers = showDeleted = False
aliasMatchPattern = isArchived = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None aliasMatchPattern = isArchived = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None
viewType = 'admin_view' viewType = 'admin_view'
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
@@ -47931,6 +48050,7 @@ def doPrintUsers(entityList=None):
sortRows = False sortRows = False
if orgUnitPath is not None and fieldsList: if orgUnitPath is not None and fieldsList:
fieldsList.append('orgUnitPath') fieldsList.append('orgUnitPath')
getIsGuestUser = not fieldsList or 'isGuestUser' in fieldsList
fields = getItemFieldsFromFieldsList('users', fieldsList) fields = getItemFieldsFromFieldsList('users', fieldsList)
itemCount = 0 itemCount = 0
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict): for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
@@ -47999,6 +48119,7 @@ def doPrintUsers(entityList=None):
return return
sortRows = True sortRows = True
# If no individual fields were specified (allfields, basic, full) or individual fields other than primaryEmail were specified, look up each user # If no individual fields were specified (allfields, basic, full) or individual fields other than primaryEmail were specified, look up each user
getIsGuestUser = not fieldsList or 'isGuestUser' in fieldsList
if isSuspended is not None and fieldsList: if isSuspended is not None and fieldsList:
fieldsList.append('suspended') fieldsList.append('suspended')
if isArchived is not None and fieldsList: if isArchived is not None and fieldsList:
@@ -50277,6 +50398,7 @@ COURSE_FIELDS_CHOICE_MAP = {
'name': 'name', 'name': 'name',
'owneremail': 'ownerId', 'owneremail': 'ownerId',
'ownerid': 'ownerId', 'ownerid': 'ownerId',
'ownername': 'ownerId',
'room': 'room', 'room': 'room',
'section': 'section', 'section': 'section',
'teacherfolder': 'teacherFolder', 'teacherfolder': 'teacherFolder',
@@ -50299,6 +50421,7 @@ COURSE_PROPERTY_PRINT_ORDER = [
'alternateLink', 'alternateLink',
'ownerEmail', 'ownerEmail',
'ownerId', 'ownerId',
'ownerName',
'creationTime', 'creationTime',
'updateTime', 'updateTime',
'calendarId', 'calendarId',
@@ -50310,7 +50433,8 @@ COURSE_PROPERTY_PRINT_ORDER = [
] ]
def _initCourseShowProperties(fields=None): def _initCourseShowProperties(fields=None):
return {'aliases': False, 'aliasesInColumns': False, 'ownerEmail': False, 'ownerEmailMatchPattern': None, 'members': 'none', 'countsOnly': False, return {'aliases': False, 'aliasesInColumns': False, 'ownerEmail': False, 'ownerEmailMatchPattern': None,
'ownerName': False, 'members': 'none', 'countsOnly': False,
'fields': fields if fields is not None else [], 'skips': []} 'fields': fields if fields is not None else [], 'skips': []}
def _getCourseShowProperties(myarg, courseShowProperties): def _getCourseShowProperties(myarg, courseShowProperties):
@@ -50325,6 +50449,8 @@ def _getCourseShowProperties(myarg, courseShowProperties):
elif myarg == 'owneremailmatchpattern': elif myarg == 'owneremailmatchpattern':
courseShowProperties['ownerEmail'] = True courseShowProperties['ownerEmail'] = True
courseShowProperties['ownerEmailMatchPattern'] = getREPattern(re.IGNORECASE) courseShowProperties['ownerEmailMatchPattern'] = getREPattern(re.IGNORECASE)
elif myarg == 'ownername':
courseShowProperties['ownerName'] = True
elif myarg == 'show': elif myarg == 'show':
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS) courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
elif myarg == 'countsonly': elif myarg == 'countsonly':
@@ -50340,6 +50466,9 @@ def _getCourseShowProperties(myarg, courseShowProperties):
elif field == 'owneremail': elif field == 'owneremail':
courseShowProperties['ownerEmail'] = True courseShowProperties['ownerEmail'] = True
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field]) courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
elif field == 'ownername':
courseShowProperties['ownerName'] = True
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
elif field == 'teachers': elif field == 'teachers':
if courseShowProperties['members'] == 'none': if courseShowProperties['members'] == 'none':
courseShowProperties['members'] = field courseShowProperties['members'] = field
@@ -50381,27 +50510,26 @@ def _setCourseFields(courseShowProperties, pagesMode, getOwnerId=False):
if not courseShowProperties['fields']: if not courseShowProperties['fields']:
return None return None
courseShowProperties['fields'].append('id') courseShowProperties['fields'].append('id')
if courseShowProperties['ownerEmail'] or getOwnerId: if courseShowProperties['ownerEmail'] or courseShowProperties['ownerName'] or getOwnerId:
courseShowProperties['fields'].append('ownerId') courseShowProperties['fields'].append('ownerId')
if not pagesMode: if not pagesMode:
return ','.join(set(courseShowProperties['fields'])) return ','.join(set(courseShowProperties['fields']))
return f'nextPageToken,courses({",".join(set(courseShowProperties["fields"]))})' return f'nextPageToken,courses({",".join(set(courseShowProperties["fields"]))})'
def _convertCourseUserIdToEmail(croom, userId, emails, entityValueList, i, count): def _convertCourseUserIdToEmailName(croom, userId, emails, entityValueList, i, count):
userEmail = emails.get(userId) if userId not in emails:
if userEmail is None:
try: try:
userEmail = callGAPI(croom.userProfiles(), 'get', result = callGAPI(croom.userProfiles(), 'get',
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE], throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS, retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userId=userId, fields='emailAddress').get('emailAddress') userId=userId, fields='emailAddress,name(fullName)')
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable): except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable):
pass result = {}
if userEmail is None: if not result:
entityDoesNotHaveItemWarning(entityValueList, i, count) entityDoesNotHaveItemWarning(entityValueList, i, count)
userEmail = 'Unknown user' emails[userId] = (result.get('emailAddress', 'Unknown user'),
emails[userId] = userEmail result.get('name', {}).get('fullName', 'Unknown user'))
return userEmail return emails[userId]
def _getCourseOwnerSA(croom, course, useOwnerAccess): def _getCourseOwnerSA(croom, course, useOwnerAccess):
if not useOwnerAccess: if not useOwnerAccess:
@@ -50523,9 +50651,13 @@ def _doInfoCourses(courseIdList):
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE], throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS, retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
id=courseId, fields=fields) id=courseId, fields=fields)
if courseShowProperties['ownerEmail']: if courseShowProperties['ownerEmail'] or courseShowProperties['ownerName']:
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails, ownerEmail, ownerName = _convertCourseUserIdToEmailName(croom, course['ownerId'], ownerEmails,
[Ent.COURSE, course['id'], Ent.OWNER_ID, course['ownerId']], i, count) [Ent.COURSE, course['id'], Ent.OWNER_ID, course['ownerId']], i, count)
if courseShowProperties['ownerEmail']:
course['ownerEmail'] = ownerEmail
if courseShowProperties['ownerName']:
course['ownerName'] = ownerName
aliases, teachers, students = _getCourseAliasesMembers(croom, courseInfo['croom'], courseId, courseShowProperties, teachersFields, studentsFields) aliases, teachers, students = _getCourseAliasesMembers(croom, courseInfo['croom'], courseId, courseShowProperties, teachersFields, studentsFields)
if FJQC.formatJSON: if FJQC.formatJSON:
if courseShowProperties['aliases']: if courseShowProperties['aliases']:
@@ -50589,14 +50721,14 @@ def _doInfoCourses(courseIdList):
ClientAPIAccessDeniedExit(str(e)) ClientAPIAccessDeniedExit(str(e))
# gam info courses <CourseEntity> [owneraccess] # gam info courses <CourseEntity> [owneraccess]
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly] # [owneremail] [ownername] [alias|aliases] [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
# [formatjson] # [formatjson]
def doInfoCourses(): def doInfoCourses():
_doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)) _doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
# gam info course <CourseID> [owneraccess] # gam info course <CourseID> [owneraccess]
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly] # [owneremail] [ownername] [alias|aliases] [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
# [formatjson] # [formatjson]
def doInfoCourse(): def doInfoCourse():
@@ -50709,7 +50841,7 @@ def _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getO
return coursesInfo return coursesInfo
# gam print courses [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>]) # gam print courses [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
# [owneremail] [owneremailmatchpattern <REMatchPattern>] # [owneremail] [owneremailmatchpattern <REMatchPattern>] [ownername]
# [alias|aliases|aliasesincolumns [delimiter <Character>]] # [alias|aliases|aliasesincolumns [delimiter <Character>]]
# [show none|all|students|teachers] [countsonly] # [show none|all|students|teachers] [countsonly]
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] # [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
@@ -50828,11 +50960,15 @@ def doPrintCourses():
ocroom = _getCourseOwnerSA(croom, course, useOwnerAccess) ocroom = _getCourseOwnerSA(croom, course, useOwnerAccess)
if not ocroom: if not ocroom:
continue continue
if courseShowProperties['ownerEmail']: if courseShowProperties['ownerEmail'] or courseShowProperties['ownerName']:
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails, ownerEmail, ownerName = _convertCourseUserIdToEmailName(croom, course['ownerId'], ownerEmails,
[Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count) [Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count)
if courseShowProperties['ownerEmailMatchPattern'] and not courseShowProperties['ownerEmailMatchPattern'].match(course['ownerEmail']): if courseShowProperties['ownerEmail']:
continue course['ownerEmail'] = ownerEmail
if courseShowProperties['ownerEmailMatchPattern'] and not courseShowProperties['ownerEmailMatchPattern'].match(ownerEmail):
continue
if courseShowProperties['ownerName']:
course['ownerName'] = ownerName
if showItemCountOnly: if showItemCountOnly:
itemCount += 1 itemCount += 1
continue continue
@@ -50934,10 +51070,14 @@ def doPrintCourseAnnouncements():
def _printCourseAnnouncement(course, courseAnnouncement, i, count): def _printCourseAnnouncement(course, courseAnnouncement, i, count):
if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter): if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter):
return return
if showCreatorEmail: if showCreatorEmail or showCreatorName:
courseAnnouncement['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseAnnouncement['creatorUserId'], creatorEmails, creatorUserEmail, creatorUserName = _convertCourseUserIdToEmailName(croom, courseAnnouncement['creatorUserId'], creatorEmails,
[Ent.COURSE, course['id'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'], [Ent.COURSE, course['id'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'],
Ent.CREATOR_ID, courseAnnouncement['creatorUserId']], i, count) Ent.CREATOR_ID, courseAnnouncement['creatorUserId']], i, count)
if showCreatorEmail:
courseAnnouncement['creatorUserEmail'] = creatorUserEmail
if showCreatorName:
courseAnnouncement['creatorUserName'] = creatorUserName
row = flattenJSON(courseAnnouncement, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS) row = flattenJSON(courseAnnouncement, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS)
if not FJQC.formatJSON: if not FJQC.formatJSON:
csvPF.WriteRowTitles(row) csvPF.WriteRowTitles(row)
@@ -50957,7 +51097,7 @@ def doPrintCourseAnnouncements():
courseAnnouncementStates = [] courseAnnouncementStates = []
OBY = OrderBy(COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP) OBY = OrderBy(COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP)
creatorEmails = {} creatorEmails = {}
countsOnly = showCreatorEmail = False countsOnly = showCreatorEmail = showCreatorName = False
items = 'courseAnnouncements' items = 'courseAnnouncements'
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
@@ -50975,6 +51115,8 @@ def doPrintCourseAnnouncements():
OBY.GetChoice() OBY.GetChoice()
elif myarg in {'showcreatoremails', 'creatoremail'}: elif myarg in {'showcreatoremails', 'creatoremail'}:
showCreatorEmail = True showCreatorEmail = True
elif myarg in {'showcreatornames', 'creatorname'}:
showCreatorName = True
elif getFieldsList(myarg, COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'): elif getFieldsList(myarg, COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
pass pass
elif myarg == 'countsonly': elif myarg == 'countsonly':
@@ -51232,10 +51374,14 @@ def doPrintCourseWM(entityIDType, entityStateType):
def _printCourseWM(course, courseWM, i, count): def _printCourseWM(course, courseWM, i, count):
if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter): if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter):
return return
if showCreatorEmail: if showCreatorEmail or showCreatorName:
courseWM['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseWM['creatorUserId'], creatorEmails, creatorUserEmail, creatorUserName = _convertCourseUserIdToEmailName(croom, courseWM['creatorUserId'], creatorEmails,
[Ent.COURSE, course['id'], entityIDType, courseWM['id'], [Ent.COURSE, course['id'], entityIDType, courseWM['id'],
Ent.CREATOR_ID, courseWM['creatorUserId']], i, count) Ent.CREATOR_ID, courseWM['creatorUserId']], i, count)
if showCreatorEmail:
courseWM['creatorUserEmail'] = creatorUserEmail
if showCreatorName:
courseWM['creatorUserName'] = creatorUserName
if showTopicNames: if showTopicNames:
topicId = courseWM.get('topicId') topicId = courseWM.get('topicId')
if topicId: if topicId:
@@ -51284,7 +51430,7 @@ def doPrintCourseWM(entityIDType, entityStateType):
courseShowProperties = _initCourseShowProperties(['name']) courseShowProperties = _initCourseShowProperties(['name'])
OBY = OrderBy(OrderbyChoiceMap) OBY = OrderBy(OrderbyChoiceMap)
creatorEmails = {} creatorEmails = {}
oneItemPerRow = showCreatorEmail = showTopicNames = False oneItemPerRow = showCreatorEmail = showCreatorName = showTopicNames = False
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
countsOnly = showStudentsAsList = False countsOnly = showStudentsAsList = False
while Cmd.ArgumentsRemaining(): while Cmd.ArgumentsRemaining():
@@ -51306,6 +51452,8 @@ def doPrintCourseWM(entityIDType, entityStateType):
csvPF.RemoveIndexedTitles('materials') csvPF.RemoveIndexedTitles('materials')
elif myarg in {'showcreatoremails', 'creatoremail'}: elif myarg in {'showcreatoremails', 'creatoremail'}:
showCreatorEmail = True showCreatorEmail = True
elif myarg in {'showcreatornames', 'creatorname'}:
showCreatorName = True
elif myarg == 'showtopicnames': elif myarg == 'showtopicnames':
showTopicNames = True showTopicNames = True
elif getFieldsList(myarg, FieldsChoiceMap, fieldsList, initialField='id'): elif getFieldsList(myarg, FieldsChoiceMap, fieldsList, initialField='id'):
@@ -51319,7 +51467,7 @@ def doPrintCourseWM(entityIDType, entityStateType):
csvPF.AddTitles(items) csvPF.AddTitles(items)
else: else:
FJQC.GetFormatJSONQuoteChar(myarg, True) FJQC.GetFormatJSONQuoteChar(myarg, True)
if showCreatorEmail and fieldsList: if (showCreatorEmail or showCreatorName) and fieldsList:
fieldsList.append('creatorUserId') fieldsList.append('creatorUserId')
if showTopicNames and fieldsList: if showTopicNames and fieldsList:
fieldsList.append('topicId') fieldsList.append('topicId')
@@ -51384,7 +51532,8 @@ def doPrintCourseWM(entityIDType, entityStateType):
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) # (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)* # (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
# (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*) # (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*)
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>] # [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
# [fields <CourseMaterialFieldNameList>]
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] # [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [oneitemperrow] # [oneitemperrow]
# [countsonly|(formatjson [quotechar <Character>])] # [countsonly|(formatjson [quotechar <Character>])]
@@ -51395,7 +51544,8 @@ def doPrintCourseMaterials():
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) # (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)* # (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*) # (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>] # [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
# [fields <CourseWorkFieldNameList>]
# [showstudentsaslist [<Boolean>]] [delimiter <Character>] # [showstudentsaslist [<Boolean>]] [delimiter <Character>]
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] # [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
# [oneitemperrow] # [oneitemperrow]

View File

@@ -134,6 +134,7 @@ class GamAction():
UNZIP = 'unzi' UNZIP = 'unzi'
USE = 'use ' USE = 'use '
VERIFY = 'vrfy' VERIFY = 'vrfy'
VERIFYITEMEXISTS = 'vexi'
WAITFORMAILBOX = 'wamb' WAITFORMAILBOX = 'wamb'
WATCH = 'watc' WATCH = 'watc'
WIPE = 'wipe' WIPE = 'wipe'
@@ -253,6 +254,7 @@ class GamAction():
UPLOAD: ['Uploaded', 'Upload'], UPLOAD: ['Uploaded', 'Upload'],
USE: ['Used', 'Use'], USE: ['Used', 'Use'],
VERIFY: ['Verified', 'Verify'], VERIFY: ['Verified', 'Verify'],
VERIFYITEMEXISTS: ['Verified Item Exists', 'Verify Item Exists'],
WAITFORMAILBOX: ['Mailbox is Setup', 'Check Mailbox is Setup'], WAITFORMAILBOX: ['Mailbox is Setup', 'Check Mailbox is Setup'],
WATCH: ['Watched', 'Watch'], WATCH: ['Watched', 'Watch'],
WIPE: ['Wiped', 'Wipe'], WIPE: ['Wiped', 'Wipe'],

View File

@@ -114,6 +114,14 @@ class GamCLArgs():
ENTITY_USERS_NA_NS = 'users_na_ns' ENTITY_USERS_NA_NS = 'users_na_ns'
ENTITY_USERS_ARCH_OR_SUSP = 'users_arch_or_susp' ENTITY_USERS_ARCH_OR_SUSP = 'users_arch_or_susp'
ENTITY_USERS_NS_SUSP = 'users_ns_susp' ENTITY_USERS_NS_SUSP = 'users_ns_susp'
ENTITY_USERS_AND_GUESTS = 'users_and_guests'
ENTITY_USERS_AND_GUESTS_NS = 'users_and_guests_ns'
ENTITY_USERS_AND_GUESTS_SUSP = 'users_and_guests_susp'
ENTITY_USERS_AND_GUESTS_NS_SUSP = 'users_and_guests_ns_susp'
ENTITY_GUESTS = 'guests'
ENTITY_GUESTS_NS = 'guests_ns'
ENTITY_GUESTS_SUSP = 'guests_susp'
ENTITY_GUESTS_NS_SUSP = 'guests_ns_susp'
# #
BROWSER_ENTITIES = [ BROWSER_ENTITIES = [
ENTITY_BROWSER, ENTITY_BROWSER,
@@ -398,6 +406,14 @@ class GamCLArgs():
ENTITY_USERS_ARCH_OR_SUSP, ENTITY_USERS_ARCH_OR_SUSP,
ENTITY_USERS_NA_NS, ENTITY_USERS_NA_NS,
ENTITY_USERS_NS_SUSP, ENTITY_USERS_NS_SUSP,
ENTITY_USERS_AND_GUESTS,
ENTITY_USERS_AND_GUESTS_NS,
ENTITY_USERS_AND_GUESTS_SUSP,
ENTITY_USERS_AND_GUESTS_NS_SUSP,
ENTITY_GUESTS,
ENTITY_GUESTS_NS,
ENTITY_GUESTS_SUSP,
ENTITY_GUESTS_NS_SUSP,
] ]
# #
ENTITY_ALL_CROS = ENTITY_SELECTOR_ALL+' '+ENTITY_CROS ENTITY_ALL_CROS = ENTITY_SELECTOR_ALL+' '+ENTITY_CROS
@@ -409,6 +425,14 @@ class GamCLArgs():
ENTITY_ALL_USERS_NA_NS = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_NA_NS ENTITY_ALL_USERS_NA_NS = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_NA_NS
ENTITY_ALL_USERS_ARCH_OR_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_ARCH_OR_SUSP ENTITY_ALL_USERS_ARCH_OR_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_ARCH_OR_SUSP
ENTITY_ALL_USERS_NS_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_NS_SUSP ENTITY_ALL_USERS_NS_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_NS_SUSP
ENTITY_ALL_USERS_AND_GUESTS = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_AND_GUESTS
ENTITY_ALL_USERS_AND_GUESTS_NS = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_AND_GUESTS_NS
ENTITY_ALL_USERS_AND_GUESTS_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_AND_GUESTS_SUSP
ENTITY_ALL_USERS_AND_GUESTS_NS_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_USERS_AND_GUESTS_NS_SUSP
ENTITY_ALL_GUESTS = ENTITY_SELECTOR_ALL+' '+ENTITY_GUESTS
ENTITY_ALL_GUESTS_NS = ENTITY_SELECTOR_ALL+' '+ENTITY_GUESTS_NS
ENTITY_ALL_GUESTS_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_GUESTS_SUSP
ENTITY_ALL_GUESTS_NS_SUSP = ENTITY_SELECTOR_ALL+' '+ENTITY_GUESTS_NS_SUSP
# #
ALL_USER_ENTITY_TYPES = { ALL_USER_ENTITY_TYPES = {
ENTITY_ALL_USERS, ENTITY_ALL_USERS,
@@ -418,6 +442,14 @@ class GamCLArgs():
ENTITY_ALL_USERS_SUSP, ENTITY_ALL_USERS_SUSP,
ENTITY_ALL_USERS_NA_NS, ENTITY_ALL_USERS_NA_NS,
ENTITY_ALL_USERS_NS_SUSP, ENTITY_ALL_USERS_NS_SUSP,
ENTITY_ALL_USERS_AND_GUESTS,
ENTITY_ALL_USERS_AND_GUESTS_NS,
ENTITY_ALL_USERS_AND_GUESTS_SUSP,
ENTITY_ALL_USERS_AND_GUESTS_NS_SUSP,
ENTITY_ALL_GUESTS,
ENTITY_ALL_GUESTS_NS,
ENTITY_ALL_GUESTS_SUSP,
ENTITY_ALL_GUESTS_NS_SUSP,
} }
DOMAIN_ENTITY_TYPES = { DOMAIN_ENTITY_TYPES = {
ENTITY_DOMAINS, ENTITY_DOMAINS,
@@ -534,13 +566,21 @@ class GamCLArgs():
} }
# #
ALL_USERS_QUERY_MAP = { ALL_USERS_QUERY_MAP = {
ENTITY_ALL_USERS: 'isSuspended=False', ENTITY_ALL_USERS: 'isSuspended=False isGuest=False',
ENTITY_ALL_USERS_NA: 'isArchived=False', ENTITY_ALL_USERS_NA: 'isArchived=False isGuest=False',
ENTITY_ALL_USERS_ARCH: 'isArchived=True', ENTITY_ALL_USERS_ARCH: 'isArchived=True isGuest=False',
ENTITY_ALL_USERS_NS: 'isSuspended=False', ENTITY_ALL_USERS_NS: 'isSuspended=False isGuest=False',
ENTITY_ALL_USERS_SUSP: 'isSuspended=True', ENTITY_ALL_USERS_SUSP: 'isSuspended=True isGuest=False',
ENTITY_ALL_USERS_NA_NS: 'isArchived=False isSuspended=False', ENTITY_ALL_USERS_NA_NS: 'isArchived=False isSuspended=False isGuest=False',
ENTITY_ALL_USERS_NS_SUSP: None, ENTITY_ALL_USERS_NS_SUSP: 'isGuest=False',
ENTITY_ALL_USERS_AND_GUESTS: 'isSuspended=False',
ENTITY_ALL_USERS_AND_GUESTS_NS: 'isSuspended=False',
ENTITY_ALL_USERS_AND_GUESTS_SUSP: 'isSuspended=True',
ENTITY_ALL_USERS_AND_GUESTS_NS_SUSP: None,
ENTITY_ALL_GUESTS: 'isSuspended=False isGuest=True',
ENTITY_ALL_GUESTS_NS: 'isSuspended=False isGuest=True',
ENTITY_ALL_GUESTS_SUSP: 'isSuspended=True isGuest=True',
ENTITY_ALL_GUESTS_NS_SUSP: 'isGuest=True',
} }
DOMAINS_QUERY_MAP = { DOMAINS_QUERY_MAP = {
ENTITY_DOMAINS: None, ENTITY_DOMAINS: None,
@@ -602,6 +642,14 @@ class GamCLArgs():
ENTITY_USERS_NA_NS: ENTITY_ALL_USERS_NA_NS, ENTITY_USERS_NA_NS: ENTITY_ALL_USERS_NA_NS,
ENTITY_USERS_ARCH_OR_SUSP: ENTITY_ALL_USERS_ARCH_OR_SUSP, ENTITY_USERS_ARCH_OR_SUSP: ENTITY_ALL_USERS_ARCH_OR_SUSP,
ENTITY_USERS_NS_SUSP: ENTITY_ALL_USERS_NS_SUSP, ENTITY_USERS_NS_SUSP: ENTITY_ALL_USERS_NS_SUSP,
ENTITY_USERS_AND_GUESTS: ENTITY_ALL_USERS_AND_GUESTS,
ENTITY_USERS_AND_GUESTS_NS: ENTITY_ALL_USERS_AND_GUESTS_NS,
ENTITY_USERS_AND_GUESTS_SUSP: ENTITY_ALL_USERS_AND_GUESTS_SUSP,
ENTITY_USERS_AND_GUESTS_NS_SUSP: ENTITY_ALL_USERS_AND_GUESTS_NS_SUSP,
ENTITY_GUESTS: ENTITY_ALL_GUESTS,
ENTITY_GUESTS_NS: ENTITY_ALL_GUESTS_NS,
ENTITY_GUESTS_SUSP: ENTITY_ALL_GUESTS_SUSP,
ENTITY_GUESTS_NS_SUSP: ENTITY_ALL_GUESTS_NS_SUSP,
} }
# Allowed values for CL source selector datafile, csvkmd # Allowed values for CL source selector datafile, csvkmd
CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES = [ CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES = [

View File

@@ -274,7 +274,7 @@ GMAIL_LIST_THROW_REASONS = [FAILED_PRECONDITION, PERMISSION_DENIED, INVALID, INV
GMAIL_SMIME_THROW_REASONS = [SERVICE_NOT_AVAILABLE, BAD_REQUEST, INVALID_ARGUMENT, FORBIDDEN, NOT_FOUND, PERMISSION_DENIED] 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_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_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] 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_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, 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, INVALID, INVALID_ARGUMENT, INVALID_PARAMETER, INVALID_ATTRIBUTE_VALUE, INVALID_INPUT,

View File

@@ -131,6 +131,8 @@ GAM_PATH = 'gpth'
GAM_TYPE = 'gtyp' GAM_TYPE = 'gtyp'
# Shared Service Account HTTP Object # Shared Service Account HTTP Object
HTTP_OBJECT = 'http' HTTP_OBJECT = 'http'
# Are we on Global Compute Engine
IS_ON_GCE = 'ogce'
# Length of last Got message # Length of last Got message
LAST_GOT_MSG_LEN = 'lgml' LAST_GOT_MSG_LEN = 'lgml'
# License SKUs # License SKUs
@@ -285,6 +287,7 @@ Globals = {
GAM_PATH: '.', GAM_PATH: '.',
GAM_TYPE: '', GAM_TYPE: '',
HTTP_OBJECT: None, HTTP_OBJECT: None,
IS_ON_GCE: False,
LAST_GOT_MSG_LEN: 0, LAST_GOT_MSG_LEN: 0,
LICENSE_SKUS: [], LICENSE_SKUS: [],
MAKE_BUILDING_ID_NAME_MAP: True, MAKE_BUILDING_ID_NAME_MAP: True,

View File

@@ -117,6 +117,8 @@ PROPERTIES = {
{CLASS: PC_STRING, TITLE: 'Full Name',}, {CLASS: PC_STRING, TITLE: 'Full Name',},
'displayName': 'displayName':
{CLASS: PC_STRING, TITLE: 'Display Name',}, {CLASS: PC_STRING, TITLE: 'Display Name',},
'primaryGuestEmail':
{CLASS: PC_STRING, TITLE: 'Primary Guest Email',},
'languages': 'languages':
{CLASS: PC_LANGUAGES, TITLE: 'Languages',}, {CLASS: PC_LANGUAGES, TITLE: 'Languages',},
'languageCode': 'languageCode':
@@ -131,6 +133,8 @@ PROPERTIES = {
{CLASS: PC_BOOLEAN, TITLE: 'Is a Super Admin',}, {CLASS: PC_BOOLEAN, TITLE: 'Is a Super Admin',},
'isDelegatedAdmin': 'isDelegatedAdmin':
{CLASS: PC_BOOLEAN, TITLE: 'Is Delegated Admin',}, {CLASS: PC_BOOLEAN, TITLE: 'Is Delegated Admin',},
'isGuestUser':
{CLASS: PC_BOOLEAN, TITLE: 'Is a Guest User',},
'isEnrolledIn2Sv': 'isEnrolledIn2Sv':
{CLASS: PC_BOOLEAN, TITLE: '2-step enrolled',}, {CLASS: PC_BOOLEAN, TITLE: '2-step enrolled',},
'isEnforcedIn2Sv': 'isEnforcedIn2Sv':

View File

@@ -30,6 +30,9 @@ function minimizeAllWindows() {
async function takeScreenshot(filename) { async function takeScreenshot(filename) {
const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const fullPath = path.join(workspace, filename); const fullPath = path.join(workspace, filename);
// Create a temporary script file path
const scriptPath = path.join(workspace, `screenshot_${Date.now()}.ps1`);
const psScript = ` const psScript = `
Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Windows.Forms;
@@ -44,47 +47,101 @@ async function takeScreenshot(filename) {
$bitmap = New-Object System.Drawing.Bitmap $Screen.Width, $Screen.Height; $bitmap = New-Object System.Drawing.Bitmap $Screen.Width, $Screen.Height;
$graphic = [System.Drawing.Graphics]::FromImage($bitmap); $graphic = [System.Drawing.Graphics]::FromImage($bitmap);
$graphic.CopyFromScreen($Screen.Left, $Screen.Top, 0, 0, $bitmap.Size); $graphic.CopyFromScreen($Screen.Left, $Screen.Top, 0, 0, $bitmap.Size);
# Save the file (using single quotes safely now)
$bitmap.Save('${fullPath}'); $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 { try {
execSync(`powershell -Command "${psScript}"`); // 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}`); console.log(`Saved screenshot: ${fullPath}`);
} catch (err) { } catch (err) {
console.error(`Failed to save screenshot ${fullPath}:`, err.message); 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 // Fire and forget application launcher with logging
function launchSSD() { 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', [], { const child = spawn('C:\\Program Files\\Certum\\SimplySign Desktop\\SimplySignDesktop.exe', [], {
detached: true, detached: true,
stdio: 'ignore' // stdio array: [stdin, stdout, stderr]
// We ignore stdin, and pipe stdout/stderr to our files
stdio: ['ignore', out, err]
}); });
child.unref();
// 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() { async function runSSD() {
await takeScreenshot('001.png'); const runner_arch = process.env.RUNNER_ARCH;
minimizeAllWindows(); if ( runner_arch === "ARM64" ) {
await sleep(2000); console.log('Running on ARM64. Tabbing through OOBE...');
await takeScreenshot('002.png'); await sleep(3000);
sendKeys('{ESC}'); await takeScreenshot('oob1.png');
await sleep(2000);
await takeScreenshot('003.png'); // Page 1: Tab through the toggles to reach the "Next" button
//sendKeys('{ESC}'); for (let i = 0; i < 7; i++) {
//await sleep(2000); sendKeys('{TAB}');
//await takeScreenshot('004.png'); await sleep(200);
//sendKeys('{ESC}'); }
//await sleep(2000); sendKeys('{ENTER}');
//await takeScreenshot('005.png'); console.log('Clicked Next');
//sendKeys('%{F4}');
//await sleep(2000); await sleep(3000);
//await takeScreenshot('006.png'); await takeScreenshot('ooob2.png');
//sendKeys('%{F4}');
//await sleep(2000);
//await takeScreenshot('007.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 // Re-execute SSD to open login dialog
launchSSD(); launchSSD();
await sleep(3000); await sleep(3000);

View File

@@ -60,6 +60,9 @@ These filters can be used alone or in conjunction with the `matchfield|skipfield
[(any|all):]notdata:<DataSelector>| [(any|all):]notdata:<DataSelector>|
[(any|all):]notregex:<RESearchPattern>| [(any|all):]notregex:<RESearchPattern>|
[(any|all):]notregexcs:<RESearchPattern>| [(any|all):]notregexcs:<RESearchPattern>|
[(any|all):]number<Operator><Number>|
[(any|all):]numberrange!=<Number>/<Number>|
[(any|all):]numberrange=<Number>/<Number>|
[(any|all):]regex:<RESearchPattern>| [(any|all):]regex:<RESearchPattern>|
[(any|all):]regexcs:<RESearchPattern>| [(any|all):]regexcs:<RESearchPattern>|
[(any|all):]text<Operator><String>| [(any|all):]text<Operator><String>|

View File

@@ -68,6 +68,9 @@ on all platforms.
[(any|all):]notdata:<DataSelector> [(any|all):]notdata:<DataSelector>
[(any|all):]notregex:<RESearchPattern>| [(any|all):]notregex:<RESearchPattern>|
[(any|all):]notregexcs:<RESearchPattern>| [(any|all):]notregexcs:<RESearchPattern>|
[(any|all):]number<Operator><Number>|
[(any|all):]numberrange!=<Number>/<Number>|
[(any|all):]numberrange=<Number>/<Number>|
[(any|all):]regex:<RESearchPattern>| [(any|all):]regex:<RESearchPattern>|
[(any|all):]regexcs:<RESearchPattern>| [(any|all):]regexcs:<RESearchPattern>|
[(any|all):]text<Operator><String>| [(any|all):]text<Operator><String>|

View File

@@ -131,6 +131,7 @@ gam user user@domain.com check|update serviceaccount
name| name|
owneremail| owneremail|
ownerid| ownerid|
ownername|
room| room|
section| section|
teacherfolder| teacherfolder|
@@ -431,14 +432,16 @@ gam courses <CourseEntity> update topic <CourseTopicIDEntity> <CourseTopic>
## Display courses ## Display courses
``` ```
gam info course <CourseID> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly] gam info course <CourseID>
[owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson] [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
gam info courses <CourseEntity> [owneremail] [alias|aliases] [show all|students|teachers] [countsonly] gam info courses <CourseEntity>
[owneremail] [ownername] [alias|aliases] [show all|students|teachers] [countsonly]
[fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson] [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
gam print courses [todrive <ToDriveAttribute>*] gam print courses [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
[owneremail] [owneremailmatchpattern <REMatchPattern>] [owneremail] [owneremailmatchpattern <REMatchPattern>] [ownername]
[alias|aliases|aliasesincolumns [delimiter <Character>]] [alias|aliases|aliasesincolumns [delimiter <Character>]]
[show all|students|teachers] [countsonly] [show all|students|teachers] [countsonly]
[timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
@@ -471,7 +474,9 @@ To get information about courses created/updated within a particular time frame,
For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified. For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified.
By default, all basic course fields are displayed; use the following options to modify the output. By default, all basic course fields are displayed; use the following options to modify the output.
* `owneremail` - Display course owner email; requires an additional API call per course. * `owneremail` - Display course owner email.
* `ownername` - Display course owner name.
* These options require an additional API call per course.
* `alias|aliases` - Display course aliases; all aliases are in the single column `Aliases` separated by a delimiter; requires an additional API call per course. * `alias|aliases` - Display course aliases; all aliases are in the single column `Aliases` separated by a delimiter; requires an additional API call per course.
* `delimiter <Character>` - Delimiter between aliases with `print` command. * `delimiter <Character>` - Delimiter between aliases with `print` command.
* `aliasesincolumn` - Display course aliases; the `Aliases` column contains the number of aliases and `Aliases.0`, `Aliases.1`, ... contain the individual aliases; requires an additional API call per course. * `aliasesincolumn` - Display course aliases; the `Aliases` column contains the number of aliases and `Aliases.0`, `Aliases.1`, ... contain the individual aliases; requires an additional API call per course.
@@ -525,7 +530,8 @@ gam print course-announcements [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
(courseannouncementids <CourseAnnouncementIDEntity>)|(announcementstates <CourseAnnouncementStateList>)* (courseannouncementids <CourseAnnouncementIDEntity>)|(announcementstates <CourseAnnouncementStateList>)*
(orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*) (orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
[creatoremail] [fields <CourseAnnouncementFieldNameList>] [showcreatoremails|creatoremail] [showcreatornames|creatorname]
[fields <CourseAnnouncementFieldNameList>]
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
[countsonly] [formatjson [quotechar <Character>]] [countsonly] [formatjson [quotechar <Character>]]
``` ```
@@ -553,7 +559,10 @@ To get information about course announcements created/updated/scheduled within a
For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified. For the filter to apply, `timefilter` and at least one of `start|starttime` and `end|endtime` must be specified.
By default, all course announcement fields are displayed; use the following options to modify the output. By default, all course announcement fields are displayed; use the following options to modify the output.
* `creatoremail` - Display course announcement creator email; requires an additional API call per course announcement. * `creatoremail` - Display course announcement creator email.
* `creatorname` - Display course announcement creator name.
* These options require an additional API call per course.
* `alias|aliases` - Display course aliases; all aliases are in the single column `Aliases` separated by a delimiter; requires an additional API call per course.
* `fields <CourseAnnouncementFieldNameList>` - Select specific fields to display. * `fields <CourseAnnouncementFieldNameList>` - Select specific fields to display.
Use the `countsonly` option to display the number of announcements in a course but not their details. Use the `countsonly` option to display the number of announcements in a course but not their details.
@@ -573,7 +582,8 @@ gam print course-materials [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
(materialids <CourseMaterialIDEntity>)|(materialstates <CourseMaterialStateList>)* (materialids <CourseMaterialIDEntity>)|(materialstates <CourseMaterialStateList>)*
(orderby <CourseMaterialOrderByFieldName> [ascending|descending])*) (orderby <CourseMaterialOrderByFieldName> [ascending|descending])*)
[showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>] [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
[fields <CourseMaterialFieldNameList>]
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
[oneitemperrow] [oneitemperrow]
[countsonly] [formatjson [quotechar <Character>]] [countsonly] [formatjson [quotechar <Character>]]
@@ -602,8 +612,10 @@ By default, all published course materials for a course are displayed; use the f
* `materialsstates <CourseMaterialsStateList>` - Display course materials with any of the specified states. * `materialsstates <CourseMaterialsStateList>` - Display course materials with any of the specified states.
By default, all course materials fields are displayed; use the following options to modify the output. By default, all course materials fields are displayed; use the following options to modify the output.
* `showcreatoremails` - Display course materials creator email; requires an additional API call per course materials. * `showcreatoremails|creatoremail` - Display course materials creator email.
* `showtopicnames` - Display topic names; requires and additional API call per course. * `showcreatornames|creatorname` - Display course materials creator name.
* These options require an additional API call per course.
* `showtopicnames` - Display topic names; requires an additional API call per course.
* `fields <CourseMaterialsFieldNameList>` - Select specific fields to display. * `fields <CourseMaterialsFieldNameList>` - Select specific fields to display.
With `print course-materials`, the materials selected for display are all output on one row/line as a repeating item with the other course fields. With `print course-materials`, the materials selected for display are all output on one row/line as a repeating item with the other course fields.
@@ -669,7 +681,8 @@ gam print course-work [todrive <ToDriveAttribute>*]
(course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>]) (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
(workids <CourseWorkIDEntity>)|(workstates <CourseWorkStateList>)* (workids <CourseWorkIDEntity>)|(workstates <CourseWorkStateList>)*
(orderby <CourseWorkOrderByFieldName> [ascending|descending])*) (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
[showcreatoremails] [showtopicnames] [fields <CourseWorkFieldNameList>] [showcreatoremails|creatoremail] [showcreatornames|creatorname] [showtopicnames]
[fields <CourseWorkFieldNameList>]
[showstudentsaslist [<Boolean>]] [delimiter <Character>] [showstudentsaslist [<Boolean>]] [delimiter <Character>]
[timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
[oneitemperrow] [oneitemperrow]
@@ -699,8 +712,10 @@ By default, all published course work for a course is displayed; use the followi
* `workstates <CourseWorkStateList>` - Display course work with any of the specified states. * `workstates <CourseWorkStateList>` - Display course work with any of the specified states.
By default, all course work fields are displayed; use the following options to modify the output. By default, all course work fields are displayed; use the following options to modify the output.
* `showcreatoremails` - Display course work creator email; requires an additional API call per course work. * `showcreatoremails|creatoremail` - Display course materials creator email.
* `showtopicnames` - Display topic names; requires and additional API call per course. * `showcreatornames|creatorname` - Display course materials creator name.
* These options require an additional API call per course.
* `showtopicnames` - Display topic names; requires an additional API call per course.
* `fields <CourseWorkFieldNameList>` - Select specific fields to display. * `fields <CourseWorkFieldNameList>` - Select specific fields to display.
By default, when course work is assigned to individual students, the student IDs are displayed in multiple indexed columns. By default, when course work is assigned to individual students, the student IDs are displayed in multiple indexed columns.

View File

@@ -3,9 +3,19 @@
- [Notes](#notes) - [Notes](#notes)
- [Definitions](#definitions) - [Definitions](#definitions)
- [User Type Entity](#user-type-entity) - [User Type Entity](#user-type-entity)
- [All non-archived Users](#all-non-archived-users)
- [All archived Users](#all-archived-Users)
- [All non-suspended Users](#all-non-suspended-users) - [All non-suspended Users](#all-non-suspended-users)
- [All suspended Users](#all-suspended-Users) - [All suspended Users](#all-suspended-Users)
- [All archived or suspended Users](#all-archived-or-suspended-users)
- [All non-archived and non-suspended Users](#all-non-archived-and-non-suspended-users)
- [All non-suspended and suspended Users](#all-non-suspended-and-suspended-users) - [All non-suspended and suspended Users](#all-non-suspended-and-suspended-users)
- [All non-suspended Guests](#all-non-suspended-guests)
- [All suspended Guests](#all-suspended-Guests)
- [All non-suspended and suspended Guests](#all-non-suspended-and-suspended-guests)
- [All non-suspended Users and Guests](#all-non-suspended-users-and-guests)
- [All suspended Users and Guests](#all-suspended-users-and-guests)
- [All non-suspended and suspended Users and Guests](#all-non-suspended-and-suspended-users-and-guests)
- [A single User](#a-single-user) - [A single User](#a-single-user)
- [A list of Users](#a-list-of-users) - [A list of Users](#a-list-of-users)
- [The admin user referenced in oauth2.txt](#the-admin-user-referenced-in-oauth2txt) - [The admin user referenced in oauth2.txt](#the-admin-user-referenced-in-oauth2txt)
@@ -40,6 +50,18 @@
## Notes ## Notes
The following items referencing guest users were added to `<UserTypeEntity>` in version 7.43.00.
```
all_guests
all_guests_ns
all_guests_susp
all_guests_ns_susp
all_users_and_guests
all_users_and_guests_ns
all_users_and_guests_susp
all_users_and_guests_ns_susp
```
The followig items referencing non-archived/archived users were added to `<UserTypeEntity>` in version 7.22.00. The followig items referencing non-archived/archived users were added to `<UserTypeEntity>` in version 7.22.00.
``` ```
all users_na all users_na
@@ -123,6 +145,8 @@ ous_and_children_na_ns
<UserTypeEntity> ::= <UserTypeEntity> ::=
(all users|users_na|users_arch|users_ns|users_susp|users_ns_susp|users_arch_or_susp|users_na_ns)| (all users|users_na|users_arch|users_ns|users_susp|users_ns_susp|users_arch_or_susp|users_na_ns)|
(all guests|guests_ns|guests_susp|guests_ns_susp)|
(all users_and_guests|users_and_guests_ns|users_and_guests_susp|users_and_guests_ns_susp)|
(user <UserItem>)| (user <UserItem>)|
(users <UserList>)| (users <UserList>)|
(oauthuser) (oauthuser)
@@ -232,6 +256,26 @@ Use these options to select users for GAM commands.
## All non-suspended and suspended Users ## All non-suspended and suspended Users
* `all users_ns_susp` * `all users_ns_susp`
## All non-suspended Guests
* `all guests`
* `all guests_ns`
## All suspended Guests
* `all guests_susp`
## All non-suspended and suspended Guests
* `all guests_ns_susp`
## All non-suspended Users and Guests
* `all users_and_guests`
* `all users_and_guests_ns`
## All suspended Users and Guests
* `all users_and_guests_susp`
## All non-suspended and suspended Users and Guests
* `all users_and_guests_ns_susp`
## A single User ## A single User
* `user <UserItem>` * `user <UserItem>`

View File

@@ -36,13 +36,15 @@ gam version
Updating from Jays GAM to GAM7 may introduce some issues. If your GAM install is critical to your product Google environment you may want to wait for some of the upgrade challenges to be ironed out in the next few weeks. Having said that, GAM stores all of its configuration in the GAM install folder so backing up that folder should preserve your old GAM config. For [upgrade instructions see the wiki](https://github.com/GAM-team/GAM/wiki/How-to-upgrade-from-Standard-GAM). Updating from Jays GAM to GAM7 may introduce some issues. If your GAM install is critical to your product Google environment you may want to wait for some of the upgrade challenges to be ironed out in the next few weeks. Having said that, GAM stores all of its configuration in the GAM install folder so backing up that folder should preserve your old GAM config. For [upgrade instructions see the wiki](https://github.com/GAM-team/GAM/wiki/How-to-upgrade-from-Standard-GAM).
# What does this mean if Im using Ross GAM-ADV? # What does this mean if Im using Ross GAM-ADV?
GAM7 is effectively the same source as GAMADV-XTD3 with minor changes to point to the [github.com/GAM-team/GAM](http://github.com/GAM-team/GAM) site. On Linux/MacOS you should be able to run: GAM7 is effectively the same source as GAMADV-XTD3 with minor changes to point to the [github.com/GAM-team/GAM](http://github.com/GAM-team/GAM) site.
On Linux/MacOS you should be able to run:
``` ```
bash <(curl -s -S -L https://git.io/gam-install) -l bash <(curl -s -S -L https://git.io/gam-install) -l
``` ```
To upgrade. On Windows, download the latest MSI from [git.io/gam-releases](http://git.io/gam-releases) and install it to the same path you had GAM-ADV installed. On Windows, download the latest EXE installer from [git.io/gam-releases](http://git.io/gam-releases), run it and install it to the same path you had GAM-ADV installed.
Both GAM7 and GAM-ADV versions use the same configuration file (gam.cfg), and credentials; they are interchangeable. Both GAM7 and GAM-ADV versions use the same configuration file (gam.cfg), and credentials; they are interchangeable.

View File

@@ -10,6 +10,78 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
See [Downloads-Installs-GAM7](https://github.com/GAM-team/GAM/wiki/Downloads-Installs) for Windows or other options, including manual installation See [Downloads-Installs-GAM7](https://github.com/GAM-team/GAM/wiki/Downloads-Installs) for Windows or other options, including manual installation
### 7.43.00
Updated `gam info user` and `gam print users` to display guest user attributes: `isGuestUser, guestAccountInfo`
Expanded `<UserTypeEntity>` to allow specification of guest users.
* See [Collections of Users](Collections-of-Users)
### 7.42.00
In versions prior to 7.42.00, when `redirect csv <FileName>` was used, GAM did not open and write `<FileName>`
until all processing was complete; if `<FileName>` was not accessible, an error was generated
and no results were saved. Now, `<FileName>` is opened initially to verify accessiblity
and then written when processing is complete.
In the unlikely event that this causes issues, you can do `redirect csv <FileName> delayopen`
to get the previous behavior.
### 7.41.03
Fixed bug in the following:
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
synonyms for `count` and `countrange`.
```
[(any|all):]number<Operator><Number>|
[(any|all):]numberrange!=<Number>/<Number>|
[(any|all):]numberrange=<Number>/<Number>|
```
### 7.41.02
Added option `ownername` to `gam info|print courses` to have GAM display the course owners full name;
there is an extra API call per course to get the name.
Added option `creatorname` to `gam print course-announcements|course-materials|course-works` to have
GAM display the item creators full name; there is an extra API call per course to get the name.
After creating a group, it may be sometime, e.g. 30-45 seconds, before members can
successfully be added to the group even though the API reported that the group was created.
The following options can be used with `gam create group` to verify that the group is actually ready to be updated.
This will be most useful in scripts that are used to create and then populate groups.
```
verifycreationretries <Integer> - Verify group creation, defaults to 0, no verification performed, range 0-20
verifycreationinitialdelay <Integer> - Number of seconds to delay before first verification performed, defaults to 5, range 0-60
verifycreationretrydelay <Integer> - Number of seconds to delay between verificaton retries, defaults to 5, range 1-60
```
If you have a script that deletes a group and then immediately tries to create a new group with the same email address,
you may run into issues. There seems to be a 30-45 second window after the deletion in which a couple
of strange errors can occur on the creation: `Resource not found` and `Duplicate`.
The following options can be used with `gam create group` to handle these errors. This will be most useful
in scripts that are used to delete and then immediately recreate groups.
```
recentdeleteretries <Integer> - Handle group delete/create errors, defaults to 0, no errors handled, range 0-20
recentdeleteretrydelay <Integer> - Number of seconds to delay between retries, defaults to 5, range 1-60
```
Added the following to `<RowValueFilter>` used in CSV input/output row filtering; these are
synonyms for `count` and `countrange`.
```
[(any|all):]number<Operator><Number>|
[(any|all):]numberrange!=<Number>/<Number>|
[(any|all):]numberrange=<Number>/<Number>|
```
### 7.41.01
Fixed bug in `gam print cigroups members managers owners countsonly totalcount internal external` that caused a trap.
### 7.41.00
Upgraded to Python 3.14.4 and OpenSSL 4.0.0.
### 7.40.03 ### 7.40.03
Added option `whocanaddexternalmembers only_owners_can_add_external_members|end_users_can_add_external_members` to `<GroupSettingsAttribute>`. Added option `whocanaddexternalmembers only_owners_can_add_external_members|end_users_can_add_external_members` to `<GroupSettingsAttribute>`.

View File

@@ -7,6 +7,8 @@
- [GUI API Group access type settings mapping](#gui-api-group-access-type-settings-mapping) - [GUI API Group access type settings mapping](#gui-api-group-access-type-settings-mapping)
- [whoCanViewMembership and whoCanDiscoverGroup interactions](#whocanviewmembership-and-whocandiscovergroup-interactions) - [whoCanViewMembership and whoCanDiscoverGroup interactions](#whocanviewmembership-and-whocandiscovergroup-interactions)
- [Manage groups](#manage-groups) - [Manage groups](#manage-groups)
- [Handle group deletion and immediate recreation](#handle-group-deletion-and-immediate-recreation)
- [Verify group creation](#verify-group-creation)
- [Update a group's primary email address](#update-a-groups-primary-email-address) - [Update a group's primary email address](#update-a-groups-primary-email-address)
- [Update a group's settings with JSON data](#update-a-groups-settings-with-json-data) - [Update a group's settings with JSON data](#update-a-groups-settings-with-json-data)
- [Display information about specific groups](#display-information-about-specific-groups) - [Display information about specific groups](#display-information-about-specific-groups)
@@ -350,6 +352,8 @@ These commands allow you to create, update and delete groups.
gam create group <EmailAddress> gam create group <EmailAddress>
[copyfrom <GroupItem>] <GroupAttribute>* [copyfrom <GroupItem>] <GroupAttribute>*
[verifynotinvitable] [verifynotinvitable]
[recentdeleteretries <Integer>] [recentdeleteretrydelay <Integer>]
[verifycreationretries <Integer>] [verifycreationinitialdelay <Integer>] [verifycreationretrydelay <Integer>]
gam update group|groups <GroupEntity> [email <EmailAddress>] gam update group|groups <GroupEntity> [email <EmailAddress>]
[updateprimaryemail <RESearchPattern> <RESubstitution> [preview]] [updateprimaryemail <RESearchPattern> <RESubstitution> [preview]]
[copyfrom <GroupItem>] <GroupAttribute>* [copyfrom <GroupItem>] <GroupAttribute>*
@@ -367,6 +371,28 @@ You can update a group to a security group with the `makesecuritygroup` option.
When deleting and `noactionifalias` is specified, no action is performed if `<GroupEntity>` specifies an alias rather than a primary email address. When deleting and `noactionifalias` is specified, no action is performed if `<GroupEntity>` specifies an alias rather than a primary email address.
## Handle group deletion and immediate recreation
If you have a script that deletes a group and then immediately tries to create a new group with the same email address,
you may run into issues. There seems to be a 30-45 second window after the deletion in which a couple
of strange errors can occur on the creation: `Resource not found` and `Duplicate`.
The following options can be used with `gam create group` to handle these errors. This will be most useful
in scripts that are used to delete and then immediately recreate groups.
```
recentdeleteretries <Integer> - Handle group delete/create errors, defaults to 0, no errors handled, range 0-20
recentdeleteretrydelay <Integer> - Number of seconds to delay between retries, defaults to 5, range 1-60
```
## Verify group creation
After creating a group, it may be sometime, e.g. 30-45 seconds, before members can
successfully be added to the group even though the API reported that the group was created.
The following options can be used with `gam create group` to verify that the group is actually ready to be updated.
This will be most useful in scripts that are used to create and then populate groups.
```
verifycreationretries <Integer> - Verify group creation, defaults to 0, no verification performed, range 0-20
verifycreationinitialdelay <Integer> - Number of seconds to delay before first verification performed, defaults to 5, range 0-60
verifycreationretrydelay <Integer> - Number of seconds to delay between verificaton retries, defaults to 5, range 1-60
```
## Update a group's primary email address ## Update a group's primary email address
You can simply update a group's primary email address with the `email` option. You can simply update a group's primary email address with the `email` option.
``` ```

View File

@@ -251,7 +251,7 @@ writes the credentials into the file oauth2.txt.
``` ```
gamteam@server:/Users/gamteam$ rm -f /Users/gamteam/GAMConfig/oauth2.txt gamteam@server:/Users/gamteam$ rm -f /Users/gamteam/GAMConfig/oauth2.txt
gamteam@server:/Users/gamteam$ gam version gamteam@server:/Users/gamteam$ gam version
GAM 7.40.03 - https://github.com/GAM-team/GAM - pyinstaller GAM 7.43.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com> GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.4 64-bit final Python 3.14.4 64-bit final
macOS Tahoe 26.4.1 arm64 macOS Tahoe 26.4.1 arm64
@@ -1034,7 +1034,7 @@ writes the credentials into the file oauth2.txt.
``` ```
C:\>del C:\GAMConfig\oauth2.txt C:\>del C:\GAMConfig\oauth2.txt
C:\>gam version C:\>gam version
GAM 7.40.03 - https://github.com/GAM-team/GAM - pythonsource GAM 7.43.00 - https://github.com/GAM-team/GAM - pythonsource
GAM Team <google-apps-manager@googlegroups.com> GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.4 64-bit final Python 3.14.4 64-bit final
Windows 11 10.0.26200 AMD64 Windows 11 10.0.26200 AMD64

View File

@@ -119,7 +119,7 @@ You can redirect CSV file output and stdout/stderr output to files. By using red
You can redirect stdout and stderr to null and stderr can be redirected to stdout. You can redirect stdout and stderr to null and stderr can be redirected to stdout.
``` ```
<Redirect> ::= <Redirect> ::=
redirect csv <FileName> [multiprocess] [append] [noheader] [charset <Charset>] redirect csv <FileName> [delayopen] [multiprocess] [append] [noheader] [charset <Charset>]
[columndelimiter <Character>] [quotechar <Character>] [noescapechar [<Boolean>]] [columndelimiter <Character>] [quotechar <Character>] [noescapechar [<Boolean>]]
[sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Boolean>]] [sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Boolean>]]
[todrive <ToDriveAttribute>*] | [todrive <ToDriveAttribute>*] |
@@ -129,12 +129,20 @@ You can redirect stdout and stderr to null and stderr can be redirected to stdou
redirect stderr null [multiprocess] | redirect stderr null [multiprocess] |
redirect stderr stdout [multiprocess] redirect stderr stdout [multiprocess]
``` ```
For `redirect`, the optional subarguments must appear in the order shown. In versions prior to 7.42.00`, the `redirect csv` optional subarguments had to be specified in the order shown.
Now the arguments can be specified in any order except that `todrive <ToDriveAttribute>*` must still be last.
If `<FileName>` specifies a relative path, the file will be put in the directory specified by `drive_dir` in gam.cfg. If `<FileName>` specifies a relative path, the file will be put in the directory specified by `drive_dir` in gam.cfg.
If `<FileName>` specifies an absolute path, the file will be put in the directory specified. If `<FileName>` specifies an absolute path, the file will be put in the directory specified.
Specify `./<FileName>` to put the file in your current working directory. Specify `./<FileName>` to put the file in your current working directory.
In versions prior to 7.42.00, when `redirect csv <FileName>` was used, GAM did not open and write `<FileName>`
until all processing was complete; if `<FileName>` was not accessible, an error was generated and no results were saved.
Now, `<FileName>` is opened initially to verify accessiblity and then written when processing is complete.
In the unlikely event that this causes issues, you can do `redirect csv <FileName> delayopen`
to get the previous behavior.
The `multiprocess` subargument allows the multiple subprocesses started by `gam csv` to write intelligently The `multiprocess` subargument allows the multiple subprocesses started by `gam csv` to write intelligently
to a single redirected CSV/stdout/stderr file. If you don't specify `multiprocess`, each subprocess to a single redirected CSV/stdout/stderr file. If you don't specify `multiprocess`, each subprocess
writes `<FileName>` independently; you end up with a single file written by the last subprocess. writes `<FileName>` independently; you end up with a single file written by the last subprocess.

View File

@@ -132,7 +132,7 @@ See: [Users - Calendars - Transfer](Users-Calendars-Transfer)
GAM7 supports domain shared contacts and user contacts. GAM7 supports domain shared contacts and user contacts.
See: [Domain Shared Contacts](Contacts) See: [Domain Shared Contacts](Domain-SharedContacts)
See: [Users - People - Contacts & Profiles](Users-People-Contacts-Profiles) See: [Users - People - Contacts & Profiles](Users-People-Contacts-Profiles)

View File

@@ -91,8 +91,8 @@ gam <UserItem> show meetconferences
[formatjson] [formatjson]
``` ```
By default, conferences are shown for all of a user's meet spaces. To limit the display use: By default, conferences are shown for all of a user's meet spaces. To limit the display use:
* `space <MeetSpaceName>` - Display conferences for a specifc space by giving its name * `space <MeetSpaceName>` - Display conferences for a specific space by giving its name
* `code <String>` - Display conferences for a specifc space by giving its code * `code <String>` - Display conferences for a specific space by giving its code
By default, Gam displays the information about the meet conferences as an indented list of keys and values. By default, Gam displays the information about the meet conferences as an indented list of keys and values.
* `formatjson` - Display the fields in JSON format. * `formatjson` - Display the fields in JSON format.
@@ -103,8 +103,8 @@ gam <UserItem> print meetconferences [todrive <ToDriveAttribute>*]
[formatjson [quotechar <Character>]] [formatjson [quotechar <Character>]]
``` ```
By default, conferences are shown for all of a user's meet spaces. To limit the display use: By default, conferences are shown for all of a user's meet spaces. To limit the display use:
* `space <MeetSpaceName>` - Display conferences for a specifc space by giving its name * `space <MeetSpaceName>` - Display conferences for a specific space by giving its name
* `code <String>` - Display conferences for a specifc space by giving its code * `code <String>` - Display conferences for a specific space by giving its code
By default, Gam displays the information as columns of fields; the following option causes the output to be in JSON format, 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. * `formatjson` - Display the fields in JSON format.

View File

@@ -3,7 +3,7 @@
Print the current version of Gam with details Print the current version of Gam with details
``` ```
gam version gam version
GAM 7.40.03 - https://github.com/GAM-team/GAM - pyinstaller GAM 7.43.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com> GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.4 64-bit final Python 3.14.4 64-bit final
macOS Tahoe 26.4.1 arm64 macOS Tahoe 26.4.1 arm64
@@ -15,7 +15,7 @@ Time: 2026-02-15T07:51:00-08:00
Print the current version of Gam with details and time offset information Print the current version of Gam with details and time offset information
``` ```
gam version timeoffset gam version timeoffset
GAM 7.40.03 - https://github.com/GAM-team/GAM - pyinstaller GAM 7.43.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com> GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.4 64-bit final Python 3.14.4 64-bit final
macOS Tahoe 26.4.1 arm64 macOS Tahoe 26.4.1 arm64
@@ -27,7 +27,7 @@ Your system time differs from www.googleapis.com by less than 1 second
Print the current version of Gam with extended details and SSL information Print the current version of Gam with extended details and SSL information
``` ```
gam version extended gam version extended
GAM 7.40.03 - https://github.com/GAM-team/GAM - pyinstaller GAM 7.43.00 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com> GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.4 64-bit final Python 3.14.4 64-bit final
macOS Tahoe 26.4.1 arm64 macOS Tahoe 26.4.1 arm64
@@ -35,7 +35,7 @@ Path: /Users/gamteam/bin/gam7
Config File: /Users/gamteam/GamConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com Config File: /Users/gamteam/GamConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
Time: 2026-02-15T07:51:00-08:00 Time: 2026-02-15T07:51:00-08:00
Your system time differs from admin.googleapis.com by less than 1 second Your system time differs from admin.googleapis.com by less than 1 second
OpenSSL 3.6.2 7 Apr 2026 OpenSSL 4.0.0 14 Apr 2026
arrow 1.4.0 arrow 1.4.0
chardet 5.2.0 chardet 5.2.0
cryptography 46.0.5 cryptography 46.0.5
@@ -68,7 +68,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/gamteam/bin/gam7 Path: /Users/gamteam/bin/gam7
Version Check: Version Check:
Current: 5.35.08 Current: 5.35.08
Latest: 7.40.03 Latest: 7.43.00
echo $? echo $?
1 1
``` ```
@@ -76,7 +76,7 @@ echo $?
Print the current version number without details Print the current version number without details
``` ```
gam version simple gam version simple
7.40.03 7.43.00
``` ```
In Linux/MacOS you can do: In Linux/MacOS you can do:
``` ```
@@ -86,7 +86,7 @@ echo $VER
Print the current version of Gam and address of this Wiki Print the current version of Gam and address of this Wiki
``` ```
gam help gam help
GAM 7.40.03 - https://github.com/GAM-team/GAM GAM 7.43.00 - https://github.com/GAM-team/GAM
GAM Team <google-apps-manager@googlegroups.com> GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.4 64-bit final Python 3.14.4 64-bit final
macOS Tahoe 26.4.1 arm64 macOS Tahoe 26.4.1 arm64