pulse-ai-cli 0.1.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.
@@ -0,0 +1,3 @@
1
+ .venv/
2
+ *.egg-info/
3
+ dist/
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: pulse-ai-cli
3
+ Version: 0.1.0
4
+ Summary: Pulse CLI — document extraction from your terminal (runpulse.com)
5
+ Project-URL: Homepage, https://runpulse.com
6
+ Project-URL: Documentation, https://docs.runpulse.com
7
+ License: Proprietary
8
+ Keywords: cli,document-extraction,ocr,pdf,pulse
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: click>=8
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: rich>=13.7
18
+ Requires-Dist: textual>=0.80
19
+ Requires-Dist: typer>=0.12
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8; extra == 'dev'
22
+ Requires-Dist: respx>=0.21; extra == 'dev'
23
+ Provides-Extra: keyring
24
+ Requires-Dist: keyring>=24; extra == 'keyring'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Pulse CLI
28
+
29
+ Document extraction from your terminal — markdown, structured JSON schemas,
30
+ splits, and tables, powered by the [Pulse](https://runpulse.com) API.
31
+
32
+ ```
33
+ uv tool install pulse-ai-cli
34
+ pulse login
35
+ pulse extract invoice.pdf --schema schemas/invoice.json
36
+ ```
37
+
38
+ - `pulse extract` — documents (PDF, Office, images, URLs) → markdown + JSON;
39
+ add `--schema` to pull structured fields in the same run
40
+ - `pulse schema generate / apply` — AI-draft a JSON Schema, run it against
41
+ any extraction
42
+ - `pulse split` / `pulse tables` — topic-based splitting, table extraction
43
+ - `pulse jobs` — watch, resume, or cancel async jobs
44
+ - `pulse usage` — plan and page/credit usage
45
+ - bare `pulse` — interactive shell with the full command set
46
+
47
+ Every command supports `--json` for pipe-safe output and `-h` for full
48
+ options. See the [docs](https://docs.runpulse.com) for the complete
49
+ reference.
@@ -0,0 +1,23 @@
1
+ # Pulse CLI
2
+
3
+ Document extraction from your terminal — markdown, structured JSON schemas,
4
+ splits, and tables, powered by the [Pulse](https://runpulse.com) API.
5
+
6
+ ```
7
+ uv tool install pulse-ai-cli
8
+ pulse login
9
+ pulse extract invoice.pdf --schema schemas/invoice.json
10
+ ```
11
+
12
+ - `pulse extract` — documents (PDF, Office, images, URLs) → markdown + JSON;
13
+ add `--schema` to pull structured fields in the same run
14
+ - `pulse schema generate / apply` — AI-draft a JSON Schema, run it against
15
+ any extraction
16
+ - `pulse split` / `pulse tables` — topic-based splitting, table extraction
17
+ - `pulse jobs` — watch, resume, or cancel async jobs
18
+ - `pulse usage` — plan and page/credit usage
19
+ - bare `pulse` — interactive shell with the full command set
20
+
21
+ Every command supports `--json` for pipe-safe output and `-h` for full
22
+ options. See the [docs](https://docs.runpulse.com) for the complete
23
+ reference.
@@ -0,0 +1,201 @@
1
+ # Pulse CLI
2
+
3
+ Extract documents, apply schemas, split, and pull tables from your terminal.
4
+ The CLI talks to the same Pulse API as the platform and MCP server — your API
5
+ key is the only credential.
6
+
7
+ ```
8
+ pulse extract invoice.pdf --schema schemas/invoice.json
9
+ ```
10
+
11
+ ## Install
12
+
13
+ ```
14
+ uv tool install pulse-ai-cli
15
+ ```
16
+
17
+ (or `pip install pulse-ai-cli`). Installs the `pulse` command. Requires
18
+ Python 3.11+.
19
+
20
+ ## Authentication
21
+
22
+ ```
23
+ pulse login
24
+ ```
25
+
26
+ Opens the Pulse platform in your browser; you approve a short code (e.g.
27
+ `ABCD-1234`) while signed in, and the CLI picks up your organization's API
28
+ key automatically (OAuth 2.1 device flow, RFC 8628). The key is validated
29
+ and stored in your OS keychain (or `~/.pulse/config.toml`, chmod 0600, if no
30
+ keychain is available).
31
+
32
+ No browser handy? `pulse login --api-key <key>` (or `pulse login --no-web`
33
+ to be prompted) logs in with an API key directly — find yours in the Pulse
34
+ platform under API Keys.
35
+
36
+ Alternatives, in priority order:
37
+
38
+ 1. `--api-key <key>` on any command
39
+ 2. `PULSE_API_KEY` environment variable (recommended for CI)
40
+ 3. stored credential from `pulse login`
41
+
42
+ Related commands:
43
+
44
+ | command | what it does |
45
+ |---|---|
46
+ | `pulse whoami` | show the active (redacted) key, engine URL, and connectivity |
47
+ | `pulse logout` | remove the stored key |
48
+
49
+ `pulse login --base-url <url>` points the CLI at a different engine
50
+ (e.g. a VPC deployment). `--platform-url <url>` overrides the platform
51
+ (default `https://platform.runpulse.com`) used for browser login and
52
+ `pulse usage`.
53
+
54
+ ## Commands
55
+
56
+ ### `pulse extract <file|dir|url>…`
57
+
58
+ Convert documents to markdown + structured JSON. Accepts any mix of file
59
+ paths, directories (recursed for supported types), and URLs.
60
+
61
+ Writes `<name>.pulse.md` and `<name>.pulse.json` next to each input
62
+ (`-o <dir>` redirects; URL inputs land in the current directory).
63
+
64
+ | flag | description |
65
+ |---|---|
66
+ | `--schema <file or inline JSON>` | pipeline: extract, then apply this JSON Schema — structured output is written to `<name>.pulse.schema.json` |
67
+ | `--pages "1-2,5"` | page range (1-indexed) |
68
+ | `--model <name>` | model override (`default`, `pulse-ultra-2`) |
69
+ | `--html` | also return an HTML rendition |
70
+ | `--footnotes` | link footnote markers to their text |
71
+ | `--figure-descriptions` | caption figures and visuals |
72
+ | `--show-images` | return image URLs for visuals |
73
+ | `--chunking semantic,page` | chunk types: `semantic`, `header`, `page`, `recursive` |
74
+ | `--chunk-size <n>` | max characters per chunk |
75
+ | `-o, --output <dir>` | directory for result files |
76
+ | `--no-wait` | submit and print the `job_id` without polling |
77
+ | `--json` | print the full JSON result to stdout, write no files |
78
+
79
+ Examples:
80
+
81
+ ```
82
+ pulse extract report.pdf
83
+ pulse extract invoice.pdf --schema '{"type":"object","properties":{"total":{"type":"number"}}}'
84
+ pulse extract ./contracts -o ./results --no-wait
85
+ pulse extract https://example.com/10k.pdf --pages "1-15" --chunking semantic
86
+ ```
87
+
88
+ Files up to 50 MB are uploaded directly; larger documents should be passed
89
+ by URL.
90
+
91
+ ### `pulse schema`
92
+
93
+ Structured extraction against a *previous* extraction (use `extract --schema` to do
94
+ both in one command).
95
+
96
+ `pulse schema generate` — AI-draft a JSON Schema:
97
+
98
+ ```
99
+ pulse schema generate -p "vendor name, line items, total due" -o invoice.json
100
+ pulse schema generate --from invoice.json -p "also capture the PO number"
101
+ ```
102
+
103
+ `pulse schema apply` — run a schema against extracted content:
104
+
105
+ ```
106
+ pulse schema apply <extraction_id> --schema invoice.json
107
+ ```
108
+
109
+ | flag | description |
110
+ |---|---|
111
+ | `--schema <file>` | JSON Schema file |
112
+ | `-p, --prompt <text>` | extra extraction guidance (works with or without `--schema`) |
113
+ | `--extraction-ids a,b,c` | combine several extractions |
114
+ | `--split-id <id>` | apply per-topic schemas to a split |
115
+ | `--split-schema-config <file>` | per-topic schema mapping (with `--split-id`) |
116
+ | `--schema-config-id <id>` | use a saved platform schema config |
117
+ | `--effort` | higher-effort extraction |
118
+ | `--pages "1-3"` | restrict to a page range |
119
+ | `-o <file>` | write result JSON to a file (default: stdout) |
120
+
121
+ ### `pulse split <extraction_id>`
122
+
123
+ Split a document into sections by topic:
124
+
125
+ ```
126
+ pulse split <extraction_id> -t "introduction:opening section" -t "financials:tables and figures"
127
+ ```
128
+
129
+ `-t "name:description"` is repeatable; `--topics-file <json>` and
130
+ `--split-config-id <id>` are alternatives. Returns per-topic page ranges and
131
+ content; the resulting `split_id` can feed `pulse schema apply --split-id`.
132
+
133
+ ### `pulse tables <extraction_id>`
134
+
135
+ Extract tables from a prior extraction:
136
+
137
+ ```
138
+ pulse tables <extraction_id> --format html
139
+ ```
140
+
141
+ `--format html|json`, `--merge` (join tables spanning pages),
142
+ `--charts-to-tables`, `-o <file>`.
143
+
144
+ ### `pulse jobs`
145
+
146
+ Every long-running command runs async under the hood and polls for you.
147
+ `--no-wait` (or Ctrl-C while polling — the job keeps running server-side)
148
+ hands control back; resume any time:
149
+
150
+ ```
151
+ pulse jobs get <job_id> --watch
152
+ pulse jobs cancel <job_id>
153
+ ```
154
+
155
+ ### `pulse open`
156
+
157
+ Jump from the terminal to the platform:
158
+
159
+ ```
160
+ pulse open <extraction_id>
161
+ pulse open
162
+ ```
163
+
164
+ Opens the extraction (or the dashboard) at `platform.runpulse.com` in your
165
+ browser.
166
+
167
+ ### `pulse usage`
168
+
169
+ Plan and usage for the current billing period — pages used vs allowance, plus
170
+ a breakdown (documents, pages, latency, by source):
171
+
172
+ ```
173
+ pulse usage
174
+ pulse usage --days 7
175
+ pulse usage --json
176
+ ```
177
+
178
+ Requires a platform URL (`pulse login --platform-url …` or
179
+ `PULSE_PLATFORM_URL`).
180
+
181
+ ### `pulse dashboard`
182
+
183
+ Full-screen status view (account, usage). Bare `pulse` in a terminal opens
184
+ the interactive shell.
185
+
186
+ ## Output conventions
187
+
188
+ - Human-readable progress goes to the terminal; `--json` switches any command
189
+ to raw, pipe-safe JSON on stdout.
190
+ - File-writing commands echo every path they write, plus the `extraction_id`
191
+ you'll need for follow-up commands.
192
+ - Exit codes: `0` success, `1` failure (any document in a batch failing),
193
+ `130` interrupted (job still running server-side).
194
+
195
+ ## Roadmap
196
+
197
+ - `extract --split … --schema …` end-to-end pipeline (split a document, then apply
198
+ a schema per topic in one command).
199
+ - Platform `/activate` approval page for `pulse login --web` (the API-side
200
+ device flow is live; approval UI is rolling out).
201
+ - Full OAuth token auth for the MCP server (auth-code + PKCE, DCR).
@@ -0,0 +1,3 @@
1
+ """Pulse CLI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from pulse_cli.app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,103 @@
1
+ """Pulse CLI root app."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+
6
+ from pulse_cli import __version__
7
+ from pulse_cli.commands.auth import login, logout, whoami
8
+ from pulse_cli.commands.extract import extract
9
+ from pulse_cli.commands.jobs import jobs_app
10
+ from pulse_cli.commands.open_platform import open_platform
11
+ from pulse_cli.commands.schema import schema_app
12
+ from pulse_cli.commands.split import split
13
+ from pulse_cli.commands.tables import tables
14
+ from pulse_cli.commands.usage import usage
15
+ from pulse_cli.errors import PulseCliError
16
+ from pulse_cli.output import err_console
17
+
18
+ # typer joins epilog paragraphs (separated by blank lines) with single
19
+ # newlines, so each example below renders on its own line.
20
+ _EXAMPLES = [
21
+ ("pulse extract report.pdf", "document -> report.pulse.md / .json"),
22
+ ("pulse extract invoice.pdf --schema schema.json", "extract + structured fields in one go"),
23
+ ("pulse extract ./docs --no-wait", "submit a folder, fetch results later"),
24
+ ('pulse schema generate -p "vendor, total, date"', "AI-draft a JSON schema"),
25
+ ("pulse schema apply <extraction_id> --schema s.json", "structured fields from a past extract"),
26
+ ('pulse split <extraction_id> -t "intro:first part"', "split a document by topic"),
27
+ ("pulse tables <extraction_id>", "pull out tables"),
28
+ ("pulse jobs get <job_id> --watch", "watch or resume an async job"),
29
+ ("pulse open <extraction_id>", "view an extraction on the platform"),
30
+ ("pulse usage", "plan + credit/page usage"),
31
+ ]
32
+
33
+ _EPILOG = "\n\n".join(
34
+ ["[bold]Examples[/bold]"]
35
+ + [f"[cyan]{cmd:<52}[/cyan] [dim]{desc}[/dim]" for cmd, desc in _EXAMPLES]
36
+ + [
37
+ "",
38
+ "Add [cyan]--json[/cyan] to any command for raw output, [cyan]-o[/cyan] to choose "
39
+ "where files go, and [cyan]-h[/cyan] after any command for its full options.",
40
+ ]
41
+ )
42
+
43
+ cli = typer.Typer(
44
+ name="pulse",
45
+ help="Pulse — document extraction from your terminal (runpulse.com).",
46
+ epilog=_EPILOG,
47
+ rich_markup_mode="rich",
48
+ context_settings={"help_option_names": ["-h", "--help"]},
49
+ )
50
+
51
+
52
+ @cli.callback(invoke_without_command=True)
53
+ def _root(ctx: typer.Context) -> None:
54
+ """Bare `pulse` in a terminal opens the interactive shell; otherwise show help."""
55
+ if ctx.invoked_subcommand is None:
56
+ import sys
57
+
58
+ if sys.stdin.isatty() and sys.stdout.isatty():
59
+ from pulse_cli.repl import run_repl
60
+
61
+ run_repl()
62
+ else:
63
+ print(ctx.get_help())
64
+ raise typer.Exit()
65
+
66
+
67
+ @cli.command()
68
+ def dashboard() -> None:
69
+ """Open the Pulse dashboard (account + usage)."""
70
+ from pulse_cli.tui.dashboard import run_dashboard
71
+
72
+ run_dashboard()
73
+
74
+
75
+ cli.command()(login)
76
+ cli.command()(logout)
77
+ cli.command()(whoami)
78
+ cli.command()(extract)
79
+ cli.command()(split)
80
+ cli.command()(tables)
81
+ cli.command()(usage)
82
+ cli.command("open")(open_platform)
83
+ cli.add_typer(schema_app, name="schema")
84
+ cli.add_typer(jobs_app, name="jobs")
85
+
86
+
87
+ @cli.command()
88
+ def version() -> None:
89
+ """Print the CLI version."""
90
+ print(f"pulse-cli {__version__}")
91
+
92
+
93
+ def app() -> None:
94
+ """Console-script entry point with clean error rendering."""
95
+ try:
96
+ cli()
97
+ except PulseCliError as exc:
98
+ err_console.print(f"[err]error:[/err] {exc}")
99
+ raise SystemExit(1)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ app()
@@ -0,0 +1,197 @@
1
+ """Sync HTTP client for the Pulse engine — a CLI port of mcp_server/pulse_client.py.
2
+
3
+ Same endpoints, same payload shapes, same submit -> poll semantics, so the CLI
4
+ and MCP gateway behave identically against the engine. Auth is the caller's API
5
+ key forwarded verbatim as ``x-api-key``; the engine enforces auth/credits/limits.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ from typing import Any, Callable, Optional
12
+
13
+ import httpx
14
+
15
+ from pulse_cli import __version__
16
+ from pulse_cli.errors import PulseEngineError
17
+ from pulse_cli.status import is_terminal
18
+
19
+ MAX_UPLOAD_BYTES = 50 * 1024 * 1024 # engine upload cap
20
+
21
+ DEFAULT_TIMEOUT = 120.0
22
+ SCHEMA_HELPER_TIMEOUT = 240.0 # /ai-schema-helper is sync and can run long
23
+ POLL_INTERVAL_SECONDS = 2.0
24
+
25
+ # (status, poll_count, job_dict) -> None
26
+ ProgressCallback = Callable[[str, int, dict], None]
27
+
28
+
29
+ def _form_value(v: Any) -> str:
30
+ if isinstance(v, bool):
31
+ return "true" if v else "false"
32
+ if isinstance(v, (dict, list)):
33
+ return json.dumps(v)
34
+ return str(v)
35
+
36
+
37
+ class PulseClient:
38
+ def __init__(self, api_key: str, base_url: str, *, transport: Optional[httpx.BaseTransport] = None) -> None:
39
+ self._client = httpx.Client(
40
+ base_url=base_url.rstrip("/"),
41
+ timeout=DEFAULT_TIMEOUT,
42
+ headers={"x-api-key": api_key, "user-agent": f"pulse-cli/{__version__}"},
43
+ transport=transport,
44
+ )
45
+
46
+ def __enter__(self) -> "PulseClient":
47
+ return self
48
+
49
+ def __exit__(self, *exc: Any) -> None:
50
+ self._client.close()
51
+
52
+ # --- core -----------------------------------------------------------------
53
+
54
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
55
+ try:
56
+ resp = self._client.request(method, path, **kwargs)
57
+ except httpx.TimeoutException as exc:
58
+ raise PulseEngineError(504, engine_message="The Pulse engine took too long to respond.") from exc
59
+ except httpx.RequestError as exc:
60
+ raise PulseEngineError(
61
+ 502, engine_message=f"Could not reach the Pulse engine at {self._client.base_url}."
62
+ ) from exc
63
+
64
+ if resp.is_success:
65
+ if "application/json" in resp.headers.get("content-type", ""):
66
+ return resp.json()
67
+ return resp.text
68
+ try:
69
+ body: Any = resp.json()
70
+ except Exception:
71
+ body = resp.text
72
+ raise PulseEngineError.from_response(resp.status_code, body)
73
+
74
+ # --- endpoints (parity with mcp_server) ------------------------------------
75
+
76
+ def get_job(self, job_id: str) -> dict:
77
+ return self._request("GET", f"/job/{job_id}")
78
+
79
+ def cancel_job(self, job_id: str) -> dict:
80
+ return self._request("POST", f"/cancel/{job_id}")
81
+
82
+ def submit_extract(self, payload: dict) -> dict:
83
+ """POST /extract (JSON) — for file_url inputs; set async=True for a job_id."""
84
+ return self._request("POST", "/extract", json=payload)
85
+
86
+ def submit_extract_multipart(self, file: tuple, options: dict) -> dict:
87
+ """POST /extract (multipart) — local file bytes + form options."""
88
+ data = {k: _form_value(v) for k, v in options.items() if v is not None}
89
+ return self._request("POST", "/extract", data=data, files={"file": file})
90
+
91
+ def apply_schema(self, payload: dict) -> dict:
92
+ return self._request("POST", "/schema", json=payload)
93
+
94
+ def split(self, payload: dict) -> dict:
95
+ return self._request("POST", "/split", json=payload)
96
+
97
+ def tables(self, payload: dict) -> dict:
98
+ return self._request("POST", "/tables", json=payload)
99
+
100
+ def generate_schema(self, *, prompt: Optional[str] = None, current_schema: Optional[str] = None) -> dict:
101
+ data: dict[str, str] = {}
102
+ if prompt is not None:
103
+ data["prompt"] = prompt
104
+ if current_schema is not None:
105
+ data["current_schema"] = current_schema
106
+ return self._request("POST", "/ai-schema-helper", data=data, timeout=SCHEMA_HELPER_TIMEOUT)
107
+
108
+ def pages(self) -> dict:
109
+ """GET /pages — total pages processed (api-key safe; also used to validate keys)."""
110
+ return self._request("GET", "/pages")
111
+
112
+ # --- polling ----------------------------------------------------------------
113
+
114
+ def wait_for_job(
115
+ self,
116
+ job_id: str,
117
+ *,
118
+ interval_seconds: float = POLL_INTERVAL_SECONDS,
119
+ on_progress: Optional[ProgressCallback] = None,
120
+ ) -> dict:
121
+ """Poll GET /job/{job_id} until terminal. The CLI polls indefinitely
122
+ (unlike MCP's inline budget) — Ctrl-C leaves the job running server-side."""
123
+ polls = 0
124
+ job = self.get_job(job_id)
125
+ while not is_terminal(job.get("status")):
126
+ polls += 1
127
+ if on_progress is not None:
128
+ on_progress(job.get("status", ""), polls, job)
129
+ time.sleep(interval_seconds)
130
+ job = self.get_job(job_id)
131
+ return job
132
+
133
+ def fetch_url(self, url: str) -> Any:
134
+ """Download an offloaded {is_url, url} result (pre-signed S3)."""
135
+ resp = httpx.get(url, timeout=DEFAULT_TIMEOUT, follow_redirects=True)
136
+ if not resp.is_success:
137
+ raise PulseEngineError(resp.status_code, engine_message="Failed to download result from URL.")
138
+ if "application/json" in resp.headers.get("content-type", ""):
139
+ return resp.json()
140
+ try:
141
+ return json.loads(resp.text)
142
+ except json.JSONDecodeError:
143
+ return resp.text
144
+
145
+
146
+ class PlatformClient:
147
+ """Minimal client for the FastAPI service (api-key-safe, read-only endpoints)."""
148
+
149
+ def __init__(self, api_key: str, base_url: str) -> None:
150
+ self._client = httpx.Client(
151
+ base_url=base_url.rstrip("/"),
152
+ timeout=30.0,
153
+ headers={"x-api-key": api_key, "user-agent": f"pulse-cli/{__version__}"},
154
+ )
155
+
156
+ def __enter__(self) -> "PlatformClient":
157
+ return self
158
+
159
+ def __exit__(self, *exc: Any) -> None:
160
+ self._client.close()
161
+
162
+ def _get(self, path: str, params: Optional[dict] = None) -> dict:
163
+ try:
164
+ resp = self._client.get(path, params=params)
165
+ except httpx.TimeoutException as exc:
166
+ raise PulseEngineError(504, engine_message="The Pulse platform took too long to respond.") from exc
167
+ except httpx.RequestError as exc:
168
+ raise PulseEngineError(
169
+ 502, engine_message=f"Could not reach the Pulse platform at {self._client.base_url}."
170
+ ) from exc
171
+ if resp.is_success:
172
+ return resp.json()
173
+ try:
174
+ body: Any = resp.json()
175
+ except Exception:
176
+ body = resp.text
177
+ raise PulseEngineError.from_response(resp.status_code, body)
178
+
179
+ def subscription(self) -> Optional[dict]:
180
+ """GET /api/v1/billing/subscription — plan/status; None if unreachable."""
181
+ try:
182
+ return self._get("/api/v1/billing/subscription")
183
+ except PulseEngineError:
184
+ return None
185
+
186
+ def current_period_usage(self) -> dict:
187
+ """GET /api/v1/analytics/usage/current-period — plan + pages for the billing cycle."""
188
+ return self._get("/api/v1/analytics/usage/current-period")
189
+
190
+ def usage_breakdown(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> dict:
191
+ """GET /api/v1/analytics/usage/breakdown — docs/pages/latency, by source.
192
+
193
+ Without dates the platform defaults to the current billing period
194
+ (or lifetime for lifetime plans).
195
+ """
196
+ params = {k: v for k, v in (("start_date", start_date), ("end_date", end_date)) if v}
197
+ return self._get("/api/v1/analytics/usage/breakdown", params=params or None)
File without changes