datoon 1.7.4__tar.gz → 1.9.0__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.
- {datoon-1.7.4 → datoon-1.9.0}/CHANGELOG.md +14 -0
- {datoon-1.7.4 → datoon-1.9.0}/PKG-INFO +7 -7
- {datoon-1.7.4 → datoon-1.9.0}/README.md +6 -6
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/_version.py +3 -3
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/cli.py +26 -3
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/converter.py +11 -11
- datoon-1.9.0/src/datoon/errors.py +20 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/mcp_server.py +5 -1
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/models.py +7 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/__init__.py +10 -4
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/jsonl.py +1 -1
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/xml.py +4 -3
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/yaml.py +16 -6
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/PKG-INFO +7 -7
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/SOURCES.txt +4 -1
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/scm_file_list.json +3 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/scm_version.json +2 -2
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_cli.py +79 -8
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_converter_policy.py +2 -2
- datoon-1.9.0/tests/unit/test_errors.py +39 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_mcp_server.py +12 -4
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_readers.py +68 -4
- datoon-1.9.0/tests/unit/test_tokens.py +43 -0
- {datoon-1.7.4 → datoon-1.9.0}/.agents/plugins/marketplace.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.claude-plugin/marketplace.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.claude-plugin/plugin.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.editorconfig +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.gitattributes +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/dependabot.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/package-installer-smoke.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/pages.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/pre-commit.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/publish-mcp.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/publish.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/release.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/tests.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/validate-artifacts.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/workflow-lint.yml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.gitignore +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.pre-commit-config.yaml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/.releaserc.yaml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/CLAUDE.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/CONTRIBUTING.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/INSTALL.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/LICENSE +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/MARKETPLACES.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/SECURITY.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/SKILL.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/assets/datoon-banner.svg +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/REPORT.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/agent_results.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/generate_payloads.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/payloads.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/results/.gitkeep +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/benchmarks/run.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/datoon/SKILL.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/datoon.skill +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/docs/.nojekyll +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/docs/app.js +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/docs/assets/datoon-banner.svg +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/docs/index.html +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/docs/styles.css +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/glama.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/install.sh +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/plugins/datoon/.codex-plugin/plugin.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/plugins/datoon/skills/datoon/SKILL.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/pyproject.toml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/scripts/ab_skill_benchmark.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/scripts/install.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/scripts/summarize_agent_skill_eval.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/scripts/validate_plugin_metadata.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/scripts/validate_skill_sync.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/server.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/setup.cfg +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/setup.sh +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/skills/datoon/README.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/skills/datoon/SKILL.md +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/smithery.yaml +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/__init__.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/analyzer.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/py.typed +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/_coerce.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/_tabular.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/columnar.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/csv.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/excel.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/numbers.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/dependency_links.txt +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/entry_points.txt +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/requires.txt +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/top_level.txt +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/fixtures/users.expected.toon +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/fixtures/users.json +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/integration/test_toon_cli_integration.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_analyzer.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_installer.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_models.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_tabular_rows.py +0 -0
- {datoon-1.7.4 → datoon-1.9.0}/uv.lock +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.9.0](https://github.com/andrii-su/datoon/compare/v1.8.0...v1.9.0) (2026-07-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **tokens:** make token encoding configurable, default to o200k_base ([#46](https://github.com/andrii-su/datoon/issues/46)) ([8483eb7](https://github.com/andrii-su/datoon/commit/8483eb7aca27c97df2827d552e0086cf9bfbe157))
|
|
7
|
+
|
|
8
|
+
# [1.8.0](https://github.com/andrii-su/datoon/compare/v1.7.4...v1.8.0) (2026-07-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **cli:** add --sheet and --table for excel/numbers input ([#39](https://github.com/andrii-su/datoon/issues/39)) ([4799712](https://github.com/andrii-su/datoon/commit/4799712b003ca4fae25cd555aec83b2b1cece136))
|
|
14
|
+
|
|
1
15
|
## [1.7.4](https://github.com/andrii-su/datoon/compare/v1.7.3...v1.7.4) (2026-07-07)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datoon
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Smart structured-data-to-TOON gateway with pragmatic auto-gating for LLM prompts.
|
|
5
5
|
Author-email: Andrii Suruhov <andrii.suruhov@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -451,12 +451,12 @@ Auto mode avoids low-benefit and high-risk payloads (`orders-nested`, `mixed-non
|
|
|
451
451
|
|
|
452
452
|
| Dataset | JSON | TOON (forced) | Raw Saved | Auto | Auto Tokens | Auto Saved |
|
|
453
453
|
|---|---:|---:|---:|---|---:|---:|
|
|
454
|
-
| users-small |
|
|
455
|
-
| events-medium |
|
|
456
|
-
| orders-nested |
|
|
457
|
-
| mixed-non-uniform | 35 |
|
|
458
|
-
| metrics-wide |
|
|
459
|
-
| **Average** | **
|
|
454
|
+
| users-small | 54 | 40 | 25.9% | convert | 40 | 25.9% |
|
|
455
|
+
| events-medium | 219 | 162 | 26.0% | convert | 162 | 26.0% |
|
|
456
|
+
| orders-nested | 106 | 116 | -9.4% | skip | 106 | 0.0% |
|
|
457
|
+
| mixed-non-uniform | 35 | 47 | -34.3% | skip | 35 | 0.0% |
|
|
458
|
+
| metrics-wide | 142 | 103 | 27.5% | convert | 103 | 27.5% |
|
|
459
|
+
| **Average** | **111** | **94** | **7.1%** | **3/5 convert** | **89** | **15.9%** |
|
|
460
460
|
|
|
461
461
|
*Forced conversion succeeded for 5/5 payloads.*
|
|
462
462
|
|
|
@@ -391,12 +391,12 @@ Auto mode avoids low-benefit and high-risk payloads (`orders-nested`, `mixed-non
|
|
|
391
391
|
|
|
392
392
|
| Dataset | JSON | TOON (forced) | Raw Saved | Auto | Auto Tokens | Auto Saved |
|
|
393
393
|
|---|---:|---:|---:|---|---:|---:|
|
|
394
|
-
| users-small |
|
|
395
|
-
| events-medium |
|
|
396
|
-
| orders-nested |
|
|
397
|
-
| mixed-non-uniform | 35 |
|
|
398
|
-
| metrics-wide |
|
|
399
|
-
| **Average** | **
|
|
394
|
+
| users-small | 54 | 40 | 25.9% | convert | 40 | 25.9% |
|
|
395
|
+
| events-medium | 219 | 162 | 26.0% | convert | 162 | 26.0% |
|
|
396
|
+
| orders-nested | 106 | 116 | -9.4% | skip | 106 | 0.0% |
|
|
397
|
+
| mixed-non-uniform | 35 | 47 | -34.3% | skip | 35 | 0.0% |
|
|
398
|
+
| metrics-wide | 142 | 103 | 27.5% | convert | 103 | 27.5% |
|
|
399
|
+
| **Average** | **111** | **94** | **7.1%** | **3/5 convert** | **89** | **15.9%** |
|
|
400
400
|
|
|
401
401
|
*Forced conversion succeeded for 5/5 payloads.*
|
|
402
402
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.9.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 9, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g412011b90'
|
|
@@ -5,12 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Sequence
|
|
10
10
|
|
|
11
11
|
from datoon import __version__
|
|
12
12
|
from datoon.converter import DatoonError, convert_json_for_llm
|
|
13
|
-
from datoon.models import ConversionConfig
|
|
13
|
+
from datoon.models import DEFAULT_TOKEN_ENCODING, ConversionConfig
|
|
14
14
|
from datoon.readers import ALL_FORMATS, BINARY_FORMATS, detect_format, read_tabular
|
|
15
15
|
|
|
16
16
|
|
|
@@ -75,6 +75,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
75
75
|
default=30,
|
|
76
76
|
help="Seconds to wait for the TOON CLI before aborting (default: 30).",
|
|
77
77
|
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--sheet",
|
|
80
|
+
type=int,
|
|
81
|
+
default=0,
|
|
82
|
+
help="Worksheet index for excel/numbers input (default: 0).",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--table",
|
|
86
|
+
type=int,
|
|
87
|
+
default=0,
|
|
88
|
+
help="Table index within a Numbers sheet (default: 0).",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--encoding",
|
|
92
|
+
default=DEFAULT_TOKEN_ENCODING,
|
|
93
|
+
help=(
|
|
94
|
+
"tiktoken encoding for token estimates "
|
|
95
|
+
f"(default: {DEFAULT_TOKEN_ENCODING})."
|
|
96
|
+
),
|
|
97
|
+
)
|
|
78
98
|
parser.add_argument(
|
|
79
99
|
"--report",
|
|
80
100
|
help="Optional path to write conversion report as JSON.",
|
|
@@ -159,6 +179,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
159
179
|
min_uniform_rows=args.min_uniform_rows,
|
|
160
180
|
force=args.force,
|
|
161
181
|
toon_cli_timeout=args.timeout,
|
|
182
|
+
token_encoding=args.encoding,
|
|
162
183
|
)
|
|
163
184
|
|
|
164
185
|
fmt = _resolve_format(args.input, args.format)
|
|
@@ -172,7 +193,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
172
193
|
f"datoon error: --format {fmt} requires a file path, not stdin.\n"
|
|
173
194
|
)
|
|
174
195
|
return 1
|
|
175
|
-
rows = read_tabular(
|
|
196
|
+
rows = read_tabular(
|
|
197
|
+
fmt, path=Path(args.input), sheet=args.sheet, table=args.table
|
|
198
|
+
)
|
|
176
199
|
json_text = json.dumps(rows, ensure_ascii=False, separators=(",", ":"))
|
|
177
200
|
outcome = convert_json_for_llm(json_text, config)
|
|
178
201
|
else:
|
|
@@ -9,7 +9,9 @@ from functools import lru_cache
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from datoon.analyzer import analyze_payload
|
|
12
|
+
from datoon.errors import DatoonError
|
|
12
13
|
from datoon.models import (
|
|
14
|
+
DEFAULT_TOKEN_ENCODING,
|
|
13
15
|
ConversionConfig,
|
|
14
16
|
ConversionOutcome,
|
|
15
17
|
ConversionReport,
|
|
@@ -18,9 +20,7 @@ from datoon.models import (
|
|
|
18
20
|
|
|
19
21
|
TOON_CLI_PACKAGE = "@toon-format/cli@2"
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
class DatoonError(RuntimeError):
|
|
23
|
-
"""Raised when datoon cannot process the payload safely."""
|
|
23
|
+
__all__ = ["DatoonError", "convert_json_for_llm", "estimate_tokens"]
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _reject_non_finite(constant: str) -> Any:
|
|
@@ -43,20 +43,20 @@ def _normalize_json(raw_text: str) -> tuple[Any, str]:
|
|
|
43
43
|
return parsed, normalized
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
@lru_cache(maxsize=
|
|
47
|
-
def _load_token_encoder() -> Any | None:
|
|
48
|
-
"""Load a token encoder once,
|
|
46
|
+
@lru_cache(maxsize=8)
|
|
47
|
+
def _load_token_encoder(encoding: str) -> Any | None:
|
|
48
|
+
"""Load a token encoder once per name, None when the optional dep is absent."""
|
|
49
49
|
try:
|
|
50
50
|
import tiktoken # type: ignore
|
|
51
51
|
except ImportError:
|
|
52
52
|
return None
|
|
53
53
|
|
|
54
|
-
return tiktoken.get_encoding(
|
|
54
|
+
return tiktoken.get_encoding(encoding)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def estimate_tokens(text: str) -> int:
|
|
57
|
+
def estimate_tokens(text: str, encoding: str = DEFAULT_TOKEN_ENCODING) -> int:
|
|
58
58
|
"""Estimate token count with tiktoken when installed, else use a char heuristic."""
|
|
59
|
-
encoder = _load_token_encoder()
|
|
59
|
+
encoder = _load_token_encoder(encoding)
|
|
60
60
|
if encoder is not None:
|
|
61
61
|
return len(encoder.encode(text))
|
|
62
62
|
|
|
@@ -119,7 +119,7 @@ def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionO
|
|
|
119
119
|
"""Convert JSON to TOON when structure and savings meet configured policy."""
|
|
120
120
|
parsed, normalized_json = _normalize_json(raw_text)
|
|
121
121
|
analysis = analyze_payload(parsed, config)
|
|
122
|
-
input_tokens = estimate_tokens(normalized_json)
|
|
122
|
+
input_tokens = estimate_tokens(normalized_json, config.token_encoding)
|
|
123
123
|
|
|
124
124
|
if not analysis.is_candidate and not config.force:
|
|
125
125
|
return _build_skip_outcome(
|
|
@@ -144,7 +144,7 @@ def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionO
|
|
|
144
144
|
analysis=analysis,
|
|
145
145
|
)
|
|
146
146
|
|
|
147
|
-
output_tokens = estimate_tokens(toon_text)
|
|
147
|
+
output_tokens = estimate_tokens(toon_text, config.token_encoding)
|
|
148
148
|
savings_ratio = (input_tokens - output_tokens) / input_tokens
|
|
149
149
|
|
|
150
150
|
if savings_ratio < config.min_savings_ratio and not config.force:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Canonical exception types for datoon.
|
|
2
|
+
|
|
3
|
+
Kept in a leaf module so any layer — readers, converter, CLI, MCP — can raise
|
|
4
|
+
the datoon error taxonomy without importing a higher-level module.
|
|
5
|
+
|
|
6
|
+
Convention:
|
|
7
|
+
|
|
8
|
+
* ``DatoonError`` — the user's *payload* is malformed or unsuitable (invalid
|
|
9
|
+
JSON/XML/YAML, non-object rows, a refused DTD). These are the errors surfaced
|
|
10
|
+
to end users of the CLI/MCP.
|
|
11
|
+
* ``ValueError`` — a *usage* error in how datoon was called (unknown format
|
|
12
|
+
name, text passed where a file path is required). Programmer-facing.
|
|
13
|
+
* ``ImportError`` — an optional extra needed for a format is not installed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatoonError(RuntimeError):
|
|
20
|
+
"""Raised when datoon cannot process the payload safely."""
|
|
@@ -15,7 +15,7 @@ except ImportError as exc:
|
|
|
15
15
|
from datoon import __version__
|
|
16
16
|
from datoon.analyzer import analyze_payload
|
|
17
17
|
from datoon.converter import DatoonError, convert_json_for_llm
|
|
18
|
-
from datoon.models import ConversionConfig
|
|
18
|
+
from datoon.models import DEFAULT_TOKEN_ENCODING, ConversionConfig
|
|
19
19
|
from datoon.readers import TEXT_FORMATS, read_tabular
|
|
20
20
|
|
|
21
21
|
mcp = FastMCP(
|
|
@@ -40,6 +40,7 @@ def convert_json(
|
|
|
40
40
|
max_depth: int = 6,
|
|
41
41
|
min_uniform_rows: int = 3,
|
|
42
42
|
timeout: int = 30,
|
|
43
|
+
encoding: str = DEFAULT_TOKEN_ENCODING,
|
|
43
44
|
) -> dict[str, Any]:
|
|
44
45
|
"""Convert a JSON payload to TOON when structure and savings meet policy.
|
|
45
46
|
|
|
@@ -53,6 +54,7 @@ def convert_json(
|
|
|
53
54
|
min_uniform_rows=min_uniform_rows,
|
|
54
55
|
force=force,
|
|
55
56
|
toon_cli_timeout=timeout,
|
|
57
|
+
token_encoding=encoding,
|
|
56
58
|
)
|
|
57
59
|
outcome = convert_json_for_llm(json_text, config)
|
|
58
60
|
return {
|
|
@@ -105,6 +107,7 @@ def convert_text(
|
|
|
105
107
|
max_depth: int = 6,
|
|
106
108
|
min_uniform_rows: int = 3,
|
|
107
109
|
timeout: int = 30,
|
|
110
|
+
encoding: str = DEFAULT_TOKEN_ENCODING,
|
|
108
111
|
) -> dict[str, Any]:
|
|
109
112
|
"""Convert structured text (csv, yaml, xml, jsonl) to TOON with policy gating.
|
|
110
113
|
|
|
@@ -127,6 +130,7 @@ def convert_text(
|
|
|
127
130
|
min_uniform_rows=min_uniform_rows,
|
|
128
131
|
force=force,
|
|
129
132
|
toon_cli_timeout=timeout,
|
|
133
|
+
token_encoding=encoding,
|
|
130
134
|
)
|
|
131
135
|
outcome = convert_json_for_llm(json_text, config)
|
|
132
136
|
return {
|
|
@@ -7,6 +7,10 @@ from typing import Literal
|
|
|
7
7
|
|
|
8
8
|
Decision = Literal["convert", "skip"]
|
|
9
9
|
|
|
10
|
+
# Default tiktoken encoding for token estimates. o200k_base matches GPT-4o and
|
|
11
|
+
# the o-series; it is closer to modern targets than the older cl100k_base.
|
|
12
|
+
DEFAULT_TOKEN_ENCODING = "o200k_base"
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
@dataclass(frozen=True, slots=True)
|
|
12
16
|
class ConversionConfig:
|
|
@@ -17,6 +21,7 @@ class ConversionConfig:
|
|
|
17
21
|
min_uniform_rows: int = 3
|
|
18
22
|
force: bool = False
|
|
19
23
|
toon_cli_timeout: int = 30
|
|
24
|
+
token_encoding: str = DEFAULT_TOKEN_ENCODING
|
|
20
25
|
|
|
21
26
|
def __post_init__(self) -> None:
|
|
22
27
|
"""Validate boundary values to avoid ambiguous runtime decisions."""
|
|
@@ -28,6 +33,8 @@ class ConversionConfig:
|
|
|
28
33
|
raise ValueError("min_uniform_rows must be at least 2.")
|
|
29
34
|
if self.toon_cli_timeout < 1:
|
|
30
35
|
raise ValueError("toon_cli_timeout must be at least 1.")
|
|
36
|
+
if not self.token_encoding:
|
|
37
|
+
raise ValueError("token_encoding must be a non-empty encoding name.")
|
|
31
38
|
|
|
32
39
|
|
|
33
40
|
@dataclass(frozen=True, slots=True)
|
|
@@ -37,16 +37,20 @@ def read_tabular(
|
|
|
37
37
|
*,
|
|
38
38
|
text: str | None = None,
|
|
39
39
|
path: Path | None = None,
|
|
40
|
+
sheet: int = 0,
|
|
41
|
+
table: int = 0,
|
|
40
42
|
) -> list[dict[str, Any]]:
|
|
41
43
|
"""Read structured data and return rows as a list of dicts.
|
|
42
44
|
|
|
43
45
|
Text formats (csv, jsonl, yaml, xml) require ``text``.
|
|
44
46
|
Binary formats (excel, parquet, orc, avro, numbers) require ``path``.
|
|
47
|
+
``sheet`` selects the worksheet for excel/numbers; ``table`` selects the
|
|
48
|
+
table within a Numbers sheet. Both are ignored by columnar formats.
|
|
45
49
|
"""
|
|
46
50
|
if fmt in BINARY_FORMATS:
|
|
47
51
|
if path is None:
|
|
48
52
|
raise ValueError(f"Format '{fmt}' requires a file path, not text input.")
|
|
49
|
-
return _read_binary(fmt, path)
|
|
53
|
+
return _read_binary(fmt, path, sheet=sheet, table=table)
|
|
50
54
|
if fmt in TEXT_FORMATS:
|
|
51
55
|
if text is None:
|
|
52
56
|
raise ValueError(f"Format '{fmt}' requires text input.")
|
|
@@ -74,11 +78,13 @@ def _read_text(fmt: str, text: str) -> list[dict[str, Any]]:
|
|
|
74
78
|
raise ValueError(f"Unknown text format: {fmt}")
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
def _read_binary(
|
|
81
|
+
def _read_binary(
|
|
82
|
+
fmt: str, path: Path, *, sheet: int = 0, table: int = 0
|
|
83
|
+
) -> list[dict[str, Any]]:
|
|
78
84
|
if fmt == "excel":
|
|
79
85
|
from datoon.readers.excel import read_excel
|
|
80
86
|
|
|
81
|
-
return read_excel(path)
|
|
87
|
+
return read_excel(path, sheet=sheet)
|
|
82
88
|
if fmt == "parquet":
|
|
83
89
|
from datoon.readers.columnar import read_parquet
|
|
84
90
|
|
|
@@ -94,5 +100,5 @@ def _read_binary(fmt: str, path: Path) -> list[dict[str, Any]]:
|
|
|
94
100
|
if fmt == "numbers":
|
|
95
101
|
from datoon.readers.numbers import read_numbers
|
|
96
102
|
|
|
97
|
-
return read_numbers(path)
|
|
103
|
+
return read_numbers(path, sheet=sheet, table=table)
|
|
98
104
|
raise ValueError(f"Unknown binary format: {fmt}")
|
|
@@ -13,6 +13,7 @@ import re
|
|
|
13
13
|
import xml.etree.ElementTree as ET
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
from datoon.errors import DatoonError
|
|
16
17
|
from datoon.readers._coerce import coerce_scalar as _coerce
|
|
17
18
|
|
|
18
19
|
_DOCTYPE_RE = re.compile(r"<!DOCTYPE", re.IGNORECASE)
|
|
@@ -21,18 +22,18 @@ _DOCTYPE_RE = re.compile(r"<!DOCTYPE", re.IGNORECASE)
|
|
|
21
22
|
def read_xml(text: str) -> list[dict[str, Any]]:
|
|
22
23
|
"""Parse XML into a list of row dicts from the dominant repeated child element."""
|
|
23
24
|
if _DOCTYPE_RE.search(text):
|
|
24
|
-
raise
|
|
25
|
+
raise DatoonError(
|
|
25
26
|
"XML with a DOCTYPE/DTD is not allowed (entity-expansion risk)."
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
try:
|
|
29
30
|
root = ET.fromstring(text)
|
|
30
31
|
except ET.ParseError as exc:
|
|
31
|
-
raise
|
|
32
|
+
raise DatoonError(f"Invalid XML: {exc}") from exc
|
|
32
33
|
|
|
33
34
|
children = list(root)
|
|
34
35
|
if not children:
|
|
35
|
-
raise
|
|
36
|
+
raise DatoonError("XML root element has no children.")
|
|
36
37
|
|
|
37
38
|
tag_counts: dict[str, int] = {}
|
|
38
39
|
for child in children:
|
|
@@ -4,6 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from datoon.errors import DatoonError
|
|
8
|
+
|
|
9
|
+
_SHAPE_ERROR = (
|
|
10
|
+
"YAML must be a list of objects, or a mapping with exactly one list value."
|
|
11
|
+
)
|
|
12
|
+
|
|
7
13
|
|
|
8
14
|
def read_yaml(text: str) -> list[dict[str, Any]]:
|
|
9
15
|
"""Parse YAML text into a list of row dicts."""
|
|
@@ -19,12 +25,16 @@ def read_yaml(text: str) -> list[dict[str, Any]]:
|
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
def _normalize(data: Any) -> list[dict[str, Any]]:
|
|
28
|
+
rows: list[Any] | None = None
|
|
22
29
|
if isinstance(data, list):
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
rows = data
|
|
31
|
+
elif isinstance(data, dict):
|
|
25
32
|
lists = [v for v in data.values() if isinstance(v, list) and v]
|
|
26
33
|
if len(lists) == 1:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
rows = lists[0]
|
|
35
|
+
|
|
36
|
+
if rows is None:
|
|
37
|
+
raise DatoonError(_SHAPE_ERROR)
|
|
38
|
+
if not all(isinstance(item, dict) for item in rows):
|
|
39
|
+
raise DatoonError(_SHAPE_ERROR)
|
|
40
|
+
return rows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datoon
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Smart structured-data-to-TOON gateway with pragmatic auto-gating for LLM prompts.
|
|
5
5
|
Author-email: Andrii Suruhov <andrii.suruhov@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -451,12 +451,12 @@ Auto mode avoids low-benefit and high-risk payloads (`orders-nested`, `mixed-non
|
|
|
451
451
|
|
|
452
452
|
| Dataset | JSON | TOON (forced) | Raw Saved | Auto | Auto Tokens | Auto Saved |
|
|
453
453
|
|---|---:|---:|---:|---|---:|---:|
|
|
454
|
-
| users-small |
|
|
455
|
-
| events-medium |
|
|
456
|
-
| orders-nested |
|
|
457
|
-
| mixed-non-uniform | 35 |
|
|
458
|
-
| metrics-wide |
|
|
459
|
-
| **Average** | **
|
|
454
|
+
| users-small | 54 | 40 | 25.9% | convert | 40 | 25.9% |
|
|
455
|
+
| events-medium | 219 | 162 | 26.0% | convert | 162 | 26.0% |
|
|
456
|
+
| orders-nested | 106 | 116 | -9.4% | skip | 106 | 0.0% |
|
|
457
|
+
| mixed-non-uniform | 35 | 47 | -34.3% | skip | 35 | 0.0% |
|
|
458
|
+
| metrics-wide | 142 | 103 | 27.5% | convert | 103 | 27.5% |
|
|
459
|
+
| **Average** | **111** | **94** | **7.1%** | **3/5 convert** | **89** | **15.9%** |
|
|
460
460
|
|
|
461
461
|
*Forced conversion succeeded for 5/5 payloads.*
|
|
462
462
|
|
|
@@ -91,6 +91,7 @@ src/datoon/_version.py
|
|
|
91
91
|
src/datoon/analyzer.py
|
|
92
92
|
src/datoon/cli.py
|
|
93
93
|
src/datoon/converter.py
|
|
94
|
+
src/datoon/errors.py
|
|
94
95
|
src/datoon/mcp_server.py
|
|
95
96
|
src/datoon/models.py
|
|
96
97
|
src/datoon/py.typed
|
|
@@ -118,8 +119,10 @@ tests/integration/test_toon_cli_integration.py
|
|
|
118
119
|
tests/unit/test_analyzer.py
|
|
119
120
|
tests/unit/test_cli.py
|
|
120
121
|
tests/unit/test_converter_policy.py
|
|
122
|
+
tests/unit/test_errors.py
|
|
121
123
|
tests/unit/test_installer.py
|
|
122
124
|
tests/unit/test_mcp_server.py
|
|
123
125
|
tests/unit/test_models.py
|
|
124
126
|
tests/unit/test_readers.py
|
|
125
|
-
tests/unit/test_tabular_rows.py
|
|
127
|
+
tests/unit/test_tabular_rows.py
|
|
128
|
+
tests/unit/test_tokens.py
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"src/datoon/models.py",
|
|
40
40
|
"src/datoon/converter.py",
|
|
41
41
|
"src/datoon/cli.py",
|
|
42
|
+
"src/datoon/errors.py",
|
|
42
43
|
"src/datoon/readers/xml.py",
|
|
43
44
|
"src/datoon/readers/_coerce.py",
|
|
44
45
|
"src/datoon/readers/__init__.py",
|
|
@@ -63,7 +64,9 @@
|
|
|
63
64
|
"tests/unit/test_mcp_server.py",
|
|
64
65
|
"tests/unit/test_tabular_rows.py",
|
|
65
66
|
"tests/unit/test_analyzer.py",
|
|
67
|
+
"tests/unit/test_tokens.py",
|
|
66
68
|
"tests/unit/test_installer.py",
|
|
69
|
+
"tests/unit/test_errors.py",
|
|
67
70
|
"tests/unit/test_cli.py",
|
|
68
71
|
".claude-plugin/plugin.json",
|
|
69
72
|
".claude-plugin/marketplace.json",
|
|
@@ -53,7 +53,9 @@ def test_main_converts_json_from_stdin(
|
|
|
53
53
|
lambda _, **kw: "rows[3]{id,v}:\n 1,a\n 2,b\n 3,c\n",
|
|
54
54
|
)
|
|
55
55
|
token_seq = iter([100, 40])
|
|
56
|
-
monkeypatch.setattr(
|
|
56
|
+
monkeypatch.setattr(
|
|
57
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
rc = main([])
|
|
59
61
|
assert rc == 0
|
|
@@ -88,7 +90,9 @@ def test_main_converts_csv_via_flag(
|
|
|
88
90
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
89
91
|
)
|
|
90
92
|
token_seq = iter([100, 30])
|
|
91
|
-
monkeypatch.setattr(
|
|
93
|
+
monkeypatch.setattr(
|
|
94
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
95
|
+
)
|
|
92
96
|
|
|
93
97
|
rc = main([str(csv_file), "--format", "csv"])
|
|
94
98
|
assert rc == 0
|
|
@@ -107,7 +111,9 @@ def test_main_auto_detects_csv_from_extension(
|
|
|
107
111
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
108
112
|
)
|
|
109
113
|
token_seq = iter([100, 30])
|
|
110
|
-
monkeypatch.setattr(
|
|
114
|
+
monkeypatch.setattr(
|
|
115
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
116
|
+
)
|
|
111
117
|
|
|
112
118
|
rc = main([str(csv_file)])
|
|
113
119
|
assert rc == 0
|
|
@@ -134,7 +140,9 @@ def test_main_converts_jsonl_from_stdin(
|
|
|
134
140
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
135
141
|
)
|
|
136
142
|
token_seq = iter([100, 30])
|
|
137
|
-
monkeypatch.setattr(
|
|
143
|
+
monkeypatch.setattr(
|
|
144
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
145
|
+
)
|
|
138
146
|
|
|
139
147
|
rc = main(["--format", "jsonl"])
|
|
140
148
|
assert rc == 0
|
|
@@ -153,7 +161,9 @@ def test_main_auto_detects_jsonl_from_extension(
|
|
|
153
161
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
154
162
|
)
|
|
155
163
|
token_seq = iter([100, 30])
|
|
156
|
-
monkeypatch.setattr(
|
|
164
|
+
monkeypatch.setattr(
|
|
165
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
166
|
+
)
|
|
157
167
|
|
|
158
168
|
rc = main([str(f)])
|
|
159
169
|
assert rc == 0
|
|
@@ -182,7 +192,9 @@ def test_main_converts_xml_via_flag(
|
|
|
182
192
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
183
193
|
)
|
|
184
194
|
token_seq = iter([100, 30])
|
|
185
|
-
monkeypatch.setattr(
|
|
195
|
+
monkeypatch.setattr(
|
|
196
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
197
|
+
)
|
|
186
198
|
|
|
187
199
|
rc = main([str(f), "--format", "xml"])
|
|
188
200
|
assert rc == 0
|
|
@@ -223,7 +235,9 @@ def test_main_csv_report_stdout(
|
|
|
223
235
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
224
236
|
)
|
|
225
237
|
token_seq = iter([100, 30])
|
|
226
|
-
monkeypatch.setattr(
|
|
238
|
+
monkeypatch.setattr(
|
|
239
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
240
|
+
)
|
|
227
241
|
|
|
228
242
|
rc = main([str(csv_file), "--report-stdout"])
|
|
229
243
|
assert rc == 0
|
|
@@ -244,7 +258,9 @@ def test_main_csv_skips_when_savings_insufficient(
|
|
|
244
258
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
245
259
|
)
|
|
246
260
|
token_seq = iter([100, 98])
|
|
247
|
-
monkeypatch.setattr(
|
|
261
|
+
monkeypatch.setattr(
|
|
262
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
263
|
+
)
|
|
248
264
|
|
|
249
265
|
rc = main([str(csv_file), "--report-stdout"])
|
|
250
266
|
assert rc == 0
|
|
@@ -280,3 +296,58 @@ def test_mcp_subcommand_missing_extra_errors(
|
|
|
280
296
|
rc = main(["mcp"])
|
|
281
297
|
assert rc == 1
|
|
282
298
|
assert "mcp" in capsys.readouterr().err
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# --sheet / --table passthrough for spreadsheet formats
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_main_forwards_sheet_and_table_for_binary(
|
|
307
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
308
|
+
tmp_path: Path,
|
|
309
|
+
) -> None:
|
|
310
|
+
captured: dict[str, int] = {}
|
|
311
|
+
|
|
312
|
+
def fake_read_tabular(
|
|
313
|
+
fmt: str,
|
|
314
|
+
*,
|
|
315
|
+
text: str | None = None,
|
|
316
|
+
path: Path | None = None,
|
|
317
|
+
sheet: int = 0,
|
|
318
|
+
table: int = 0,
|
|
319
|
+
) -> list[dict[str, object]]:
|
|
320
|
+
captured["sheet"] = sheet
|
|
321
|
+
captured["table"] = table
|
|
322
|
+
return [{"id": 1, "v": "a"}]
|
|
323
|
+
|
|
324
|
+
monkeypatch.setattr("datoon.cli.read_tabular", fake_read_tabular)
|
|
325
|
+
monkeypatch.setattr("datoon.converter._run_toon_cli", lambda _, **kw: "x\n")
|
|
326
|
+
xlsx = tmp_path / "data.xlsx"
|
|
327
|
+
xlsx.write_bytes(b"")
|
|
328
|
+
|
|
329
|
+
rc = main(
|
|
330
|
+
["--format", "excel", "--sheet", "2", "--table", "1", "--force", str(xlsx)]
|
|
331
|
+
)
|
|
332
|
+
assert rc == 0
|
|
333
|
+
assert captured == {"sheet": 2, "table": 1}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_main_forwards_encoding_to_estimate(
|
|
337
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
338
|
+
) -> None:
|
|
339
|
+
payload = '{"rows":[{"id":1,"v":"a"},{"id":2,"v":"b"},{"id":3,"v":"c"}]}'
|
|
340
|
+
monkeypatch.setattr("sys.stdin", __import__("io").StringIO(payload))
|
|
341
|
+
monkeypatch.setattr("datoon.converter._run_toon_cli", lambda _, **kw: "x\n")
|
|
342
|
+
|
|
343
|
+
seen: list[str] = []
|
|
344
|
+
|
|
345
|
+
def fake_estimate(text: str, encoding: str = "o200k_base") -> int:
|
|
346
|
+
seen.append(encoding)
|
|
347
|
+
return 50 if len(seen) == 1 else 10
|
|
348
|
+
|
|
349
|
+
monkeypatch.setattr("datoon.converter.estimate_tokens", fake_estimate)
|
|
350
|
+
|
|
351
|
+
rc = main(["--force", "--encoding", "cl100k_base"])
|
|
352
|
+
assert rc == 0
|
|
353
|
+
assert seen and all(enc == "cl100k_base" for enc in seen)
|
|
@@ -107,7 +107,7 @@ def test_policy_converts_when_savings_above_threshold(
|
|
|
107
107
|
|
|
108
108
|
token_sequence = iter([100, 60]) # input, output
|
|
109
109
|
monkeypatch.setattr(
|
|
110
|
-
"datoon.converter.estimate_tokens", lambda
|
|
110
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_sequence)
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
outcome = convert_json_for_llm(payload, ConversionConfig(min_savings_ratio=0.15))
|
|
@@ -127,7 +127,7 @@ def test_policy_skips_when_savings_below_threshold(
|
|
|
127
127
|
|
|
128
128
|
token_sequence = iter([100, 95]) # input, output
|
|
129
129
|
monkeypatch.setattr(
|
|
130
|
-
"datoon.converter.estimate_tokens", lambda
|
|
130
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_sequence)
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
outcome = convert_json_for_llm(payload, ConversionConfig(min_savings_ratio=0.10))
|