datoon 1.8.0__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.8.0 → datoon-1.9.0}/CHANGELOG.md +7 -0
- {datoon-1.8.0 → datoon-1.9.0}/PKG-INFO +7 -7
- {datoon-1.8.0 → datoon-1.9.0}/README.md +6 -6
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/_version.py +3 -3
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/cli.py +11 -2
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/converter.py +11 -11
- datoon-1.9.0/src/datoon/errors.py +20 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/mcp_server.py +5 -1
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/models.py +7 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/jsonl.py +1 -1
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/xml.py +4 -3
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/yaml.py +16 -6
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/PKG-INFO +7 -7
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/SOURCES.txt +4 -1
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/scm_file_list.json +3 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/scm_version.json +2 -2
- {datoon-1.8.0 → datoon-1.9.0}/tests/unit/test_cli.py +44 -8
- {datoon-1.8.0 → 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.8.0 → datoon-1.9.0}/tests/unit/test_mcp_server.py +12 -4
- {datoon-1.8.0 → datoon-1.9.0}/tests/unit/test_readers.py +38 -4
- datoon-1.9.0/tests/unit/test_tokens.py +43 -0
- {datoon-1.8.0 → datoon-1.9.0}/.agents/plugins/marketplace.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.claude-plugin/marketplace.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.claude-plugin/plugin.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.editorconfig +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.gitattributes +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/dependabot.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/package-installer-smoke.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/pages.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/pre-commit.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/publish-mcp.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/publish.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/release.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/tests.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/validate-artifacts.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.github/workflows/workflow-lint.yml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.gitignore +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.pre-commit-config.yaml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/.releaserc.yaml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/CLAUDE.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/CONTRIBUTING.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/INSTALL.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/LICENSE +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/MARKETPLACES.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/SECURITY.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/SKILL.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/assets/datoon-banner.svg +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/REPORT.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/agent_results.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/generate_payloads.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/payloads.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/results/.gitkeep +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/benchmarks/run.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/datoon/SKILL.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/datoon.skill +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/docs/.nojekyll +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/docs/app.js +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/docs/assets/datoon-banner.svg +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/docs/index.html +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/docs/styles.css +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/glama.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/install.sh +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/plugins/datoon/.codex-plugin/plugin.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/plugins/datoon/skills/datoon/SKILL.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/pyproject.toml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/scripts/ab_skill_benchmark.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/scripts/install.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/scripts/summarize_agent_skill_eval.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/scripts/validate_plugin_metadata.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/scripts/validate_skill_sync.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/server.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/setup.cfg +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/setup.sh +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/skills/datoon/README.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/skills/datoon/SKILL.md +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/smithery.yaml +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/__init__.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/analyzer.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/py.typed +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/__init__.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/_coerce.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/_tabular.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/columnar.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/csv.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/excel.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon/readers/numbers.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/dependency_links.txt +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/entry_points.txt +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/requires.txt +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/src/datoon.egg-info/top_level.txt +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/fixtures/users.expected.toon +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/fixtures/users.json +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/integration/test_toon_cli_integration.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/unit/test_analyzer.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/unit/test_installer.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/unit/test_models.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/tests/unit/test_tabular_rows.py +0 -0
- {datoon-1.8.0 → datoon-1.9.0}/uv.lock +0 -0
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
# [1.8.0](https://github.com/andrii-su/datoon/compare/v1.7.4...v1.8.0) (2026-07-07)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -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
|
|
|
@@ -87,6 +87,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
87
87
|
default=0,
|
|
88
88
|
help="Table index within a Numbers sheet (default: 0).",
|
|
89
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
|
+
)
|
|
90
98
|
parser.add_argument(
|
|
91
99
|
"--report",
|
|
92
100
|
help="Optional path to write conversion report as JSON.",
|
|
@@ -171,6 +179,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
171
179
|
min_uniform_rows=args.min_uniform_rows,
|
|
172
180
|
force=args.force,
|
|
173
181
|
toon_cli_timeout=args.timeout,
|
|
182
|
+
token_encoding=args.encoding,
|
|
174
183
|
)
|
|
175
184
|
|
|
176
185
|
fmt = _resolve_format(args.input, args.format)
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -315,3 +331,23 @@ def test_main_forwards_sheet_and_table_for_binary(
|
|
|
315
331
|
)
|
|
316
332
|
assert rc == 0
|
|
317
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))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tests for the datoon.errors module and its use across layers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_datoon_error_lives_in_errors_module() -> None:
|
|
11
|
+
from datoon.errors import DatoonError
|
|
12
|
+
|
|
13
|
+
assert issubclass(DatoonError, RuntimeError)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_converter_reexports_the_same_error_object() -> None:
|
|
17
|
+
# Backward compatibility: `from datoon.converter import DatoonError` must
|
|
18
|
+
# keep working and refer to the single canonical class.
|
|
19
|
+
from datoon.converter import DatoonError as FromConverter
|
|
20
|
+
from datoon.errors import DatoonError as Canonical
|
|
21
|
+
|
|
22
|
+
assert FromConverter is Canonical
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_jsonl_reader_does_not_depend_on_converter() -> None:
|
|
26
|
+
# The low-level reader must not import the high-level conversion service
|
|
27
|
+
# just for an exception type (layering inversion).
|
|
28
|
+
import datoon.readers.jsonl as jsonl
|
|
29
|
+
|
|
30
|
+
source = inspect.getsource(jsonl)
|
|
31
|
+
assert "datoon.converter" not in source
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_jsonl_raises_canonical_error() -> None:
|
|
35
|
+
from datoon.errors import DatoonError
|
|
36
|
+
from datoon.readers.jsonl import read_jsonl
|
|
37
|
+
|
|
38
|
+
with pytest.raises(DatoonError, match="line 2"):
|
|
39
|
+
read_jsonl('{"a":1}\nbad json\n')
|
|
@@ -92,7 +92,9 @@ def test_convert_json_converts_when_savings_sufficient(
|
|
|
92
92
|
lambda _, **kw: "rows[3]{id,v}:\n 1,a\n 2,b\n 3,c\n",
|
|
93
93
|
)
|
|
94
94
|
token_seq = iter([100, 60])
|
|
95
|
-
monkeypatch.setattr(
|
|
95
|
+
monkeypatch.setattr(
|
|
96
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
97
|
+
)
|
|
96
98
|
|
|
97
99
|
result = convert_json(payload, min_savings=0.15)
|
|
98
100
|
assert "error" not in result
|
|
@@ -122,7 +124,9 @@ def test_convert_text_csv_converts(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
122
124
|
**kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n 2,Lin,analyst\n 3,Grace,viewer\n",
|
|
123
125
|
)
|
|
124
126
|
token_seq = iter([100, 40])
|
|
125
|
-
monkeypatch.setattr(
|
|
127
|
+
monkeypatch.setattr(
|
|
128
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
129
|
+
)
|
|
126
130
|
|
|
127
131
|
result = convert_text(csv_text, fmt="csv")
|
|
128
132
|
assert "error" not in result
|
|
@@ -141,7 +145,9 @@ def test_convert_text_jsonl_converts(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
141
145
|
lambda _, **kw: "rows[3]{id,name,role}:\n 1,Ada,admin\n",
|
|
142
146
|
)
|
|
143
147
|
token_seq = iter([100, 40])
|
|
144
|
-
monkeypatch.setattr(
|
|
148
|
+
monkeypatch.setattr(
|
|
149
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
150
|
+
)
|
|
145
151
|
|
|
146
152
|
result = convert_text(jsonl_text, fmt="jsonl")
|
|
147
153
|
assert "error" not in result
|
|
@@ -162,7 +168,9 @@ def test_convert_text_xml_converts(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
162
168
|
lambda _, **kw: "rows[3]{id,name}:\n 1,Ada\n",
|
|
163
169
|
)
|
|
164
170
|
token_seq = iter([100, 40])
|
|
165
|
-
monkeypatch.setattr(
|
|
171
|
+
monkeypatch.setattr(
|
|
172
|
+
"datoon.converter.estimate_tokens", lambda *_a, **_k: next(token_seq)
|
|
173
|
+
)
|
|
166
174
|
|
|
167
175
|
result = convert_text(xml_text, fmt="xml")
|
|
168
176
|
assert "error" not in result
|
|
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
+
from datoon.errors import DatoonError
|
|
11
12
|
from datoon.readers import (
|
|
12
13
|
ALL_FORMATS,
|
|
13
14
|
BINARY_FORMATS,
|
|
@@ -215,6 +216,39 @@ def test_read_yaml_missing_dep(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
215
216
|
yaml_reader.read_yaml("- id: 1")
|
|
216
217
|
|
|
217
218
|
|
|
219
|
+
def test_read_yaml_rejects_list_of_scalars(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
220
|
+
yaml_mock = MagicMock()
|
|
221
|
+
yaml_mock.safe_load.return_value = [1, 2, 3]
|
|
222
|
+
monkeypatch.setitem(sys.modules, "yaml", yaml_mock)
|
|
223
|
+
|
|
224
|
+
from datoon.readers.yaml import read_yaml
|
|
225
|
+
|
|
226
|
+
with pytest.raises(DatoonError, match="list of objects"):
|
|
227
|
+
read_yaml("- 1\n- 2\n")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_read_yaml_rejects_mixed_list(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
231
|
+
yaml_mock = MagicMock()
|
|
232
|
+
yaml_mock.safe_load.return_value = [{"id": 1}, "oops", {"id": 3}]
|
|
233
|
+
monkeypatch.setitem(sys.modules, "yaml", yaml_mock)
|
|
234
|
+
|
|
235
|
+
from datoon.readers.yaml import read_yaml
|
|
236
|
+
|
|
237
|
+
with pytest.raises(DatoonError, match="list of objects"):
|
|
238
|
+
read_yaml("- id: 1\n- oops\n")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_read_yaml_rejects_bad_shape(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
242
|
+
yaml_mock = MagicMock()
|
|
243
|
+
yaml_mock.safe_load.return_value = 42
|
|
244
|
+
monkeypatch.setitem(sys.modules, "yaml", yaml_mock)
|
|
245
|
+
|
|
246
|
+
from datoon.readers.yaml import read_yaml
|
|
247
|
+
|
|
248
|
+
with pytest.raises(DatoonError, match="list of objects"):
|
|
249
|
+
read_yaml("42\n")
|
|
250
|
+
|
|
251
|
+
|
|
218
252
|
# ---------------------------------------------------------------------------
|
|
219
253
|
# XML reader
|
|
220
254
|
# ---------------------------------------------------------------------------
|
|
@@ -258,7 +292,7 @@ def test_read_xml_rejects_dtd_billion_laughs() -> None:
|
|
|
258
292
|
'<!ENTITY lol2 "&lol;&lol;&lol;">]>'
|
|
259
293
|
"<rows><r>&lol2;</r></rows>"
|
|
260
294
|
)
|
|
261
|
-
with pytest.raises(
|
|
295
|
+
with pytest.raises(DatoonError, match="DOCTYPE"):
|
|
262
296
|
read_xml(payload)
|
|
263
297
|
|
|
264
298
|
|
|
@@ -267,17 +301,17 @@ def test_read_xml_rejects_external_entity_dtd() -> None:
|
|
|
267
301
|
'<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'
|
|
268
302
|
"<rows><r>&xxe;</r></rows>"
|
|
269
303
|
)
|
|
270
|
-
with pytest.raises(
|
|
304
|
+
with pytest.raises(DatoonError, match="DOCTYPE"):
|
|
271
305
|
read_xml(payload)
|
|
272
306
|
|
|
273
307
|
|
|
274
308
|
def test_read_xml_invalid_raises() -> None:
|
|
275
|
-
with pytest.raises(
|
|
309
|
+
with pytest.raises(DatoonError, match="Invalid XML"):
|
|
276
310
|
read_xml("<not closed>")
|
|
277
311
|
|
|
278
312
|
|
|
279
313
|
def test_read_xml_empty_root_raises() -> None:
|
|
280
|
-
with pytest.raises(
|
|
314
|
+
with pytest.raises(DatoonError, match="no children"):
|
|
281
315
|
read_xml("<root/>")
|
|
282
316
|
|
|
283
317
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tests for configurable token-encoding estimation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from datoon.converter import estimate_tokens
|
|
8
|
+
from datoon.models import DEFAULT_TOKEN_ENCODING, ConversionConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_default_encoding_is_o200k() -> None:
|
|
12
|
+
assert DEFAULT_TOKEN_ENCODING == "o200k_base"
|
|
13
|
+
assert ConversionConfig().token_encoding == "o200k_base"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_config_rejects_empty_encoding() -> None:
|
|
17
|
+
with pytest.raises(ValueError, match="token_encoding"):
|
|
18
|
+
ConversionConfig(token_encoding="")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_estimate_tokens_respects_encoding() -> None:
|
|
22
|
+
tiktoken = pytest.importorskip("tiktoken")
|
|
23
|
+
text = '{"rows":[{"id":1,"name":"Ada","role":"admin"}]}'
|
|
24
|
+
|
|
25
|
+
for name in ("o200k_base", "cl100k_base"):
|
|
26
|
+
expected = len(tiktoken.get_encoding(name).encode(text))
|
|
27
|
+
assert estimate_tokens(text, encoding=name) == expected
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_estimate_tokens_falls_back_to_heuristic_without_tiktoken(
|
|
31
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
32
|
+
) -> None:
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
import datoon.converter as conv
|
|
36
|
+
|
|
37
|
+
conv._load_token_encoder.cache_clear()
|
|
38
|
+
monkeypatch.setitem(sys.modules, "tiktoken", None) # type: ignore[arg-type]
|
|
39
|
+
try:
|
|
40
|
+
# 30 chars -> ceil(30 / 3) == 10 by the char heuristic.
|
|
41
|
+
assert estimate_tokens("x" * 30, encoding="o200k_base") == 10
|
|
42
|
+
finally:
|
|
43
|
+
conv._load_token_encoder.cache_clear()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|