maybeai-sheet-cli 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (32) hide show
  1. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/PKG-INFO +48 -3
  2. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/README.md +47 -2
  3. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/docs/verification-matrix-fallback.md +1 -1
  4. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/pyproject.toml +1 -1
  5. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/__init__.py +1 -1
  6. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/cli.py +2 -0
  7. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/client.py +10 -1
  8. maybeai_sheet_cli-0.3.0/src/maybeai_sheet/commands/share.py +344 -0
  9. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/workbook.py +30 -4
  10. maybeai_sheet_cli-0.3.0/src/maybeai_sheet/models/share.py +47 -0
  11. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_cli.py +228 -1
  12. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_update.py +1 -1
  13. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/.claude/settings.local.json +0 -0
  14. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/.gitignore +0 -0
  15. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/CLAUDE.md +0 -0
  16. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/cli-packaging-plan.md +0 -0
  17. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/cli-plan.md +0 -0
  18. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/__init__.py +0 -0
  19. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/raw.py +0 -0
  20. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/sheet.py +0 -0
  21. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/config.py +0 -0
  22. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/errors.py +0 -0
  23. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/formatters.py +0 -0
  24. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/models/__init__.py +0 -0
  25. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/models/sheet.py +0 -0
  26. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/models/workbook.py +0 -0
  27. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/options.py +0 -0
  28. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/resolver.py +0 -0
  29. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/update.py +0 -0
  30. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_formatters.py +0 -0
  31. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_resolver.py +0 -0
  32. {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maybeai-sheet-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CLI for common MaybeAI spreadsheet operations
5
5
  Project-URL: Homepage, https://github.com/OmniMCP-AI/maybeai-sheet-cli
6
6
  Project-URL: Repository, https://github.com/OmniMCP-AI/maybeai-sheet-cli
@@ -30,6 +30,19 @@ CLI for MaybeAI spreadsheet operations.
30
30
 
31
31
  ## Install
32
32
 
33
+ On Debian/Ubuntu and other **PEP 668** systems, do **not** use bare `pip install` on system Python. Use **pipx** (recommended for CLIs):
34
+
35
+ ```bash
36
+ sudo apt install -y pipx python3-venv
37
+ pipx ensurepath
38
+ # re-login or: source ~/.bashrc
39
+
40
+ pipx install maybeai-sheet-cli
41
+ maybeai-sheet --version
42
+ ```
43
+
44
+ For local venvs or macOS/dev machines, plain pip is fine:
45
+
33
46
  ```bash
34
47
  pip install maybeai-sheet-cli
35
48
  ```
@@ -37,13 +50,22 @@ pip install maybeai-sheet-cli
37
50
  Upgrade to the latest release:
38
51
 
39
52
  ```bash
40
- maybeai-sheet update
53
+ maybeai-sheet update # when installed via pip or pipx
54
+ pipx upgrade maybeai-sheet-cli # alternative on pipx installs
41
55
  ```
42
56
 
43
57
  Install a specific version:
44
58
 
45
59
  ```bash
46
- maybeai-sheet update --version 0.2.0
60
+ maybeai-sheet update --version 0.3.0
61
+ pipx install maybeai-sheet-cli==0.3.0 --force # pipx reinstall pin
62
+ ```
63
+
64
+ Uninstall:
65
+
66
+ ```bash
67
+ pipx uninstall maybeai-sheet-cli
68
+ # not: pip uninstall -U (-U is invalid for uninstall)
47
69
  ```
48
70
 
49
71
  ## Requirements
@@ -92,12 +114,30 @@ Create a workbook:
92
114
  maybeai-sheet workbook create --title "Board Pack"
93
115
  ```
94
116
 
117
+ Upload a workbook-style `.xlsx` through the default Excelize path:
118
+
119
+ ```bash
120
+ maybeai-sheet workbook create-from-file ./report.xlsx
121
+ ```
122
+
123
+ Import a large table-like `.xlsx` into SheetTable/PG:
124
+
125
+ ```bash
126
+ maybeai-sheet workbook create-from-file ./large-table.xlsx --engine postgres
127
+ ```
128
+
95
129
  Append rows and verify:
96
130
 
97
131
  ```bash
98
132
  maybeai-sheet sheet append --doc-id abc123 --gid 3 --rows rows.json --verify
99
133
  ```
100
134
 
135
+ Make a workbook public for viewers:
136
+
137
+ ```bash
138
+ maybeai-sheet share visibility --doc-id abc123 --visibility public --public-permission viewer
139
+ ```
140
+
101
141
  ## Commands
102
142
 
103
143
  - `update`
@@ -105,6 +145,11 @@ maybeai-sheet sheet append --doc-id abc123 --gid 3 --rows rows.json --verify
105
145
  - `workbook create-from-file`
106
146
  - `workbook manifest`
107
147
  - `workbook capabilities`
148
+ - `share visibility`
149
+ - `share grant`
150
+ - `share permission`
151
+ - `share remove`
152
+ - `share list`
108
153
  - `sheet read`
109
154
  - `sheet read-range`
110
155
  - `sheet read-many`
@@ -6,6 +6,19 @@ CLI for MaybeAI spreadsheet operations.
6
6
 
7
7
  ## Install
8
8
 
9
+ On Debian/Ubuntu and other **PEP 668** systems, do **not** use bare `pip install` on system Python. Use **pipx** (recommended for CLIs):
10
+
11
+ ```bash
12
+ sudo apt install -y pipx python3-venv
13
+ pipx ensurepath
14
+ # re-login or: source ~/.bashrc
15
+
16
+ pipx install maybeai-sheet-cli
17
+ maybeai-sheet --version
18
+ ```
19
+
20
+ For local venvs or macOS/dev machines, plain pip is fine:
21
+
9
22
  ```bash
10
23
  pip install maybeai-sheet-cli
11
24
  ```
@@ -13,13 +26,22 @@ pip install maybeai-sheet-cli
13
26
  Upgrade to the latest release:
14
27
 
15
28
  ```bash
16
- maybeai-sheet update
29
+ maybeai-sheet update # when installed via pip or pipx
30
+ pipx upgrade maybeai-sheet-cli # alternative on pipx installs
17
31
  ```
18
32
 
19
33
  Install a specific version:
20
34
 
21
35
  ```bash
22
- maybeai-sheet update --version 0.2.0
36
+ maybeai-sheet update --version 0.3.0
37
+ pipx install maybeai-sheet-cli==0.3.0 --force # pipx reinstall pin
38
+ ```
39
+
40
+ Uninstall:
41
+
42
+ ```bash
43
+ pipx uninstall maybeai-sheet-cli
44
+ # not: pip uninstall -U (-U is invalid for uninstall)
23
45
  ```
24
46
 
25
47
  ## Requirements
@@ -68,12 +90,30 @@ Create a workbook:
68
90
  maybeai-sheet workbook create --title "Board Pack"
69
91
  ```
70
92
 
93
+ Upload a workbook-style `.xlsx` through the default Excelize path:
94
+
95
+ ```bash
96
+ maybeai-sheet workbook create-from-file ./report.xlsx
97
+ ```
98
+
99
+ Import a large table-like `.xlsx` into SheetTable/PG:
100
+
101
+ ```bash
102
+ maybeai-sheet workbook create-from-file ./large-table.xlsx --engine postgres
103
+ ```
104
+
71
105
  Append rows and verify:
72
106
 
73
107
  ```bash
74
108
  maybeai-sheet sheet append --doc-id abc123 --gid 3 --rows rows.json --verify
75
109
  ```
76
110
 
111
+ Make a workbook public for viewers:
112
+
113
+ ```bash
114
+ maybeai-sheet share visibility --doc-id abc123 --visibility public --public-permission viewer
115
+ ```
116
+
77
117
  ## Commands
78
118
 
79
119
  - `update`
@@ -81,6 +121,11 @@ maybeai-sheet sheet append --doc-id abc123 --gid 3 --rows rows.json --verify
81
121
  - `workbook create-from-file`
82
122
  - `workbook manifest`
83
123
  - `workbook capabilities`
124
+ - `share visibility`
125
+ - `share grant`
126
+ - `share permission`
127
+ - `share remove`
128
+ - `share list`
84
129
  - `sheet read`
85
130
  - `sheet read-range`
86
131
  - `sheet read-many`
@@ -32,4 +32,4 @@ Includes `test_sheet_worksheets_falls_back_to_v1_on_404`, `test_sheet_read_falls
32
32
  - **curl skill** — no automatic fallback; agent assembles raw HTTP
33
33
  - **MCP tools** — separate path; do not use CLI fallback
34
34
  - **Non-404 failures** — payload mismatch, auth, 5xx are not retried on v1
35
- - **Full v3 wrapper plan** — see `docs/finclaw-excel-api-v3-wrapper-plan.md`
35
+ - **Full v3 wrapper plan** — see `docs/maybeai_sheet/roadmap-excel-api-v3-wrapper.md`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "maybeai-sheet-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "CLI for common MaybeAI spreadsheet operations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.0"
@@ -6,6 +6,7 @@ from typing import Optional
6
6
  import typer
7
7
 
8
8
  from .commands.raw import app as raw_app
9
+ from .commands.share import app as share_app
9
10
  from .commands.sheet import app as sheet_app
10
11
  from .commands.workbook import app as workbook_app
11
12
  from .config import CLIContext
@@ -136,6 +137,7 @@ def update_cli(
136
137
 
137
138
  app.add_typer(workbook_app, name="workbook")
138
139
  app.add_typer(sheet_app, name="sheet")
140
+ app.add_typer(share_app, name="share")
139
141
  app.add_typer(raw_app, name="raw")
140
142
 
141
143
 
@@ -58,12 +58,21 @@ class MaybeAIClient:
58
58
  self._raise_for_error(response, endpoint=endpoint)
59
59
  return response.content
60
60
 
61
- def upload_file(self, path: str, file_path: Path, *, user_id: str | None = None) -> dict[str, Any]:
61
+ def upload_file(
62
+ self,
63
+ path: str,
64
+ file_path: Path,
65
+ *,
66
+ user_id: str | None = None,
67
+ engine: str | None = None,
68
+ ) -> dict[str, Any]:
62
69
  endpoint = self._endpoint(path)
63
70
  files = {"file": (file_path.name, file_path.open("rb"))}
64
71
  data: dict[str, str] = {}
65
72
  if user_id:
66
73
  data["user_id"] = user_id
74
+ if engine:
75
+ data["engine"] = engine
67
76
  try:
68
77
  with httpx.Client(timeout=self.state.timeout, trust_env=False) as client:
69
78
  response = client.post(
@@ -0,0 +1,344 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from ..client import MaybeAIClient
8
+ from ..config import CLIContext
9
+ from ..errors import UsageError
10
+ from ..formatters import build_command_output, handle_cli_error, render_output
11
+ from ..models.share import (
12
+ SheetPermissionRequest,
13
+ SheetPermissionUpdateRequest,
14
+ SheetRemoveAccessRequest,
15
+ SheetShareListRequest,
16
+ SheetVisibilityRequest,
17
+ )
18
+ from ..options import output_option, resolve_command_state, timeout_option, verbose_option
19
+ from ..resolver import ResolvedTarget, build_document_url, parse_document_target
20
+
21
+ app = typer.Typer(help="Sheet sharing and permission operations.", no_args_is_help=True)
22
+
23
+ _VISIBILITIES = frozenset({"private", "public"})
24
+ _PERMISSIONS = frozenset({"viewer", "editor"})
25
+
26
+
27
+ @app.callback()
28
+ def share_options(
29
+ ctx: typer.Context,
30
+ doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
31
+ url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
32
+ uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
33
+ gid: Optional[int] = typer.Option(None, "--gid", help="Optional permission scope gid."),
34
+ output: Optional[str] = output_option(),
35
+ verbose: Optional[bool] = verbose_option(),
36
+ timeout: Optional[float] = timeout_option(),
37
+ ) -> None:
38
+ ctx.obj = resolve_command_state(
39
+ ctx,
40
+ doc_id=doc_id,
41
+ url=url,
42
+ uri=uri,
43
+ gid=gid,
44
+ output=output,
45
+ verbose=verbose,
46
+ timeout=timeout,
47
+ )
48
+
49
+
50
+ def _normalize_choice(value: str, *, allowed: frozenset[str], name: str) -> str:
51
+ normalized = value.strip().lower()
52
+ if normalized not in allowed:
53
+ raise UsageError(f"{name} must be one of: {', '.join(sorted(allowed))}.")
54
+ return normalized
55
+
56
+
57
+ def _resolve_sheet_id(state: CLIContext, sheet_id: str | None) -> str:
58
+ if sheet_id:
59
+ return sheet_id
60
+ if state.doc_id:
61
+ return state.doc_id
62
+ if state.url:
63
+ return state.url
64
+ if state.uri:
65
+ return state.uri
66
+ raise UsageError("Provide one of --sheet-id, --doc-id, --url, or --uri.")
67
+
68
+
69
+ def _target_for_share(sheet_id: str, gid: int | None) -> ResolvedTarget:
70
+ parsed_doc_id, _ = parse_document_target(sheet_id)
71
+ document_id = parsed_doc_id
72
+ if document_id is None and not sheet_id.startswith(("http://", "https://")):
73
+ document_id = sheet_id
74
+ url = build_document_url(document_id) if document_id else None
75
+ uri = sheet_id if sheet_id.startswith(("http://", "https://")) else (url or sheet_id)
76
+ return ResolvedTarget(
77
+ document_id=document_id,
78
+ url=url,
79
+ uri=uri,
80
+ gid=gid,
81
+ worksheet_name=None,
82
+ )
83
+
84
+
85
+ @app.command("visibility")
86
+ def update_visibility(
87
+ ctx: typer.Context,
88
+ doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
89
+ url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
90
+ uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
91
+ sheet_id: Optional[str] = typer.Option(
92
+ None,
93
+ "--sheet-id",
94
+ help="Raw sheet document ID or Maybe Sheet URL. Overrides --doc-id/--url/--uri.",
95
+ ),
96
+ visibility: str = typer.Option(
97
+ ...,
98
+ "--visibility",
99
+ help="Sheet visibility: private or public.",
100
+ ),
101
+ public_permission: Optional[str] = typer.Option(
102
+ None,
103
+ "--public-permission",
104
+ help="Public access when visibility is public: viewer or editor.",
105
+ ),
106
+ output: Optional[str] = output_option(),
107
+ verbose: Optional[bool] = verbose_option(),
108
+ timeout: Optional[float] = timeout_option(),
109
+ ) -> None:
110
+ try:
111
+ state: CLIContext = resolve_command_state(
112
+ ctx,
113
+ doc_id=doc_id,
114
+ url=url,
115
+ uri=uri,
116
+ output=output,
117
+ verbose=verbose,
118
+ timeout=timeout,
119
+ )
120
+ normalized_visibility = _normalize_choice(
121
+ visibility,
122
+ allowed=_VISIBILITIES,
123
+ name="visibility",
124
+ )
125
+ normalized_permission = None
126
+ if public_permission is not None:
127
+ normalized_permission = _normalize_choice(
128
+ public_permission,
129
+ allowed=_PERMISSIONS,
130
+ name="public_permission",
131
+ )
132
+ if normalized_visibility == "public" and normalized_permission is None:
133
+ raise UsageError("--public-permission is required when --visibility is public.")
134
+
135
+ resolved_sheet_id = _resolve_sheet_id(state, sheet_id)
136
+ client = MaybeAIClient(state)
137
+ payload = SheetVisibilityRequest(
138
+ sheet_id=resolved_sheet_id,
139
+ visibility=normalized_visibility, # type: ignore[arg-type]
140
+ public_permission=normalized_permission, # type: ignore[arg-type]
141
+ )
142
+ endpoint = "/api/v1/share/sheet/visibility"
143
+ response = client.post_json(endpoint, payload.model_dump(exclude_none=True))
144
+ render_output(
145
+ build_command_output(
146
+ endpoint=endpoint,
147
+ target=_target_for_share(resolved_sheet_id, state.gid),
148
+ result=response,
149
+ ),
150
+ state.output,
151
+ )
152
+ except Exception as error: # pragma: no cover - CLI boundary
153
+ handle_cli_error(error)
154
+
155
+
156
+ @app.command("grant")
157
+ def grant_permission(
158
+ ctx: typer.Context,
159
+ doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
160
+ url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
161
+ uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
162
+ email: str = typer.Option(..., "--email", help="Existing MaybeAI user email."),
163
+ permission: str = typer.Option(..., "--permission", help="Permission: viewer or editor."),
164
+ sheet_id: Optional[str] = typer.Option(
165
+ None,
166
+ "--sheet-id",
167
+ help="Raw sheet document ID or Maybe Sheet URL. Overrides --doc-id/--url/--uri.",
168
+ ),
169
+ gid: Optional[int] = typer.Option(None, "--gid", help="Optional permission scope gid."),
170
+ output: Optional[str] = output_option(),
171
+ verbose: Optional[bool] = verbose_option(),
172
+ timeout: Optional[float] = timeout_option(),
173
+ ) -> None:
174
+ try:
175
+ state: CLIContext = resolve_command_state(
176
+ ctx,
177
+ doc_id=doc_id,
178
+ url=url,
179
+ uri=uri,
180
+ gid=gid,
181
+ output=output,
182
+ verbose=verbose,
183
+ timeout=timeout,
184
+ )
185
+ normalized_permission = _normalize_choice(
186
+ permission,
187
+ allowed=_PERMISSIONS,
188
+ name="permission",
189
+ )
190
+ resolved_sheet_id = _resolve_sheet_id(state, sheet_id)
191
+ client = MaybeAIClient(state)
192
+ payload = SheetPermissionUpdateRequest(
193
+ sheet_id=resolved_sheet_id,
194
+ email=email,
195
+ permission=normalized_permission, # type: ignore[arg-type]
196
+ gid=state.gid,
197
+ )
198
+ endpoint = "/api/v1/share/sheet/update-permission"
199
+ response = client.post_json(endpoint, payload.model_dump())
200
+ render_output(
201
+ build_command_output(
202
+ endpoint=endpoint,
203
+ target=_target_for_share(resolved_sheet_id, state.gid),
204
+ result=response,
205
+ ),
206
+ state.output,
207
+ )
208
+ except Exception as error: # pragma: no cover - CLI boundary
209
+ handle_cli_error(error)
210
+
211
+
212
+ @app.command("permission")
213
+ def current_permission(
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
+ sheet_id: Optional[str] = typer.Option(
219
+ None,
220
+ "--sheet-id",
221
+ help="Raw sheet document ID or Maybe Sheet URL. Overrides --doc-id/--url/--uri.",
222
+ ),
223
+ gid: Optional[int] = typer.Option(None, "--gid", help="Optional permission scope gid."),
224
+ output: Optional[str] = output_option(),
225
+ verbose: Optional[bool] = verbose_option(),
226
+ timeout: Optional[float] = timeout_option(),
227
+ ) -> None:
228
+ try:
229
+ state: CLIContext = resolve_command_state(
230
+ ctx,
231
+ doc_id=doc_id,
232
+ url=url,
233
+ uri=uri,
234
+ gid=gid,
235
+ output=output,
236
+ verbose=verbose,
237
+ timeout=timeout,
238
+ )
239
+ resolved_sheet_id = _resolve_sheet_id(state, sheet_id)
240
+ client = MaybeAIClient(state)
241
+ payload = SheetPermissionRequest(sheet_id=resolved_sheet_id, gid=state.gid)
242
+ endpoint = "/api/v1/share/sheet/permission"
243
+ response = client.post_json(endpoint, payload.model_dump())
244
+ render_output(
245
+ build_command_output(
246
+ endpoint=endpoint,
247
+ target=_target_for_share(resolved_sheet_id, state.gid),
248
+ result=response,
249
+ ),
250
+ state.output,
251
+ )
252
+ except Exception as error: # pragma: no cover - CLI boundary
253
+ handle_cli_error(error)
254
+
255
+
256
+ @app.command("remove")
257
+ def remove_access(
258
+ ctx: typer.Context,
259
+ doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
260
+ url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
261
+ uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
262
+ email: str = typer.Option(..., "--email", help="User email to remove from sharing."),
263
+ sheet_id: Optional[str] = typer.Option(
264
+ None,
265
+ "--sheet-id",
266
+ help="Raw sheet document ID or Maybe Sheet URL. Overrides --doc-id/--url/--uri.",
267
+ ),
268
+ gid: Optional[int] = typer.Option(None, "--gid", help="Optional permission scope gid."),
269
+ output: Optional[str] = output_option(),
270
+ verbose: Optional[bool] = verbose_option(),
271
+ timeout: Optional[float] = timeout_option(),
272
+ ) -> None:
273
+ try:
274
+ state: CLIContext = resolve_command_state(
275
+ ctx,
276
+ doc_id=doc_id,
277
+ url=url,
278
+ uri=uri,
279
+ gid=gid,
280
+ output=output,
281
+ verbose=verbose,
282
+ timeout=timeout,
283
+ )
284
+ resolved_sheet_id = _resolve_sheet_id(state, sheet_id)
285
+ client = MaybeAIClient(state)
286
+ payload = SheetRemoveAccessRequest(
287
+ sheet_id=resolved_sheet_id,
288
+ email=email,
289
+ gid=state.gid,
290
+ )
291
+ endpoint = "/api/v1/share/sheet/remove-access"
292
+ response = client.post_json(endpoint, payload.model_dump())
293
+ render_output(
294
+ build_command_output(
295
+ endpoint=endpoint,
296
+ target=_target_for_share(resolved_sheet_id, state.gid),
297
+ result=response,
298
+ ),
299
+ state.output,
300
+ )
301
+ except Exception as error: # pragma: no cover - CLI boundary
302
+ handle_cli_error(error)
303
+
304
+
305
+ @app.command("list")
306
+ def list_shares(
307
+ ctx: typer.Context,
308
+ doc_id: Optional[str] = typer.Option(None, "--doc-id", help="MaybeAI document ID."),
309
+ url: Optional[str] = typer.Option(None, "--url", help="MaybeAI workbook URL."),
310
+ uri: Optional[str] = typer.Option(None, "--uri", help="Resolved workbook URI."),
311
+ sheet_id: Optional[str] = typer.Option(
312
+ None,
313
+ "--sheet-id",
314
+ help="Raw sheet document ID or Maybe Sheet URL. Overrides --doc-id/--url/--uri.",
315
+ ),
316
+ output: Optional[str] = output_option(),
317
+ verbose: Optional[bool] = verbose_option(),
318
+ timeout: Optional[float] = timeout_option(),
319
+ ) -> None:
320
+ try:
321
+ state: CLIContext = resolve_command_state(
322
+ ctx,
323
+ doc_id=doc_id,
324
+ url=url,
325
+ uri=uri,
326
+ output=output,
327
+ verbose=verbose,
328
+ timeout=timeout,
329
+ )
330
+ resolved_sheet_id = _resolve_sheet_id(state, sheet_id)
331
+ client = MaybeAIClient(state)
332
+ payload = SheetShareListRequest(sheet_id=resolved_sheet_id)
333
+ endpoint = "/api/v1/share/sheet/list"
334
+ response = client.post_json(endpoint, payload.model_dump())
335
+ render_output(
336
+ build_command_output(
337
+ endpoint=endpoint,
338
+ target=_target_for_share(resolved_sheet_id, state.gid),
339
+ result=response,
340
+ ),
341
+ state.output,
342
+ )
343
+ except Exception as error: # pragma: no cover - CLI boundary
344
+ handle_cli_error(error)
@@ -7,6 +7,7 @@ import typer
7
7
 
8
8
  from ..client import MaybeAIClient
9
9
  from ..config import CLIContext
10
+ from ..errors import UsageError
10
11
  from ..formatters import build_command_output, handle_cli_error, load_json_file, render_output
11
12
  from ..models.workbook import CreateWorkbookRequest
12
13
  from ..options import output_option, resolve_command_state, timeout_option, verbose_option
@@ -15,6 +16,17 @@ from ..resolver import ResolvedTarget, parse_document_target, resolve_target
15
16
  app = typer.Typer(help="Workbook-level operations.", no_args_is_help=True)
16
17
 
17
18
 
19
+ def _normalize_create_from_file_engine(engine: str | None) -> str | None:
20
+ if engine is None:
21
+ return None
22
+ normalized = engine.strip().lower()
23
+ if normalized in {"", "excelize"}:
24
+ return None
25
+ if normalized in {"pg", "postgres", "postgresql"}:
26
+ return "postgres"
27
+ raise UsageError("Unsupported engine. Use 'excelize', 'postgres', or 'pg'.")
28
+
29
+
18
30
  @app.callback()
19
31
  def workbook_options(
20
32
  ctx: typer.Context,
@@ -113,6 +125,11 @@ def create_from_file(
113
125
  "--user-id",
114
126
  help="Optional compatibility field for upload.",
115
127
  ),
128
+ engine: Optional[str] = typer.Option(
129
+ None,
130
+ "--engine",
131
+ help="Creation engine: excelize (default) or postgres/pg for SheetTable.",
132
+ ),
116
133
  output: Optional[str] = output_option(),
117
134
  verbose: Optional[bool] = verbose_option(),
118
135
  timeout: Optional[float] = timeout_option(),
@@ -125,11 +142,20 @@ def create_from_file(
125
142
  timeout=timeout,
126
143
  )
127
144
  client = MaybeAIClient(state)
128
- endpoint = "/api/v1/excel/upload"
129
- response = client.upload_file(endpoint, file, user_id=user_id)
130
- spreadsheet_url = str(response.get("uri") or response.get("spreadsheet_url") or "")
145
+ resolved_engine = _normalize_create_from_file_engine(engine)
146
+ endpoint = "/api/v1/excel/import" if resolved_engine == "postgres" else "/api/v1/excel/upload"
147
+ response = client.upload_file(endpoint, file, user_id=user_id, engine=resolved_engine)
148
+ spreadsheet_url = str(
149
+ response.get("uri")
150
+ or response.get("fileUri")
151
+ or response.get("spreadsheet_url")
152
+ or ""
153
+ )
131
154
  response_doc_id = str(
132
- response.get("document_id") or response.get("spreadsheet_id") or ""
155
+ response.get("document_id")
156
+ or response.get("documentId")
157
+ or response.get("spreadsheet_id")
158
+ or ""
133
159
  ) or None
134
160
  _, response_gid = parse_document_target(spreadsheet_url)
135
161
  target = ResolvedTarget(
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+
8
+ SheetVisibility = Literal["private", "public"]
9
+ SheetPermission = Literal["viewer", "editor"]
10
+
11
+
12
+ class SheetVisibilityRequest(BaseModel):
13
+ model_config = ConfigDict(extra="forbid")
14
+
15
+ sheet_id: str
16
+ visibility: SheetVisibility
17
+ public_permission: SheetPermission | None = None
18
+
19
+
20
+ class SheetPermissionUpdateRequest(BaseModel):
21
+ model_config = ConfigDict(extra="forbid")
22
+
23
+ sheet_id: str
24
+ email: str
25
+ permission: SheetPermission
26
+ gid: int | None = None
27
+
28
+
29
+ class SheetPermissionRequest(BaseModel):
30
+ model_config = ConfigDict(extra="forbid")
31
+
32
+ sheet_id: str
33
+ gid: int | None = None
34
+
35
+
36
+ class SheetRemoveAccessRequest(BaseModel):
37
+ model_config = ConfigDict(extra="forbid")
38
+
39
+ sheet_id: str
40
+ email: str
41
+ gid: int | None = None
42
+
43
+
44
+ class SheetShareListRequest(BaseModel):
45
+ model_config = ConfigDict(extra="forbid")
46
+
47
+ sheet_id: str
@@ -5,7 +5,7 @@ import sys
5
5
  from pathlib import Path
6
6
  import tempfile
7
7
  import unittest
8
- from unittest.mock import patch
8
+ from unittest.mock import MagicMock, patch
9
9
 
10
10
  from typer.testing import CliRunner
11
11
 
@@ -15,6 +15,8 @@ if str(SRC) not in sys.path:
15
15
  sys.path.insert(0, str(SRC))
16
16
 
17
17
  from maybeai_sheet.cli import app
18
+ from maybeai_sheet.client import MaybeAIClient
19
+ from maybeai_sheet.config import CLIContext
18
20
 
19
21
 
20
22
  class CLITests(unittest.TestCase):
@@ -39,6 +41,76 @@ class CLITests(unittest.TestCase):
39
41
  self.assertEqual(payload["filename"], "Board Pack.xlsx")
40
42
  self.assertEqual(payload["data"], [{}])
41
43
 
44
+ def test_workbook_create_from_file_defaults_to_excelize_upload(self) -> None:
45
+ with tempfile.NamedTemporaryFile(suffix=".xlsx") as temp_file:
46
+ with patch("maybeai_sheet.commands.workbook.MaybeAIClient.upload_file") as upload_file:
47
+ upload_file.return_value = {
48
+ "success": True,
49
+ "uri": "https://www.maybe.ai/docs/spreadsheets/d/doc1?gid=0",
50
+ }
51
+ result = self.runner.invoke(
52
+ app,
53
+ ["workbook", "create-from-file", temp_file.name],
54
+ env=self.env,
55
+ )
56
+
57
+ self.assertEqual(result.exit_code, 0, result.output)
58
+ endpoint, file_path = upload_file.call_args.args
59
+ self.assertEqual(endpoint, "/api/v1/excel/upload")
60
+ self.assertEqual(Path(file_path).name, Path(temp_file.name).name)
61
+ self.assertIsNone(upload_file.call_args.kwargs.get("engine"))
62
+
63
+ def test_workbook_create_from_file_postgres_uses_import_endpoint(self) -> None:
64
+ with tempfile.NamedTemporaryFile(suffix=".xlsx") as temp_file:
65
+ with patch("maybeai_sheet.commands.workbook.MaybeAIClient.upload_file") as upload_file:
66
+ upload_file.return_value = {
67
+ "success": True,
68
+ "fileUri": "https://www.maybe.ai/docs/spreadsheets/d/doc1",
69
+ "documentId": "doc1",
70
+ }
71
+ result = self.runner.invoke(
72
+ app,
73
+ [
74
+ "workbook",
75
+ "create-from-file",
76
+ temp_file.name,
77
+ "--engine",
78
+ "postgres",
79
+ ],
80
+ env=self.env,
81
+ )
82
+
83
+ self.assertEqual(result.exit_code, 0, result.output)
84
+ endpoint, file_path = upload_file.call_args.args
85
+ self.assertEqual(endpoint, "/api/v1/excel/import")
86
+ self.assertEqual(Path(file_path).name, Path(temp_file.name).name)
87
+ self.assertEqual(upload_file.call_args.kwargs.get("engine"), "postgres")
88
+
89
+ def test_upload_file_sends_engine_in_multipart_form_data(self) -> None:
90
+ response = MagicMock()
91
+ response.status_code = 200
92
+ response.json.return_value = {"success": True}
93
+
94
+ http_client = MagicMock()
95
+ http_client.__enter__.return_value.post.return_value = response
96
+ http_client.__exit__.return_value = None
97
+
98
+ with tempfile.NamedTemporaryFile(suffix=".xlsx") as temp_file:
99
+ Path(temp_file.name).write_bytes(b"fake xlsx bytes")
100
+ with patch("maybeai_sheet.client.httpx.Client", return_value=http_client):
101
+ client = MaybeAIClient(
102
+ CLIContext(token="test-token", base_url="https://play-be.omnimcp.ai")
103
+ )
104
+ result = client.upload_file(
105
+ "/api/v1/excel/import",
106
+ Path(temp_file.name),
107
+ engine="postgres",
108
+ )
109
+
110
+ self.assertEqual(result, {"success": True})
111
+ post_kwargs = http_client.__enter__.return_value.post.call_args.kwargs
112
+ self.assertEqual(post_kwargs["data"], {"engine": "postgres"})
113
+
42
114
  def test_sheet_read_accepts_output_on_command(self) -> None:
43
115
  with patch("maybeai_sheet.commands.sheet.MaybeAIClient.post_json") as post_json:
44
116
  post_json.return_value = {"success": True, "values": [["Name"], ["Alice"]]}
@@ -538,6 +610,161 @@ class CLITests(unittest.TestCase):
538
610
  self.assertEqual(endpoint, "/api/v1/excel/write_new_worksheet")
539
611
  self.assertEqual(payload["worksheet_name"], "Summary")
540
612
 
613
+ def test_share_visibility_public_uses_v1_endpoint(self) -> None:
614
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
615
+ post_json.return_value = {"sheet_id": "abc123", "visibility": "public"}
616
+ result = self.runner.invoke(
617
+ app,
618
+ [
619
+ "share",
620
+ "visibility",
621
+ "--doc-id",
622
+ "abc123",
623
+ "--visibility",
624
+ "public",
625
+ "--public-permission",
626
+ "viewer",
627
+ ],
628
+ env=self.env,
629
+ )
630
+
631
+ self.assertEqual(result.exit_code, 0, result.output)
632
+ endpoint, payload = post_json.call_args.args
633
+ self.assertEqual(endpoint, "/api/v1/share/sheet/visibility")
634
+ self.assertEqual(payload["sheet_id"], "abc123")
635
+ self.assertEqual(payload["visibility"], "public")
636
+ self.assertEqual(payload["public_permission"], "viewer")
637
+
638
+ def test_share_visibility_private_omits_public_permission(self) -> None:
639
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
640
+ post_json.return_value = {"sheet_id": "abc123", "visibility": "private"}
641
+ result = self.runner.invoke(
642
+ app,
643
+ [
644
+ "share",
645
+ "visibility",
646
+ "--sheet-id",
647
+ "abc123",
648
+ "--visibility",
649
+ "private",
650
+ ],
651
+ env=self.env,
652
+ )
653
+
654
+ self.assertEqual(result.exit_code, 0, result.output)
655
+ endpoint, payload = post_json.call_args.args
656
+ self.assertEqual(endpoint, "/api/v1/share/sheet/visibility")
657
+ self.assertEqual(payload, {"sheet_id": "abc123", "visibility": "private"})
658
+
659
+ def test_share_visibility_public_requires_public_permission(self) -> None:
660
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
661
+ result = self.runner.invoke(
662
+ app,
663
+ [
664
+ "share",
665
+ "visibility",
666
+ "--doc-id",
667
+ "abc123",
668
+ "--visibility",
669
+ "public",
670
+ ],
671
+ env=self.env,
672
+ )
673
+
674
+ self.assertEqual(result.exit_code, 2, result.output)
675
+ self.assertIn("--public-permission is required", result.output)
676
+ post_json.assert_not_called()
677
+
678
+ def test_share_grant_uses_v1_endpoint_with_gid(self) -> None:
679
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
680
+ post_json.return_value = {"sheet_id": "abc123", "updated": True}
681
+ result = self.runner.invoke(
682
+ app,
683
+ [
684
+ "share",
685
+ "grant",
686
+ "--doc-id",
687
+ "abc123",
688
+ "--email",
689
+ "alice@example.com",
690
+ "--permission",
691
+ "editor",
692
+ "--gid",
693
+ "5",
694
+ ],
695
+ env=self.env,
696
+ )
697
+
698
+ self.assertEqual(result.exit_code, 0, result.output)
699
+ endpoint, payload = post_json.call_args.args
700
+ self.assertEqual(endpoint, "/api/v1/share/sheet/update-permission")
701
+ self.assertEqual(payload["sheet_id"], "abc123")
702
+ self.assertEqual(payload["email"], "alice@example.com")
703
+ self.assertEqual(payload["permission"], "editor")
704
+ self.assertEqual(payload["gid"], 5)
705
+
706
+ def test_share_permission_uses_v1_endpoint(self) -> None:
707
+ sheet_url = "https://www.maybe.ai/docs/spreadsheets/d/abc123"
708
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
709
+ post_json.return_value = {"sheet_id": "abc123", "access": "viewer"}
710
+ result = self.runner.invoke(
711
+ app,
712
+ [
713
+ "share",
714
+ "permission",
715
+ "--url",
716
+ sheet_url,
717
+ ],
718
+ env=self.env,
719
+ )
720
+
721
+ self.assertEqual(result.exit_code, 0, result.output)
722
+ endpoint, payload = post_json.call_args.args
723
+ self.assertEqual(endpoint, "/api/v1/share/sheet/permission")
724
+ self.assertEqual(payload, {"sheet_id": sheet_url, "gid": None})
725
+
726
+ def test_share_remove_uses_v1_endpoint(self) -> None:
727
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
728
+ post_json.return_value = {"sheet_id": "abc123", "removed": True}
729
+ result = self.runner.invoke(
730
+ app,
731
+ [
732
+ "share",
733
+ "remove",
734
+ "--doc-id",
735
+ "abc123",
736
+ "--email",
737
+ "alice@example.com",
738
+ ],
739
+ env=self.env,
740
+ )
741
+
742
+ self.assertEqual(result.exit_code, 0, result.output)
743
+ endpoint, payload = post_json.call_args.args
744
+ self.assertEqual(endpoint, "/api/v1/share/sheet/remove-access")
745
+ self.assertEqual(payload["sheet_id"], "abc123")
746
+ self.assertEqual(payload["email"], "alice@example.com")
747
+ self.assertIsNone(payload["gid"])
748
+
749
+ def test_share_list_uses_v1_endpoint(self) -> None:
750
+ with patch("maybeai_sheet.commands.share.MaybeAIClient.post_json") as post_json:
751
+ post_json.return_value = {"sheet_id": "abc123", "shares": []}
752
+ result = self.runner.invoke(
753
+ app,
754
+ [
755
+ "share",
756
+ "list",
757
+ "--doc-id",
758
+ "abc123",
759
+ ],
760
+ env=self.env,
761
+ )
762
+
763
+ self.assertEqual(result.exit_code, 0, result.output)
764
+ endpoint, payload = post_json.call_args.args
765
+ self.assertEqual(endpoint, "/api/v1/share/sheet/list")
766
+ self.assertEqual(payload, {"sheet_id": "abc123"})
767
+
541
768
 
542
769
  if __name__ == "__main__":
543
770
  unittest.main()
@@ -31,7 +31,7 @@ class UpdateTests(unittest.TestCase):
31
31
  "maybeai_sheet.update.version",
32
32
  side_effect=PackageNotFoundError("maybeai-sheet-cli"),
33
33
  ):
34
- self.assertEqual(installed_version(), "0.2.0")
34
+ self.assertEqual(installed_version(), "0.3.0")
35
35
 
36
36
  def test_build_pip_install_command_latest(self) -> None:
37
37
  command = build_pip_install_command(target_version=None)