jabb-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.
- jabb_cli/__init__.py +0 -0
- jabb_cli/cli.py +77 -0
- jabb_cli/client.py +56 -0
- jabb_cli/output.py +18 -0
- jabb_cli-0.1.0.dist-info/METADATA +9 -0
- jabb_cli-0.1.0.dist-info/RECORD +9 -0
- jabb_cli-0.1.0.dist-info/WHEEL +5 -0
- jabb_cli-0.1.0.dist-info/entry_points.txt +2 -0
- jabb_cli-0.1.0.dist-info/top_level.txt +1 -0
jabb_cli/__init__.py
ADDED
|
File without changes
|
jabb_cli/cli.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json as jsonlib
|
|
3
|
+
import click
|
|
4
|
+
from jabb_cli.client import ApiClient, AuthError, RateLimitError, ApiError
|
|
5
|
+
from jabb_cli.output import render_table
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
@click.option("--json", "json_mode", is_flag=True, help="Print raw JSON envelope instead of a table. Use this when piping output to another program or an agent.")
|
|
10
|
+
@click.option("--detailed", is_flag=True, help="Request response_format=detailed (more fields per row). Costs more tokens — use only when concise fields are insufficient.")
|
|
11
|
+
@click.pass_context
|
|
12
|
+
def main(ctx, json_mode, detailed):
|
|
13
|
+
"""jabb — CLI for JABB Public API v1 (locations, scores, reviews, pnif, sentiment).
|
|
14
|
+
|
|
15
|
+
Auth: set JABB_API_KEY to a key minted by your JABB admin.
|
|
16
|
+
"""
|
|
17
|
+
ctx.ensure_object(dict)
|
|
18
|
+
ctx.obj["json"] = json_mode
|
|
19
|
+
ctx.obj["detailed"] = detailed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_resource(ctx, path):
|
|
23
|
+
params = {"limit": ctx.obj.get("limit", 20)}
|
|
24
|
+
if ctx.obj.get("cursor"):
|
|
25
|
+
params["cursor"] = ctx.obj["cursor"]
|
|
26
|
+
params["response_format"] = "detailed" if ctx.obj["detailed"] else "concise"
|
|
27
|
+
try:
|
|
28
|
+
client = ApiClient()
|
|
29
|
+
result = client.get(path, params=params)
|
|
30
|
+
except RateLimitError as e:
|
|
31
|
+
click.echo(jsonlib.dumps({"error": {"code": e.code, "message": e.message, "hint": e.hint}}) if ctx.obj["json"] else f"Rate limited: {e.message}. Retry after {e.retry_after}s. {e.hint}", err=True)
|
|
32
|
+
sys.exit(3)
|
|
33
|
+
except AuthError as e:
|
|
34
|
+
click.echo(jsonlib.dumps({"error": {"code": e.code, "message": e.message, "hint": e.hint}}) if ctx.obj["json"] else f"Auth error: {e.message}. {e.hint}", err=True)
|
|
35
|
+
sys.exit(2)
|
|
36
|
+
except ApiError as e:
|
|
37
|
+
click.echo(jsonlib.dumps({"error": {"code": e.code, "message": e.message, "hint": e.hint}}) if ctx.obj["json"] else f"API error: {e.message}. {e.hint}", err=True)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
if ctx.obj["json"]:
|
|
41
|
+
click.echo(jsonlib.dumps(result))
|
|
42
|
+
else:
|
|
43
|
+
data = result["data"]
|
|
44
|
+
rows = data if isinstance(data, list) else [data]
|
|
45
|
+
click.echo(render_table(rows))
|
|
46
|
+
if result.get("next_cursor"):
|
|
47
|
+
click.echo(f"\n(more results: --cursor {result['next_cursor']})", err=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
RESOURCES = {
|
|
51
|
+
"locations": ("/api/public/v1/locations", "Business locations. Call when the agent needs a store/venue list or IDs to cross-reference other resources."),
|
|
52
|
+
"scores": ("/api/public/v1/cx-scores", "CX scores per entity. Call when the agent needs quantitative quality metrics, not raw review text."),
|
|
53
|
+
"reviews": ("/api/public/v1/quick-jabbs", "Quick JABB evaluations (reviews). Call when the agent needs individual review content/ratings."),
|
|
54
|
+
"pnif": ("/api/public/v1/pnif", "PNIF analysis. Call when the agent needs the Price/Need/Impact/Frequency breakdown, not raw scores."),
|
|
55
|
+
"sentiment": ("/api/public/v1/sentiment", "Sentiment mapping. Call when the agent needs sentiment labels/trends, not raw scores or reviews."),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _make_group(name, path, help_text):
|
|
60
|
+
@click.group(name=name, help=help_text)
|
|
61
|
+
def group():
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@group.command(name="list", help=f"List {name} (paginated). {help_text}")
|
|
65
|
+
@click.option("--cursor", default=None, help="Opaque pagination cursor from a previous response's next_cursor.")
|
|
66
|
+
@click.option("--limit", default=20, type=click.IntRange(1, 100), help="Rows per page (1-100, default 20). Keep low for agent contexts.")
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def list_cmd(ctx, cursor, limit):
|
|
69
|
+
ctx.obj["cursor"] = cursor
|
|
70
|
+
ctx.obj["limit"] = limit
|
|
71
|
+
list_resource(ctx, path)
|
|
72
|
+
|
|
73
|
+
return group
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
for _name, (_path, _help) in RESOURCES.items():
|
|
77
|
+
main.add_command(_make_group(_name, _path, _help))
|
jabb_cli/client.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
DEFAULT_BASE_URL = "https://api.jabb.cx"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ApiError(Exception):
|
|
8
|
+
def __init__(self, code, message, hint):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.code = code
|
|
11
|
+
self.message = message
|
|
12
|
+
self.hint = hint
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthError(ApiError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RateLimitError(ApiError):
|
|
20
|
+
def __init__(self, code, message, hint, retry_after):
|
|
21
|
+
super().__init__(code, message, hint)
|
|
22
|
+
self.retry_after = retry_after
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApiClient:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
key = os.environ.get("JABB_API_KEY")
|
|
28
|
+
if not key:
|
|
29
|
+
raise AuthError(
|
|
30
|
+
"missing_api_key",
|
|
31
|
+
"JABB_API_KEY is not set.",
|
|
32
|
+
"export JABB_API_KEY=jabb_live_... (mint one via your JABB admin).",
|
|
33
|
+
)
|
|
34
|
+
self.key = key
|
|
35
|
+
self.base_url = os.environ.get("JABB_API_BASE_URL", DEFAULT_BASE_URL)
|
|
36
|
+
|
|
37
|
+
def get(self, path, params=None):
|
|
38
|
+
resp = requests.request(
|
|
39
|
+
"GET",
|
|
40
|
+
url=f"{self.base_url}{path}",
|
|
41
|
+
headers={"Authorization": f"Bearer {self.key}"},
|
|
42
|
+
params=params or {},
|
|
43
|
+
timeout=15,
|
|
44
|
+
)
|
|
45
|
+
if resp.status_code == 200:
|
|
46
|
+
return resp.json()
|
|
47
|
+
try:
|
|
48
|
+
err = resp.json().get("error", {})
|
|
49
|
+
except (ValueError, KeyError):
|
|
50
|
+
raise ApiError("unknown", f"HTTP {resp.status_code}", "Server returned non-JSON response.")
|
|
51
|
+
code, message, hint = err.get("code"), err.get("message"), err.get("hint")
|
|
52
|
+
if resp.status_code == 429:
|
|
53
|
+
raise RateLimitError(code, message, hint, int(resp.headers.get("Retry-After", 0)))
|
|
54
|
+
if resp.status_code in (401, 403):
|
|
55
|
+
raise AuthError(code, message, hint)
|
|
56
|
+
raise ApiError(code, message, hint)
|
jabb_cli/output.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MAX_CELL = 40
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _cell(v):
|
|
5
|
+
s = str(v)
|
|
6
|
+
return s if len(s) <= MAX_CELL else s[: MAX_CELL - 3] + "..."
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_table(rows):
|
|
10
|
+
if not rows:
|
|
11
|
+
return "(no results)"
|
|
12
|
+
cols = list(rows[0].keys())
|
|
13
|
+
cells = [[_cell(r.get(c, "")) for c in cols] for r in rows]
|
|
14
|
+
widths = [max(len(cols[i]), *(len(row[i]) for row in cells)) for i in range(len(cols))]
|
|
15
|
+
header = " ".join(c.ljust(w) for c, w in zip(cols, widths))
|
|
16
|
+
sep = " ".join("-" * w for w in widths)
|
|
17
|
+
body = "\n".join(" ".join(cell.ljust(w) for cell, w in zip(row, widths)) for row in cells)
|
|
18
|
+
return f"{header}\n{sep}\n{body}"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jabb-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: click>=8.1
|
|
7
|
+
Requires-Dist: requests>=2.31
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
jabb_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
jabb_cli/cli.py,sha256=L0EPSOAiLg0aAY0jrtWeZ6BJl8JDBs9GiJWxXwzif7Y,3671
|
|
3
|
+
jabb_cli/client.py,sha256=YIa3zoKpwjVvVlRnmD3aY2n_XSKCxz9JveA2WokkABE,1769
|
|
4
|
+
jabb_cli/output.py,sha256=VihSnEeUf-N6OsAQOFp83pg0Whz2h27VE8wB1qLiwJ8,616
|
|
5
|
+
jabb_cli-0.1.0.dist-info/METADATA,sha256=BtVBjCso0_p9jt0nhPjeDvL15YIQa5ZzBaTa-fOwNcE,283
|
|
6
|
+
jabb_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
jabb_cli-0.1.0.dist-info/entry_points.txt,sha256=RqoERoKtCqbnEbdA3UPO6NDr_BTrM2TX16gYrn3Cb3o,43
|
|
8
|
+
jabb_cli-0.1.0.dist-info/top_level.txt,sha256=JbTNlL8xVB3ySz9psM3tDCNQQ1OI0JSpKZ_ZPzkGevM,9
|
|
9
|
+
jabb_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jabb_cli
|