mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-05 13:21:35 +00:00
fix test, remove more gdata refs
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -947,7 +947,7 @@ jobs:
|
|||||||
run_gam config enable_dasa false save
|
run_gam config enable_dasa false save
|
||||||
# don't expose policy output
|
# don't expose policy output
|
||||||
run_gam show policies > policies.csv
|
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 user $gam_user print peoplecontacts
|
||||||
run_gam print privileges
|
run_gam print privileges
|
||||||
run_gam config enable_dasa true save
|
run_gam config enable_dasa true save
|
||||||
|
|||||||
276
developer-guide-rearchitecture.md
Normal file
276
developer-guide-rearchitecture.md
Normal 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.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""GAM API utility functions.
|
"""GAM API utility functions.
|
||||||
|
|
||||||
HTTP transport, OAuth credential management, Google API service
|
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
|
import http.client
|
||||||
|
|||||||
@@ -1079,7 +1079,7 @@ def _finalizeConfig(status, sectionName):
|
|||||||
else:
|
else:
|
||||||
GM.Globals[GM.CACHE_DIR] = GC.Values[GC.CACHE_DIR]
|
GM.Globals[GM.CACHE_DIR] = GC.Values[GC.CACHE_DIR]
|
||||||
GM.Globals[GM.CACHE_DISCOVERY_ONLY] = GC.Values[GC.CACHE_DISCOVERY_ONLY]
|
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['REQUESTS_CA_BUNDLE'] = GC.Values[GC.CACERTS_PEM]
|
||||||
os.environ['DEFAULT_CA_BUNDLE_PATH'] = 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]
|
os.environ['HTTPLIB2_CA_CERTS'] = GC.Values[GC.CACERTS_PEM]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from gam.var import Act, Cmd, Ent, Ind
|
|||||||
from gam.constants import (
|
from gam.constants import (
|
||||||
ACTION_FAILED_RC, CLIENT_SECRETS_JSON_REQUIRED_RC,
|
ACTION_FAILED_RC, CLIENT_SECRETS_JSON_REQUIRED_RC,
|
||||||
ENTITY_DOES_NOT_EXIST_RC, ENTITY_IS_NOT_UNIQUE_RC,
|
ENTITY_DOES_NOT_EXIST_RC, ENTITY_IS_NOT_UNIQUE_RC,
|
||||||
|
FN_GAMCOMMANDS_TXT, GAM_WIKI,
|
||||||
INVALID_JSON_RC, OAUTH2_TXT_REQUIRED_RC,
|
INVALID_JSON_RC, OAUTH2_TXT_REQUIRED_RC,
|
||||||
OAUTH2SERVICE_JSON_REQUIRED_RC, USAGE_ERROR_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):
|
def usageErrorExit(message, extraneous=False):
|
||||||
writeStderr(Cmd.CommandLineWithBadArgumentMarked(extraneous))
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(extraneous))
|
||||||
stderrErrorMsg(message)
|
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_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
|
||||||
writeStderr(Msg.HELP_WIKI.format(GAM_WIKI))
|
writeStderr(Msg.HELP_WIKI.format(GAM_WIKI))
|
||||||
sys.exit(USAGE_ERROR_RC)
|
sys.exit(USAGE_ERROR_RC)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class TestAllModulesImport:
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
if stripped.startswith('import ') and not stripped.startswith(
|
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')):
|
'import arrow', 'import distro', 'import termios')):
|
||||||
stdlib_imports.append(stripped)
|
stdlib_imports.append(stripped)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user