fix test, remove more gdata refs

This commit is contained in:
Jay Lee
2026-07-04 18:15:32 -04:00
parent c5615f9935
commit 2c6bb93d22
6 changed files with 281 additions and 6 deletions

View File

@@ -947,7 +947,7 @@ jobs:
run_gam config enable_dasa false save
# don't expose policy output
run_gam show policies > policies.csv
run_gam user $gam_user create peoplecontact names givenname GHA familyname "$JID" emailaddresses work "${newbase}@example.com" primary
run_gam user $gam_user create peoplecontact givenname GHA familyname "$JID" email work "${newbase}@example.com" primary
run_gam user $gam_user print peoplecontacts
run_gam print privileges
run_gam config enable_dasa true save

View File

@@ -0,0 +1,276 @@
# GAM Codebase Rearchitecture — Developer Guide
## 1. Goals, Benefits & What Changes for Users
### What Changed for Users
**Nothing.** GAM behaves identically. Every command, flag, and output format is unchanged. Performance is the same or slightly better. This was a purely internal restructuring.
### Why We Did This
`gam/__init__.py` was a **72,735-line monolith** — a single file containing virtually all of GAM's logic. Every Google Workspace API surface (Drive, Gmail, Calendar, Classroom, Groups, Vault, Chat, ChromeOS, etc.) lived in one file. This created real problems:
- **Navigating the codebase was painful.** Finding where a command was implemented meant scrolling through 73K lines or relying on grep. No IDE could provide useful file-level structure because everything was in one file.
- **Merge conflicts were constant.** Any two developers touching different features (say, Drive and Gmail) would conflict because they were editing the same file.
- **Understanding scope was impossible.** Which constants belong to Drive? Which helper functions are shared vs. specific to Gmail? When everything is in one namespace, you can't tell.
- **Code review was difficult.** A PR that modifies "Drive label permissions" shouldn't require a reviewer to open a 73K-line file and locate the relevant section.
### What the New Layout Achieves
| Before | After |
|--------|-------|
| 1 file, 72,735 lines | 95 files, all under 100KB |
| `gam/__init__.py` does everything | `gam/__init__.py` is a thin hub (~4,660 lines) |
| Find a function: scroll or grep the monolith | Find a function: look in the obvious module |
| Merge conflicts on every PR | Parallel work on Drive, Gmail, etc. with no conflicts |
**Concrete benefits for developers:**
1. **Find things fast.** Working on Gmail labels? Open `gam/cmd/gmail/labels.py`. Working on shared drive permissions? Open `gam/cmd/drive/shareddrives.py`. The file name tells you what's inside.
2. **Smaller blast radius.** A change to Drive copy/move logic only touches `gam/cmd/drive/copymove/`. Reviewers can focus on the relevant 50-90KB file instead of a 73K-line monolith.
3. **Better IDE support.** File outlines, go-to-definition, and symbol search all work better with reasonably-sized files. Your IDE's file explorer now shows a meaningful tree.
4. **Easier onboarding.** A new contributor can understand the codebase structure by looking at the directory tree. "Oh, Chat commands are in `cmd/chat/`, Drive stuff is in `cmd/drive/`" — it's self-documenting.
5. **No performance cost.** `gam print users` runs in ~0.39s, identical to before. Python's import system caches modules, so the split adds negligible startup overhead.
---
## 2. Where Did Everything Go?
### The New Layout
```
src/gam/
├── __init__.py # Hub: re-exports, dispatch tables, core infra (~4,660 lines)
├── __main__.py # Entry point
├── cmd/ # ALL command implementations live here
│ ├── __init__.py
│ │
│ │── admin.py # Admin roles & privileges
│ │── alerts.py # Alert Center
│ │── aliases.py # User/group aliases
│ │── analytics.py # Google Analytics
│ │── audit.py # Email Audit monitors (removed — stub)
│ │── browsers.py # Chrome browser management
│ │── caa.py # Context-Aware Access levels
│ │── calendar.py # Calendar ACLs, events, settings
│ │── chromeapps.py # Chrome app management
│ │── chromepolicies.py # Chrome policy management
│ │── cidevices.py # Cloud Identity devices
│ │── ciuserinvitations.py # Cloud Identity user invitations
│ │── cloudstorage.py # Cloud Storage management
│ │── contacts.py # Domain Shared Contacts (removed — stub) + People API utilities
│ │── cros.py # ChromeOS device management
│ │── customer.py # Customer/domain settings
│ │── datatransfer.py # Data transfer management
│ │── delegates.py # Domain-wide delegation
│ │── domains.py # Domain management
│ │── licenses.py # License management (domain-level)
│ │── meet.py # Google Meet
│ │── mobile.py # Mobile device management
│ │── notes.py # Google Keep notes
│ │── oauth.py # OAuth credential management
│ │── orgunits.py # Organizational units
│ │── people.py # People API / contacts
│ │── printers.py # Printer management
│ │── project.py # API project management
│ │── reports.py # Admin reports
│ │── reseller.py # Reseller operations
│ │── resources.py # Calendar resources, buildings, features
│ │── schemas.py # User schema management
│ │── send_email.py # Send email commands
│ │── sites.py # Google Sites
│ │── sso.py # Inbound SSO profiles & credentials
│ │── tasks.py # Google Tasks & Tag Manager
│ │── userservices.py # ASPs, backup codes, calendars, working locations
│ │
│ │── chat/ # Google Chat (sub-package)
│ │ ├── __init__.py # Re-exports from sub-modules
│ │ ├── setup.py # Chat setup, emoji, sections
│ │ ├── spaces.py # Chat space CRUD
│ │ └── members.py # Members, messages, reactions
│ │
│ │── cigroups/ # Cloud Identity Groups (sub-package)
│ │ ├── __init__.py
│ │ ├── groups.py # CI group CRUD
│ │ └── members.py # CI group members
│ │
│ │── courses/ # Google Classroom (sub-package)
│ │ ├── __init__.py
│ │ ├── courses.py # Course CRUD, info, listing
│ │ ├── content.py # Announcements, topics, materials, work
│ │ ├── participants.py # Student/teacher add/remove/sync
│ │ └── guardians.py # Guardian invitations
│ │
│ │── drive/ # Google Drive (sub-package)
│ │ ├── __init__.py # Re-exports all drive symbols
│ │ ├── core.py # Search, entities, file attributes, MIME types
│ │ ├── activity.py # Drive activity reporting, settings
│ │ ├── filepaths.py # Path resolution, field mapping
│ │ ├── revisions.py # File revision management
│ │ ├── filetree.py # File tree building, permission matching
│ │ ├── filelist.py # File listing & printing
│ │ ├── fileinfo.py # File counts, comments, disk usage
│ │ ├── files.py # File create/update/shortcuts
│ │ ├── permissions.py # File ACLs & permissions
│ │ ├── labels.py # Drive labels & label permissions
│ │ ├── shareddrives.py # Shared drive management
│ │ ├── looker.py # Looker Studio assets
│ │ ├── copymove/ # Copy & move (nested sub-package)
│ │ │ ├── __init__.py
│ │ │ ├── copymove_util.py # Statistics, options, copy logic
│ │ │ └── copymove_move.py # Move logic, permission updates
│ │ └── transfer/ # Transfer & ownership (nested sub-package)
│ │ ├── __init__.py
│ │ ├── fileops.py # Delete, trash, download, documents
│ │ └── ownership.py # Transfer drive, claim ownership
│ │
│ │── gmail/ # Gmail (sub-package)
│ │ ├── __init__.py
│ │ ├── profile.py # Gmail profile, watch
│ │ ├── labels.py # Label CRUD
│ │ ├── messages.py # Messages, threads, drafts, export, forward
│ │ ├── delegates.py # Mail delegation
│ │ ├── filters.py # Mail filters
│ │ ├── forms.py # Google Forms
│ │ ├── settings.py # Forwarding, IMAP, POP, language, SendAs
│ │ ├── smime.py # S/MIME certificates
│ │ ├── cse.py # Client-side encryption
│ │ └── signature.py # Signatures & vacation responders
│ │
│ │── groups/ # Google Groups (sub-package)
│ │ ├── __init__.py
│ │ ├── groups.py # Group CRUD, settings, info
│ │ └── members.py # Member management, display, sync
│ │
│ │── userop/ # Per-user operations (sub-package)
│ │ ├── __init__.py
│ │ ├── usergroups.py # User group membership, Looker Studio
│ │ ├── licenses.py # Per-user license management
│ │ ├── photos.py # User photos & profile
│ │ ├── sheets.py # Google Sheets operations
│ │ └── tokens.py # OAuth tokens & deprovisioning
│ │
│ │── users/ # User management (sub-package)
│ │ ├── __init__.py
│ │ ├── manage.py # User CRUD, attributes, schemas
│ │ └── display.py # User info & print/show
│ │
│ └── vault/ # Google Vault (sub-package)
│ ├── __init__.py
│ ├── matters.py # Vault matters & exports
│ └── holds.py # Vault holds, queries, counts
├── gamlib/ # GAM library: config, constants, API definitions
├── gapi/ # Google API wrappers
└── util/ # Utilities: CSV, batch, args, entity, api
```
### What Stayed in `__init__.py`
The hub file (`~4,660 lines`) still contains:
| Section | Purpose |
|---------|---------|
| **Imports** (~300 lines) | Standard library + GAM internal imports |
| **Re-export blocks** (~2,300 lines) | `from gam.cmd.X import (...)` — backward compatibility |
| **Core infrastructure** (~500 lines) | Error handling, entity operations, output formatting |
| **Dispatch tables** (~800 lines) | `MAIN_COMMANDS`, `MAIN_COMMANDS_WITH_OBJECTS` — maps command names to handler functions |
| **Command processing** (~700 lines) | `ProcessGAMCommand()`, batch/CSV handling, resource command routing |
The re-export blocks ensure that any code doing `gam.copyDriveFile()` or `from gam import copyDriveFile` continues to work without changes.
### How to Find a Function
**Method 1: Intuition.** The module names are descriptive. If you're looking for Gmail label code, check `gam/cmd/gmail/labels.py`. If you want Drive shared drive logic, check `gam/cmd/drive/shareddrives.py`.
**Method 2: grep.**
```bash
# Find where a function is defined
grep -rn "def copyDriveFile" src/gam/cmd/
# Result: src/gam/cmd/drive/copymove/copymove_util.py:947
```
**Method 3: Check the dispatch table.** If you know the GAM command name, search the dispatch tables in `__init__.py`:
```bash
grep "copyDriveFile\|copy.*drivefile" src/gam/__init__.py
# Shows: Cmd.ARG_DRIVEFILE: copyDriveFile,
# Then grep for the import to find its module:
# from gam.cmd.drive import ( ... copyDriveFile, ... )
```
**Method 4: Follow the re-export chain.** Every function is re-exported from `__init__.py`. Search for the name there to see which `cmd/` module it comes from:
```bash
grep -B50 "copyDriveFile," src/gam/__init__.py | grep "^from"
# Result: from gam.cmd.drive import (
# Then check drive/__init__.py to find the sub-module:
grep -B20 "copyDriveFile," src/gam/cmd/drive/__init__.py | grep "^from"
# Result: from gam.cmd.drive.copymove import (
# And finally:
grep -B20 "copyDriveFile," src/gam/cmd/drive/copymove/__init__.py | grep "^from"
# Result: from gam.cmd.drive.copymove.copymove_util import (
```
**Method 5: Your IDE.** Open `__init__.py`, Ctrl/Cmd-click on any function name, and your IDE will jump to the definition in the correct `cmd/` sub-module.
### The Module Pattern
Every extracted module follows the same pattern:
```python
"""Brief description of what this module handles."""
import sys
from gamlib import glaction
from gamlib import glapi as API
from gamlib import glcfg as GC
from gamlib import glclargs
from gamlib import glentity
from gamlib import glindent
# ... other imports as needed
Act = glaction.GamAction()
Ent = glentity.GamEntity()
Ind = glindent.GamIndent()
Cmd = glclargs.GamCLArgs()
def _getMain():
return sys.modules['gam']
```
Key points:
- **`_getMain()`** provides lazy runtime access to the `gam` module (i.e., `__init__.py`). This avoids circular imports — modules can't directly `import gam` during initialization because `gam` is still loading. Instead, they call `_getMain()` at runtime when the function is actually executed. Any function defined in `__init__.py` or re-exported through it is accessible as `_getMain().functionName()`.
- **`Act`, `Ent`, `Ind`, `Cmd`** are local instances of the standard GAM helper classes. These four classes (`GamAction`, `GamEntity`, `GamIndent`, `GamCLArgs`) use **class-level shared state**, meaning all instances across all modules see the same values. When `__init__.py` calls `Act.Set(Act.CREATE)` before dispatching to a command handler, every module's `Act` instance immediately reflects that action. This was necessary because the original monolith had a single instance of each; the refactoring created multiple instances that must stay synchronized.
- **Module-level constants** that were originally in `__init__.py` (like `MIMETYPE_GA_FOLDER`, `ME_IN_OWNERS`, `UNKNOWN`, `UTF8`, etc.) are duplicated into the modules that need them. This avoids import-time dependencies on the not-yet-loaded hub.
- **Cross-module function calls** use `_getMain()` for GAM-internal functions (e.g., `_getMain().entityUnknownWarning(...)`) rather than direct imports, which would cause circular import errors during module loading. Standard library and third-party imports (e.g., `from passlib.hash import sha512_crypt`) are safe to import directly at module level.
---
## 3. What Happened to GData (Audit and Domain Shared Contacts)?
GAM previously used two legacy Google APIs built on the GData XML protocol:
1. **Email Audit API**`gam audit monitor create/delete/list`
2. **Domain Shared Contacts API**`gam create/update/delete/print contacts`
Both APIs, along with the vendored `gdata/` and `atom/` libraries (~10,000 lines), have been **completely removed**.
The affected commands now print a deprecation notice:
```
ERROR: GAM no longer supports the legacy <API> API and this command.
If you must use this API you can install a copy of GAM 7.46.07
which is the last version to support this command.
```
`contacts.py` still contains the People API utilities (shared constants, `PeopleManager`, etc.) used by `people.py` for modern user contact management.

View File

@@ -1,7 +1,7 @@
"""GAM API utility functions.
HTTP transport, OAuth credential management, Google API service
construction, and GAPI/GData call wrappers with retry logic.
construction, and GAPI call wrappers with retry logic.
"""
import http.client

View File

@@ -1079,7 +1079,7 @@ def _finalizeConfig(status, sectionName):
else:
GM.Globals[GM.CACHE_DIR] = GC.Values[GC.CACHE_DIR]
GM.Globals[GM.CACHE_DISCOVERY_ONLY] = GC.Values[GC.CACHE_DISCOVERY_ONLY]
# Set environment variables so GData API can find cacerts.pem
# Set environment variables so HTTP libraries can find cacerts.pem
os.environ['REQUESTS_CA_BUNDLE'] = GC.Values[GC.CACERTS_PEM]
os.environ['DEFAULT_CA_BUNDLE_PATH'] = GC.Values[GC.CACERTS_PEM]
os.environ['HTTPLIB2_CA_CERTS'] = GC.Values[GC.CACERTS_PEM]

View File

@@ -12,6 +12,7 @@ from gam.var import Act, Cmd, Ent, Ind
from gam.constants import (
ACTION_FAILED_RC, CLIENT_SECRETS_JSON_REQUIRED_RC,
ENTITY_DOES_NOT_EXIST_RC, ENTITY_IS_NOT_UNIQUE_RC,
FN_GAMCOMMANDS_TXT, GAM_WIKI,
INVALID_JSON_RC, OAUTH2_TXT_REQUIRED_RC,
OAUTH2SERVICE_JSON_REQUIRED_RC, USAGE_ERROR_RC,
)
@@ -83,8 +84,6 @@ def entityIsNotUniqueExit(entityType, entityName, valueType, valueList, i=0, cou
def usageErrorExit(message, extraneous=False):
writeStderr(Cmd.CommandLineWithBadArgumentMarked(extraneous))
stderrErrorMsg(message)
FN_GAMCOMMANDS_TXT = _getConst('FN_GAMCOMMANDS_TXT')
GAM_WIKI = _getConst('GAM_WIKI')
writeStderr(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
writeStderr(Msg.HELP_WIKI.format(GAM_WIKI))
sys.exit(USAGE_ERROR_RC)

View File

@@ -70,7 +70,7 @@ class TestAllModulesImport:
for line in lines:
stripped = line.strip()
if stripped.startswith('import ') and not stripped.startswith(
('import gam', 'import gdata', 'import google', 'import httplib2',
('import gam', 'import google', 'import httplib2',
'import arrow', 'import distro', 'import termios')):
stdlib_imports.append(stripped)