Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/_test-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Test Command Line Interface

on:
workflow_call:
workflow_dispatch:

env:
MINDEE_API_KEY: ${{ secrets.MINDEE_API_KEY_SE_TESTS }}
MINDEE_V2_API_KEY: ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }}
MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }}
MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }}
MINDEE_V2_SE_TESTS_CROP_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }}
MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }}
MINDEE_V2_SE_TESTS_OCR_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }}
permissions:
contents: read

jobs:
test:
name: Run CLI tests
timeout-minutes: 30
strategy:
max-parallel: 4
matrix:
os:
- "ubuntu-22.04"
- "macos-latest"
- "windows-2022"
python-version:
- "3.10"
- "3.14"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
with:
submodules: recursive

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Cache dependencies
uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-cli-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-cli-

- name: Install dependencies
run: |
python -m pip install pip
pip install -e '.'

- name: Test V1 CLI
shell: sh
run: |
./tests/test_v1_cli.sh ./tests/data/file_types/pdf/blank_1.pdf python

- name: Test V2 CLI
shell: sh
run: |
./tests/test_v2_cli.sh ./tests/data/file_types/pdf/blank_1.pdf python

- name: Notify Slack Action on Failure
uses: ravsamhq/notify-slack-action@2.5.0
if: ${{ always() && github.ref_name == 'main' }}
with:
status: ${{ job.status }}
notify_when: "failure"
notification_title: "[Python] CLI test '{workflow}' is failing"
env:
SLACK_WEBHOOK_URL: ${{ secrets.PRODUCTION_ISSUES_SLACK_HOOK_URL }}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
8 changes: 8 additions & 0 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ on:
schedule:
- cron: '32 0 * * *'

permissions:
contents: read
packages: read
actions: read

jobs:
test-regressions:
uses: mindee/mindee-api-python/.github/workflows/_test-regressions.yml@main
secrets: inherit
test-code-samples:
uses: mindee/mindee-api-python/.github/workflows/_smoke-test.yml@main
secrets: inherit
test-cli:
uses: mindee/mindee-api-python/.github/workflows/_test-cli.yml@main
secrets: inherit
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
4 changes: 4 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ jobs:
uses: ./.github/workflows/_smoke-test.yml
needs: test-units
secrets: inherit
test-cli:
uses: ./.github/workflows/_test-cli.yml
needs: test-units
secrets: inherit
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Mindee Python Client Library Changelog

## Unreleased
### ¡Breaking Changes!
* :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
* :boom: :coffin: remove the separate `mindeeV2` script entry point; use `mindee` for both V1 (via `mindee v1 …`) and V2
* :boom: :recycle: refactor the V2 CLI from a central `InferenceCommand` config to one class per command (`ClassificationCommand`, `CropCommand`, `ExtractionCommand`, `OcrCommand`, `SplitCommand`) extending a new `BaseInferenceCommand`. `InferenceCommand` and `InferenceCommandOptions` are no longer exported from `mindee.v2.commands`.
### Changes
* :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`
* :sparkles: add `ocr` subcommand to the V2 CLI
* :sparkles: add `--verbose/-v` flag on the CLI (repeat for debug-level output)
* :sparkles: `SearchResponse` now extends `CommonResponse` and exposes `raw_http`
* :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

## v5.0.0 - 2026-06-17
### ¡Breaking Changes!
* :boom: :recycle: update V1 & V2 syntaxes to match other SDKs
Expand Down
74 changes: 71 additions & 3 deletions mindee/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,75 @@
from mindee.v1.commands.cli_parser import MindeeParser
import logging
import sys

from mindee.v2.commands.cli_parser import MindeeParser

_V1_DASHV_PRODUCTS = ("custom", "generated")


def _find_v1_dashv_boundary(argv: list[str]) -> int | None:
"""Locate the position after which ``-v`` belongs to V1 ``custom`` /
``generated``.

These two V1 products register ``-v/--version``; tokens at or beyond
the returned index must be left alone by the verbose pre-scan.
"""
for i, token in enumerate(argv):
if token == "v1" and i + 1 < len(argv) and argv[i + 1] in _V1_DASHV_PRODUCTS:
return i + 2
return None


def _extract_verbose_level(argv: list[str]) -> tuple[int, list[str]]:
"""Pre-scan ``argv`` for ``--verbose`` / ``-v`` flags.

Mirrors ``mindee-api-dotnet``'s ``args.Contains("--verbose")`` check:
the flag is consumed before argparse runs so it can appear anywhere
on the command line.

* ``--verbose`` is consumed at any position (no conflict).
* ``-v`` is consumed at any position *except* after a ``v1 custom``
or ``v1 generated`` invocation, where it is the V1 product's own
``--version`` option.

:returns: ``(level, remaining_argv)`` where ``level`` counts the number
of recognized verbose-flag occurrences.
"""
level = 0
remaining: list[str] = []
v1_dashv_start = _find_v1_dashv_boundary(argv)
for i, token in enumerate(argv):
if token == "--verbose":
level += 1
continue
if token == "-v" and (v1_dashv_start is None or i < v1_dashv_start):
level += 1
continue
remaining.append(token)
return level, remaining


def _configure_logging(verbose_level: int) -> None:
"""Set the ``mindee`` logger level based on the verbose count."""
if verbose_level <= 0:
return
target = logging.INFO if verbose_level == 1 else logging.DEBUG
logging.getLogger("mindee").setLevel(target)
logging.getLogger().setLevel(target)


def main() -> None:
"""Run the Command Line Interface."""
"""Run the Command Line Interface.

The unified ``mindee`` binary exposes V2 inference commands and the
``search-models`` utility at the root, with all V1 product commands
wrapped under a ``v1`` subcommand — mirroring the canonical
``mindee-api-dotnet`` CLI.

Pass ``--verbose`` (or ``-v``) to enable diagnostic logging; repeat
the flag (``--verbose --verbose``) for debug-level output.
"""
verbose_level, argv = _extract_verbose_level(sys.argv[1:])
_configure_logging(verbose_level)
sys.argv = [sys.argv[0], *argv]
parser = MindeeParser()
parser.call_parse()
sys.exit(parser.call_parse() or 0)
2 changes: 1 addition & 1 deletion mindee/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import os

LOGLEVEL = os.environ.get("MINDEE_LOGLEVEL", "INFO").upper()
LOGLEVEL = os.environ.get("MINDEE_LOGLEVEL", "WARNING").upper()
logging.basicConfig(level=LOGLEVEL)

logger = logging.getLogger("mindee")
7 changes: 6 additions & 1 deletion mindee/v1/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from mindee.v1.commands.cli_parser import MindeeArgumentParser, MindeeParser
from mindee.v1.commands.cli_parser import (
MindeeArgumentParser,
MindeeParser,
register_v1_product_subparsers,
)
from mindee.v1.commands.cli_products import PRODUCTS, CommandConfig

__all__ = [
"PRODUCTS",
"CommandConfig",
"MindeeArgumentParser",
"MindeeParser",
"register_v1_product_subparsers",
]
77 changes: 44 additions & 33 deletions mindee/v1/commands/cli_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,54 @@ def add_custom_options(self) -> None:
)


def register_v1_product_subparsers(parser: ArgumentParser) -> None:
"""
Register V1 product subparsers under the given ``parser``.

Used both by the legacy ``mindee`` binary and by the ``v1`` group of
the unified ``mindeeV2`` CLI.
"""
parse_product_subparsers = parser.add_subparsers(
dest="product_name",
required=True,
parser_class=MindeeArgumentParser,
)

for name, info in PRODUCTS.items():
parse_subparser = parse_product_subparsers.add_parser(name, help=info.help)

parse_subparser.add_main_options()
parse_subparser.add_sending_options()
parse_subparser.add_display_options()
if name in ("custom", "generated"):
parse_subparser.add_custom_options()
else:
parse_subparser.add_argument(
"-t",
"--full-text",
dest="include_words",
action="store_true",
help="include full document text in response",
)

if info.is_async and info.is_sync:
parse_subparser.add_argument(
"-A",
"--asynchronous",
dest="async_parse",
help="Parse asynchronously",
action="store_true",
required=False,
default=False,
)


class MindeeParser:
"""Custom parser for the Mindee CLI."""

parser: MindeeArgumentParser
"""Parser options."""

parsed_args: Namespace
"""Stores attributes relating to parsing."""
client: Client
Expand Down Expand Up @@ -230,39 +273,7 @@ def _doc_str(output_type: str, doc_response: Document) -> str:

def _set_args(self) -> Namespace:
"""Parse command line arguments."""
parse_product_subparsers = self.parser.add_subparsers(
dest="product_name",
required=True,
)

for name, info in PRODUCTS.items():
parse_subparser = parse_product_subparsers.add_parser(name, help=info.help)

parse_subparser.add_main_options()
parse_subparser.add_sending_options()
parse_subparser.add_display_options()
if name in ("custom", "generated"):
parse_subparser.add_custom_options()
else:
parse_subparser.add_argument(
"-t",
"--full-text",
dest="include_words",
action="store_true",
help="include full document text in response",
)

if info.is_async and info.is_sync:
parse_subparser.add_argument(
"-A",
"--asynchronous",
dest="async_parse",
help="Parse asynchronously",
action="store_true",
required=False,
default=False,
)

register_v1_product_subparsers(self.parser)
parsed_args = self.parser.parse_args()
return parsed_args

Expand Down
11 changes: 0 additions & 11 deletions mindee/v2/cli.py

This file was deleted.

22 changes: 22 additions & 0 deletions mindee/v2/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from mindee.v2.commands.base_inference_command import BaseInferenceCommand
from mindee.v2.commands.classification_command import ClassificationCommand
from mindee.v2.commands.cli_parser import MindeeArgumentParser, MindeeParser
from mindee.v2.commands.crop_command import CropCommand
from mindee.v2.commands.extraction_command import ExtractionCommand
from mindee.v2.commands.ocr_command import OcrCommand
from mindee.v2.commands.output_type import OutputType
from mindee.v2.commands.search_models_command import SearchModelsCommand
from mindee.v2.commands.split_command import SplitCommand

__all__ = [
"BaseInferenceCommand",
"ClassificationCommand",
"CropCommand",
"ExtractionCommand",
"MindeeArgumentParser",
"MindeeParser",
"OcrCommand",
"OutputType",
"SearchModelsCommand",
"SplitCommand",
]
Loading
Loading