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.
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/PKG-INFO +48 -3
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/README.md +47 -2
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/docs/verification-matrix-fallback.md +1 -1
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/pyproject.toml +1 -1
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/__init__.py +1 -1
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/cli.py +2 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/client.py +10 -1
- maybeai_sheet_cli-0.3.0/src/maybeai_sheet/commands/share.py +344 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/workbook.py +30 -4
- maybeai_sheet_cli-0.3.0/src/maybeai_sheet/models/share.py +47 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_cli.py +228 -1
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_update.py +1 -1
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/.claude/settings.local.json +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/.gitignore +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/CLAUDE.md +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/cli-packaging-plan.md +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/cli-plan.md +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/__init__.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/raw.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/commands/sheet.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/config.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/errors.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/formatters.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/models/__init__.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/models/sheet.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/models/workbook.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/options.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/resolver.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/src/maybeai_sheet/update.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_formatters.py +0 -0
- {maybeai_sheet_cli-0.2.0 → maybeai_sheet_cli-0.3.0}/tests/test_resolver.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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/
|
|
35
|
+
- **Full v3 wrapper plan** — see `docs/maybeai_sheet/roadmap-excel-api-v3-wrapper.md`
|
|
@@ -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(
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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")
|
|
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.
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|