Skip to content

Commit 96e4e63

Browse files
♻️ harmonize V2 CLI with other SDKs, add tests
1 parent ff950f6 commit 96e4e63

19 files changed

Lines changed: 1065 additions & 152 deletions

.github/workflows/_test-cli.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Test Command Line Interface
2+
3+
on:
4+
workflow_call:
5+
workflow_dispatch:
6+
7+
env:
8+
MINDEE_API_KEY: ${{ secrets.MINDEE_API_KEY_SE_TESTS }}
9+
MINDEE_V2_API_KEY: ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }}
10+
MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }}
11+
MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }}
12+
MINDEE_V2_SE_TESTS_CROP_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }}
13+
MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }}
14+
MINDEE_V2_SE_TESTS_OCR_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }}
15+
16+
jobs:
17+
test:
18+
name: Run CLI tests
19+
timeout-minutes: 30
20+
strategy:
21+
max-parallel: 4
22+
matrix:
23+
os:
24+
- "ubuntu-22.04"
25+
- "macos-latest"
26+
- "windows-2022"
27+
python-version:
28+
- "3.10"
29+
- "3.14"
30+
runs-on: ${{ matrix.os }}
31+
steps:
32+
- uses: actions/checkout@v6
33+
with:
34+
submodules: recursive
35+
36+
- name: Set up Python ${{ matrix.python-version }}
37+
uses: actions/setup-python@v6
38+
with:
39+
python-version: ${{ matrix.python-version }}
40+
41+
- name: Cache dependencies
42+
uses: actions/cache@v5
43+
with:
44+
path: ~/.cache/pip
45+
key: ${{ runner.os }}-cli-${{ hashFiles('**/pyproject.toml') }}
46+
restore-keys: |
47+
${{ runner.os }}-cli-
48+
49+
- name: Install dependencies
50+
run: |
51+
python -m pip install pip
52+
pip install -e '.'
53+
54+
- name: Test V1 CLI
55+
shell: sh
56+
run: |
57+
./tests/test_v1_cli.sh ./tests/data/file_types/pdf/blank_1.pdf python
58+
59+
- name: Test V2 CLI
60+
shell: sh
61+
run: |
62+
./tests/test_v2_cli.sh ./tests/data/file_types/pdf/blank_1.pdf python
63+
64+
- name: Notify Slack Action on Failure
65+
uses: ravsamhq/notify-slack-action@2.5.0
66+
if: ${{ always() && github.ref_name == 'main' }}
67+
with:
68+
status: ${{ job.status }}
69+
notify_when: "failure"
70+
notification_title: "[Python] CLI test '{workflow}' is failing"
71+
env:
72+
SLACK_WEBHOOK_URL: ${{ secrets.PRODUCTION_ISSUES_SLACK_HOOK_URL }}

.github/workflows/cron.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ jobs:
1111
test-code-samples:
1212
uses: mindee/mindee-api-python/.github/workflows/_smoke-test.yml@main
1313
secrets: inherit
14+
test-cli:
15+
uses: mindee/mindee-api-python/.github/workflows/_test-cli.yml@main
16+
secrets: inherit

.github/workflows/pull-request.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ jobs:
2929
uses: ./.github/workflows/_smoke-test.yml
3030
needs: test-units
3131
secrets: inherit
32+
test-cli:
33+
uses: ./.github/workflows/_test-cli.yml
34+
needs: test-units
35+
secrets: inherit

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Mindee Python Client Library Changelog
22

3+
## Unreleased
4+
### ¡Breaking Changes!
5+
* :boom: :sparkles: unify the `mindee` CLI to mirror the .NET reference: V2 inference commands (`classification`, `crop`, `extraction`, `ocr`, `split`) and `search-models` at the root, V1 product commands wrapped under a `v1` subcommand
6+
* :boom: :coffin: remove the separate `mindeeV2` script entry point; use `mindee` for both V1 (via `mindee v1 …`) and V2
7+
### Changes
8+
* :sparkles: add `--output/-o` (`summary` / `full` / `raw`) plus per-product `--api-key/-k`, `--model-id/-m`, `--alias/-a` flags; expose extraction-only `--rag/-g`, `--raw-text/-r`, `--confidence/-c`, `--polygon/-p`, `--text-context/-t`
9+
* :sparkles: add `ocr` subcommand to the V2 CLI
10+
* :sparkles: add `--verbose/-v` flag on the CLI (repeat for debug-level output)
11+
* :sparkles: `SearchResponse` now extends `CommonResponse` and exposes `raw_http`
12+
* :white_check_mark: add `tests/test_v1_cli.sh` and `tests/test_v2_cli.sh` integration scripts and a `_test-cli.yml` workflow mirroring the .NET CLI test setup
13+
314
## v5.0.0 - 2026-06-17
415
### ¡Breaking Changes!
516
* :boom: :recycle: update V1 & V2 syntaxes to match other SDKs

mindee/cli.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,75 @@
1-
from mindee.v1.commands.cli_parser import MindeeParser
1+
import logging
2+
import sys
3+
4+
from mindee.v2.commands.cli_parser import MindeeParser
5+
6+
_V1_DASHV_PRODUCTS = ("custom", "generated")
7+
8+
9+
def _find_v1_dashv_boundary(argv: list[str]) -> int | None:
10+
"""Locate the position after which ``-v`` belongs to V1 ``custom`` /
11+
``generated``.
12+
13+
These two V1 products register ``-v/--version``; tokens at or beyond
14+
the returned index must be left alone by the verbose pre-scan.
15+
"""
16+
for i, token in enumerate(argv):
17+
if token == "v1" and i + 1 < len(argv) and argv[i + 1] in _V1_DASHV_PRODUCTS:
18+
return i + 2
19+
return None
20+
21+
22+
def _extract_verbose_level(argv: list[str]) -> tuple[int, list[str]]:
23+
"""Pre-scan ``argv`` for ``--verbose`` / ``-v`` flags.
24+
25+
Mirrors ``mindee-api-dotnet``'s ``args.Contains("--verbose")`` check:
26+
the flag is consumed before argparse runs so it can appear anywhere
27+
on the command line.
28+
29+
* ``--verbose`` is consumed at any position (no conflict).
30+
* ``-v`` is consumed at any position *except* after a ``v1 custom``
31+
or ``v1 generated`` invocation, where it is the V1 product's own
32+
``--version`` option.
33+
34+
:returns: ``(level, remaining_argv)`` where ``level`` counts the number
35+
of recognized verbose-flag occurrences.
36+
"""
37+
level = 0
38+
remaining: list[str] = []
39+
v1_dashv_start = _find_v1_dashv_boundary(argv)
40+
for i, token in enumerate(argv):
41+
if token == "--verbose":
42+
level += 1
43+
continue
44+
if token == "-v" and (v1_dashv_start is None or i < v1_dashv_start):
45+
level += 1
46+
continue
47+
remaining.append(token)
48+
return level, remaining
49+
50+
51+
def _configure_logging(verbose_level: int) -> None:
52+
"""Set the ``mindee`` logger level based on the verbose count."""
53+
if verbose_level <= 0:
54+
return
55+
target = logging.INFO if verbose_level == 1 else logging.DEBUG
56+
logging.getLogger("mindee").setLevel(target)
57+
logging.getLogger().setLevel(target)
258

359

460
def main() -> None:
5-
"""Run the Command Line Interface."""
61+
"""Run the Command Line Interface.
62+
63+
The unified ``mindee`` binary exposes V2 inference commands and the
64+
``search-models`` utility at the root, with all V1 product commands
65+
wrapped under a ``v1`` subcommand — mirroring the canonical
66+
``mindee-api-dotnet`` CLI.
67+
68+
Pass ``--verbose`` (or ``-v``) to enable diagnostic logging; repeat
69+
the flag (``--verbose --verbose``) for debug-level output.
70+
"""
71+
verbose_level, argv = _extract_verbose_level(sys.argv[1:])
72+
_configure_logging(verbose_level)
73+
sys.argv = [sys.argv[0], *argv]
674
parser = MindeeParser()
7-
parser.call_parse()
75+
sys.exit(parser.call_parse() or 0)

mindee/logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import os
55

6-
LOGLEVEL = os.environ.get("MINDEE_LOGLEVEL", "INFO").upper()
6+
LOGLEVEL = os.environ.get("MINDEE_LOGLEVEL", "WARNING").upper()
77
logging.basicConfig(level=LOGLEVEL)
88

99
logger = logging.getLogger("mindee")

mindee/v1/commands/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from mindee.v1.commands.cli_parser import MindeeArgumentParser, MindeeParser
1+
from mindee.v1.commands.cli_parser import (
2+
MindeeArgumentParser,
3+
MindeeParser,
4+
register_v1_product_subparsers,
5+
)
26
from mindee.v1.commands.cli_products import PRODUCTS, CommandConfig
37

48
__all__ = [
59
"PRODUCTS",
610
"CommandConfig",
711
"MindeeArgumentParser",
812
"MindeeParser",
13+
"register_v1_product_subparsers",
914
]

mindee/v1/commands/cli_parser.py

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,54 @@ def add_custom_options(self) -> None:
103103
)
104104

105105

106+
def register_v1_product_subparsers(parser: ArgumentParser) -> None:
107+
"""
108+
Register V1 product subparsers under the given ``parser``.
109+
110+
Used both by the legacy ``mindee`` binary and by the ``v1`` group of
111+
the unified ``mindeeV2`` CLI.
112+
"""
113+
parse_product_subparsers = parser.add_subparsers(
114+
dest="product_name",
115+
required=True,
116+
parser_class=MindeeArgumentParser,
117+
)
118+
119+
for name, info in PRODUCTS.items():
120+
parse_subparser = parse_product_subparsers.add_parser(name, help=info.help)
121+
122+
parse_subparser.add_main_options()
123+
parse_subparser.add_sending_options()
124+
parse_subparser.add_display_options()
125+
if name in ("custom", "generated"):
126+
parse_subparser.add_custom_options()
127+
else:
128+
parse_subparser.add_argument(
129+
"-t",
130+
"--full-text",
131+
dest="include_words",
132+
action="store_true",
133+
help="include full document text in response",
134+
)
135+
136+
if info.is_async and info.is_sync:
137+
parse_subparser.add_argument(
138+
"-A",
139+
"--asynchronous",
140+
dest="async_parse",
141+
help="Parse asynchronously",
142+
action="store_true",
143+
required=False,
144+
default=False,
145+
)
146+
147+
106148
class MindeeParser:
107149
"""Custom parser for the Mindee CLI."""
108150

109151
parser: MindeeArgumentParser
110152
"""Parser options."""
153+
111154
parsed_args: Namespace
112155
"""Stores attributes relating to parsing."""
113156
client: Client
@@ -230,39 +273,7 @@ def _doc_str(output_type: str, doc_response: Document) -> str:
230273

231274
def _set_args(self) -> Namespace:
232275
"""Parse command line arguments."""
233-
parse_product_subparsers = self.parser.add_subparsers(
234-
dest="product_name",
235-
required=True,
236-
)
237-
238-
for name, info in PRODUCTS.items():
239-
parse_subparser = parse_product_subparsers.add_parser(name, help=info.help)
240-
241-
parse_subparser.add_main_options()
242-
parse_subparser.add_sending_options()
243-
parse_subparser.add_display_options()
244-
if name in ("custom", "generated"):
245-
parse_subparser.add_custom_options()
246-
else:
247-
parse_subparser.add_argument(
248-
"-t",
249-
"--full-text",
250-
dest="include_words",
251-
action="store_true",
252-
help="include full document text in response",
253-
)
254-
255-
if info.is_async and info.is_sync:
256-
parse_subparser.add_argument(
257-
"-A",
258-
"--asynchronous",
259-
dest="async_parse",
260-
help="Parse asynchronously",
261-
action="store_true",
262-
required=False,
263-
default=False,
264-
)
265-
276+
register_v1_product_subparsers(self.parser)
266277
parsed_args = self.parser.parse_args()
267278
return parsed_args
268279

mindee/v2/cli.py

Lines changed: 0 additions & 11 deletions
This file was deleted.

mindee/v2/commands/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from mindee.v2.commands.cli_parser import MindeeArgumentParser, MindeeParser
2+
from mindee.v2.commands.inference_command import (
3+
InferenceCommand,
4+
InferenceCommandOptions,
5+
)
6+
from mindee.v2.commands.output_type import OutputType
7+
from mindee.v2.commands.search_models_command import SearchModelsCommand
8+
9+
__all__ = [
10+
"InferenceCommand",
11+
"InferenceCommandOptions",
12+
"MindeeArgumentParser",
13+
"MindeeParser",
14+
"OutputType",
15+
"SearchModelsCommand",
16+
]

0 commit comments

Comments
 (0)