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.
Files changed (130) hide show
  1. {datoon-1.7.4 → datoon-1.9.0}/CHANGELOG.md +14 -0
  2. {datoon-1.7.4 → datoon-1.9.0}/PKG-INFO +7 -7
  3. {datoon-1.7.4 → datoon-1.9.0}/README.md +6 -6
  4. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/_version.py +3 -3
  5. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/cli.py +26 -3
  6. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/converter.py +11 -11
  7. datoon-1.9.0/src/datoon/errors.py +20 -0
  8. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/mcp_server.py +5 -1
  9. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/models.py +7 -0
  10. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/__init__.py +10 -4
  11. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/jsonl.py +1 -1
  12. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/xml.py +4 -3
  13. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/yaml.py +16 -6
  14. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/PKG-INFO +7 -7
  15. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/SOURCES.txt +4 -1
  16. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/scm_file_list.json +3 -0
  17. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/scm_version.json +2 -2
  18. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_cli.py +79 -8
  19. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_converter_policy.py +2 -2
  20. datoon-1.9.0/tests/unit/test_errors.py +39 -0
  21. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_mcp_server.py +12 -4
  22. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_readers.py +68 -4
  23. datoon-1.9.0/tests/unit/test_tokens.py +43 -0
  24. {datoon-1.7.4 → datoon-1.9.0}/.agents/plugins/marketplace.json +0 -0
  25. {datoon-1.7.4 → datoon-1.9.0}/.claude-plugin/marketplace.json +0 -0
  26. {datoon-1.7.4 → datoon-1.9.0}/.claude-plugin/plugin.json +0 -0
  27. {datoon-1.7.4 → datoon-1.9.0}/.editorconfig +0 -0
  28. {datoon-1.7.4 → datoon-1.9.0}/.gitattributes +0 -0
  29. {datoon-1.7.4 → datoon-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {datoon-1.7.4 → datoon-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {datoon-1.7.4 → datoon-1.9.0}/.github/dependabot.yml +0 -0
  32. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/package-installer-smoke.yml +0 -0
  33. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/pages.yml +0 -0
  34. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/pre-commit.yml +0 -0
  35. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/publish-mcp.yml +0 -0
  36. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/publish.yml +0 -0
  37. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/release.yml +0 -0
  38. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/tests.yml +0 -0
  39. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/validate-artifacts.yml +0 -0
  40. {datoon-1.7.4 → datoon-1.9.0}/.github/workflows/workflow-lint.yml +0 -0
  41. {datoon-1.7.4 → datoon-1.9.0}/.gitignore +0 -0
  42. {datoon-1.7.4 → datoon-1.9.0}/.pre-commit-config.yaml +0 -0
  43. {datoon-1.7.4 → datoon-1.9.0}/.releaserc.yaml +0 -0
  44. {datoon-1.7.4 → datoon-1.9.0}/CLAUDE.md +0 -0
  45. {datoon-1.7.4 → datoon-1.9.0}/CONTRIBUTING.md +0 -0
  46. {datoon-1.7.4 → datoon-1.9.0}/INSTALL.md +0 -0
  47. {datoon-1.7.4 → datoon-1.9.0}/LICENSE +0 -0
  48. {datoon-1.7.4 → datoon-1.9.0}/MARKETPLACES.md +0 -0
  49. {datoon-1.7.4 → datoon-1.9.0}/SECURITY.md +0 -0
  50. {datoon-1.7.4 → datoon-1.9.0}/SKILL.md +0 -0
  51. {datoon-1.7.4 → datoon-1.9.0}/assets/datoon-banner.svg +0 -0
  52. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/REPORT.md +0 -0
  53. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/agent_results.json +0 -0
  54. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
  55. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/generate_payloads.py +0 -0
  56. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
  57. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
  58. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
  59. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
  60. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
  61. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
  62. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
  63. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
  64. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
  65. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
  66. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
  67. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
  68. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
  69. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
  70. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
  71. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
  72. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
  73. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
  74. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
  75. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
  76. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
  77. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
  78. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
  79. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
  80. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
  81. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
  82. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
  83. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
  84. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/payloads.json +0 -0
  85. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/results/.gitkeep +0 -0
  86. {datoon-1.7.4 → datoon-1.9.0}/benchmarks/run.py +0 -0
  87. {datoon-1.7.4 → datoon-1.9.0}/datoon/SKILL.md +0 -0
  88. {datoon-1.7.4 → datoon-1.9.0}/datoon.skill +0 -0
  89. {datoon-1.7.4 → datoon-1.9.0}/docs/.nojekyll +0 -0
  90. {datoon-1.7.4 → datoon-1.9.0}/docs/app.js +0 -0
  91. {datoon-1.7.4 → datoon-1.9.0}/docs/assets/datoon-banner.svg +0 -0
  92. {datoon-1.7.4 → datoon-1.9.0}/docs/index.html +0 -0
  93. {datoon-1.7.4 → datoon-1.9.0}/docs/styles.css +0 -0
  94. {datoon-1.7.4 → datoon-1.9.0}/glama.json +0 -0
  95. {datoon-1.7.4 → datoon-1.9.0}/install.sh +0 -0
  96. {datoon-1.7.4 → datoon-1.9.0}/plugins/datoon/.codex-plugin/plugin.json +0 -0
  97. {datoon-1.7.4 → datoon-1.9.0}/plugins/datoon/skills/datoon/SKILL.md +0 -0
  98. {datoon-1.7.4 → datoon-1.9.0}/pyproject.toml +0 -0
  99. {datoon-1.7.4 → datoon-1.9.0}/scripts/ab_skill_benchmark.py +0 -0
  100. {datoon-1.7.4 → datoon-1.9.0}/scripts/install.py +0 -0
  101. {datoon-1.7.4 → datoon-1.9.0}/scripts/summarize_agent_skill_eval.py +0 -0
  102. {datoon-1.7.4 → datoon-1.9.0}/scripts/validate_plugin_metadata.py +0 -0
  103. {datoon-1.7.4 → datoon-1.9.0}/scripts/validate_skill_sync.py +0 -0
  104. {datoon-1.7.4 → datoon-1.9.0}/server.json +0 -0
  105. {datoon-1.7.4 → datoon-1.9.0}/setup.cfg +0 -0
  106. {datoon-1.7.4 → datoon-1.9.0}/setup.sh +0 -0
  107. {datoon-1.7.4 → datoon-1.9.0}/skills/datoon/README.md +0 -0
  108. {datoon-1.7.4 → datoon-1.9.0}/skills/datoon/SKILL.md +0 -0
  109. {datoon-1.7.4 → datoon-1.9.0}/smithery.yaml +0 -0
  110. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/__init__.py +0 -0
  111. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/analyzer.py +0 -0
  112. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/py.typed +0 -0
  113. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/_coerce.py +0 -0
  114. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/_tabular.py +0 -0
  115. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/columnar.py +0 -0
  116. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/csv.py +0 -0
  117. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/excel.py +0 -0
  118. {datoon-1.7.4 → datoon-1.9.0}/src/datoon/readers/numbers.py +0 -0
  119. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/dependency_links.txt +0 -0
  120. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/entry_points.txt +0 -0
  121. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/requires.txt +0 -0
  122. {datoon-1.7.4 → datoon-1.9.0}/src/datoon.egg-info/top_level.txt +0 -0
  123. {datoon-1.7.4 → datoon-1.9.0}/tests/fixtures/users.expected.toon +0 -0
  124. {datoon-1.7.4 → datoon-1.9.0}/tests/fixtures/users.json +0 -0
  125. {datoon-1.7.4 → datoon-1.9.0}/tests/integration/test_toon_cli_integration.py +0 -0
  126. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_analyzer.py +0 -0
  127. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_installer.py +0 -0
  128. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_models.py +0 -0
  129. {datoon-1.7.4 → datoon-1.9.0}/tests/unit/test_tabular_rows.py +0 -0
  130. {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.7.4
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 | 56 | 31 | 44.6% | convert | 31 | 44.6% |
455
- | events-medium | 198 | 111 | 43.9% | convert | 111 | 43.9% |
456
- | orders-nested | 93 | 91 | 2.2% | skip | 93 | 0.0% |
457
- | mixed-non-uniform | 35 | 37 | -5.7% | skip | 35 | 0.0% |
458
- | metrics-wide | 133 | 63 | 52.6% | convert | 63 | 52.6% |
459
- | **Average** | **103** | **67** | **27.5%** | **3/5 convert** | **67** | **28.2%** |
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 | 56 | 31 | 44.6% | convert | 31 | 44.6% |
395
- | events-medium | 198 | 111 | 43.9% | convert | 111 | 43.9% |
396
- | orders-nested | 93 | 91 | 2.2% | skip | 93 | 0.0% |
397
- | mixed-non-uniform | 35 | 37 | -5.7% | skip | 35 | 0.0% |
398
- | metrics-wide | 133 | 63 | 52.6% | convert | 63 | 52.6% |
399
- | **Average** | **103** | **67** | **27.5%** | **3/5 convert** | **67** | **28.2%** |
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.7.4'
22
- __version_tuple__ = version_tuple = (1, 7, 4)
21
+ __version__ = version = '1.9.0'
22
+ __version_tuple__ = version_tuple = (1, 9, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g9c1017702'
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(fmt, path=Path(args.input))
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=1)
47
- def _load_token_encoder() -> Any | None:
48
- """Load a token encoder once, returning None when optional dependency is absent."""
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("cl100k_base")
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(fmt: str, path: Path) -> list[dict[str, Any]]:
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}")
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  from typing import Any
7
7
 
8
- from datoon.converter import DatoonError
8
+ from datoon.errors import DatoonError
9
9
 
10
10
 
11
11
  def read_jsonl(text: str) -> list[dict[str, Any]]:
@@ -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 ValueError(
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 ValueError(f"Invalid XML: {exc}") from exc
32
+ raise DatoonError(f"Invalid XML: {exc}") from exc
32
33
 
33
34
  children = list(root)
34
35
  if not children:
35
- raise ValueError("XML root element has no children.")
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
- return data
24
- if isinstance(data, dict):
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
- return lists[0]
28
- raise ValueError(
29
- "YAML must be a list of objects, or a mapping with exactly one list value."
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.7.4
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 | 56 | 31 | 44.6% | convert | 31 | 44.6% |
455
- | events-medium | 198 | 111 | 43.9% | convert | 111 | 43.9% |
456
- | orders-nested | 93 | 91 | 2.2% | skip | 93 | 0.0% |
457
- | mixed-non-uniform | 35 | 37 | -5.7% | skip | 35 | 0.0% |
458
- | metrics-wide | 133 | 63 | 52.6% | convert | 63 | 52.6% |
459
- | **Average** | **103** | **67** | **27.5%** | **3/5 convert** | **67** | **28.2%** |
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",
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "1.7.4",
2
+ "tag": "1.9.0",
3
3
  "distance": 0,
4
- "node": "g9c10177027b097628e6bb075bb54501482175b78",
4
+ "node": "g412011b90239e0e07c401c668d255671348628c2",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-07-07"
@@ -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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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("datoon.converter.estimate_tokens", lambda _: next(token_seq))
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 _: next(token_sequence)
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 _: next(token_sequence)
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))