colacloud-cli 0.2.0__tar.gz → 0.3.1__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.
Files changed (26) hide show
  1. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/PKG-INFO +3 -3
  2. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/README.md +1 -1
  3. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/pyproject.toml +2 -2
  4. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/api.py +157 -0
  5. colacloud_cli-0.3.1/src/colacloud_cli/commands/avas.py +91 -0
  6. colacloud_cli-0.3.1/src/colacloud_cli/commands/processing_times.py +153 -0
  7. colacloud_cli-0.3.1/src/colacloud_cli/commands/production_reports.py +89 -0
  8. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/formatters.py +16 -14
  9. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/main.py +10 -1
  10. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/tests/test_api.py +214 -0
  11. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/tests/test_cli.py +269 -0
  12. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/.gitignore +0 -0
  13. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/.mcp.json +0 -0
  14. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/LICENSE +0 -0
  15. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/scripts/smoke_test.py +0 -0
  16. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/__init__.py +0 -0
  17. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/__init__.py +0 -0
  18. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/barcode.py +0 -0
  19. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/colas.py +0 -0
  20. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/config.py +0 -0
  21. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/permittees.py +0 -0
  22. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/usage.py +0 -0
  23. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/commands/utils.py +0 -0
  24. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/src/colacloud_cli/config.py +0 -0
  25. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/tests/__init__.py +0 -0
  26. {colacloud_cli-0.2.0 → colacloud_cli-0.3.1}/tests/test_config.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: colacloud-cli
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Command-line interface for the COLA Cloud API
5
5
  Project-URL: Homepage, https://colacloud.us
6
- Project-URL: Documentation, https://colacloud.us/docs/api
6
+ Project-URL: Documentation, https://docs.colacloud.us/api-reference
7
7
  Project-URL: Repository, https://github.com/cola-cloud-us/colacloud-cli
8
8
  Author-email: Jay Sobel <jay@colacloud.us>
9
9
  License: MIT
@@ -300,5 +300,5 @@ MIT License - see [LICENSE](LICENSE) for details.
300
300
  ## Links
301
301
 
302
302
  - [COLA Cloud Website](https://colacloud.us)
303
- - [API Documentation](https://colacloud.us/docs/api)
303
+ - [API Documentation](https://docs.colacloud.us/api-reference)
304
304
  - [GitHub Repository](https://github.com/cola-cloud-us/colacloud-cli)
@@ -273,5 +273,5 @@ MIT License - see [LICENSE](LICENSE) for details.
273
273
  ## Links
274
274
 
275
275
  - [COLA Cloud Website](https://colacloud.us)
276
- - [API Documentation](https://colacloud.us/docs/api)
276
+ - [API Documentation](https://docs.colacloud.us/api-reference)
277
277
  - [GitHub Repository](https://github.com/cola-cloud-us/colacloud-cli)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "colacloud-cli"
3
- version = "0.2.0"
3
+ version = "0.3.1"
4
4
  description = "Command-line interface for the COLA Cloud API"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -33,7 +33,7 @@ cola = "colacloud_cli.main:cli"
33
33
 
34
34
  [project.urls]
35
35
  Homepage = "https://colacloud.us"
36
- Documentation = "https://colacloud.us/docs/api"
36
+ Documentation = "https://docs.colacloud.us/api-reference"
37
37
  Repository = "https://github.com/cola-cloud-us/colacloud-cli"
38
38
 
39
39
  [build-system]
@@ -311,6 +311,163 @@ class ColaCloudClient:
311
311
  response = self._client.get(f"/barcode/{barcode_value}")
312
312
  return self._handle_response(response)
313
313
 
314
+ # Processing times endpoints
315
+
316
+ def list_processing_times(
317
+ self,
318
+ commodity: str | None = None,
319
+ ) -> dict[str, Any]:
320
+ """Get COLA processing times.
321
+
322
+ Args:
323
+ commodity: Filter by commodity type.
324
+
325
+ Returns:
326
+ API response with processing times data.
327
+ """
328
+ self._require_api_key()
329
+
330
+ params: dict[str, Any] = {}
331
+ if commodity:
332
+ params["commodity"] = commodity
333
+
334
+ response = self._client.get("/processing-times", params=params)
335
+ return self._handle_response(response)
336
+
337
+ def list_formula_processing_times(
338
+ self,
339
+ formula_type: str | None = None,
340
+ commodity: str | None = None,
341
+ ) -> dict[str, Any]:
342
+ """Get formula processing times.
343
+
344
+ Args:
345
+ formula_type: Filter by formula type.
346
+ commodity: Filter by commodity type.
347
+
348
+ Returns:
349
+ API response with formula processing times data.
350
+ """
351
+ self._require_api_key()
352
+
353
+ params: dict[str, Any] = {}
354
+ if formula_type:
355
+ params["formula_type"] = formula_type
356
+ if commodity:
357
+ params["commodity"] = commodity
358
+
359
+ response = self._client.get("/processing-times/formula", params=params)
360
+ return self._handle_response(response)
361
+
362
+ def list_registration_processing_times(
363
+ self,
364
+ category: str | None = None,
365
+ application_type: str | None = None,
366
+ ) -> dict[str, Any]:
367
+ """Get registration processing times.
368
+
369
+ Args:
370
+ category: Filter by category.
371
+ application_type: Filter by application type.
372
+
373
+ Returns:
374
+ API response with registration processing times data.
375
+ """
376
+ self._require_api_key()
377
+
378
+ params: dict[str, Any] = {}
379
+ if category:
380
+ params["category"] = category
381
+ if application_type:
382
+ params["application_type"] = application_type
383
+
384
+ response = self._client.get("/processing-times/registration", params=params)
385
+ return self._handle_response(response)
386
+
387
+ # Production reports endpoint
388
+
389
+ def list_production_reports(
390
+ self,
391
+ commodity: str | None = None,
392
+ year: int | None = None,
393
+ month: int | None = None,
394
+ report_type: str | None = None,
395
+ statistical_group: str | None = None,
396
+ page: int = 1,
397
+ per_page: int = 100,
398
+ ) -> dict[str, Any]:
399
+ """Get production reports.
400
+
401
+ Args:
402
+ commodity: Filter by commodity type.
403
+ year: Filter by year.
404
+ month: Filter by month.
405
+ report_type: Filter by report type.
406
+ statistical_group: Filter by statistical group.
407
+ page: Page number.
408
+ per_page: Results per page (max 100).
409
+
410
+ Returns:
411
+ API response with production reports data.
412
+ """
413
+ self._require_api_key()
414
+
415
+ params: dict[str, Any] = {"page": page, "per_page": per_page}
416
+ if commodity:
417
+ params["commodity"] = commodity
418
+ if year is not None:
419
+ params["year"] = year
420
+ if month is not None:
421
+ params["month"] = month
422
+ if report_type:
423
+ params["report_type"] = report_type
424
+ if statistical_group:
425
+ params["statistical_group"] = statistical_group
426
+
427
+ response = self._client.get("/production-reports", params=params)
428
+ return self._handle_response(response)
429
+
430
+ # AVA endpoints
431
+
432
+ def list_avas(
433
+ self,
434
+ state: str | None = None,
435
+ query: str | None = None,
436
+ ) -> dict[str, Any]:
437
+ """List American Viticultural Areas (AVAs).
438
+
439
+ Args:
440
+ state: Filter by state.
441
+ query: Search by name.
442
+
443
+ Returns:
444
+ API response with AVA data.
445
+ """
446
+ self._require_api_key()
447
+
448
+ params: dict[str, Any] = {}
449
+ if state:
450
+ params["state"] = state
451
+ if query:
452
+ params["q"] = query
453
+
454
+ response = self._client.get("/avas", params=params)
455
+ return self._handle_response(response)
456
+
457
+ def get_ava(self, ava_id: str) -> dict[str, Any]:
458
+ """Get a single AVA by ID.
459
+
460
+ Args:
461
+ ava_id: The AVA identifier.
462
+
463
+ Returns:
464
+ API response with AVA details.
465
+ """
466
+ self._require_api_key()
467
+
468
+ response = self._client.get(f"/avas/{ava_id}")
469
+ return self._handle_response(response)
470
+
314
471
  # Usage endpoint
315
472
 
316
473
  def get_usage(self) -> dict[str, Any]:
@@ -0,0 +1,91 @@
1
+ """AVA 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
+
10
+
11
+ @click.group(name="avas")
12
+ def avas_group():
13
+ """Browse American Viticultural Areas (AVAs)."""
14
+ pass
15
+
16
+
17
+ @avas_group.command(name="list")
18
+ @click.option("--state", help="Filter by state (e.g., CA, OR, WA).")
19
+ @click.option("-q", "--query", help="Search by AVA name.")
20
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
21
+ def list_avas(state: str | None, query: str | None, as_json: bool):
22
+ """List American Viticultural Areas.
23
+
24
+ Browse and search federally recognized wine grape-growing regions.
25
+
26
+ Examples:
27
+
28
+ \b
29
+ # List all AVAs
30
+ cola avas list
31
+
32
+ \b
33
+ # Filter by state
34
+ cola avas list --state CA
35
+
36
+ \b
37
+ # Search by name
38
+ cola avas list -q "napa"
39
+ """
40
+ try:
41
+ with get_client() as client:
42
+ result = client.list_avas(state=state, query=query)
43
+
44
+ if as_json:
45
+ click.echo(json.dumps(result, indent=2))
46
+ else:
47
+ data = result.get("data", [])
48
+ meta = result.get("meta", {})
49
+
50
+ if not data:
51
+ console.print("[yellow]No AVAs found matching your criteria.[/]")
52
+ return
53
+
54
+ console.print(json.dumps(data, indent=2))
55
+ total = meta.get("total", len(data))
56
+ console.print(f"\n[dim]{total} result(s)[/]")
57
+
58
+ except APIError as e:
59
+ handle_api_error(e)
60
+
61
+
62
+ @avas_group.command(name="get")
63
+ @click.argument("ava_id")
64
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
65
+ def get_ava(ava_id: str, as_json: bool):
66
+ """Get detailed information about a specific AVA.
67
+
68
+ AVA_ID is the unique identifier for the American Viticultural Area.
69
+
70
+ Examples:
71
+
72
+ \b
73
+ # Get AVA details
74
+ cola avas get napa-valley
75
+
76
+ \b
77
+ # Output as JSON
78
+ cola avas get napa-valley --json
79
+ """
80
+ try:
81
+ with get_client() as client:
82
+ result = client.get_ava(ava_id)
83
+
84
+ if as_json:
85
+ click.echo(json.dumps(result, indent=2))
86
+ else:
87
+ data = result.get("data", {})
88
+ console.print(json.dumps(data, indent=2))
89
+
90
+ except APIError as e:
91
+ handle_api_error(e)
@@ -0,0 +1,153 @@
1
+ """Processing times 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
+
10
+
11
+ @click.group(name="processing-times")
12
+ def processing_times_group():
13
+ """View TTB processing times for COLAs, formulas, and registrations."""
14
+ pass
15
+
16
+
17
+ @processing_times_group.command(name="list")
18
+ @click.option("--commodity", help="Filter by commodity type.")
19
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
20
+ def list_processing_times(commodity: str | None, as_json: bool):
21
+ """List COLA processing times.
22
+
23
+ Shows how long it takes TTB to process COLA applications,
24
+ optionally filtered by commodity type.
25
+
26
+ Examples:
27
+
28
+ \b
29
+ # List all processing times
30
+ cola processing-times list
31
+
32
+ \b
33
+ # Filter by commodity
34
+ cola processing-times list --commodity wine
35
+ """
36
+ try:
37
+ with get_client() as client:
38
+ result = client.list_processing_times(commodity=commodity)
39
+
40
+ if as_json:
41
+ click.echo(json.dumps(result, indent=2))
42
+ else:
43
+ data = result.get("data", [])
44
+ meta = result.get("meta", {})
45
+
46
+ if not data:
47
+ console.print(
48
+ "[yellow]No processing times found matching your criteria.[/]"
49
+ )
50
+ return
51
+
52
+ console.print(json.dumps(data, indent=2))
53
+ total = meta.get("total", len(data))
54
+ console.print(f"\n[dim]{total} result(s)[/]")
55
+
56
+ except APIError as e:
57
+ handle_api_error(e)
58
+
59
+
60
+ @processing_times_group.command(name="formula")
61
+ @click.option("--formula-type", help="Filter by formula type.")
62
+ @click.option("--commodity", help="Filter by commodity type.")
63
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
64
+ def formula_processing_times(
65
+ formula_type: str | None, commodity: str | None, as_json: bool
66
+ ):
67
+ """List formula processing times.
68
+
69
+ Shows how long it takes TTB to process formula applications.
70
+
71
+ Examples:
72
+
73
+ \b
74
+ # List all formula processing times
75
+ cola processing-times formula
76
+
77
+ \b
78
+ # Filter by formula type and commodity
79
+ cola processing-times formula --formula-type domestic --commodity wine
80
+ """
81
+ try:
82
+ with get_client() as client:
83
+ result = client.list_formula_processing_times(
84
+ formula_type=formula_type, commodity=commodity
85
+ )
86
+
87
+ if as_json:
88
+ click.echo(json.dumps(result, indent=2))
89
+ else:
90
+ data = result.get("data", [])
91
+ meta = result.get("meta", {})
92
+
93
+ if not data:
94
+ console.print(
95
+ "[yellow]No formula processing times found"
96
+ " matching your criteria.[/]"
97
+ )
98
+ return
99
+
100
+ console.print(json.dumps(data, indent=2))
101
+ total = meta.get("total", len(data))
102
+ console.print(f"\n[dim]{total} result(s)[/]")
103
+
104
+ except APIError as e:
105
+ handle_api_error(e)
106
+
107
+
108
+ @processing_times_group.command(name="registration")
109
+ @click.option("--category", help="Filter by category.")
110
+ @click.option("--application-type", help="Filter by application type.")
111
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
112
+ def registration_processing_times(
113
+ category: str | None, application_type: str | None, as_json: bool
114
+ ):
115
+ """List registration processing times.
116
+
117
+ Shows how long it takes TTB to process registration applications.
118
+
119
+ Examples:
120
+
121
+ \b
122
+ # List all registration processing times
123
+ cola processing-times registration
124
+
125
+ \b
126
+ # Filter by category
127
+ cola processing-times registration --category beverage
128
+ """
129
+ try:
130
+ with get_client() as client:
131
+ result = client.list_registration_processing_times(
132
+ category=category, application_type=application_type
133
+ )
134
+
135
+ if as_json:
136
+ click.echo(json.dumps(result, indent=2))
137
+ else:
138
+ data = result.get("data", [])
139
+ meta = result.get("meta", {})
140
+
141
+ if not data:
142
+ console.print(
143
+ "[yellow]No registration processing times found"
144
+ " matching your criteria.[/]"
145
+ )
146
+ return
147
+
148
+ console.print(json.dumps(data, indent=2))
149
+ total = meta.get("total", len(data))
150
+ console.print(f"\n[dim]{total} result(s)[/]")
151
+
152
+ except APIError as e:
153
+ handle_api_error(e)
@@ -0,0 +1,89 @@
1
+ """Production reports 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
+
10
+
11
+ @click.group(name="production-reports")
12
+ def production_reports_group():
13
+ """View TTB production reports and statistics."""
14
+ pass
15
+
16
+
17
+ @production_reports_group.command(name="list")
18
+ @click.option("--commodity", help="Filter by commodity type.")
19
+ @click.option("--year", type=int, help="Filter by year.")
20
+ @click.option("--month", type=int, help="Filter by month (1-12).")
21
+ @click.option("--report-type", help="Filter by report type.")
22
+ @click.option("--statistical-group", help="Filter by statistical group.")
23
+ @click.option(
24
+ "--limit", "per_page", default=100, type=int, help="Results per page (max 100)."
25
+ )
26
+ @click.option("--page", default=1, type=int, help="Page number.")
27
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON.")
28
+ def list_production_reports(
29
+ commodity: str | None,
30
+ year: int | None,
31
+ month: int | None,
32
+ report_type: str | None,
33
+ statistical_group: str | None,
34
+ per_page: int,
35
+ page: int,
36
+ as_json: bool,
37
+ ):
38
+ """List production reports.
39
+
40
+ Browse TTB production report data with optional filters.
41
+
42
+ Examples:
43
+
44
+ \b
45
+ # List all production reports
46
+ cola production-reports list
47
+
48
+ \b
49
+ # Filter by commodity and year
50
+ cola production-reports list --commodity wine --year 2024
51
+
52
+ \b
53
+ # Filter by month
54
+ cola production-reports list --year 2024 --month 6
55
+ """
56
+ try:
57
+ with get_client() as client:
58
+ result = client.list_production_reports(
59
+ commodity=commodity,
60
+ year=year,
61
+ month=month,
62
+ report_type=report_type,
63
+ statistical_group=statistical_group,
64
+ page=page,
65
+ per_page=min(per_page, 100),
66
+ )
67
+
68
+ if as_json:
69
+ click.echo(json.dumps(result, indent=2))
70
+ else:
71
+ data = result.get("data", [])
72
+ meta = result.get("meta", {})
73
+
74
+ if not data:
75
+ console.print(
76
+ "[yellow]No production reports found matching your criteria.[/]"
77
+ )
78
+ return
79
+
80
+ console.print(json.dumps(data, indent=2))
81
+ total = meta.get("total", len(data))
82
+ page_num = meta.get("page", page)
83
+ has_more = meta.get("has_more", False)
84
+ console.print(f"\n[dim]{total} total result(s), page {page_num}[/]")
85
+ if has_more:
86
+ console.print("[dim]More results available. Use --page to paginate.[/]")
87
+
88
+ except APIError as e:
89
+ handle_api_error(e)
@@ -383,23 +383,25 @@ def format_permittee_detail(permittee: dict[str, Any], console: Console) -> None
383
383
  console.print("[bold]Company Information[/]")
384
384
  console.print(info_table)
385
385
 
386
- # COLA stats
387
- stats_table = Table(show_header=False, box=None, padding=(0, 2))
388
- stats_table.add_column("Field", style="dim")
389
- stats_table.add_column("Value")
390
-
391
- stats_table.add_row("Total COLAs", format_number(permittee.get("colas")))
392
- stats_table.add_row(
393
- "Approved COLAs", format_number(permittee.get("colas_approved"))
394
- )
395
- if permittee.get("last_cola_application_date"):
386
+ # COLA stats (paid plans only)
387
+ if permittee.get("colas") is not None:
388
+ stats_table = Table(show_header=False, box=None, padding=(0, 2))
389
+ stats_table.add_column("Field", style="dim")
390
+ stats_table.add_column("Value")
391
+
392
+ stats_table.add_row("Total COLAs", format_number(permittee.get("colas")))
396
393
  stats_table.add_row(
397
- "Last Application", format_date(permittee.get("last_cola_application_date"))
394
+ "Approved COLAs", format_number(permittee.get("colas_approved"))
398
395
  )
396
+ if permittee.get("last_cola_application_date"):
397
+ stats_table.add_row(
398
+ "Last Application",
399
+ format_date(permittee.get("last_cola_application_date")),
400
+ )
399
401
 
400
- console.print()
401
- console.print("[bold]COLA Statistics[/]")
402
- console.print(stats_table)
402
+ console.print()
403
+ console.print("[bold]COLA Statistics[/]")
404
+ console.print(stats_table)
403
405
 
404
406
  # Recent COLAs
405
407
  recent_colas = permittee.get("recent_colas", [])
@@ -4,10 +4,13 @@ import click
4
4
  from rich.console import Console
5
5
 
6
6
  from colacloud_cli import __version__
7
+ from colacloud_cli.commands.avas import avas_group
7
8
  from colacloud_cli.commands.barcode import barcode_command
8
9
  from colacloud_cli.commands.colas import colas_group
9
10
  from colacloud_cli.commands.config import config_group
10
11
  from colacloud_cli.commands.permittees import permittees_group
12
+ from colacloud_cli.commands.processing_times import processing_times_group
13
+ from colacloud_cli.commands.production_reports import production_reports_group
11
14
  from colacloud_cli.commands.usage import usage_command
12
15
 
13
16
  console = Console()
@@ -29,6 +32,9 @@ class AliasedGroup(click.Group):
29
32
  "c": "config",
30
33
  "b": "barcode",
31
34
  "u": "usage",
35
+ "pt": "processing-times",
36
+ "pr": "production-reports",
37
+ "a": "avas",
32
38
  }
33
39
 
34
40
  if cmd_name in aliases:
@@ -76,7 +82,7 @@ def cli(ctx: click.Context) -> None:
76
82
  cola barcode 012345678901
77
83
  cola usage
78
84
 
79
- For more information, visit https://colacloud.us/docs/api
85
+ For more information, visit https://docs.colacloud.us
80
86
  """
81
87
  ctx.ensure_object(dict)
82
88
 
@@ -87,6 +93,9 @@ cli.add_command(colas_group)
87
93
  cli.add_command(permittees_group)
88
94
  cli.add_command(barcode_command)
89
95
  cli.add_command(usage_command)
96
+ cli.add_command(processing_times_group)
97
+ cli.add_command(production_reports_group)
98
+ cli.add_command(avas_group)
90
99
 
91
100
 
92
101
  def main() -> None:
@@ -195,6 +195,220 @@ class TestUsage:
195
195
  assert result["data"]["detail_views"]["used"] == 100
196
196
 
197
197
 
198
+ class TestProcessingTimes:
199
+ @respx.mock
200
+ def test_list_processing_times(self, client):
201
+ """list_processing_times returns results."""
202
+ respx.get("https://test.colacloud.us/api/v1/processing-times").mock(
203
+ return_value=httpx.Response(
204
+ 200,
205
+ json={
206
+ "data": [{"commodity": "wine", "avg_days": 30}],
207
+ "meta": {"total": 1},
208
+ },
209
+ )
210
+ )
211
+
212
+ result = client.list_processing_times()
213
+ assert len(result["data"]) == 1
214
+ assert result["data"][0]["commodity"] == "wine"
215
+
216
+ @respx.mock
217
+ def test_list_processing_times_with_commodity(self, client):
218
+ """list_processing_times passes commodity filter."""
219
+ route = respx.get("https://test.colacloud.us/api/v1/processing-times").mock(
220
+ return_value=httpx.Response(
221
+ 200, json={"data": [], "meta": {"total": 0}}
222
+ )
223
+ )
224
+
225
+ client.list_processing_times(commodity="wine")
226
+ request = route.calls[0].request
227
+ assert "commodity=wine" in str(request.url)
228
+
229
+ @respx.mock
230
+ def test_list_formula_processing_times(self, client):
231
+ """list_formula_processing_times returns results."""
232
+ respx.get(
233
+ "https://test.colacloud.us/api/v1/processing-times/formula"
234
+ ).mock(
235
+ return_value=httpx.Response(
236
+ 200,
237
+ json={
238
+ "data": [{"formula_type": "domestic", "avg_days": 20}],
239
+ "meta": {"total": 1},
240
+ },
241
+ )
242
+ )
243
+
244
+ result = client.list_formula_processing_times()
245
+ assert len(result["data"]) == 1
246
+
247
+ @respx.mock
248
+ def test_list_formula_processing_times_with_filters(self, client):
249
+ """list_formula_processing_times passes filters."""
250
+ route = respx.get(
251
+ "https://test.colacloud.us/api/v1/processing-times/formula"
252
+ ).mock(
253
+ return_value=httpx.Response(
254
+ 200, json={"data": [], "meta": {"total": 0}}
255
+ )
256
+ )
257
+
258
+ client.list_formula_processing_times(
259
+ formula_type="domestic", commodity="wine"
260
+ )
261
+ request = route.calls[0].request
262
+ assert "formula_type=domestic" in str(request.url)
263
+ assert "commodity=wine" in str(request.url)
264
+
265
+ @respx.mock
266
+ def test_list_registration_processing_times(self, client):
267
+ """list_registration_processing_times returns results."""
268
+ respx.get(
269
+ "https://test.colacloud.us/api/v1/processing-times/registration"
270
+ ).mock(
271
+ return_value=httpx.Response(
272
+ 200,
273
+ json={
274
+ "data": [{"category": "beverage", "avg_days": 15}],
275
+ "meta": {"total": 1},
276
+ },
277
+ )
278
+ )
279
+
280
+ result = client.list_registration_processing_times()
281
+ assert len(result["data"]) == 1
282
+
283
+ @respx.mock
284
+ def test_list_registration_processing_times_with_filters(self, client):
285
+ """list_registration_processing_times passes filters."""
286
+ route = respx.get(
287
+ "https://test.colacloud.us/api/v1/processing-times/registration"
288
+ ).mock(
289
+ return_value=httpx.Response(
290
+ 200, json={"data": [], "meta": {"total": 0}}
291
+ )
292
+ )
293
+
294
+ client.list_registration_processing_times(
295
+ category="beverage", application_type="original"
296
+ )
297
+ request = route.calls[0].request
298
+ assert "category=beverage" in str(request.url)
299
+ assert "application_type=original" in str(request.url)
300
+
301
+
302
+ class TestProductionReports:
303
+ @respx.mock
304
+ def test_list_production_reports(self, client):
305
+ """list_production_reports returns results."""
306
+ respx.get("https://test.colacloud.us/api/v1/production-reports").mock(
307
+ return_value=httpx.Response(
308
+ 200,
309
+ json={
310
+ "data": [{"commodity": "wine", "year": 2024}],
311
+ "meta": {"total": 1, "page": 1, "per_page": 100, "has_more": False},
312
+ },
313
+ )
314
+ )
315
+
316
+ result = client.list_production_reports()
317
+ assert len(result["data"]) == 1
318
+ assert result["data"][0]["commodity"] == "wine"
319
+
320
+ @respx.mock
321
+ def test_list_production_reports_with_filters(self, client):
322
+ """list_production_reports passes filter parameters."""
323
+ route = respx.get(
324
+ "https://test.colacloud.us/api/v1/production-reports"
325
+ ).mock(
326
+ return_value=httpx.Response(
327
+ 200,
328
+ json={
329
+ "data": [],
330
+ "meta": {"total": 0, "page": 1, "per_page": 100, "has_more": False},
331
+ },
332
+ )
333
+ )
334
+
335
+ client.list_production_reports(
336
+ commodity="wine", year=2024, month=6, report_type="monthly"
337
+ )
338
+ request = route.calls[0].request
339
+ assert "commodity=wine" in str(request.url)
340
+ assert "year=2024" in str(request.url)
341
+ assert "month=6" in str(request.url)
342
+ assert "report_type=monthly" in str(request.url)
343
+
344
+
345
+ class TestAVAs:
346
+ @respx.mock
347
+ def test_list_avas(self, client):
348
+ """list_avas returns results."""
349
+ respx.get("https://test.colacloud.us/api/v1/avas").mock(
350
+ return_value=httpx.Response(
351
+ 200,
352
+ json={
353
+ "data": [{"id": "napa-valley", "name": "Napa Valley"}],
354
+ "meta": {"total": 1},
355
+ },
356
+ )
357
+ )
358
+
359
+ result = client.list_avas()
360
+ assert len(result["data"]) == 1
361
+ assert result["data"][0]["name"] == "Napa Valley"
362
+
363
+ @respx.mock
364
+ def test_list_avas_with_filters(self, client):
365
+ """list_avas passes filter parameters."""
366
+ route = respx.get("https://test.colacloud.us/api/v1/avas").mock(
367
+ return_value=httpx.Response(
368
+ 200, json={"data": [], "meta": {"total": 0}}
369
+ )
370
+ )
371
+
372
+ client.list_avas(state="CA", query="napa")
373
+ request = route.calls[0].request
374
+ assert "state=CA" in str(request.url)
375
+ assert "q=napa" in str(request.url)
376
+
377
+ @respx.mock
378
+ def test_get_ava(self, client):
379
+ """get_ava returns AVA details."""
380
+ respx.get("https://test.colacloud.us/api/v1/avas/napa-valley").mock(
381
+ return_value=httpx.Response(
382
+ 200,
383
+ json={
384
+ "data": {
385
+ "id": "napa-valley",
386
+ "name": "Napa Valley",
387
+ "state": "CA",
388
+ }
389
+ },
390
+ )
391
+ )
392
+
393
+ result = client.get_ava("napa-valley")
394
+ assert result["data"]["name"] == "Napa Valley"
395
+
396
+ @respx.mock
397
+ def test_get_ava_not_found(self, client):
398
+ """get_ava raises APIError for 404."""
399
+ respx.get("https://test.colacloud.us/api/v1/avas/nonexistent").mock(
400
+ return_value=httpx.Response(
401
+ 404,
402
+ json={"error": {"code": "not_found", "message": "AVA not found"}},
403
+ )
404
+ )
405
+
406
+ with pytest.raises(APIError) as exc_info:
407
+ client.get_ava("nonexistent")
408
+
409
+ assert exc_info.value.status_code == 404
410
+
411
+
198
412
  class TestErrorHandling:
199
413
  @respx.mock
200
414
  def test_authentication_error(self, client):
@@ -238,6 +238,275 @@ class TestConfigCommands:
238
238
  assert "config" in result.output.lower() or "api" in result.output.lower()
239
239
 
240
240
 
241
+ class TestProcessingTimesCommands:
242
+ @respx.mock
243
+ def test_processing_times_list(self, runner, api_key):
244
+ """processing-times list returns results."""
245
+ respx.get("https://app.colacloud.us/api/v1/processing-times").mock(
246
+ return_value=httpx.Response(
247
+ 200,
248
+ json={
249
+ "data": [{"commodity": "wine", "avg_days": 30}],
250
+ "meta": {"total": 1},
251
+ },
252
+ )
253
+ )
254
+
255
+ result = runner.invoke(cli, ["processing-times", "list"])
256
+ assert result.exit_code == 0
257
+ assert "wine" in result.output
258
+
259
+ @respx.mock
260
+ def test_processing_times_list_json(self, runner, api_key):
261
+ """processing-times list --json outputs JSON."""
262
+ respx.get("https://app.colacloud.us/api/v1/processing-times").mock(
263
+ return_value=httpx.Response(
264
+ 200,
265
+ json={
266
+ "data": [{"commodity": "wine"}],
267
+ "meta": {"total": 1},
268
+ },
269
+ )
270
+ )
271
+
272
+ result = runner.invoke(cli, ["processing-times", "list", "--json"])
273
+ assert result.exit_code == 0
274
+ data = json.loads(result.output)
275
+ assert "data" in data
276
+
277
+ @respx.mock
278
+ def test_processing_times_list_empty(self, runner, api_key):
279
+ """processing-times list shows message when no results."""
280
+ respx.get("https://app.colacloud.us/api/v1/processing-times").mock(
281
+ return_value=httpx.Response(
282
+ 200, json={"data": [], "meta": {"total": 0}}
283
+ )
284
+ )
285
+
286
+ result = runner.invoke(cli, ["processing-times", "list"])
287
+ assert result.exit_code == 0
288
+ assert "No processing times found" in result.output
289
+
290
+ @respx.mock
291
+ def test_processing_times_formula(self, runner, api_key):
292
+ """processing-times formula returns results."""
293
+ respx.get(
294
+ "https://app.colacloud.us/api/v1/processing-times/formula"
295
+ ).mock(
296
+ return_value=httpx.Response(
297
+ 200,
298
+ json={
299
+ "data": [{"formula_type": "domestic", "avg_days": 20}],
300
+ "meta": {"total": 1},
301
+ },
302
+ )
303
+ )
304
+
305
+ result = runner.invoke(cli, ["processing-times", "formula"])
306
+ assert result.exit_code == 0
307
+ assert "domestic" in result.output
308
+
309
+ @respx.mock
310
+ def test_processing_times_registration(self, runner, api_key):
311
+ """processing-times registration returns results."""
312
+ respx.get(
313
+ "https://app.colacloud.us/api/v1/processing-times/registration"
314
+ ).mock(
315
+ return_value=httpx.Response(
316
+ 200,
317
+ json={
318
+ "data": [{"category": "beverage", "avg_days": 15}],
319
+ "meta": {"total": 1},
320
+ },
321
+ )
322
+ )
323
+
324
+ result = runner.invoke(cli, ["processing-times", "registration"])
325
+ assert result.exit_code == 0
326
+ assert "beverage" in result.output
327
+
328
+
329
+ class TestProductionReportsCommands:
330
+ @respx.mock
331
+ def test_production_reports_list(self, runner, api_key):
332
+ """production-reports list returns results."""
333
+ respx.get("https://app.colacloud.us/api/v1/production-reports").mock(
334
+ return_value=httpx.Response(
335
+ 200,
336
+ json={
337
+ "data": [{"commodity": "wine", "year": 2024, "month": 6}],
338
+ "meta": {
339
+ "total": 1,
340
+ "page": 1,
341
+ "per_page": 100,
342
+ "has_more": False,
343
+ },
344
+ },
345
+ )
346
+ )
347
+
348
+ result = runner.invoke(cli, ["production-reports", "list"])
349
+ assert result.exit_code == 0
350
+ assert "wine" in result.output
351
+
352
+ @respx.mock
353
+ def test_production_reports_list_json(self, runner, api_key):
354
+ """production-reports list --json outputs JSON."""
355
+ respx.get("https://app.colacloud.us/api/v1/production-reports").mock(
356
+ return_value=httpx.Response(
357
+ 200,
358
+ json={
359
+ "data": [{"commodity": "wine"}],
360
+ "meta": {
361
+ "total": 1,
362
+ "page": 1,
363
+ "per_page": 100,
364
+ "has_more": False,
365
+ },
366
+ },
367
+ )
368
+ )
369
+
370
+ result = runner.invoke(cli, ["production-reports", "list", "--json"])
371
+ assert result.exit_code == 0
372
+ data = json.loads(result.output)
373
+ assert "data" in data
374
+
375
+ @respx.mock
376
+ def test_production_reports_list_empty(self, runner, api_key):
377
+ """production-reports list shows message when no results."""
378
+ respx.get("https://app.colacloud.us/api/v1/production-reports").mock(
379
+ return_value=httpx.Response(
380
+ 200,
381
+ json={
382
+ "data": [],
383
+ "meta": {
384
+ "total": 0,
385
+ "page": 1,
386
+ "per_page": 100,
387
+ "has_more": False,
388
+ },
389
+ },
390
+ )
391
+ )
392
+
393
+ result = runner.invoke(cli, ["production-reports", "list"])
394
+ assert result.exit_code == 0
395
+ assert "No production reports found" in result.output
396
+
397
+ @respx.mock
398
+ def test_production_reports_list_with_pagination(self, runner, api_key):
399
+ """production-reports list shows pagination hint when has_more."""
400
+ respx.get("https://app.colacloud.us/api/v1/production-reports").mock(
401
+ return_value=httpx.Response(
402
+ 200,
403
+ json={
404
+ "data": [{"commodity": "wine"}],
405
+ "meta": {
406
+ "total": 200,
407
+ "page": 1,
408
+ "per_page": 100,
409
+ "has_more": True,
410
+ },
411
+ },
412
+ )
413
+ )
414
+
415
+ result = runner.invoke(cli, ["production-reports", "list"])
416
+ assert result.exit_code == 0
417
+ assert "More results available" in result.output
418
+
419
+
420
+ class TestAVAsCommands:
421
+ @respx.mock
422
+ def test_avas_list(self, runner, api_key):
423
+ """avas list returns results."""
424
+ respx.get("https://app.colacloud.us/api/v1/avas").mock(
425
+ return_value=httpx.Response(
426
+ 200,
427
+ json={
428
+ "data": [{"id": "napa-valley", "name": "Napa Valley"}],
429
+ "meta": {"total": 1},
430
+ },
431
+ )
432
+ )
433
+
434
+ result = runner.invoke(cli, ["avas", "list"])
435
+ assert result.exit_code == 0
436
+ assert "Napa Valley" in result.output
437
+
438
+ @respx.mock
439
+ def test_avas_list_json(self, runner, api_key):
440
+ """avas list --json outputs JSON."""
441
+ respx.get("https://app.colacloud.us/api/v1/avas").mock(
442
+ return_value=httpx.Response(
443
+ 200,
444
+ json={
445
+ "data": [{"id": "napa-valley", "name": "Napa Valley"}],
446
+ "meta": {"total": 1},
447
+ },
448
+ )
449
+ )
450
+
451
+ result = runner.invoke(cli, ["avas", "list", "--json"])
452
+ assert result.exit_code == 0
453
+ data = json.loads(result.output)
454
+ assert "data" in data
455
+
456
+ @respx.mock
457
+ def test_avas_list_empty(self, runner, api_key):
458
+ """avas list shows message when no results."""
459
+ respx.get("https://app.colacloud.us/api/v1/avas").mock(
460
+ return_value=httpx.Response(
461
+ 200, json={"data": [], "meta": {"total": 0}}
462
+ )
463
+ )
464
+
465
+ result = runner.invoke(cli, ["avas", "list"])
466
+ assert result.exit_code == 0
467
+ assert "No AVAs found" in result.output
468
+
469
+ @respx.mock
470
+ def test_avas_get(self, runner, api_key):
471
+ """avas get returns AVA details."""
472
+ respx.get("https://app.colacloud.us/api/v1/avas/napa-valley").mock(
473
+ return_value=httpx.Response(
474
+ 200,
475
+ json={
476
+ "data": {
477
+ "id": "napa-valley",
478
+ "name": "Napa Valley",
479
+ "state": "CA",
480
+ }
481
+ },
482
+ )
483
+ )
484
+
485
+ result = runner.invoke(cli, ["avas", "get", "napa-valley"])
486
+ assert result.exit_code == 0
487
+ assert "Napa Valley" in result.output
488
+
489
+ @respx.mock
490
+ def test_avas_get_json(self, runner, api_key):
491
+ """avas get --json outputs JSON."""
492
+ respx.get("https://app.colacloud.us/api/v1/avas/napa-valley").mock(
493
+ return_value=httpx.Response(
494
+ 200,
495
+ json={
496
+ "data": {
497
+ "id": "napa-valley",
498
+ "name": "Napa Valley",
499
+ }
500
+ },
501
+ )
502
+ )
503
+
504
+ result = runner.invoke(cli, ["avas", "get", "napa-valley", "--json"])
505
+ assert result.exit_code == 0
506
+ data = json.loads(result.output)
507
+ assert "data" in data
508
+
509
+
241
510
  class TestCommandAliases:
242
511
  @respx.mock
243
512
  def test_shortcut_s_for_colas(self, runner, api_key):
File without changes
File without changes
File without changes