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.
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/package-installer-smoke.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/pages.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/pre-commit.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/publish-mcp.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/publish.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/release.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/tests.yml +2 -2
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/validate-artifacts.yml +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/.github/workflows/workflow-lint.yml +2 -2
- {datoon-1.7.1 → datoon-1.7.2}/CHANGELOG.md +7 -0
- {datoon-1.7.1 → datoon-1.7.2}/PKG-INFO +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/_version.py +3 -3
- datoon-1.7.2/src/datoon/readers/_coerce.py +48 -0
- datoon-1.7.2/src/datoon/readers/_tabular.py +46 -0
- datoon-1.7.2/src/datoon/readers/csv.py +17 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/excel.py +3 -9
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/numbers.py +3 -9
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/xml.py +2 -20
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/PKG-INFO +1 -1
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/SOURCES.txt +4 -1
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/scm_file_list.json +3 -0
- datoon-1.7.2/src/datoon.egg-info/scm_version.json +8 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_readers.py +41 -0
- datoon-1.7.2/tests/unit/test_tabular_rows.py +44 -0
- {datoon-1.7.1 → datoon-1.7.2}/uv.lock +6 -6
- datoon-1.7.1/src/datoon/readers/csv.py +0 -35
- datoon-1.7.1/src/datoon.egg-info/scm_version.json +0 -8
- {datoon-1.7.1 → datoon-1.7.2}/.agents/plugins/marketplace.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.claude-plugin/marketplace.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.claude-plugin/plugin.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.editorconfig +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.gitattributes +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.github/dependabot.yml +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.gitignore +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.pre-commit-config.yaml +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/.releaserc.yaml +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/CLAUDE.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/CONTRIBUTING.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/INSTALL.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/LICENSE +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/MARKETPLACES.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/README.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/SECURITY.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/SKILL.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/assets/datoon-banner.svg +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/REPORT.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/agent_results.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/generate_payloads.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/payloads.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/results/.gitkeep +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/benchmarks/run.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/datoon/SKILL.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/datoon.skill +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/docs/.nojekyll +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/docs/app.js +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/docs/assets/datoon-banner.svg +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/docs/index.html +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/docs/styles.css +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/glama.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/install.sh +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/plugins/datoon/.codex-plugin/plugin.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/plugins/datoon/skills/datoon/SKILL.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/pyproject.toml +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/scripts/ab_skill_benchmark.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/scripts/install.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/scripts/summarize_agent_skill_eval.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/scripts/validate_plugin_metadata.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/scripts/validate_skill_sync.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/server.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/setup.cfg +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/setup.sh +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/skills/datoon/README.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/skills/datoon/SKILL.md +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/smithery.yaml +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/__init__.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/analyzer.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/cli.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/converter.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/mcp_server.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/models.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/py.typed +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/__init__.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/columnar.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/jsonl.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon/readers/yaml.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/dependency_links.txt +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/entry_points.txt +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/requires.txt +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/src/datoon.egg-info/top_level.txt +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/fixtures/users.expected.toon +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/fixtures/users.json +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/integration/test_toon_cli_integration.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_analyzer.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_cli.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_converter_policy.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_installer.py +0 -0
- {datoon-1.7.1 → datoon-1.7.2}/tests/unit/test_mcp_server.py +0 -0
- {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@
|
|
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@
|
|
30
|
+
uses: actions/checkout@v7
|
|
31
31
|
|
|
32
32
|
- name: Setup uv
|
|
33
33
|
uses: astral-sh/setup-uv@v7
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
24
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
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@
|
|
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
|
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 7,
|
|
21
|
+
__version__ = version = '1.7.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 7, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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",
|
|
@@ -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.
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
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]
|
|
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
|