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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jabb = jabb_cli.cli:main
@@ -0,0 +1 @@
1
+ jabb_cli