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.
@@ -0,0 +1,5 @@
1
+ """MaybeAI Sheet CLI package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.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()
@@ -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)
@@ -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
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ maybeai-sheet = maybeai_sheet.cli:app