algo-cli-runtime 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- ollama_cli/__init__.py +67 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""Google Workspace REST client.
|
|
2
|
+
|
|
3
|
+
Read/write access to Drive, Docs, Sheets, and Calendar plus Gmail read/draft
|
|
4
|
+
access over HTTPS using only the standard library (urllib.request /
|
|
5
|
+
urllib.parse). Mirrors the zero deps posture of google_workspace_auth.py.
|
|
6
|
+
|
|
7
|
+
The client auto-refreshes access tokens via google_workspace_auth and raises
|
|
8
|
+
GoogleWorkspaceError on non-2xx responses. Methods return parsed JSON dicts
|
|
9
|
+
or raw bytes for media downloads; they never write to disk.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
from email.message import EmailMessage
|
|
16
|
+
from email.utils import formatdate
|
|
17
|
+
import json
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import urllib.request
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from . import google_workspace_auth as auth
|
|
24
|
+
|
|
25
|
+
API_BASE = "https://www.googleapis.com"
|
|
26
|
+
DRIVE_BASE = f"{API_BASE}/drive/v3"
|
|
27
|
+
DOCS_BASE = f"{API_BASE}/documents/v1"
|
|
28
|
+
SHEETS_BASE = f"{API_BASE}/spreadsheets/v4"
|
|
29
|
+
CALENDAR_BASE = f"{API_BASE}/calendar/v3"
|
|
30
|
+
GMAIL_BASE = f"{API_BASE}/gmail/v1"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GoogleWorkspaceError(RuntimeError):
|
|
34
|
+
"""Raised on a non-2xx Google API response."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, status: int, detail: str, *, body: dict[str, Any] | None = None):
|
|
37
|
+
super().__init__(f"Google API HTTP {status}: {detail}")
|
|
38
|
+
self.status = status
|
|
39
|
+
self.detail = detail
|
|
40
|
+
self.body = body or {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Client
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GoogleWorkspaceClient:
|
|
49
|
+
"""Thin urllib wrapper around Google Workspace REST APIs."""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.last_user_info: dict[str, Any] | None = None
|
|
53
|
+
|
|
54
|
+
# -- HTTP plumbing ----------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def _request(
|
|
57
|
+
self,
|
|
58
|
+
method: str,
|
|
59
|
+
url: str,
|
|
60
|
+
*,
|
|
61
|
+
params: dict[str, Any] | None = None,
|
|
62
|
+
json_body: Any = None,
|
|
63
|
+
raw_body: bytes | None = None,
|
|
64
|
+
headers: dict[str, str] | None = None,
|
|
65
|
+
accept: str = "application/json",
|
|
66
|
+
timeout: float = 30.0,
|
|
67
|
+
) -> tuple[int, bytes, dict[str, str]]:
|
|
68
|
+
if params:
|
|
69
|
+
# Drop empty values so we don't pollute URLs with `key=`.
|
|
70
|
+
cleaned = {k: v for k, v in params.items() if v not in (None, "")}
|
|
71
|
+
if cleaned:
|
|
72
|
+
url = f"{url}?{urllib.parse.urlencode(cleaned, doseq=True)}"
|
|
73
|
+
token = auth.get_valid_token()
|
|
74
|
+
if not token:
|
|
75
|
+
raise GoogleWorkspaceError(
|
|
76
|
+
401,
|
|
77
|
+
"Not authenticated. Run /google-login to start the OAuth flow.",
|
|
78
|
+
)
|
|
79
|
+
req_headers: dict[str, str] = {
|
|
80
|
+
"Authorization": f"Bearer {token}",
|
|
81
|
+
"Accept": accept,
|
|
82
|
+
}
|
|
83
|
+
if headers:
|
|
84
|
+
req_headers.update(headers)
|
|
85
|
+
data: bytes | None = None
|
|
86
|
+
if json_body is not None:
|
|
87
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
88
|
+
req_headers.setdefault("Content-Type", "application/json; charset=utf-8")
|
|
89
|
+
elif raw_body is not None:
|
|
90
|
+
data = raw_body
|
|
91
|
+
request = urllib.request.Request(url, data=data, method=method, headers=req_headers)
|
|
92
|
+
try:
|
|
93
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
94
|
+
return response.status, response.read(), dict(response.headers.items())
|
|
95
|
+
except urllib.error.HTTPError as exc:
|
|
96
|
+
body_bytes = exc.read() or b""
|
|
97
|
+
try:
|
|
98
|
+
body_obj = json.loads(body_bytes.decode("utf-8") or "{}")
|
|
99
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
100
|
+
body_obj = None
|
|
101
|
+
detail = body_obj.get("error", {}).get("message") if isinstance(body_obj, dict) else None
|
|
102
|
+
raise GoogleWorkspaceError(
|
|
103
|
+
exc.code,
|
|
104
|
+
detail or body_bytes.decode("utf-8", errors="replace") or exc.reason,
|
|
105
|
+
body=body_obj if isinstance(body_obj, dict) else None,
|
|
106
|
+
) from exc
|
|
107
|
+
except urllib.error.URLError as exc:
|
|
108
|
+
raise GoogleWorkspaceError(0, f"Network error: {exc.reason}") from exc
|
|
109
|
+
|
|
110
|
+
def _get_json(
|
|
111
|
+
self,
|
|
112
|
+
url: str,
|
|
113
|
+
*,
|
|
114
|
+
params: dict[str, Any] | None = None,
|
|
115
|
+
timeout: float = 30.0,
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
status, body, _ = self._request("GET", url, params=params, timeout=timeout)
|
|
118
|
+
if not (200 <= status < 300):
|
|
119
|
+
detail = body.decode("utf-8", errors="replace")
|
|
120
|
+
raise GoogleWorkspaceError(status, detail)
|
|
121
|
+
if not body:
|
|
122
|
+
return {}
|
|
123
|
+
try:
|
|
124
|
+
return json.loads(body.decode("utf-8"))
|
|
125
|
+
except json.JSONDecodeError as exc:
|
|
126
|
+
raise GoogleWorkspaceError(status, f"Non-JSON response: {body!r}") from exc
|
|
127
|
+
|
|
128
|
+
def _get_bytes(
|
|
129
|
+
self,
|
|
130
|
+
url: str,
|
|
131
|
+
*,
|
|
132
|
+
params: dict[str, Any] | None = None,
|
|
133
|
+
accept: str = "*/*",
|
|
134
|
+
timeout: float = 60.0,
|
|
135
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
136
|
+
status, body, headers = self._request(
|
|
137
|
+
"GET", url, params=params, accept=accept, timeout=timeout
|
|
138
|
+
)
|
|
139
|
+
if not (200 <= status < 300):
|
|
140
|
+
detail = body.decode("utf-8", errors="replace")
|
|
141
|
+
raise GoogleWorkspaceError(status, detail)
|
|
142
|
+
return body, headers
|
|
143
|
+
|
|
144
|
+
# -- Identity ---------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def user_info(self) -> dict[str, Any]:
|
|
147
|
+
info = self._get_json(auth.GOOGLE_USERINFO_URL)
|
|
148
|
+
self.last_user_info = info
|
|
149
|
+
return info
|
|
150
|
+
|
|
151
|
+
# -- Drive ------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def drive_list(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
query: str | None = None,
|
|
157
|
+
page_size: int = 20,
|
|
158
|
+
fields: str = "files(id,name,mimeType,size,modifiedTime,webViewLink),nextPageToken",
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
params: dict[str, Any] = {
|
|
161
|
+
"pageSize": max(1, min(int(page_size), 1000)),
|
|
162
|
+
"fields": fields,
|
|
163
|
+
}
|
|
164
|
+
if query:
|
|
165
|
+
params["q"] = query
|
|
166
|
+
return self._get_json(f"{DRIVE_BASE}/files", params=params)
|
|
167
|
+
|
|
168
|
+
def drive_get(
|
|
169
|
+
self,
|
|
170
|
+
file_id: str,
|
|
171
|
+
*,
|
|
172
|
+
fields: str = "id,name,mimeType,size,modifiedTime,createdTime,webViewLink,owners,parents,description",
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
return self._get_json(
|
|
175
|
+
f"{DRIVE_BASE}/files/{urllib.parse.quote(file_id, safe='')}",
|
|
176
|
+
params={"fields": fields, "supportsAllDrives": "true"},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def drive_search(
|
|
180
|
+
self,
|
|
181
|
+
name_contains: str,
|
|
182
|
+
*,
|
|
183
|
+
mime_type: str | None = None,
|
|
184
|
+
page_size: int = 20,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
# Escape single quotes for the Drive query language.
|
|
187
|
+
safe = name_contains.replace("\\", "\\\\").replace("'", "\\'")
|
|
188
|
+
clauses = [f"name contains '{safe}'", "trashed = false"]
|
|
189
|
+
if mime_type:
|
|
190
|
+
clauses.append(f"mimeType = '{mime_type}'")
|
|
191
|
+
return self.drive_list(query=" and ".join(clauses), page_size=page_size)
|
|
192
|
+
|
|
193
|
+
def drive_download(self, file_id: str) -> tuple[bytes, dict[str, str]]:
|
|
194
|
+
return self._get_bytes(
|
|
195
|
+
f"{DRIVE_BASE}/files/{urllib.parse.quote(file_id, safe='')}",
|
|
196
|
+
params={"alt": "media", "supportsAllDrives": "true"},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def drive_export(
|
|
200
|
+
self,
|
|
201
|
+
file_id: str,
|
|
202
|
+
*,
|
|
203
|
+
mime_type: str = "text/plain",
|
|
204
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
205
|
+
return self._get_bytes(
|
|
206
|
+
f"{DRIVE_BASE}/files/{urllib.parse.quote(file_id, safe='')}/export",
|
|
207
|
+
params={"mimeType": mime_type},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# -- Docs -------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def docs_get(self, document_id: str) -> dict[str, Any]:
|
|
213
|
+
return self._get_json(
|
|
214
|
+
f"{DOCS_BASE}/documents/{urllib.parse.quote(document_id, safe='')}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def docs_to_plain_text(self, document: dict[str, Any]) -> str:
|
|
218
|
+
"""Flatten a docs.get response into a plain-text body for display."""
|
|
219
|
+
body = document.get("body") or {}
|
|
220
|
+
chunks: list[str] = []
|
|
221
|
+
|
|
222
|
+
def walk(element: dict[str, Any]) -> None:
|
|
223
|
+
for child in element.get("content", []) or []:
|
|
224
|
+
kind = child.get("type")
|
|
225
|
+
if kind == "paragraph":
|
|
226
|
+
walk(child)
|
|
227
|
+
chunks.append("")
|
|
228
|
+
elif kind == "table":
|
|
229
|
+
for row in child.get("tableRows", []) or []:
|
|
230
|
+
for cell in row.get("tableCells", []) or []:
|
|
231
|
+
walk(cell)
|
|
232
|
+
chunks.append("")
|
|
233
|
+
elif kind == "textRun":
|
|
234
|
+
content = (child.get("textRun") or {}).get("content", "")
|
|
235
|
+
if content:
|
|
236
|
+
chunks.append(content)
|
|
237
|
+
|
|
238
|
+
walk(body)
|
|
239
|
+
return "\n".join(chunks).rstrip() + "\n"
|
|
240
|
+
|
|
241
|
+
# -- Sheets -----------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def sheets_values_get(
|
|
244
|
+
self,
|
|
245
|
+
spreadsheet_id: str,
|
|
246
|
+
range_a1: str,
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
return self._get_json(
|
|
249
|
+
f"{SHEETS_BASE}/spreadsheets/{urllib.parse.quote(spreadsheet_id, safe='')}"
|
|
250
|
+
f"/values/{urllib.parse.quote(range_a1, safe='')}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# -- Calendar ---------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def calendar_events_list(
|
|
256
|
+
self,
|
|
257
|
+
*,
|
|
258
|
+
time_min: str | None = None,
|
|
259
|
+
time_max: str | None = None,
|
|
260
|
+
max_results: int = 20,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
params: dict[str, Any] = {
|
|
263
|
+
"maxResults": max(1, min(int(max_results), 250)),
|
|
264
|
+
"singleEvents": "true",
|
|
265
|
+
"orderBy": "startTime",
|
|
266
|
+
}
|
|
267
|
+
if time_min:
|
|
268
|
+
params["timeMin"] = time_min
|
|
269
|
+
if time_max:
|
|
270
|
+
params["timeMax"] = time_max
|
|
271
|
+
return self._get_json(f"{CALENDAR_BASE}/calendars/primary/events", params=params)
|
|
272
|
+
|
|
273
|
+
# -- Gmail ------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
def gmail_list(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
query: str | None = None,
|
|
279
|
+
max_results: int = 20,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
params: dict[str, Any] = {"maxResults": max(1, min(int(max_results), 500))}
|
|
282
|
+
if query:
|
|
283
|
+
params["q"] = query
|
|
284
|
+
return self._get_json(f"{GMAIL_BASE}/users/me/messages", params=params)
|
|
285
|
+
|
|
286
|
+
def gmail_get(
|
|
287
|
+
self,
|
|
288
|
+
message_id: str,
|
|
289
|
+
*,
|
|
290
|
+
fmt: str = "metadata",
|
|
291
|
+
) -> dict[str, Any]:
|
|
292
|
+
allowed = {"metadata", "full", "raw", "minimal"}
|
|
293
|
+
if fmt not in allowed:
|
|
294
|
+
raise ValueError(f"fmt must be one of {sorted(allowed)}")
|
|
295
|
+
return self._get_json(
|
|
296
|
+
f"{GMAIL_BASE}/users/me/messages/{urllib.parse.quote(message_id, safe='')}",
|
|
297
|
+
params={"format": fmt},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def gmail_create_draft(
|
|
301
|
+
self,
|
|
302
|
+
*,
|
|
303
|
+
to: str,
|
|
304
|
+
subject: str,
|
|
305
|
+
html_body: str | None = None,
|
|
306
|
+
text_body: str | None = None,
|
|
307
|
+
cc: str | None = None,
|
|
308
|
+
bcc: str | None = None,
|
|
309
|
+
) -> dict[str, Any]:
|
|
310
|
+
"""Create a Gmail draft. Does not send mail."""
|
|
311
|
+
if not to.strip():
|
|
312
|
+
raise ValueError("to is required")
|
|
313
|
+
if not subject.strip():
|
|
314
|
+
raise ValueError("subject is required")
|
|
315
|
+
body_text = text_body or ""
|
|
316
|
+
body_html = html_body or ""
|
|
317
|
+
if not body_text and not body_html:
|
|
318
|
+
raise ValueError("html_body or text_body is required")
|
|
319
|
+
|
|
320
|
+
msg = EmailMessage()
|
|
321
|
+
msg["To"] = to
|
|
322
|
+
msg["From"] = "me"
|
|
323
|
+
msg["Subject"] = subject
|
|
324
|
+
msg["Date"] = formatdate(localtime=True)
|
|
325
|
+
if cc:
|
|
326
|
+
msg["Cc"] = cc
|
|
327
|
+
if bcc:
|
|
328
|
+
msg["Bcc"] = bcc
|
|
329
|
+
if body_html:
|
|
330
|
+
msg.set_content(body_text or "This email contains an HTML body.")
|
|
331
|
+
msg.add_alternative(body_html, subtype="html")
|
|
332
|
+
else:
|
|
333
|
+
msg.set_content(body_text)
|
|
334
|
+
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii")
|
|
335
|
+
status, body, _headers = self._request(
|
|
336
|
+
"POST",
|
|
337
|
+
f"{GMAIL_BASE}/users/me/drafts",
|
|
338
|
+
json_body={"message": {"raw": raw}},
|
|
339
|
+
timeout=60.0,
|
|
340
|
+
)
|
|
341
|
+
if not (200 <= status < 300):
|
|
342
|
+
detail = body.decode("utf-8", errors="replace")
|
|
343
|
+
raise GoogleWorkspaceError(status, detail)
|
|
344
|
+
return json.loads(body.decode("utf-8") or "{}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
# Convenience formatters for slash-command display
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def format_drive_files(payload: dict[str, Any]) -> list[str]:
|
|
353
|
+
lines: list[str] = []
|
|
354
|
+
for item in payload.get("files", []) or []:
|
|
355
|
+
if not isinstance(item, dict):
|
|
356
|
+
continue
|
|
357
|
+
fid = item.get("id", "?")
|
|
358
|
+
name = item.get("name", "(unnamed)")
|
|
359
|
+
mime = item.get("mimeType", "")
|
|
360
|
+
size = item.get("size")
|
|
361
|
+
size_s = f" {int(size):,}B" if size else ""
|
|
362
|
+
lines.append(f" - {name} [muted]({mime}{size_s})[/] id={fid}")
|
|
363
|
+
if not lines:
|
|
364
|
+
lines.append(" (no files)")
|
|
365
|
+
return lines
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def format_docs_plain_text(document: dict[str, Any], client: GoogleWorkspaceClient) -> str:
|
|
369
|
+
return client.docs_to_plain_text(document)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def format_sheet_values(payload: dict[str, Any]) -> str:
|
|
373
|
+
rows = payload.get("values", []) or []
|
|
374
|
+
if not rows:
|
|
375
|
+
return "(empty range)"
|
|
376
|
+
width = max(len(row) for row in rows)
|
|
377
|
+
out: list[str] = []
|
|
378
|
+
for row in rows:
|
|
379
|
+
padded = list(row) + [""] * (width - len(row))
|
|
380
|
+
out.append(" | ".join(str(cell) for cell in padded))
|
|
381
|
+
return "\n".join(out)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def format_calendar_events(payload: dict[str, Any]) -> list[str]:
|
|
385
|
+
lines: list[str] = []
|
|
386
|
+
for item in payload.get("items", []) or []:
|
|
387
|
+
if not isinstance(item, dict):
|
|
388
|
+
continue
|
|
389
|
+
summary = item.get("summary", "(no title)")
|
|
390
|
+
start = (item.get("start") or {}).get("dateTime") or (item.get("start") or {}).get("date")
|
|
391
|
+
end = (item.get("end") or {}).get("dateTime") or (item.get("end") or {}).get("date")
|
|
392
|
+
eid = item.get("id", "")
|
|
393
|
+
when = f"{start} → {end}" if start and end else (start or "?")
|
|
394
|
+
lines.append(f" - {summary} [muted]({when})[/] id={eid}")
|
|
395
|
+
if not lines:
|
|
396
|
+
lines.append(" (no events)")
|
|
397
|
+
return lines
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def format_gmail_message(message: dict[str, Any]) -> str:
|
|
401
|
+
headers = message.get("payload", {}).get("headers", []) or []
|
|
402
|
+
by_name = {h.get("name", "").lower(): h.get("value", "") for h in headers if isinstance(h, dict)}
|
|
403
|
+
subject = by_name.get("subject", "(no subject)")
|
|
404
|
+
frm = by_name.get("from", "?")
|
|
405
|
+
date = by_name.get("date", "?")
|
|
406
|
+
snippet = message.get("snippet", "")
|
|
407
|
+
return f" Subject: {subject}\n From: {frm}\n Date: {date}\n ID: {message.get('id', '?')}\n Snippet: {snippet}"
|