maybeai-sheet-cli 0.1.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.
- maybeai_sheet/__init__.py +5 -0
- maybeai_sheet/cli.py +117 -0
- maybeai_sheet/client.py +78 -0
- maybeai_sheet/commands/__init__.py +1 -0
- maybeai_sheet/commands/raw.py +35 -0
- maybeai_sheet/commands/sheet.py +440 -0
- maybeai_sheet/commands/workbook.py +109 -0
- maybeai_sheet/config.py +21 -0
- maybeai_sheet/errors.py +26 -0
- maybeai_sheet/formatters.py +103 -0
- maybeai_sheet/models/__init__.py +1 -0
- maybeai_sheet/models/sheet.py +61 -0
- maybeai_sheet/models/workbook.py +14 -0
- maybeai_sheet/resolver.py +96 -0
- maybeai_sheet_cli-0.1.0.dist-info/METADATA +163 -0
- maybeai_sheet_cli-0.1.0.dist-info/RECORD +18 -0
- maybeai_sheet_cli-0.1.0.dist-info/WHEEL +4 -0
- maybeai_sheet_cli-0.1.0.dist-info/entry_points.txt +2 -0
maybeai_sheet/cli.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from .commands.raw import app as raw_app
|
|
9
|
+
from .commands.sheet import app as sheet_app
|
|
10
|
+
from .commands.workbook import app as workbook_app
|
|
11
|
+
from .config import CLIContext
|
|
12
|
+
from .formatters import handle_cli_error
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
help="CLI for common MaybeAI spreadsheet operations.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
rich_markup_mode="markdown",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _version_callback(value: bool) -> None:
|
|
22
|
+
if not value:
|
|
23
|
+
return
|
|
24
|
+
from . import __version__
|
|
25
|
+
|
|
26
|
+
typer.echo(__version__)
|
|
27
|
+
raise typer.Exit()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback()
|
|
31
|
+
def main(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
token: Optional[str] = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
"--token",
|
|
36
|
+
envvar="MAYBEAI_API_TOKEN",
|
|
37
|
+
help="MaybeAI API token. Defaults to MAYBEAI_API_TOKEN.",
|
|
38
|
+
),
|
|
39
|
+
base_url: str = typer.Option(
|
|
40
|
+
"https://play-be.omnimcp.ai",
|
|
41
|
+
"--base-url",
|
|
42
|
+
help="MaybeAI API base URL.",
|
|
43
|
+
),
|
|
44
|
+
doc_id: Optional[str] = typer.Option(
|
|
45
|
+
None,
|
|
46
|
+
"--doc-id",
|
|
47
|
+
help="MaybeAI document ID.",
|
|
48
|
+
),
|
|
49
|
+
url: Optional[str] = typer.Option(
|
|
50
|
+
None,
|
|
51
|
+
"--url",
|
|
52
|
+
help="MaybeAI workbook URL. The CLI will parse document_id and gid when possible.",
|
|
53
|
+
),
|
|
54
|
+
uri: Optional[str] = typer.Option(
|
|
55
|
+
None,
|
|
56
|
+
"--uri",
|
|
57
|
+
help="Fully resolved MaybeAI workbook URI.",
|
|
58
|
+
),
|
|
59
|
+
gid: Optional[int] = typer.Option(
|
|
60
|
+
None,
|
|
61
|
+
"--gid",
|
|
62
|
+
help="Worksheet gid.",
|
|
63
|
+
),
|
|
64
|
+
worksheet_name: Optional[str] = typer.Option(
|
|
65
|
+
None,
|
|
66
|
+
"--worksheet-name",
|
|
67
|
+
help="Worksheet name. Prefer this when the endpoint supports it.",
|
|
68
|
+
),
|
|
69
|
+
output: str = typer.Option(
|
|
70
|
+
"json",
|
|
71
|
+
"--output",
|
|
72
|
+
help="Output format: json, table, yaml.",
|
|
73
|
+
),
|
|
74
|
+
timeout: float = typer.Option(
|
|
75
|
+
30.0,
|
|
76
|
+
"--timeout",
|
|
77
|
+
min=1.0,
|
|
78
|
+
help="HTTP timeout in seconds.",
|
|
79
|
+
),
|
|
80
|
+
verbose: bool = typer.Option(
|
|
81
|
+
False,
|
|
82
|
+
"--verbose",
|
|
83
|
+
help="Show extra resolution details.",
|
|
84
|
+
),
|
|
85
|
+
version: bool = typer.Option(
|
|
86
|
+
False,
|
|
87
|
+
"--version",
|
|
88
|
+
callback=_version_callback,
|
|
89
|
+
is_eager=True,
|
|
90
|
+
help="Show version and exit.",
|
|
91
|
+
),
|
|
92
|
+
) -> None:
|
|
93
|
+
del version
|
|
94
|
+
try:
|
|
95
|
+
ctx.obj = CLIContext(
|
|
96
|
+
token=token or os.getenv("MAYBEAI_API_TOKEN"),
|
|
97
|
+
base_url=base_url,
|
|
98
|
+
doc_id=doc_id,
|
|
99
|
+
url=url,
|
|
100
|
+
uri=uri,
|
|
101
|
+
gid=gid,
|
|
102
|
+
worksheet_name=worksheet_name,
|
|
103
|
+
output=output, # type: ignore[arg-type]
|
|
104
|
+
timeout=timeout,
|
|
105
|
+
verbose=verbose,
|
|
106
|
+
)
|
|
107
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
108
|
+
handle_cli_error(error)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
app.add_typer(workbook_app, name="workbook")
|
|
112
|
+
app.add_typer(sheet_app, name="sheet")
|
|
113
|
+
app.add_typer(raw_app, name="raw")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
app()
|
maybeai_sheet/client.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import CLIContext
|
|
10
|
+
from .errors import APIRequestError, UsageError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MaybeAIClient:
|
|
14
|
+
def __init__(self, state: CLIContext) -> None:
|
|
15
|
+
if not state.token:
|
|
16
|
+
raise UsageError("Missing MaybeAI token. Set --token or MAYBEAI_API_TOKEN.")
|
|
17
|
+
self.state = state
|
|
18
|
+
|
|
19
|
+
def _headers(self, *, content_type: bool = True) -> dict[str, str]:
|
|
20
|
+
headers = {"Authorization": f"Bearer {self.state.token}"}
|
|
21
|
+
if content_type:
|
|
22
|
+
headers["Content-Type"] = "application/json"
|
|
23
|
+
return headers
|
|
24
|
+
|
|
25
|
+
def _endpoint(self, path: str) -> str:
|
|
26
|
+
base = self.state.base_url.rstrip("/")
|
|
27
|
+
if path.startswith("http://") or path.startswith("https://"):
|
|
28
|
+
return path
|
|
29
|
+
if not path.startswith("/"):
|
|
30
|
+
path = f"/{path}"
|
|
31
|
+
return f"{base}{path}"
|
|
32
|
+
|
|
33
|
+
def _raise_for_error(self, response: httpx.Response, *, endpoint: str) -> None:
|
|
34
|
+
if response.status_code < 400:
|
|
35
|
+
return
|
|
36
|
+
try:
|
|
37
|
+
body: object = response.json()
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
body = response.text
|
|
40
|
+
raise APIRequestError(
|
|
41
|
+
f"Request failed with status {response.status_code}",
|
|
42
|
+
status_code=response.status_code,
|
|
43
|
+
endpoint=endpoint,
|
|
44
|
+
body=body,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
endpoint = self._endpoint(path)
|
|
49
|
+
with httpx.Client(timeout=self.state.timeout, trust_env=False) as client:
|
|
50
|
+
response = client.post(endpoint, json=payload, headers=self._headers())
|
|
51
|
+
self._raise_for_error(response, endpoint=endpoint)
|
|
52
|
+
return response.json()
|
|
53
|
+
|
|
54
|
+
def get_bytes(self, path: str) -> bytes:
|
|
55
|
+
endpoint = self._endpoint(path)
|
|
56
|
+
with httpx.Client(timeout=self.state.timeout, trust_env=False) as client:
|
|
57
|
+
response = client.get(endpoint, headers=self._headers(content_type=False))
|
|
58
|
+
self._raise_for_error(response, endpoint=endpoint)
|
|
59
|
+
return response.content
|
|
60
|
+
|
|
61
|
+
def upload_file(self, path: str, file_path: Path, *, user_id: str | None = None) -> dict[str, Any]:
|
|
62
|
+
endpoint = self._endpoint(path)
|
|
63
|
+
files = {"file": (file_path.name, file_path.open("rb"))}
|
|
64
|
+
data: dict[str, str] = {}
|
|
65
|
+
if user_id:
|
|
66
|
+
data["user_id"] = user_id
|
|
67
|
+
try:
|
|
68
|
+
with httpx.Client(timeout=self.state.timeout, trust_env=False) as client:
|
|
69
|
+
response = client.post(
|
|
70
|
+
endpoint,
|
|
71
|
+
headers=self._headers(content_type=False),
|
|
72
|
+
files=files,
|
|
73
|
+
data=data,
|
|
74
|
+
)
|
|
75
|
+
finally:
|
|
76
|
+
files["file"][1].close()
|
|
77
|
+
self._raise_for_error(response, endpoint=endpoint)
|
|
78
|
+
return response.json()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command groups for MaybeAI Sheet."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..client import MaybeAIClient
|
|
9
|
+
from ..config import CLIContext
|
|
10
|
+
from ..formatters import build_command_output, handle_cli_error, load_json_file, render_output
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Low-level API escape hatch.", no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("post")
|
|
16
|
+
def post(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
path: str = typer.Argument(..., help="API path, for example /api/v1/excel/read_sheet."),
|
|
19
|
+
body: Optional[Path] = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
"--body",
|
|
22
|
+
exists=True,
|
|
23
|
+
readable=True,
|
|
24
|
+
resolve_path=True,
|
|
25
|
+
help="Optional JSON request body.",
|
|
26
|
+
),
|
|
27
|
+
) -> None:
|
|
28
|
+
try:
|
|
29
|
+
state: CLIContext = ctx.obj
|
|
30
|
+
client = MaybeAIClient(state)
|
|
31
|
+
payload = load_json_file(body) if body else {}
|
|
32
|
+
response = client.post_json(path, payload)
|
|
33
|
+
render_output(build_command_output(endpoint=path, result=response), state.output)
|
|
34
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
35
|
+
handle_cli_error(error)
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..client import MaybeAIClient
|
|
9
|
+
from ..config import CLIContext
|
|
10
|
+
from ..formatters import build_command_output, handle_cli_error, load_json_file, render_output
|
|
11
|
+
from ..models.sheet import (
|
|
12
|
+
AppendRowsRequest,
|
|
13
|
+
CreateWorksheetRequest,
|
|
14
|
+
ListWorksheetsRequest,
|
|
15
|
+
ReadHeadersRequest,
|
|
16
|
+
ReadSheetRequest,
|
|
17
|
+
UpdateRangeRequest,
|
|
18
|
+
UpsertRowsRequest,
|
|
19
|
+
)
|
|
20
|
+
from ..resolver import resolve_target
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(help="Worksheet read/write operations.", no_args_is_help=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _state_with_target_overrides(
|
|
26
|
+
state: CLIContext,
|
|
27
|
+
*,
|
|
28
|
+
doc_id: str | None,
|
|
29
|
+
url: str | None,
|
|
30
|
+
uri: str | None,
|
|
31
|
+
gid: int | None,
|
|
32
|
+
worksheet_name: str | None,
|
|
33
|
+
) -> CLIContext:
|
|
34
|
+
return CLIContext(
|
|
35
|
+
token=state.token,
|
|
36
|
+
base_url=state.base_url,
|
|
37
|
+
doc_id=doc_id or state.doc_id,
|
|
38
|
+
url=url or state.url,
|
|
39
|
+
uri=uri or state.uri,
|
|
40
|
+
gid=gid if gid is not None else state.gid,
|
|
41
|
+
worksheet_name=worksheet_name or state.worksheet_name,
|
|
42
|
+
output=state.output,
|
|
43
|
+
timeout=state.timeout,
|
|
44
|
+
verbose=state.verbose,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_verify(
|
|
49
|
+
client: MaybeAIClient,
|
|
50
|
+
*,
|
|
51
|
+
endpoint: str,
|
|
52
|
+
uri: str,
|
|
53
|
+
worksheet_name: str | None,
|
|
54
|
+
cell_range: str | None = None,
|
|
55
|
+
) -> dict[str, object]:
|
|
56
|
+
payload = ReadSheetRequest(
|
|
57
|
+
uri=uri,
|
|
58
|
+
worksheet_name=worksheet_name,
|
|
59
|
+
range_address=cell_range,
|
|
60
|
+
)
|
|
61
|
+
return client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command("read")
|
|
65
|
+
def read_sheet(
|
|
66
|
+
ctx: typer.Context,
|
|
67
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
68
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
69
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
70
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
71
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
72
|
+
cell_range: Optional[str] = typer.Option(None, "--range", help="Optional A1-style range."),
|
|
73
|
+
value_render_option: Optional[str] = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--value-render-option",
|
|
76
|
+
help="Optional MaybeAI render option such as FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA.",
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
try:
|
|
80
|
+
state: CLIContext = _state_with_target_overrides(
|
|
81
|
+
ctx.obj,
|
|
82
|
+
doc_id=doc_id,
|
|
83
|
+
url=url,
|
|
84
|
+
uri=uri,
|
|
85
|
+
gid=gid,
|
|
86
|
+
worksheet_name=worksheet_name,
|
|
87
|
+
)
|
|
88
|
+
client = MaybeAIClient(state)
|
|
89
|
+
target = resolve_target(
|
|
90
|
+
state,
|
|
91
|
+
force_gid_uri=bool(state.gid is not None and not state.worksheet_name),
|
|
92
|
+
)
|
|
93
|
+
payload = ReadSheetRequest(
|
|
94
|
+
uri=target.uri,
|
|
95
|
+
worksheet_name=target.worksheet_name,
|
|
96
|
+
range_address=cell_range,
|
|
97
|
+
value_render_option=value_render_option,
|
|
98
|
+
)
|
|
99
|
+
endpoint = "/api/v1/excel/read_sheet"
|
|
100
|
+
response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
101
|
+
render_output(
|
|
102
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
103
|
+
state.output,
|
|
104
|
+
)
|
|
105
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
106
|
+
handle_cli_error(error)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command("read-range")
|
|
110
|
+
def read_range(
|
|
111
|
+
ctx: typer.Context,
|
|
112
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
113
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
114
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
115
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
116
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
117
|
+
cell_range: str = typer.Option(..., "--range", help="A1-style range to read."),
|
|
118
|
+
) -> None:
|
|
119
|
+
read_sheet(
|
|
120
|
+
ctx,
|
|
121
|
+
doc_id=doc_id,
|
|
122
|
+
url=url,
|
|
123
|
+
uri=uri,
|
|
124
|
+
gid=gid,
|
|
125
|
+
worksheet_name=worksheet_name,
|
|
126
|
+
cell_range=cell_range,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command("headers")
|
|
131
|
+
def headers(
|
|
132
|
+
ctx: typer.Context,
|
|
133
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
134
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
135
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
136
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
137
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
138
|
+
) -> None:
|
|
139
|
+
try:
|
|
140
|
+
state: CLIContext = _state_with_target_overrides(
|
|
141
|
+
ctx.obj,
|
|
142
|
+
doc_id=doc_id,
|
|
143
|
+
url=url,
|
|
144
|
+
uri=uri,
|
|
145
|
+
gid=gid,
|
|
146
|
+
worksheet_name=worksheet_name,
|
|
147
|
+
)
|
|
148
|
+
client = MaybeAIClient(state)
|
|
149
|
+
target = resolve_target(state, force_gid_uri=True)
|
|
150
|
+
payload = ReadHeadersRequest(uri=target.uri)
|
|
151
|
+
endpoint = "/api/v1/excel/read_headers"
|
|
152
|
+
response = client.post_json(endpoint, payload.model_dump())
|
|
153
|
+
render_output(
|
|
154
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
155
|
+
state.output,
|
|
156
|
+
)
|
|
157
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
158
|
+
handle_cli_error(error)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command("worksheets")
|
|
162
|
+
def worksheets(
|
|
163
|
+
ctx: typer.Context,
|
|
164
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
165
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
166
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
167
|
+
) -> None:
|
|
168
|
+
try:
|
|
169
|
+
state: CLIContext = _state_with_target_overrides(
|
|
170
|
+
ctx.obj,
|
|
171
|
+
doc_id=doc_id,
|
|
172
|
+
url=url,
|
|
173
|
+
uri=uri,
|
|
174
|
+
gid=None,
|
|
175
|
+
worksheet_name=None,
|
|
176
|
+
)
|
|
177
|
+
client = MaybeAIClient(state)
|
|
178
|
+
target = resolve_target(state)
|
|
179
|
+
payload = ListWorksheetsRequest(uri=target.uri)
|
|
180
|
+
endpoint = "/api/v1/excel/list_worksheets"
|
|
181
|
+
response = client.post_json(endpoint, payload.model_dump())
|
|
182
|
+
render_output(
|
|
183
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
184
|
+
state.output,
|
|
185
|
+
)
|
|
186
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
187
|
+
handle_cli_error(error)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@app.command("formulas")
|
|
191
|
+
def formulas(
|
|
192
|
+
ctx: typer.Context,
|
|
193
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
194
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
195
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
196
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
197
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
198
|
+
cell_range: Optional[str] = typer.Option(None, "--range", help="Optional A1-style range."),
|
|
199
|
+
) -> None:
|
|
200
|
+
read_sheet(
|
|
201
|
+
ctx,
|
|
202
|
+
doc_id=doc_id,
|
|
203
|
+
url=url,
|
|
204
|
+
uri=uri,
|
|
205
|
+
gid=gid,
|
|
206
|
+
worksheet_name=worksheet_name,
|
|
207
|
+
cell_range=cell_range,
|
|
208
|
+
value_render_option="FORMULA",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.command("write-range")
|
|
213
|
+
def write_range(
|
|
214
|
+
ctx: typer.Context,
|
|
215
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
216
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
217
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
218
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
219
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
220
|
+
cell_range: str = typer.Option(..., "--range", help="A1-style range to write."),
|
|
221
|
+
values: Path = typer.Option(
|
|
222
|
+
...,
|
|
223
|
+
"--values",
|
|
224
|
+
exists=True,
|
|
225
|
+
readable=True,
|
|
226
|
+
resolve_path=True,
|
|
227
|
+
help="Path to JSON values payload.",
|
|
228
|
+
),
|
|
229
|
+
verify: bool = typer.Option(False, "--verify", help="Read back after write."),
|
|
230
|
+
) -> None:
|
|
231
|
+
try:
|
|
232
|
+
state: CLIContext = _state_with_target_overrides(
|
|
233
|
+
ctx.obj,
|
|
234
|
+
doc_id=doc_id,
|
|
235
|
+
url=url,
|
|
236
|
+
uri=uri,
|
|
237
|
+
gid=gid,
|
|
238
|
+
worksheet_name=worksheet_name,
|
|
239
|
+
)
|
|
240
|
+
client = MaybeAIClient(state)
|
|
241
|
+
target = resolve_target(state)
|
|
242
|
+
payload = UpdateRangeRequest(
|
|
243
|
+
uri=target.uri,
|
|
244
|
+
worksheet_name=target.worksheet_name,
|
|
245
|
+
range_address=cell_range,
|
|
246
|
+
values=load_json_file(values),
|
|
247
|
+
)
|
|
248
|
+
endpoint = "/api/v1/excel/update_range"
|
|
249
|
+
response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
250
|
+
if verify:
|
|
251
|
+
verify_result = _read_verify(
|
|
252
|
+
client,
|
|
253
|
+
endpoint="/api/v1/excel/read_sheet",
|
|
254
|
+
uri=target.uri,
|
|
255
|
+
worksheet_name=target.worksheet_name,
|
|
256
|
+
cell_range=cell_range,
|
|
257
|
+
)
|
|
258
|
+
render_output(
|
|
259
|
+
build_command_output(
|
|
260
|
+
endpoint=endpoint,
|
|
261
|
+
target=target,
|
|
262
|
+
result=response,
|
|
263
|
+
verify=verify_result,
|
|
264
|
+
),
|
|
265
|
+
state.output,
|
|
266
|
+
)
|
|
267
|
+
return
|
|
268
|
+
render_output(
|
|
269
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
270
|
+
state.output,
|
|
271
|
+
)
|
|
272
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
273
|
+
handle_cli_error(error)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command("append")
|
|
277
|
+
def append_rows(
|
|
278
|
+
ctx: typer.Context,
|
|
279
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
280
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
281
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
282
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
283
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
284
|
+
rows: Path = typer.Option(
|
|
285
|
+
...,
|
|
286
|
+
"--rows",
|
|
287
|
+
exists=True,
|
|
288
|
+
readable=True,
|
|
289
|
+
resolve_path=True,
|
|
290
|
+
help="Path to JSON rows payload.",
|
|
291
|
+
),
|
|
292
|
+
verify: bool = typer.Option(False, "--verify", help="Read back after write."),
|
|
293
|
+
) -> None:
|
|
294
|
+
try:
|
|
295
|
+
state: CLIContext = _state_with_target_overrides(
|
|
296
|
+
ctx.obj,
|
|
297
|
+
doc_id=doc_id,
|
|
298
|
+
url=url,
|
|
299
|
+
uri=uri,
|
|
300
|
+
gid=gid,
|
|
301
|
+
worksheet_name=worksheet_name,
|
|
302
|
+
)
|
|
303
|
+
client = MaybeAIClient(state)
|
|
304
|
+
target = resolve_target(state, force_gid_uri=True)
|
|
305
|
+
payload = AppendRowsRequest(
|
|
306
|
+
uri=target.uri,
|
|
307
|
+
worksheet_name=target.worksheet_name,
|
|
308
|
+
data=load_json_file(rows),
|
|
309
|
+
)
|
|
310
|
+
endpoint = "/api/v1/excel/append_rows"
|
|
311
|
+
response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
312
|
+
if verify:
|
|
313
|
+
verify_result = _read_verify(
|
|
314
|
+
client,
|
|
315
|
+
endpoint="/api/v1/excel/read_sheet",
|
|
316
|
+
uri=target.uri,
|
|
317
|
+
worksheet_name=target.worksheet_name,
|
|
318
|
+
)
|
|
319
|
+
render_output(
|
|
320
|
+
build_command_output(
|
|
321
|
+
endpoint=endpoint,
|
|
322
|
+
target=target,
|
|
323
|
+
result=response,
|
|
324
|
+
verify=verify_result,
|
|
325
|
+
),
|
|
326
|
+
state.output,
|
|
327
|
+
)
|
|
328
|
+
return
|
|
329
|
+
render_output(
|
|
330
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
331
|
+
state.output,
|
|
332
|
+
)
|
|
333
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
334
|
+
handle_cli_error(error)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@app.command("upsert")
|
|
338
|
+
def upsert_rows(
|
|
339
|
+
ctx: typer.Context,
|
|
340
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
341
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
342
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
343
|
+
gid: Optional[int] = typer.Option(None, "--gid", help="Worksheet gid."),
|
|
344
|
+
worksheet_name: Optional[str] = typer.Option(None, "--worksheet-name", help="Worksheet name."),
|
|
345
|
+
key: str = typer.Option(..., "--key", help="Business key column."),
|
|
346
|
+
rows: Path = typer.Option(
|
|
347
|
+
...,
|
|
348
|
+
"--rows",
|
|
349
|
+
exists=True,
|
|
350
|
+
readable=True,
|
|
351
|
+
resolve_path=True,
|
|
352
|
+
help="Path to JSON rows payload.",
|
|
353
|
+
),
|
|
354
|
+
verify: bool = typer.Option(False, "--verify", help="Read back after write."),
|
|
355
|
+
) -> None:
|
|
356
|
+
try:
|
|
357
|
+
state: CLIContext = _state_with_target_overrides(
|
|
358
|
+
ctx.obj,
|
|
359
|
+
doc_id=doc_id,
|
|
360
|
+
url=url,
|
|
361
|
+
uri=uri,
|
|
362
|
+
gid=gid,
|
|
363
|
+
worksheet_name=worksheet_name,
|
|
364
|
+
)
|
|
365
|
+
client = MaybeAIClient(state)
|
|
366
|
+
target = resolve_target(state, force_gid_uri=True)
|
|
367
|
+
payload = UpsertRowsRequest(
|
|
368
|
+
uri=target.uri,
|
|
369
|
+
data=load_json_file(rows),
|
|
370
|
+
on=[key],
|
|
371
|
+
override=False,
|
|
372
|
+
skip_recalculation=False,
|
|
373
|
+
)
|
|
374
|
+
endpoint = "/api/v1/excel/update_range_by_lookup"
|
|
375
|
+
response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
376
|
+
if verify:
|
|
377
|
+
verify_result = _read_verify(
|
|
378
|
+
client,
|
|
379
|
+
endpoint="/api/v1/excel/read_sheet",
|
|
380
|
+
uri=target.uri,
|
|
381
|
+
worksheet_name=target.worksheet_name,
|
|
382
|
+
)
|
|
383
|
+
render_output(
|
|
384
|
+
build_command_output(
|
|
385
|
+
endpoint=endpoint,
|
|
386
|
+
target=target,
|
|
387
|
+
result=response,
|
|
388
|
+
verify=verify_result,
|
|
389
|
+
),
|
|
390
|
+
state.output,
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
render_output(
|
|
394
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
395
|
+
state.output,
|
|
396
|
+
)
|
|
397
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
398
|
+
handle_cli_error(error)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@app.command("create-worksheet")
|
|
402
|
+
def create_worksheet(
|
|
403
|
+
ctx: typer.Context,
|
|
404
|
+
doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
|
|
405
|
+
url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
|
|
406
|
+
uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
|
|
407
|
+
name: str = typer.Option(..., "--name", help="Worksheet name to create."),
|
|
408
|
+
values: Optional[Path] = typer.Option(
|
|
409
|
+
None,
|
|
410
|
+
"--values",
|
|
411
|
+
exists=True,
|
|
412
|
+
readable=True,
|
|
413
|
+
resolve_path=True,
|
|
414
|
+
help="Optional path to starter JSON values payload.",
|
|
415
|
+
),
|
|
416
|
+
) -> None:
|
|
417
|
+
try:
|
|
418
|
+
state: CLIContext = _state_with_target_overrides(
|
|
419
|
+
ctx.obj,
|
|
420
|
+
doc_id=doc_id,
|
|
421
|
+
url=url,
|
|
422
|
+
uri=uri,
|
|
423
|
+
gid=None,
|
|
424
|
+
worksheet_name=None,
|
|
425
|
+
)
|
|
426
|
+
client = MaybeAIClient(state)
|
|
427
|
+
target = resolve_target(state)
|
|
428
|
+
payload = CreateWorksheetRequest(
|
|
429
|
+
uri=target.uri,
|
|
430
|
+
worksheet_name=name,
|
|
431
|
+
values=load_json_file(values) if values is not None else None,
|
|
432
|
+
)
|
|
433
|
+
endpoint = "/api/v1/excel/write_new_worksheet"
|
|
434
|
+
response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
435
|
+
render_output(
|
|
436
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
437
|
+
state.output,
|
|
438
|
+
)
|
|
439
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
440
|
+
handle_cli_error(error)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..client import MaybeAIClient
|
|
9
|
+
from ..config import CLIContext
|
|
10
|
+
from ..formatters import build_command_output, handle_cli_error, load_json_file, render_output
|
|
11
|
+
from ..models.workbook import CreateWorkbookRequest
|
|
12
|
+
from ..resolver import ResolvedTarget, parse_document_target
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Workbook-level operations.", no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("create")
|
|
18
|
+
def create_workbook(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
title: str = typer.Option(..., "--title", help="Workbook title."),
|
|
21
|
+
data: Optional[Path] = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--data",
|
|
24
|
+
exists=True,
|
|
25
|
+
readable=True,
|
|
26
|
+
resolve_path=True,
|
|
27
|
+
help="Optional JSON file containing a list of row objects.",
|
|
28
|
+
),
|
|
29
|
+
sheet_name: Optional[str] = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--sheet-name",
|
|
32
|
+
help="Optional initial worksheet name.",
|
|
33
|
+
),
|
|
34
|
+
column_order: Optional[str] = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--column-order",
|
|
37
|
+
help="Comma-separated explicit column order for row-object data.",
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
try:
|
|
41
|
+
state: CLIContext = ctx.obj
|
|
42
|
+
client = MaybeAIClient(state)
|
|
43
|
+
rows = load_json_file(data) if data else [{}]
|
|
44
|
+
payload = CreateWorkbookRequest(
|
|
45
|
+
sheet_name=sheet_name or "Sheet1",
|
|
46
|
+
filename=f"{title}.xlsx",
|
|
47
|
+
data=rows,
|
|
48
|
+
)
|
|
49
|
+
if column_order:
|
|
50
|
+
payload = payload.model_copy(
|
|
51
|
+
update={
|
|
52
|
+
"column_order": [part.strip() for part in column_order.split(",") if part.strip()]
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
endpoint = "/api/v1/excel/write_new_sheet"
|
|
56
|
+
response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
|
|
57
|
+
spreadsheet_url = str(response.get("spreadsheet_url") or "")
|
|
58
|
+
response_doc_id = str(
|
|
59
|
+
response.get("spreadsheet_id") or response.get("document_id") or ""
|
|
60
|
+
) or None
|
|
61
|
+
_, response_gid = parse_document_target(spreadsheet_url)
|
|
62
|
+
target = ResolvedTarget(
|
|
63
|
+
document_id=response_doc_id,
|
|
64
|
+
url=spreadsheet_url or None,
|
|
65
|
+
uri=spreadsheet_url or "",
|
|
66
|
+
gid=response_gid,
|
|
67
|
+
worksheet_name=sheet_name or "Sheet1",
|
|
68
|
+
)
|
|
69
|
+
render_output(
|
|
70
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
71
|
+
state.output,
|
|
72
|
+
)
|
|
73
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
74
|
+
handle_cli_error(error)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("create-from-file")
|
|
78
|
+
def create_from_file(
|
|
79
|
+
ctx: typer.Context,
|
|
80
|
+
file: Path = typer.Argument(..., exists=True, readable=True, resolve_path=True),
|
|
81
|
+
user_id: Optional[str] = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--user-id",
|
|
84
|
+
help="Optional compatibility field for upload.",
|
|
85
|
+
),
|
|
86
|
+
) -> None:
|
|
87
|
+
try:
|
|
88
|
+
state: CLIContext = ctx.obj
|
|
89
|
+
client = MaybeAIClient(state)
|
|
90
|
+
endpoint = "/api/v1/excel/upload"
|
|
91
|
+
response = client.upload_file(endpoint, file, user_id=user_id)
|
|
92
|
+
spreadsheet_url = str(response.get("uri") or response.get("spreadsheet_url") or "")
|
|
93
|
+
response_doc_id = str(
|
|
94
|
+
response.get("document_id") or response.get("spreadsheet_id") or ""
|
|
95
|
+
) or None
|
|
96
|
+
_, response_gid = parse_document_target(spreadsheet_url)
|
|
97
|
+
target = ResolvedTarget(
|
|
98
|
+
document_id=response_doc_id,
|
|
99
|
+
url=spreadsheet_url or None,
|
|
100
|
+
uri=spreadsheet_url or "",
|
|
101
|
+
gid=response_gid,
|
|
102
|
+
worksheet_name=None,
|
|
103
|
+
)
|
|
104
|
+
render_output(
|
|
105
|
+
build_command_output(endpoint=endpoint, target=target, result=response),
|
|
106
|
+
state.output,
|
|
107
|
+
)
|
|
108
|
+
except Exception as error: # pragma: no cover - CLI boundary
|
|
109
|
+
handle_cli_error(error)
|
maybeai_sheet/config.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
OutputFormat = Literal["json", "table", "yaml"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class CLIContext:
|
|
12
|
+
token: str | None = None
|
|
13
|
+
base_url: str = "https://play-be.omnimcp.ai"
|
|
14
|
+
doc_id: str | None = None
|
|
15
|
+
url: str | None = None
|
|
16
|
+
uri: str | None = None
|
|
17
|
+
gid: int | None = None
|
|
18
|
+
worksheet_name: str | None = None
|
|
19
|
+
output: OutputFormat = "json"
|
|
20
|
+
timeout: float = 30.0
|
|
21
|
+
verbose: bool = False
|
maybeai_sheet/errors.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MaybeAISheetError(Exception):
|
|
5
|
+
"""Base exception for CLI failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UsageError(MaybeAISheetError):
|
|
9
|
+
"""User input is incomplete or invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class APIRequestError(MaybeAISheetError):
|
|
13
|
+
"""HTTP request failed."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
*,
|
|
19
|
+
status_code: int | None = None,
|
|
20
|
+
endpoint: str | None = None,
|
|
21
|
+
body: object | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.endpoint = endpoint
|
|
26
|
+
self.body = body
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from .config import OutputFormat
|
|
13
|
+
from .errors import APIRequestError, MaybeAISheetError, UsageError
|
|
14
|
+
from .resolver import ResolvedTarget
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_json_file(path: Path) -> Any:
|
|
20
|
+
return json.loads(path.read_text())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_command_output(
|
|
24
|
+
*,
|
|
25
|
+
endpoint: str,
|
|
26
|
+
result: Any,
|
|
27
|
+
target: ResolvedTarget | None = None,
|
|
28
|
+
verify: Any = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
payload: dict[str, Any] = {
|
|
31
|
+
"success": bool(
|
|
32
|
+
result.get("success", True)
|
|
33
|
+
if isinstance(result, dict)
|
|
34
|
+
else True
|
|
35
|
+
),
|
|
36
|
+
"endpoint": endpoint,
|
|
37
|
+
"result": result,
|
|
38
|
+
}
|
|
39
|
+
if target is not None:
|
|
40
|
+
payload["target"] = target.as_dict()
|
|
41
|
+
if verify is not None:
|
|
42
|
+
payload["verify"] = verify
|
|
43
|
+
return payload
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_output(data: Any, output: OutputFormat) -> None:
|
|
47
|
+
if output == "json":
|
|
48
|
+
typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
|
|
49
|
+
return
|
|
50
|
+
if output == "yaml":
|
|
51
|
+
typer.echo(yaml.safe_dump(data, allow_unicode=True, sort_keys=False))
|
|
52
|
+
return
|
|
53
|
+
_render_table(data)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _render_table(data: Any) -> None:
|
|
57
|
+
if isinstance(data, list) and data and all(isinstance(item, dict) for item in data):
|
|
58
|
+
keys = list({key for item in data for key in item.keys()})
|
|
59
|
+
table = Table(show_header=True, header_style="bold")
|
|
60
|
+
for key in keys:
|
|
61
|
+
table.add_column(str(key))
|
|
62
|
+
for item in data:
|
|
63
|
+
table.add_row(*[stringify(item.get(key)) for key in keys])
|
|
64
|
+
console.print(table)
|
|
65
|
+
return
|
|
66
|
+
if isinstance(data, dict):
|
|
67
|
+
for list_key in ("worksheets", "results", "data", "rows"):
|
|
68
|
+
value = data.get(list_key)
|
|
69
|
+
if isinstance(value, list) and value and all(isinstance(item, dict) for item in value):
|
|
70
|
+
_render_table(value)
|
|
71
|
+
return
|
|
72
|
+
table = Table(show_header=False)
|
|
73
|
+
table.add_column("key", style="bold")
|
|
74
|
+
table.add_column("value")
|
|
75
|
+
for key, value in data.items():
|
|
76
|
+
table.add_row(str(key), stringify(value))
|
|
77
|
+
console.print(table)
|
|
78
|
+
return
|
|
79
|
+
typer.echo(stringify(data))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def stringify(value: Any) -> str:
|
|
83
|
+
if value is None:
|
|
84
|
+
return ""
|
|
85
|
+
if isinstance(value, (dict, list)):
|
|
86
|
+
return json.dumps(value, ensure_ascii=False)
|
|
87
|
+
return str(value)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def handle_cli_error(error: Exception) -> None:
|
|
91
|
+
if isinstance(error, APIRequestError):
|
|
92
|
+
payload = {
|
|
93
|
+
"error": str(error),
|
|
94
|
+
"status_code": error.status_code,
|
|
95
|
+
"endpoint": error.endpoint,
|
|
96
|
+
"body": error.body,
|
|
97
|
+
}
|
|
98
|
+
typer.echo(json.dumps(payload, ensure_ascii=False, indent=2), err=True)
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
if isinstance(error, (UsageError, MaybeAISheetError)):
|
|
101
|
+
typer.echo(str(error), err=True)
|
|
102
|
+
raise typer.Exit(2)
|
|
103
|
+
raise error
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pydantic models for MaybeAI Sheet CLI payload validation."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReadSheetRequest(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
uri: str
|
|
12
|
+
worksheet_name: str | None = None
|
|
13
|
+
range_address: str | None = None
|
|
14
|
+
value_render_option: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReadHeadersRequest(BaseModel):
|
|
18
|
+
model_config = ConfigDict(extra="forbid")
|
|
19
|
+
|
|
20
|
+
uri: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ListWorksheetsRequest(BaseModel):
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
uri: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UpdateRangeRequest(BaseModel):
|
|
30
|
+
model_config = ConfigDict(extra="forbid")
|
|
31
|
+
|
|
32
|
+
uri: str
|
|
33
|
+
range_address: str
|
|
34
|
+
values: list[list[Any]]
|
|
35
|
+
worksheet_name: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AppendRowsRequest(BaseModel):
|
|
39
|
+
model_config = ConfigDict(extra="forbid")
|
|
40
|
+
|
|
41
|
+
uri: str
|
|
42
|
+
data: list[dict[str, Any]]
|
|
43
|
+
worksheet_name: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UpsertRowsRequest(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="forbid")
|
|
48
|
+
|
|
49
|
+
uri: str
|
|
50
|
+
data: list[dict[str, Any]]
|
|
51
|
+
on: list[str]
|
|
52
|
+
override: bool = False
|
|
53
|
+
skip_recalculation: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CreateWorksheetRequest(BaseModel):
|
|
57
|
+
model_config = ConfigDict(extra="forbid")
|
|
58
|
+
|
|
59
|
+
uri: str
|
|
60
|
+
worksheet_name: str
|
|
61
|
+
values: list[list[Any]] | None = None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CreateWorkbookRequest(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
sheet_name: str
|
|
12
|
+
filename: str
|
|
13
|
+
data: list[dict[str, Any]]
|
|
14
|
+
column_order: list[str] | None = None
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
5
|
+
|
|
6
|
+
from .config import CLIContext
|
|
7
|
+
from .errors import UsageError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class ResolvedTarget:
|
|
12
|
+
document_id: str | None
|
|
13
|
+
url: str | None
|
|
14
|
+
uri: str
|
|
15
|
+
gid: int | None
|
|
16
|
+
worksheet_name: str | None
|
|
17
|
+
|
|
18
|
+
def as_dict(self) -> dict[str, object]:
|
|
19
|
+
return asdict(self)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_document_url(document_id: str) -> str:
|
|
23
|
+
return f"https://www.maybe.ai/docs/spreadsheets/d/{document_id}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_document_target(url_or_uri: str) -> tuple[str | None, int | None]:
|
|
27
|
+
parsed = urlparse(url_or_uri)
|
|
28
|
+
query = parse_qs(parsed.query)
|
|
29
|
+
gid = None
|
|
30
|
+
raw_gid = query.get("gid", [None])[0]
|
|
31
|
+
if raw_gid not in (None, ""):
|
|
32
|
+
try:
|
|
33
|
+
gid = int(raw_gid)
|
|
34
|
+
except (TypeError, ValueError):
|
|
35
|
+
gid = None
|
|
36
|
+
document_id = None
|
|
37
|
+
marker = "/spreadsheets/d/"
|
|
38
|
+
if marker in parsed.path:
|
|
39
|
+
document_id = parsed.path.split(marker, 1)[1].split("/", 1)[0]
|
|
40
|
+
return document_id, gid
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def append_gid(uri: str, gid: int | None) -> str:
|
|
44
|
+
if gid is None:
|
|
45
|
+
return uri
|
|
46
|
+
parsed = urlparse(uri)
|
|
47
|
+
query = parse_qs(parsed.query)
|
|
48
|
+
query["gid"] = [str(gid)]
|
|
49
|
+
return urlunparse(parsed._replace(query=urlencode(query, doseq=True)))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_target(
|
|
53
|
+
state: CLIContext,
|
|
54
|
+
*,
|
|
55
|
+
require_workbook: bool = True,
|
|
56
|
+
force_gid_uri: bool = False,
|
|
57
|
+
) -> ResolvedTarget:
|
|
58
|
+
document_id = state.doc_id
|
|
59
|
+
url = state.url
|
|
60
|
+
uri = state.uri
|
|
61
|
+
gid = state.gid
|
|
62
|
+
|
|
63
|
+
if url:
|
|
64
|
+
parsed_doc_id, parsed_gid = parse_document_target(url)
|
|
65
|
+
document_id = document_id or parsed_doc_id
|
|
66
|
+
if gid is None:
|
|
67
|
+
gid = parsed_gid
|
|
68
|
+
if uri is None:
|
|
69
|
+
uri = url
|
|
70
|
+
|
|
71
|
+
if uri:
|
|
72
|
+
parsed_doc_id, parsed_gid = parse_document_target(uri)
|
|
73
|
+
document_id = document_id or parsed_doc_id
|
|
74
|
+
if gid is None:
|
|
75
|
+
gid = parsed_gid
|
|
76
|
+
|
|
77
|
+
if uri is None and document_id:
|
|
78
|
+
url = url or build_document_url(document_id)
|
|
79
|
+
uri = url
|
|
80
|
+
|
|
81
|
+
if require_workbook and not uri:
|
|
82
|
+
raise UsageError("Provide one of --url, --uri, or --doc-id.")
|
|
83
|
+
|
|
84
|
+
if uri is None:
|
|
85
|
+
uri = ""
|
|
86
|
+
|
|
87
|
+
if force_gid_uri:
|
|
88
|
+
uri = append_gid(uri, gid)
|
|
89
|
+
|
|
90
|
+
return ResolvedTarget(
|
|
91
|
+
document_id=document_id,
|
|
92
|
+
url=url or (build_document_url(document_id) if document_id else None),
|
|
93
|
+
uri=uri,
|
|
94
|
+
gid=gid,
|
|
95
|
+
worksheet_name=state.worksheet_name,
|
|
96
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maybeai-sheet-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for common MaybeAI spreadsheet operations
|
|
5
|
+
Project-URL: Homepage, https://github.com/OmniMCP-AI/maybeai-sheet-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/OmniMCP-AI/maybeai-sheet-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/OmniMCP-AI/maybeai-sheet-cli/issues
|
|
8
|
+
Author: OmniMCP-AI
|
|
9
|
+
License: Proprietary
|
|
10
|
+
Keywords: cli,excel,maybeai,spreadsheet
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx<1,>=0.27
|
|
19
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
20
|
+
Requires-Dist: pyyaml<7,>=6
|
|
21
|
+
Requires-Dist: rich<14,>=13.7
|
|
22
|
+
Requires-Dist: typer<1,>=0.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# maybeai-sheet-cli
|
|
26
|
+
|
|
27
|
+
CLI for MaybeAI spreadsheet operations.
|
|
28
|
+
|
|
29
|
+
`maybeai-sheet` wraps the MaybeAI spreadsheet HTTP APIs behind a stable command-line interface so humans, CI jobs, and agents can perform common workbook operations without dynamically generating curl or Python glue.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install maybeai-sheet-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.10+
|
|
40
|
+
- `MAYBEAI_API_TOKEN`
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
Set your token:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export MAYBEAI_API_TOKEN="YOUR_TOKEN"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
List worksheets in a workbook:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
maybeai-sheet sheet worksheets --doc-id 6a3b3ec9b225d9fe7982ff36
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Read a worksheet:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
maybeai-sheet sheet read --doc-id 6a3b3ec9b225d9fe7982ff36 --worksheet-name "利润分析"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Read headers from a specific worksheet gid:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
maybeai-sheet sheet headers --doc-id 6a3b3ec9b225d9fe7982ff36 --gid 3
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Create a workbook:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
maybeai-sheet workbook create --title "Board Pack"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Append rows and read back:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
maybeai-sheet sheet append \
|
|
78
|
+
--doc-id 6a3b3ec9b225d9fe7982ff36 \
|
|
79
|
+
--gid 3 \
|
|
80
|
+
--rows rows.json \
|
|
81
|
+
--verify
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Command Groups
|
|
85
|
+
|
|
86
|
+
- `workbook`
|
|
87
|
+
- `create`
|
|
88
|
+
- `create-from-file`
|
|
89
|
+
- `sheet`
|
|
90
|
+
- `read`
|
|
91
|
+
- `read-range`
|
|
92
|
+
- `headers`
|
|
93
|
+
- `worksheets`
|
|
94
|
+
- `formulas`
|
|
95
|
+
- `write-range`
|
|
96
|
+
- `append`
|
|
97
|
+
- `upsert`
|
|
98
|
+
- `create-worksheet`
|
|
99
|
+
- `raw`
|
|
100
|
+
- `post`
|
|
101
|
+
|
|
102
|
+
## Output
|
|
103
|
+
|
|
104
|
+
The CLI defaults to JSON output and returns a stable envelope containing:
|
|
105
|
+
|
|
106
|
+
- `success`
|
|
107
|
+
- `endpoint`
|
|
108
|
+
- `target`
|
|
109
|
+
- `result`
|
|
110
|
+
- optional `verify`
|
|
111
|
+
|
|
112
|
+
Alternative output modes:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
maybeai-sheet sheet worksheets --doc-id <DOC_ID> --output table
|
|
116
|
+
maybeai-sheet sheet worksheets --doc-id <DOC_ID> --output yaml
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
Create a virtual environment and install editable dependencies:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python3 -m venv .venv
|
|
125
|
+
. .venv/bin/activate
|
|
126
|
+
pip install -U pip
|
|
127
|
+
pip install -e .
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Run tests:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python -m unittest discover -s tests -v
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Build distributions:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pip install build twine
|
|
140
|
+
python -m build
|
|
141
|
+
twine check dist/*
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Repository Split
|
|
145
|
+
|
|
146
|
+
This repository owns the software artifact:
|
|
147
|
+
|
|
148
|
+
- `src/`
|
|
149
|
+
- `tests/`
|
|
150
|
+
- `pyproject.toml`
|
|
151
|
+
- packaging and release concerns
|
|
152
|
+
|
|
153
|
+
Skill assets were intentionally split out into the sibling repository directory:
|
|
154
|
+
|
|
155
|
+
- `../maybeai-sheet-cli-skill`
|
|
156
|
+
|
|
157
|
+
That skill repo owns:
|
|
158
|
+
|
|
159
|
+
- `SKILL.md`
|
|
160
|
+
- `agents/`
|
|
161
|
+
- `references/`
|
|
162
|
+
- `scripts/`
|
|
163
|
+
- agent-facing workflow documentation
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
maybeai_sheet/__init__.py,sha256=mITbNPcozbaTRi78SCeYTJzXp8mOieNGUARfRfHoMPA,83
|
|
2
|
+
maybeai_sheet/cli.py,sha256=Z8KZLv04gju5Dc2fvI9VlcawdWG3iy-g7yFgIBE4wQs,2901
|
|
3
|
+
maybeai_sheet/client.py,sha256=Ul3WA1Pd6Ka42S6pOZnzBP1Omr0Xz02c4ms-NmU9gMI,2893
|
|
4
|
+
maybeai_sheet/config.py,sha256=7G9PidCSOSgQHuPFvDi2B28JL44-ki1vo8N_tBiLaqo,503
|
|
5
|
+
maybeai_sheet/errors.py,sha256=AnNDsXpSZxw6YKuZwFea8xSoQHAHa3wpINxlo6mZ0gI,602
|
|
6
|
+
maybeai_sheet/formatters.py,sha256=0KoVqAceaIGocHPkiv7DYdmSGTj2FQjWNXee31JdmJg,3045
|
|
7
|
+
maybeai_sheet/resolver.py,sha256=KlnYamHKUmbpvQUxCgolnhKkmJkaaaBWDz_klEBCjMo,2523
|
|
8
|
+
maybeai_sheet/commands/__init__.py,sha256=IpQsdrGmiWn-kZM3PLIIARedIt33UGizUYoh_eSvW6k,44
|
|
9
|
+
maybeai_sheet/commands/raw.py,sha256=FpO9Nl15UPUeFdvYY-fElmRo2EAYYiL1xlBR2VNomLw,1076
|
|
10
|
+
maybeai_sheet/commands/sheet.py,sha256=OVZlEBAnbNrBbPH0f9z6F6oWuTElCxfwtC-HibceV7Y,15632
|
|
11
|
+
maybeai_sheet/commands/workbook.py,sha256=2bGeMzlbQNTH3joB5vqMJZ1QbwmBFQFeI7ztEhyx_T4,3816
|
|
12
|
+
maybeai_sheet/models/__init__.py,sha256=_BlDSPULY-OOZSzoaZ_xZcBiGwI756dfFzz_Vyik-ms,64
|
|
13
|
+
maybeai_sheet/models/sheet.py,sha256=XPmtARMo7xTiFiXeYy8eackL7hWhUXFKR1ltLlibwV0,1254
|
|
14
|
+
maybeai_sheet/models/workbook.py,sha256=sU1_YIQtv6TwwDintuyBD5zBpPkkaZkg51FzrlE_nQ8,303
|
|
15
|
+
maybeai_sheet_cli-0.1.0.dist-info/METADATA,sha256=m1C8ktRNhBX82GnfsK_j-XkZhOptTDDL9f65W2uQXmM,3266
|
|
16
|
+
maybeai_sheet_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
17
|
+
maybeai_sheet_cli-0.1.0.dist-info/entry_points.txt,sha256=9FJHuqT3qUty3ysFlRxu4ntJM7_n7fFEP0GGAE_9rDs,56
|
|
18
|
+
maybeai_sheet_cli-0.1.0.dist-info/RECORD,,
|