colacloud-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.
- colacloud_cli/__init__.py +3 -0
- colacloud_cli/api.py +337 -0
- colacloud_cli/commands/__init__.py +1 -0
- colacloud_cli/commands/barcode.py +48 -0
- colacloud_cli/commands/colas.py +179 -0
- colacloud_cli/commands/config.py +80 -0
- colacloud_cli/commands/permittees.py +110 -0
- colacloud_cli/commands/usage.py +42 -0
- colacloud_cli/commands/utils.py +35 -0
- colacloud_cli/config.py +142 -0
- colacloud_cli/formatters.py +500 -0
- colacloud_cli/main.py +105 -0
- colacloud_cli-0.1.0.dist-info/METADATA +304 -0
- colacloud_cli-0.1.0.dist-info/RECORD +17 -0
- colacloud_cli-0.1.0.dist-info/WHEEL +4 -0
- colacloud_cli-0.1.0.dist-info/entry_points.txt +2 -0
- colacloud_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
colacloud_cli/api.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""COLA Cloud API client wrapper."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from colacloud_cli import __version__
|
|
8
|
+
from colacloud_cli.config import get_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class APIError(Exception):
|
|
12
|
+
"""Exception raised for API errors."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
status_code: Optional[int] = None,
|
|
18
|
+
error_code: Optional[str] = None,
|
|
19
|
+
details: Optional[dict] = None,
|
|
20
|
+
):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.message = message
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.error_code = error_code
|
|
25
|
+
self.details = details or {}
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
parts = [self.message]
|
|
29
|
+
if self.status_code:
|
|
30
|
+
parts.insert(0, f"[{self.status_code}]")
|
|
31
|
+
if self.error_code:
|
|
32
|
+
parts.insert(0, f"({self.error_code})")
|
|
33
|
+
return " ".join(parts)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthenticationError(APIError):
|
|
37
|
+
"""Exception raised for authentication errors."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RateLimitError(APIError):
|
|
43
|
+
"""Exception raised when rate limit is exceeded."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
message: str,
|
|
48
|
+
retry_after: Optional[int] = None,
|
|
49
|
+
**kwargs,
|
|
50
|
+
):
|
|
51
|
+
super().__init__(message, **kwargs)
|
|
52
|
+
self.retry_after = retry_after
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ColaCloudClient:
|
|
56
|
+
"""HTTP client for the COLA Cloud API."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
api_key: Optional[str] = None,
|
|
61
|
+
base_url: Optional[str] = None,
|
|
62
|
+
timeout: float = 30.0,
|
|
63
|
+
):
|
|
64
|
+
"""Initialize the API client.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
api_key: API key for authentication. If not provided,
|
|
68
|
+
will be read from config.
|
|
69
|
+
base_url: API base URL. If not provided, will use default.
|
|
70
|
+
timeout: Request timeout in seconds.
|
|
71
|
+
"""
|
|
72
|
+
config = get_config()
|
|
73
|
+
self.api_key = api_key or config.get_api_key()
|
|
74
|
+
self.base_url = (base_url or config.get_api_base_url()).rstrip("/")
|
|
75
|
+
self.timeout = timeout
|
|
76
|
+
|
|
77
|
+
self._client = httpx.Client(
|
|
78
|
+
base_url=self.base_url,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
headers=self._get_headers(),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _get_headers(self) -> dict[str, str]:
|
|
84
|
+
"""Get request headers including authentication."""
|
|
85
|
+
headers = {
|
|
86
|
+
"Accept": "application/json",
|
|
87
|
+
"User-Agent": f"colacloud-cli/{__version__}",
|
|
88
|
+
}
|
|
89
|
+
if self.api_key:
|
|
90
|
+
headers["X-API-Key"] = self.api_key
|
|
91
|
+
return headers
|
|
92
|
+
|
|
93
|
+
def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
|
|
94
|
+
"""Handle API response and raise appropriate exceptions.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
response: HTTP response object.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Parsed JSON response data.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
AuthenticationError: If authentication fails.
|
|
104
|
+
RateLimitError: If rate limit is exceeded.
|
|
105
|
+
APIError: For other API errors.
|
|
106
|
+
"""
|
|
107
|
+
# Try to parse JSON response
|
|
108
|
+
try:
|
|
109
|
+
data = response.json()
|
|
110
|
+
except (ValueError, httpx.DecodingError):
|
|
111
|
+
data = {}
|
|
112
|
+
|
|
113
|
+
# Handle successful responses
|
|
114
|
+
if response.is_success:
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
# Extract error information
|
|
118
|
+
error_info = data.get("error", {})
|
|
119
|
+
error_code = error_info.get("code", "unknown_error")
|
|
120
|
+
error_message = error_info.get("message", response.reason_phrase or "Unknown error")
|
|
121
|
+
error_details = error_info.get("details", {})
|
|
122
|
+
|
|
123
|
+
# Handle specific error types
|
|
124
|
+
if response.status_code == 401:
|
|
125
|
+
raise AuthenticationError(
|
|
126
|
+
error_message,
|
|
127
|
+
status_code=response.status_code,
|
|
128
|
+
error_code=error_code,
|
|
129
|
+
details=error_details,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if response.status_code == 429:
|
|
133
|
+
retry_after = None
|
|
134
|
+
if "Retry-After" in response.headers:
|
|
135
|
+
try:
|
|
136
|
+
retry_after = int(response.headers["Retry-After"])
|
|
137
|
+
except ValueError:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
raise RateLimitError(
|
|
141
|
+
error_message,
|
|
142
|
+
status_code=response.status_code,
|
|
143
|
+
error_code=error_code,
|
|
144
|
+
details=error_details,
|
|
145
|
+
retry_after=retry_after,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
raise APIError(
|
|
149
|
+
error_message,
|
|
150
|
+
status_code=response.status_code,
|
|
151
|
+
error_code=error_code,
|
|
152
|
+
details=error_details,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _require_api_key(self) -> None:
|
|
156
|
+
"""Ensure API key is configured.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
AuthenticationError: If API key is not configured.
|
|
160
|
+
"""
|
|
161
|
+
if not self.api_key:
|
|
162
|
+
raise AuthenticationError(
|
|
163
|
+
"API key not configured. Run 'cola config set-key' to set your API key, "
|
|
164
|
+
"or set the COLACLOUD_API_KEY environment variable."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def close(self) -> None:
|
|
168
|
+
"""Close the HTTP client."""
|
|
169
|
+
self._client.close()
|
|
170
|
+
|
|
171
|
+
def __enter__(self) -> "ColaCloudClient":
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
def __exit__(self, *args) -> None:
|
|
175
|
+
self.close()
|
|
176
|
+
|
|
177
|
+
# COLA endpoints
|
|
178
|
+
|
|
179
|
+
def list_colas(
|
|
180
|
+
self,
|
|
181
|
+
query: Optional[str] = None,
|
|
182
|
+
product_type: Optional[str] = None,
|
|
183
|
+
origin: Optional[str] = None,
|
|
184
|
+
brand_name: Optional[str] = None,
|
|
185
|
+
approval_date_from: Optional[str] = None,
|
|
186
|
+
approval_date_to: Optional[str] = None,
|
|
187
|
+
abv_min: Optional[float] = None,
|
|
188
|
+
abv_max: Optional[float] = None,
|
|
189
|
+
page: int = 1,
|
|
190
|
+
per_page: int = 20,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Search and filter COLAs.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
query: Full-text search query.
|
|
196
|
+
product_type: Filter by product type (malt beverage, wine, distilled spirits).
|
|
197
|
+
origin: Filter by country/state.
|
|
198
|
+
brand_name: Filter by brand name (partial match).
|
|
199
|
+
approval_date_from: Filter by minimum approval date (YYYY-MM-DD).
|
|
200
|
+
approval_date_to: Filter by maximum approval date (YYYY-MM-DD).
|
|
201
|
+
abv_min: Filter by minimum ABV.
|
|
202
|
+
abv_max: Filter by maximum ABV.
|
|
203
|
+
page: Page number.
|
|
204
|
+
per_page: Results per page (max 100).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
API response with data and pagination info.
|
|
208
|
+
"""
|
|
209
|
+
self._require_api_key()
|
|
210
|
+
|
|
211
|
+
params = {"page": page, "per_page": per_page}
|
|
212
|
+
|
|
213
|
+
if query:
|
|
214
|
+
params["q"] = query
|
|
215
|
+
if product_type:
|
|
216
|
+
params["product_type"] = product_type
|
|
217
|
+
if origin:
|
|
218
|
+
params["origin"] = origin
|
|
219
|
+
if brand_name:
|
|
220
|
+
params["brand_name"] = brand_name
|
|
221
|
+
if approval_date_from:
|
|
222
|
+
params["approval_date_from"] = approval_date_from
|
|
223
|
+
if approval_date_to:
|
|
224
|
+
params["approval_date_to"] = approval_date_to
|
|
225
|
+
if abv_min is not None:
|
|
226
|
+
params["abv_min"] = abv_min
|
|
227
|
+
if abv_max is not None:
|
|
228
|
+
params["abv_max"] = abv_max
|
|
229
|
+
|
|
230
|
+
response = self._client.get("/colas", params=params)
|
|
231
|
+
return self._handle_response(response)
|
|
232
|
+
|
|
233
|
+
def get_cola(self, ttb_id: str) -> dict[str, Any]:
|
|
234
|
+
"""Get a single COLA by TTB ID.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
ttb_id: The TTB ID of the COLA.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
API response with COLA details.
|
|
241
|
+
"""
|
|
242
|
+
self._require_api_key()
|
|
243
|
+
|
|
244
|
+
response = self._client.get(f"/colas/{ttb_id}")
|
|
245
|
+
return self._handle_response(response)
|
|
246
|
+
|
|
247
|
+
# Permittee endpoints
|
|
248
|
+
|
|
249
|
+
def list_permittees(
|
|
250
|
+
self,
|
|
251
|
+
query: Optional[str] = None,
|
|
252
|
+
state: Optional[str] = None,
|
|
253
|
+
is_active: Optional[bool] = None,
|
|
254
|
+
page: int = 1,
|
|
255
|
+
per_page: int = 20,
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
"""Search permittees.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
query: Search by company name (partial match).
|
|
261
|
+
state: Filter by state.
|
|
262
|
+
is_active: Filter by active status.
|
|
263
|
+
page: Page number.
|
|
264
|
+
per_page: Results per page (max 100).
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
API response with data and pagination info.
|
|
268
|
+
"""
|
|
269
|
+
self._require_api_key()
|
|
270
|
+
|
|
271
|
+
params = {"page": page, "per_page": per_page}
|
|
272
|
+
|
|
273
|
+
if query:
|
|
274
|
+
params["q"] = query
|
|
275
|
+
if state:
|
|
276
|
+
params["state"] = state.upper()
|
|
277
|
+
if is_active is not None:
|
|
278
|
+
params["is_active"] = "true" if is_active else "false"
|
|
279
|
+
|
|
280
|
+
response = self._client.get("/permittees", params=params)
|
|
281
|
+
return self._handle_response(response)
|
|
282
|
+
|
|
283
|
+
def get_permittee(self, permit_number: str) -> dict[str, Any]:
|
|
284
|
+
"""Get a single permittee by permit number.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
permit_number: The permit number.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
API response with permittee details.
|
|
291
|
+
"""
|
|
292
|
+
self._require_api_key()
|
|
293
|
+
|
|
294
|
+
response = self._client.get(f"/permittees/{permit_number}")
|
|
295
|
+
return self._handle_response(response)
|
|
296
|
+
|
|
297
|
+
# Barcode endpoint
|
|
298
|
+
|
|
299
|
+
def lookup_barcode(self, barcode_value: str) -> dict[str, Any]:
|
|
300
|
+
"""Look up COLAs by barcode.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
barcode_value: The barcode value (UPC, EAN, etc.).
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
API response with barcode info and associated COLAs.
|
|
307
|
+
"""
|
|
308
|
+
self._require_api_key()
|
|
309
|
+
|
|
310
|
+
response = self._client.get(f"/barcode/{barcode_value}")
|
|
311
|
+
return self._handle_response(response)
|
|
312
|
+
|
|
313
|
+
# Usage endpoint
|
|
314
|
+
|
|
315
|
+
def get_usage(self) -> dict[str, Any]:
|
|
316
|
+
"""Get current API usage statistics.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
API response with usage data.
|
|
320
|
+
"""
|
|
321
|
+
self._require_api_key()
|
|
322
|
+
|
|
323
|
+
response = self._client.get("/usage")
|
|
324
|
+
return self._handle_response(response)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Convenience function to get a client instance
|
|
328
|
+
def get_client(**kwargs) -> ColaCloudClient:
|
|
329
|
+
"""Get a ColaCloudClient instance.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
**kwargs: Arguments to pass to ColaCloudClient constructor.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
ColaCloudClient instance.
|
|
336
|
+
"""
|
|
337
|
+
return ColaCloudClient(**kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules."""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Barcode lookup command for COLA Cloud CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from colacloud_cli.api import APIError, get_client
|
|
8
|
+
from colacloud_cli.commands.utils import console, handle_api_error
|
|
9
|
+
from colacloud_cli.formatters import format_barcode_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command(name="barcode")
|
|
13
|
+
@click.argument("value")
|
|
14
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
15
|
+
def barcode_command(value: str, as_json: bool):
|
|
16
|
+
"""Look up COLAs by barcode (UPC, EAN, etc.).
|
|
17
|
+
|
|
18
|
+
VALUE is the barcode number from the product label.
|
|
19
|
+
|
|
20
|
+
This command searches the COLA database for products that have been
|
|
21
|
+
registered with the specified barcode.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
# Look up a UPC barcode
|
|
27
|
+
cola barcode 012345678901
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
# Look up an EAN barcode
|
|
31
|
+
cola barcode 5000281025155
|
|
32
|
+
|
|
33
|
+
\b
|
|
34
|
+
# Output as JSON
|
|
35
|
+
cola barcode 012345678901 --json
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
with get_client() as client:
|
|
39
|
+
result = client.lookup_barcode(value)
|
|
40
|
+
|
|
41
|
+
if as_json:
|
|
42
|
+
click.echo(json.dumps(result, indent=2))
|
|
43
|
+
else:
|
|
44
|
+
data = result.get("data", {})
|
|
45
|
+
format_barcode_result(data, console)
|
|
46
|
+
|
|
47
|
+
except APIError as e:
|
|
48
|
+
handle_api_error(e)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""COLA commands for COLA Cloud CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from colacloud_cli.api import APIError, get_client
|
|
8
|
+
from colacloud_cli.commands.utils import console, handle_api_error
|
|
9
|
+
from colacloud_cli.formatters import (
|
|
10
|
+
format_cola_detail,
|
|
11
|
+
format_cola_table,
|
|
12
|
+
format_pagination,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(name="colas")
|
|
17
|
+
def colas_group():
|
|
18
|
+
"""Search and retrieve COLA records."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@colas_group.command(name="list")
|
|
23
|
+
@click.option("-q", "--query", help="Full-text search query.")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--product-type",
|
|
26
|
+
type=click.Choice(["malt beverage", "wine", "distilled spirits"], case_sensitive=False),
|
|
27
|
+
help="Filter by product type.",
|
|
28
|
+
)
|
|
29
|
+
@click.option("--origin", help="Filter by origin (country/state).")
|
|
30
|
+
@click.option("--brand", "brand_name", help="Filter by brand name (partial match).")
|
|
31
|
+
@click.option("--date-from", "approval_date_from", help="Filter by minimum approval date (YYYY-MM-DD).")
|
|
32
|
+
@click.option("--date-to", "approval_date_to", help="Filter by maximum approval date (YYYY-MM-DD).")
|
|
33
|
+
@click.option("--abv-min", type=float, help="Filter by minimum ABV.")
|
|
34
|
+
@click.option("--abv-max", type=float, help="Filter by maximum ABV.")
|
|
35
|
+
@click.option("--limit", "per_page", default=20, type=int, help="Results per page (max 100).")
|
|
36
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
37
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
38
|
+
def list_colas(
|
|
39
|
+
query: str | None,
|
|
40
|
+
product_type: str | None,
|
|
41
|
+
origin: str | None,
|
|
42
|
+
brand_name: str | None,
|
|
43
|
+
approval_date_from: str | None,
|
|
44
|
+
approval_date_to: str | None,
|
|
45
|
+
abv_min: float | None,
|
|
46
|
+
abv_max: float | None,
|
|
47
|
+
per_page: int,
|
|
48
|
+
page: int,
|
|
49
|
+
as_json: bool,
|
|
50
|
+
):
|
|
51
|
+
"""List and search COLA records.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
|
|
55
|
+
\b
|
|
56
|
+
# Search for bourbon
|
|
57
|
+
cola colas list -q "bourbon"
|
|
58
|
+
|
|
59
|
+
\b
|
|
60
|
+
# List wines from California
|
|
61
|
+
cola colas list --product-type wine --origin california
|
|
62
|
+
|
|
63
|
+
\b
|
|
64
|
+
# Find high-ABV spirits
|
|
65
|
+
cola colas list --product-type "distilled spirits" --abv-min 50
|
|
66
|
+
|
|
67
|
+
\b
|
|
68
|
+
# Search by brand
|
|
69
|
+
cola colas list --brand "buffalo trace"
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
with get_client() as client:
|
|
73
|
+
result = client.list_colas(
|
|
74
|
+
query=query,
|
|
75
|
+
product_type=product_type,
|
|
76
|
+
origin=origin,
|
|
77
|
+
brand_name=brand_name,
|
|
78
|
+
approval_date_from=approval_date_from,
|
|
79
|
+
approval_date_to=approval_date_to,
|
|
80
|
+
abv_min=abv_min,
|
|
81
|
+
abv_max=abv_max,
|
|
82
|
+
page=page,
|
|
83
|
+
per_page=min(per_page, 100),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if as_json:
|
|
87
|
+
click.echo(json.dumps(result, indent=2))
|
|
88
|
+
else:
|
|
89
|
+
colas = result.get("data", [])
|
|
90
|
+
pagination = result.get("pagination", {})
|
|
91
|
+
|
|
92
|
+
if not colas:
|
|
93
|
+
console.print("[yellow]No COLAs found matching your criteria.[/]")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
table = format_cola_table(colas, console)
|
|
97
|
+
console.print(table)
|
|
98
|
+
format_pagination(pagination, console)
|
|
99
|
+
|
|
100
|
+
except APIError as e:
|
|
101
|
+
handle_api_error(e)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@colas_group.command(name="get")
|
|
105
|
+
@click.argument("ttb_id")
|
|
106
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
107
|
+
def get_cola(ttb_id: str, as_json: bool):
|
|
108
|
+
"""Get detailed information about a specific COLA.
|
|
109
|
+
|
|
110
|
+
TTB_ID is the unique identifier for the COLA (e.g., 24001234).
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
|
|
114
|
+
\b
|
|
115
|
+
# Get COLA details
|
|
116
|
+
cola colas get 24001234
|
|
117
|
+
|
|
118
|
+
\b
|
|
119
|
+
# Output as JSON
|
|
120
|
+
cola colas get 24001234 --json
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
with get_client() as client:
|
|
124
|
+
result = client.get_cola(ttb_id)
|
|
125
|
+
|
|
126
|
+
if as_json:
|
|
127
|
+
click.echo(json.dumps(result, indent=2))
|
|
128
|
+
else:
|
|
129
|
+
cola = result.get("data", {})
|
|
130
|
+
format_cola_detail(cola, console)
|
|
131
|
+
|
|
132
|
+
except APIError as e:
|
|
133
|
+
handle_api_error(e)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@colas_group.command(name="search")
|
|
137
|
+
@click.argument("query")
|
|
138
|
+
@click.option("--limit", "per_page", default=20, type=int, help="Results per page (max 100).")
|
|
139
|
+
@click.option("--page", default=1, type=int, help="Page number.")
|
|
140
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
141
|
+
def search_colas(query: str, per_page: int, page: int, as_json: bool):
|
|
142
|
+
"""Quick search for COLAs.
|
|
143
|
+
|
|
144
|
+
This is a shortcut for 'cola colas list -q <query>'.
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
|
|
148
|
+
\b
|
|
149
|
+
# Search for whiskey
|
|
150
|
+
cola colas search "whiskey"
|
|
151
|
+
|
|
152
|
+
\b
|
|
153
|
+
# Search for a specific brand
|
|
154
|
+
cola colas search "buffalo trace"
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
with get_client() as client:
|
|
158
|
+
result = client.list_colas(
|
|
159
|
+
query=query,
|
|
160
|
+
page=page,
|
|
161
|
+
per_page=min(per_page, 100),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if as_json:
|
|
165
|
+
click.echo(json.dumps(result, indent=2))
|
|
166
|
+
else:
|
|
167
|
+
colas = result.get("data", [])
|
|
168
|
+
pagination = result.get("pagination", {})
|
|
169
|
+
|
|
170
|
+
if not colas:
|
|
171
|
+
console.print(f"[yellow]No COLAs found for '{query}'.[/]")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
table = format_cola_table(colas, console)
|
|
175
|
+
console.print(table)
|
|
176
|
+
format_pagination(pagination, console)
|
|
177
|
+
|
|
178
|
+
except APIError as e:
|
|
179
|
+
handle_api_error(e)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Configuration commands for COLA Cloud CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from colacloud_cli.config import get_config
|
|
9
|
+
from colacloud_cli.formatters import format_config
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="config")
|
|
15
|
+
def config_group():
|
|
16
|
+
"""Manage CLI configuration."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@config_group.command(name="set-key")
|
|
21
|
+
@click.option(
|
|
22
|
+
"--key",
|
|
23
|
+
"-k",
|
|
24
|
+
help="API key to set. If not provided, will prompt for input.",
|
|
25
|
+
)
|
|
26
|
+
def set_key(key: str | None):
|
|
27
|
+
"""Set your COLA Cloud API key.
|
|
28
|
+
|
|
29
|
+
The API key will be saved to ~/.colacloud/config.json with restricted
|
|
30
|
+
permissions (readable only by you).
|
|
31
|
+
|
|
32
|
+
You can also set the COLACLOUD_API_KEY environment variable instead.
|
|
33
|
+
"""
|
|
34
|
+
config = get_config()
|
|
35
|
+
|
|
36
|
+
if key is None:
|
|
37
|
+
# Prompt for API key with hidden input
|
|
38
|
+
key = click.prompt(
|
|
39
|
+
"Enter your API key",
|
|
40
|
+
hide_input=True,
|
|
41
|
+
confirmation_prompt=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if not key or not key.strip():
|
|
45
|
+
console.print("[red]Error:[/] API key cannot be empty.")
|
|
46
|
+
raise SystemExit(1)
|
|
47
|
+
|
|
48
|
+
key = key.strip()
|
|
49
|
+
config.set_api_key(key)
|
|
50
|
+
|
|
51
|
+
console.print("[green]Success![/] API key saved to ~/.colacloud/config.json")
|
|
52
|
+
console.print("[dim]The config file has been set to mode 600 (owner read/write only).[/]")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@config_group.command(name="show")
|
|
56
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
|
|
57
|
+
def show(as_json: bool):
|
|
58
|
+
"""Show current configuration.
|
|
59
|
+
|
|
60
|
+
API key will be partially masked for security.
|
|
61
|
+
"""
|
|
62
|
+
config = get_config()
|
|
63
|
+
config_data = config.to_dict()
|
|
64
|
+
|
|
65
|
+
if as_json:
|
|
66
|
+
click.echo(json.dumps(config_data, indent=2))
|
|
67
|
+
else:
|
|
68
|
+
format_config(config_data, console)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@config_group.command(name="clear")
|
|
72
|
+
@click.confirmation_option(prompt="Are you sure you want to clear your configuration?")
|
|
73
|
+
def clear():
|
|
74
|
+
"""Clear all saved configuration.
|
|
75
|
+
|
|
76
|
+
This will remove your saved API key and any other settings.
|
|
77
|
+
"""
|
|
78
|
+
config = get_config()
|
|
79
|
+
config.clear()
|
|
80
|
+
console.print("[green]Configuration cleared.[/]")
|