datoon 1.7.1__tar.gz → 1.7.2__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 (129) hide show
  1. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/package-installer-smoke.yml +1 -1
  2. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/pages.yml +1 -1
  3. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/pre-commit.yml +1 -1
  4. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/publish-mcp.yml +1 -1
  5. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/publish.yml +1 -1
  6. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/release.yml +1 -1
  7. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/tests.yml +2 -2
  8. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/validate-artifacts.yml +1 -1
  9. {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/workflow-lint.yml +2 -2
  10. {datoon-1.7.1 → datoon-1.7.2}/CHANGELOG.md +7 -0
  11. {datoon-1.7.1 → datoon-1.7.2}/PKG-INFO +1 -1
  12. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/_version.py +3 -3
  13. datoon-1.7.2/src/datoon/readers/_coerce.py +48 -0
  14. datoon-1.7.2/src/datoon/readers/_tabular.py +46 -0
  15. datoon-1.7.2/src/datoon/readers/csv.py +17 -0
  16. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/excel.py +3 -9
  17. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/numbers.py +3 -9
  18. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/xml.py +2 -20
  19. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/PKG-INFO +1 -1
  20. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/SOURCES.txt +4 -1
  21. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/scm_file_list.json +3 -0
  22. datoon-1.7.2/src/datoon.egg-info/scm_version.json +8 -0
  23. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_readers.py +41 -0
  24. datoon-1.7.2/tests/unit/test_tabular_rows.py +44 -0
  25. {datoon-1.7.1 → datoon-1.7.2}/uv.lock +6 -6
  26. datoon-1.7.1/src/datoon/readers/csv.py +0 -35
  27. datoon-1.7.1/src/datoon.egg-info/scm_version.json +0 -8
  28. {datoon-1.7.1 → datoon-1.7.2}/.agents/plugins/marketplace.json +0 -0
  29. {datoon-1.7.1 → datoon-1.7.2}/.claude-plugin/marketplace.json +0 -0
  30. {datoon-1.7.1 → datoon-1.7.2}/.claude-plugin/plugin.json +0 -0
  31. {datoon-1.7.1 → datoon-1.7.2}/.editorconfig +0 -0
  32. {datoon-1.7.1 → datoon-1.7.2}/.gitattributes +0 -0
  33. {datoon-1.7.1 → datoon-1.7.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {datoon-1.7.1 → datoon-1.7.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  35. {datoon-1.7.1 → datoon-1.7.2}/.github/dependabot.yml +0 -0
  36. {datoon-1.7.1 → datoon-1.7.2}/.gitignore +0 -0
  37. {datoon-1.7.1 → datoon-1.7.2}/.pre-commit-config.yaml +0 -0
  38. {datoon-1.7.1 → datoon-1.7.2}/.releaserc.yaml +0 -0
  39. {datoon-1.7.1 → datoon-1.7.2}/CLAUDE.md +0 -0
  40. {datoon-1.7.1 → datoon-1.7.2}/CONTRIBUTING.md +0 -0
  41. {datoon-1.7.1 → datoon-1.7.2}/INSTALL.md +0 -0
  42. {datoon-1.7.1 → datoon-1.7.2}/LICENSE +0 -0
  43. {datoon-1.7.1 → datoon-1.7.2}/MARKETPLACES.md +0 -0
  44. {datoon-1.7.1 → datoon-1.7.2}/README.md +0 -0
  45. {datoon-1.7.1 → datoon-1.7.2}/SECURITY.md +0 -0
  46. {datoon-1.7.1 → datoon-1.7.2}/SKILL.md +0 -0
  47. {datoon-1.7.1 → datoon-1.7.2}/assets/datoon-banner.svg +0 -0
  48. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/REPORT.md +0 -0
  49. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/agent_results.json +0 -0
  50. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
  51. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/generate_payloads.py +0 -0
  52. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
  53. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
  54. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
  55. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
  56. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
  57. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
  58. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
  59. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
  60. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
  61. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
  62. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
  63. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
  64. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
  65. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
  66. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
  67. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
  68. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
  69. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
  70. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
  71. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
  72. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
  73. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
  74. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
  75. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
  76. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
  77. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
  78. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
  79. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
  80. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/payloads.json +0 -0
  81. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/results/.gitkeep +0 -0
  82. {datoon-1.7.1 → datoon-1.7.2}/benchmarks/run.py +0 -0
  83. {datoon-1.7.1 → datoon-1.7.2}/datoon/SKILL.md +0 -0
  84. {datoon-1.7.1 → datoon-1.7.2}/datoon.skill +0 -0
  85. {datoon-1.7.1 → datoon-1.7.2}/docs/.nojekyll +0 -0
  86. {datoon-1.7.1 → datoon-1.7.2}/docs/app.js +0 -0
  87. {datoon-1.7.1 → datoon-1.7.2}/docs/assets/datoon-banner.svg +0 -0
  88. {datoon-1.7.1 → datoon-1.7.2}/docs/index.html +0 -0
  89. {datoon-1.7.1 → datoon-1.7.2}/docs/styles.css +0 -0
  90. {datoon-1.7.1 → datoon-1.7.2}/glama.json +0 -0
  91. {datoon-1.7.1 → datoon-1.7.2}/install.sh +0 -0
  92. {datoon-1.7.1 → datoon-1.7.2}/plugins/datoon/.codex-plugin/plugin.json +0 -0
  93. {datoon-1.7.1 → datoon-1.7.2}/plugins/datoon/skills/datoon/SKILL.md +0 -0
  94. {datoon-1.7.1 → datoon-1.7.2}/pyproject.toml +0 -0
  95. {datoon-1.7.1 → datoon-1.7.2}/scripts/ab_skill_benchmark.py +0 -0
  96. {datoon-1.7.1 → datoon-1.7.2}/scripts/install.py +0 -0
  97. {datoon-1.7.1 → datoon-1.7.2}/scripts/summarize_agent_skill_eval.py +0 -0
  98. {datoon-1.7.1 → datoon-1.7.2}/scripts/validate_plugin_metadata.py +0 -0
  99. {datoon-1.7.1 → datoon-1.7.2}/scripts/validate_skill_sync.py +0 -0
  100. {datoon-1.7.1 → datoon-1.7.2}/server.json +0 -0
  101. {datoon-1.7.1 → datoon-1.7.2}/setup.cfg +0 -0
  102. {datoon-1.7.1 → datoon-1.7.2}/setup.sh +0 -0
  103. {datoon-1.7.1 → datoon-1.7.2}/skills/datoon/README.md +0 -0
  104. {datoon-1.7.1 → datoon-1.7.2}/skills/datoon/SKILL.md +0 -0
  105. {datoon-1.7.1 → datoon-1.7.2}/smithery.yaml +0 -0
  106. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/__init__.py +0 -0
  107. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/analyzer.py +0 -0
  108. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/cli.py +0 -0
  109. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/converter.py +0 -0
  110. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/mcp_server.py +0 -0
  111. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/models.py +0 -0
  112. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/py.typed +0 -0
  113. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/__init__.py +0 -0
  114. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/columnar.py +0 -0
  115. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/jsonl.py +0 -0
  116. {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/yaml.py +0 -0
  117. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/dependency_links.txt +0 -0
  118. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/entry_points.txt +0 -0
  119. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/requires.txt +0 -0
  120. {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/top_level.txt +0 -0
  121. {datoon-1.7.1 → datoon-1.7.2}/tests/fixtures/users.expected.toon +0 -0
  122. {datoon-1.7.1 → datoon-1.7.2}/tests/fixtures/users.json +0 -0
  123. {datoon-1.7.1 → datoon-1.7.2}/tests/integration/test_toon_cli_integration.py +0 -0
  124. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_analyzer.py +0 -0
  125. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_cli.py +0 -0
  126. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_converter_policy.py +0 -0
  127. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_installer.py +0 -0
  128. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_mcp_server.py +0 -0
  129. {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_models.py +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
17
  - name: Checkout
18
- uses: actions/checkout@v6
18
+ uses: actions/checkout@v7
19
19
 
20
20
  - name: Setup Python
21
21
  uses: actions/setup-python@v6
@@ -26,7 +26,7 @@ jobs:
26
26
  runs-on: ubuntu-latest
27
27
  steps:
28
28
  - name: Checkout
29
- uses: actions/checkout@v6
29
+ uses: actions/checkout@v7
30
30
 
31
31
  - name: Setup Pages
32
32
  id: setup_pages
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
17
  - name: Checkout
18
- uses: actions/checkout@v6
18
+ uses: actions/checkout@v7
19
19
 
20
20
  - name: Setup uv
21
21
  uses: astral-sh/setup-uv@v7
@@ -26,7 +26,7 @@ jobs:
26
26
  || github.event.workflow_run.conclusion == 'success'
27
27
  runs-on: ubuntu-latest
28
28
  steps:
29
- - uses: actions/checkout@v6
29
+ - uses: actions/checkout@v7
30
30
  with:
31
31
  ref: main
32
32
  fetch-depth: 0
@@ -24,7 +24,7 @@ jobs:
24
24
  outputs:
25
25
  version: ${{ steps.ver.outputs.version }}
26
26
  steps:
27
- - uses: actions/checkout@v6
27
+ - uses: actions/checkout@v7
28
28
  with:
29
29
  ref: main
30
30
  fetch-depth: 0
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
17
  - name: Checkout
18
- uses: actions/checkout@v6
18
+ uses: actions/checkout@v7
19
19
  with:
20
20
  fetch-depth: 0
21
21
 
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
17
  - name: Checkout
18
- uses: actions/checkout@v6
18
+ uses: actions/checkout@v7
19
19
 
20
20
  - name: Setup uv
21
21
  uses: astral-sh/setup-uv@v7
@@ -27,7 +27,7 @@ jobs:
27
27
  runs-on: ubuntu-latest
28
28
  steps:
29
29
  - name: Checkout
30
- uses: actions/checkout@v6
30
+ uses: actions/checkout@v7
31
31
 
32
32
  - name: Setup uv
33
33
  uses: astral-sh/setup-uv@v7
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
20
  - name: Checkout
21
- uses: actions/checkout@v6
21
+ uses: actions/checkout@v7
22
22
 
23
23
  - name: Setup Python
24
24
  uses: actions/setup-python@v6
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  steps:
24
24
  - name: Checkout
25
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
25
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
26
26
  with:
27
27
  persist-credentials: false
28
28
 
@@ -35,7 +35,7 @@ jobs:
35
35
  runs-on: ubuntu-latest
36
36
  steps:
37
37
  - name: Checkout
38
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
38
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
39
39
  with:
40
40
  persist-credentials: false
41
41
 
@@ -1,3 +1,10 @@
1
+ ## [1.7.2](https://github.com/andrii-su/datoon/compare/v1.7.1...v1.7.2) (2026-07-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **readers:** preserve semantic values during scalar and row coercion ([#36](https://github.com/andrii-su/datoon/issues/36)) ([5b24c00](https://github.com/andrii-su/datoon/commit/5b24c0098da620e14158b457a04d639663ba33f0))
7
+
1
8
  ## [1.7.1](https://github.com/andrii-su/datoon/compare/v1.7.0...v1.7.1) (2026-06-22)
2
9
 
3
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datoon
3
- Version: 1.7.1
3
+ Version: 1.7.2
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
@@ -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.1'
22
- __version_tuple__ = version_tuple = (1, 7, 1)
21
+ __version__ = version = '1.7.2'
22
+ __version_tuple__ = version_tuple = (1, 7, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g7a983c8dc'
24
+ __commit_id__ = commit_id = 'ga011eb92d'
@@ -0,0 +1,48 @@
1
+ """Shared scalar coercion for text readers (CSV, XML).
2
+
3
+ Coercion must never change semantic values. Two traps are guarded here:
4
+
5
+ * Leading-zero integers (ZIP codes, phone numbers, IDs) stay strings — only
6
+ values that round-trip through ``int`` are coerced.
7
+ * ``inf``/``nan`` and overflowing exponents would serialize to ``Infinity``/
8
+ ``NaN`` (invalid JSON), so non-finite floats stay strings.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import re
15
+
16
+ # Canonical decimal/scientific literal: no leading zeros, no underscores. Guards
17
+ # against float("00755") -> 755.0 and float("1_000") -> 1000.0 quietly rewriting
18
+ # IDs and formatted numbers.
19
+ _FLOAT_RE = re.compile(r"^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$")
20
+
21
+
22
+ def coerce_scalar(value: str) -> int | float | bool | str | None:
23
+ """Coerce a raw string cell to a JSON-safe scalar without losing meaning."""
24
+ stripped = value.strip()
25
+ if stripped == "":
26
+ return None
27
+
28
+ low = stripped.lower()
29
+ if low == "true":
30
+ return True
31
+ if low == "false":
32
+ return False
33
+
34
+ # Only accept integers that round-trip: rejects "00123", "+5", "1_000".
35
+ try:
36
+ as_int = int(stripped)
37
+ except ValueError:
38
+ pass
39
+ else:
40
+ if str(as_int) == stripped:
41
+ return as_int
42
+
43
+ if _FLOAT_RE.match(stripped):
44
+ as_float = float(stripped)
45
+ # Reject overflowing exponents (1e400 -> inf) that break JSON serialization.
46
+ if math.isfinite(as_float):
47
+ return as_float
48
+ return stripped
@@ -0,0 +1,46 @@
1
+ """Shared header/data-row normalization for spreadsheet readers (Excel, Numbers)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def header_rows_to_dicts(rows: list[Any]) -> list[dict[str, Any]]:
9
+ """Turn ``[header, *data]`` cell tuples into row dicts with stable keys.
10
+
11
+ * A ``None`` header cell becomes ``col{index}``.
12
+ * Data rows wider than the header keep their overflow cells under synthetic
13
+ ``col{index}`` keys instead of being silently dropped.
14
+ * Data rows narrower than the header get ``None`` for missing trailing cells,
15
+ so every emitted row shares an identical key set.
16
+ * Rows whose cells are all ``None`` are skipped.
17
+ """
18
+ if not rows:
19
+ return []
20
+
21
+ data_rows = rows[1:]
22
+ if not data_rows:
23
+ return []
24
+
25
+ widest = max((len(row) for row in data_rows), default=0)
26
+ header_row = rows[0]
27
+ column_count = max(len(header_row), widest)
28
+
29
+ headers: list[str] = []
30
+ for i in range(column_count):
31
+ raw = header_row[i] if i < len(header_row) else None
32
+ headers.append(str(raw) if raw is not None else f"col{i}")
33
+
34
+ result: list[dict[str, Any]] = []
35
+ for row in data_rows:
36
+ if not any(cell is not None for cell in row):
37
+ continue
38
+ result.append(dict(zip(headers, _pad(row, column_count))))
39
+ return result
40
+
41
+
42
+ def _pad(row: Any, width: int) -> list[Any]:
43
+ cells = list(row)
44
+ if len(cells) < width:
45
+ cells.extend([None] * (width - len(cells)))
46
+ return cells
@@ -0,0 +1,17 @@
1
+ """CSV reader with basic type coercion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import io
7
+ from typing import Any
8
+
9
+ from datoon.readers._coerce import coerce_scalar
10
+
11
+
12
+ def read_csv(text: str, *, coerce_types: bool = True) -> list[dict[str, Any]]:
13
+ """Parse CSV text into a list of row dicts."""
14
+ reader = csv.DictReader(io.StringIO(text))
15
+ if coerce_types:
16
+ return [{k: coerce_scalar(str(v)) for k, v in row.items()} for row in reader]
17
+ return [dict(row) for row in reader]
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ from datoon.readers._tabular import header_rows_to_dicts
9
+
8
10
 
9
11
  def read_excel(path: Path, *, sheet: int = 0) -> list[dict[str, Any]]:
10
12
  """Read the first sheet of an Excel file into a list of row dicts."""
@@ -20,12 +22,4 @@ def read_excel(path: Path, *, sheet: int = 0) -> list[dict[str, Any]]:
20
22
  rows = list(ws.iter_rows(values_only=True))
21
23
  wb.close()
22
24
 
23
- if not rows:
24
- return []
25
-
26
- headers = [str(h) if h is not None else f"col{i}" for i, h in enumerate(rows[0])]
27
- return [
28
- {h: v for h, v in zip(headers, row)}
29
- for row in rows[1:]
30
- if any(v is not None for v in row)
31
- ]
25
+ return header_rows_to_dicts(rows)
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ from datoon.readers._tabular import header_rows_to_dicts
9
+
8
10
 
9
11
  def read_numbers(path: Path, *, sheet: int = 0, table: int = 0) -> list[dict[str, Any]]:
10
12
  """Read an Apple Numbers file into a list of row dicts."""
@@ -19,12 +21,4 @@ def read_numbers(path: Path, *, sheet: int = 0, table: int = 0) -> list[dict[str
19
21
  tbl = doc.sheets[sheet].tables[table]
20
22
  rows = list(tbl.rows(values_only=True))
21
23
 
22
- if not rows:
23
- return []
24
-
25
- headers = [str(h) if h is not None else f"col{i}" for i, h in enumerate(rows[0])]
26
- return [
27
- {h: v for h, v in zip(headers, row)}
28
- for row in rows[1:]
29
- if any(v is not None for v in row)
30
- ]
24
+ return header_rows_to_dicts(rows)
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import xml.etree.ElementTree as ET
6
6
  from typing import Any
7
7
 
8
+ from datoon.readers._coerce import coerce_scalar as _coerce
9
+
8
10
 
9
11
  def read_xml(text: str) -> list[dict[str, Any]]:
10
12
  """Parse XML into a list of row dicts from the dominant repeated child element."""
@@ -36,23 +38,3 @@ def _element_to_dict(element: ET.Element) -> dict[str, Any]:
36
38
  if not list(element) and element.text and element.text.strip():
37
39
  result["_text"] = element.text.strip()
38
40
  return result
39
-
40
-
41
- def _coerce(value: str) -> int | float | bool | str | None:
42
- stripped = value.strip()
43
- if not stripped:
44
- return None
45
- low = stripped.lower()
46
- if low == "true":
47
- return True
48
- if low == "false":
49
- return False
50
- try:
51
- return int(stripped)
52
- except ValueError:
53
- pass
54
- try:
55
- return float(stripped)
56
- except ValueError:
57
- pass
58
- return stripped
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datoon
3
- Version: 1.7.1
3
+ Version: 1.7.2
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
@@ -103,6 +103,8 @@ src/datoon.egg-info/scm_file_list.json
103
103
  src/datoon.egg-info/scm_version.json
104
104
  src/datoon.egg-info/top_level.txt
105
105
  src/datoon/readers/__init__.py
106
+ src/datoon/readers/_coerce.py
107
+ src/datoon/readers/_tabular.py
106
108
  src/datoon/readers/columnar.py
107
109
  src/datoon/readers/csv.py
108
110
  src/datoon/readers/excel.py
@@ -119,4 +121,5 @@ tests/unit/test_converter_policy.py
119
121
  tests/unit/test_installer.py
120
122
  tests/unit/test_mcp_server.py
121
123
  tests/unit/test_models.py
122
- tests/unit/test_readers.py
124
+ tests/unit/test_readers.py
125
+ tests/unit/test_tabular_rows.py
@@ -40,11 +40,13 @@
40
40
  "src/datoon/converter.py",
41
41
  "src/datoon/cli.py",
42
42
  "src/datoon/readers/xml.py",
43
+ "src/datoon/readers/_coerce.py",
43
44
  "src/datoon/readers/__init__.py",
44
45
  "src/datoon/readers/jsonl.py",
45
46
  "src/datoon/readers/numbers.py",
46
47
  "src/datoon/readers/columnar.py",
47
48
  "src/datoon/readers/csv.py",
49
+ "src/datoon/readers/_tabular.py",
48
50
  "src/datoon/readers/excel.py",
49
51
  "src/datoon/readers/yaml.py",
50
52
  "assets/datoon-banner.svg",
@@ -59,6 +61,7 @@
59
61
  "tests/unit/test_models.py",
60
62
  "tests/unit/test_converter_policy.py",
61
63
  "tests/unit/test_mcp_server.py",
64
+ "tests/unit/test_tabular_rows.py",
62
65
  "tests/unit/test_analyzer.py",
63
66
  "tests/unit/test_installer.py",
64
67
  "tests/unit/test_cli.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.7.2",
3
+ "distance": 0,
4
+ "node": "ga011eb92d6c550e4d87db4ebdd3b5efe9cb4f533",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-07"
8
+ }
@@ -104,6 +104,33 @@ def test_read_csv_no_coercion() -> None:
104
104
  assert rows[0]["id"] == "1"
105
105
 
106
106
 
107
+ def test_read_csv_preserves_leading_zero_ids() -> None:
108
+ # Zip codes, phone numbers, and IDs must not lose leading zeros.
109
+ rows = read_csv("code\n00123\n0042\n")
110
+ assert rows[0]["code"] == "00123"
111
+ assert rows[1]["code"] == "0042"
112
+
113
+
114
+ def test_read_csv_rejects_non_finite_floats() -> None:
115
+ # float("inf")/float("nan") would serialize to invalid JSON (Infinity/NaN).
116
+ rows = read_csv("v\ninf\nnan\n-inf\nInfinity\n")
117
+ assert rows[0]["v"] == "inf"
118
+ assert rows[1]["v"] == "nan"
119
+ assert rows[2]["v"] == "-inf"
120
+ assert rows[3]["v"] == "Infinity"
121
+
122
+
123
+ def test_read_csv_float_overflow_stays_string() -> None:
124
+ # 1e400 overflows to inf; keep the original string instead.
125
+ rows = read_csv("v\n1e400\n")
126
+ assert rows[0]["v"] == "1e400"
127
+
128
+
129
+ def test_read_csv_underscore_number_stays_string() -> None:
130
+ rows = read_csv("v\n1_000\n")
131
+ assert rows[0]["v"] == "1_000"
132
+
133
+
107
134
  # ---------------------------------------------------------------------------
108
135
  # JSONL reader
109
136
  # ---------------------------------------------------------------------------
@@ -209,6 +236,20 @@ def test_read_xml_type_coercion() -> None:
209
236
  assert isinstance(rows[0]["id"], int)
210
237
 
211
238
 
239
+ def test_read_xml_preserves_leading_zero() -> None:
240
+ xml = "<rows><r><code>00755</code></r><r><code>00644</code></r></rows>"
241
+ rows = read_xml(xml)
242
+ assert rows[0]["code"] == "00755"
243
+ assert rows[1]["code"] == "00644"
244
+
245
+
246
+ def test_read_xml_rejects_non_finite() -> None:
247
+ xml = "<rows><r><v>inf</v></r><r><v>nan</v></r></rows>"
248
+ rows = read_xml(xml)
249
+ assert rows[0]["v"] == "inf"
250
+ assert rows[1]["v"] == "nan"
251
+
252
+
212
253
  def test_read_xml_invalid_raises() -> None:
213
254
  with pytest.raises(ValueError, match="Invalid XML"):
214
255
  read_xml("<not closed>")
@@ -0,0 +1,44 @@
1
+ """Unit tests for the shared header-rows-to-dicts helper used by spreadsheet readers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datoon.readers._tabular import header_rows_to_dicts
6
+
7
+
8
+ def test_uniform_rows() -> None:
9
+ raw = [("id", "name"), (1, "Ada"), (2, "Lin")]
10
+ assert header_rows_to_dicts(raw) == [
11
+ {"id": 1, "name": "Ada"},
12
+ {"id": 2, "name": "Lin"},
13
+ ]
14
+
15
+
16
+ def test_wider_data_row_keeps_overflow_cells() -> None:
17
+ # A data row with more cells than headers must not silently drop data.
18
+ raw = [("a", "b"), (1, 2, 3)]
19
+ assert header_rows_to_dicts(raw) == [{"a": 1, "b": 2, "col2": 3}]
20
+
21
+
22
+ def test_shorter_row_gets_none_for_missing_keys() -> None:
23
+ # Missing trailing cells become None so every row keeps identical keys
24
+ # (uniform-array detection depends on stable key sets).
25
+ raw = [("a", "b", "c"), (1, 2)]
26
+ assert header_rows_to_dicts(raw) == [{"a": 1, "b": 2, "c": None}]
27
+
28
+
29
+ def test_skips_all_none_rows() -> None:
30
+ raw = [("a", "b"), (None, None), (1, 2)]
31
+ assert header_rows_to_dicts(raw) == [{"a": 1, "b": 2}]
32
+
33
+
34
+ def test_empty_input_returns_empty() -> None:
35
+ assert header_rows_to_dicts([]) == []
36
+
37
+
38
+ def test_header_only_returns_empty() -> None:
39
+ assert header_rows_to_dicts([("a", "b")]) == []
40
+
41
+
42
+ def test_none_header_becomes_synthetic_column() -> None:
43
+ raw = [("a", None), (1, 2)]
44
+ assert header_rows_to_dicts(raw) == [{"a": 1, "col1": 2}]
@@ -588,7 +588,7 @@ wheels = [
588
588
 
589
589
  [[package]]
590
590
  name = "mcp"
591
- version = "1.28.0"
591
+ version = "1.28.1"
592
592
  source = { registry = "https://pypi.org/simple" }
593
593
  dependencies = [
594
594
  { name = "anyio" },
@@ -606,9 +606,9 @@ dependencies = [
606
606
  { name = "typing-inspection" },
607
607
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
608
608
  ]
609
- sdist = { url = "https://files.pythonhosted.org/packages/c1/ee/94c6c50ffc5b5cf4737052275d11b57367f32d1a8516e31dcd60591b3916/mcp-1.28.0.tar.gz", hash = "sha256:559d3f9943674cafbe5744c5d3794f3237e8b47f9bbc58e20c0fad680d8487c2", size = 636040, upload-time = "2026-06-16T21:37:17.996Z" }
609
+ sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
610
610
  wheels = [
611
- { url = "https://files.pythonhosted.org/packages/2e/e1/4c1dc1fbb688641a712d34650c3d58bbbdcb314ddb75bc5817bbf33515a4/mcp-1.28.0-py3-none-any.whl", hash = "sha256:9c1e7cf3a9125557e418ecd4fed8e9adddce81b0dfdae4d6601d700f5beb71a4", size = 221959, upload-time = "2026-06-16T21:37:16.579Z" },
611
+ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
612
612
  ]
613
613
 
614
614
  [[package]]
@@ -886,7 +886,7 @@ crypto = [
886
886
 
887
887
  [[package]]
888
888
  name = "pytest"
889
- version = "9.0.3"
889
+ version = "9.1.1"
890
890
  source = { registry = "https://pypi.org/simple" }
891
891
  dependencies = [
892
892
  { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -895,9 +895,9 @@ dependencies = [
895
895
  { name = "pluggy" },
896
896
  { name = "pygments" },
897
897
  ]
898
- sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
898
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
899
899
  wheels = [
900
- { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
900
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
901
901
  ]
902
902
 
903
903
  [[package]]
@@ -1,35 +0,0 @@
1
- """CSV reader with basic type coercion."""
2
-
3
- from __future__ import annotations
4
-
5
- import csv
6
- import io
7
- from typing import Any
8
-
9
-
10
- def _coerce(value: str) -> int | float | bool | str | None:
11
- stripped = value.strip()
12
- if stripped == "":
13
- return None
14
- low = stripped.lower()
15
- if low == "true":
16
- return True
17
- if low == "false":
18
- return False
19
- try:
20
- return int(stripped)
21
- except ValueError:
22
- pass
23
- try:
24
- return float(stripped)
25
- except ValueError:
26
- pass
27
- return stripped
28
-
29
-
30
- def read_csv(text: str, *, coerce_types: bool = True) -> list[dict[str, Any]]:
31
- """Parse CSV text into a list of row dicts."""
32
- reader = csv.DictReader(io.StringIO(text))
33
- if coerce_types:
34
- return [{k: _coerce(str(v)) for k, v in row.items()} for row in reader]
35
- return [dict(row) for row in reader]
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.7.1",
3
- "distance": 0,
4
- "node": "g7a983c8dcb2664bc8656790578bfc4a48a7f47c3",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-06-22"
8
- }
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