datoon 1.7.3__tar.gz → 1.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {datoon-1.7.3 → datoon-1.8.0}/CHANGELOG.md +14 -0
- {datoon-1.7.3 → datoon-1.8.0}/PKG-INFO +1 -1
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/_version.py +3 -3
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/cli.py +15 -1
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/__init__.py +10 -4
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/xml.py +16 -1
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/PKG-INFO +1 -1
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/scm_version.json +2 -2
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_cli.py +35 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_readers.py +50 -0
- {datoon-1.7.3 → datoon-1.8.0}/.agents/plugins/marketplace.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.claude-plugin/marketplace.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.claude-plugin/plugin.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.editorconfig +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.gitattributes +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/dependabot.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/package-installer-smoke.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/pages.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/pre-commit.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/publish-mcp.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/publish.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/release.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/tests.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/validate-artifacts.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.github/workflows/workflow-lint.yml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.gitignore +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.pre-commit-config.yaml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/.releaserc.yaml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/CLAUDE.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/CONTRIBUTING.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/INSTALL.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/LICENSE +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/MARKETPLACES.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/README.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/SECURITY.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/SKILL.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/assets/datoon-banner.svg +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/REPORT.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/agent_results.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/generate_payloads.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/payloads.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/results/.gitkeep +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/benchmarks/run.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/datoon/SKILL.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/datoon.skill +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/docs/.nojekyll +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/docs/app.js +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/docs/assets/datoon-banner.svg +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/docs/index.html +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/docs/styles.css +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/glama.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/install.sh +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/plugins/datoon/.codex-plugin/plugin.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/plugins/datoon/skills/datoon/SKILL.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/pyproject.toml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/scripts/ab_skill_benchmark.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/scripts/install.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/scripts/summarize_agent_skill_eval.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/scripts/validate_plugin_metadata.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/scripts/validate_skill_sync.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/server.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/setup.cfg +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/setup.sh +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/skills/datoon/README.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/skills/datoon/SKILL.md +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/smithery.yaml +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/__init__.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/analyzer.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/converter.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/mcp_server.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/models.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/py.typed +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/_coerce.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/_tabular.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/columnar.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/csv.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/excel.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/jsonl.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/numbers.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon/readers/yaml.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/SOURCES.txt +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/dependency_links.txt +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/entry_points.txt +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/requires.txt +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/scm_file_list.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/src/datoon.egg-info/top_level.txt +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/fixtures/users.expected.toon +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/fixtures/users.json +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/integration/test_toon_cli_integration.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_analyzer.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_converter_policy.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_installer.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_mcp_server.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_models.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/tests/unit/test_tabular_rows.py +0 -0
- {datoon-1.7.3 → datoon-1.8.0}/uv.lock +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.8.0](https://github.com/andrii-su/datoon/compare/v1.7.4...v1.8.0) (2026-07-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **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))
|
|
7
|
+
|
|
8
|
+
## [1.7.4](https://github.com/andrii-su/datoon/compare/v1.7.3...v1.7.4) (2026-07-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **xml:** refuse DTD/DOCTYPE to block entity-expansion DoS ([#38](https://github.com/andrii-su/datoon/issues/38)) ([ef6a35e](https://github.com/andrii-su/datoon/commit/ef6a35e624176aab0a648794715c2d99411421d7))
|
|
14
|
+
|
|
1
15
|
## [1.7.3](https://github.com/andrii-su/datoon/compare/v1.7.2...v1.7.3) (2026-07-07)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.8.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 8, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gacb329fb8'
|
|
@@ -75,6 +75,18 @@ 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
|
+
)
|
|
78
90
|
parser.add_argument(
|
|
79
91
|
"--report",
|
|
80
92
|
help="Optional path to write conversion report as JSON.",
|
|
@@ -172,7 +184,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
172
184
|
f"datoon error: --format {fmt} requires a file path, not stdin.\n"
|
|
173
185
|
)
|
|
174
186
|
return 1
|
|
175
|
-
rows = read_tabular(
|
|
187
|
+
rows = read_tabular(
|
|
188
|
+
fmt, path=Path(args.input), sheet=args.sheet, table=args.table
|
|
189
|
+
)
|
|
176
190
|
json_text = json.dumps(rows, ensure_ascii=False, separators=(",", ":"))
|
|
177
191
|
outcome = convert_json_for_llm(json_text, config)
|
|
178
192
|
else:
|
|
@@ -37,16 +37,20 @@ def read_tabular(
|
|
|
37
37
|
*,
|
|
38
38
|
text: str | None = None,
|
|
39
39
|
path: Path | None = None,
|
|
40
|
+
sheet: int = 0,
|
|
41
|
+
table: int = 0,
|
|
40
42
|
) -> list[dict[str, Any]]:
|
|
41
43
|
"""Read structured data and return rows as a list of dicts.
|
|
42
44
|
|
|
43
45
|
Text formats (csv, jsonl, yaml, xml) require ``text``.
|
|
44
46
|
Binary formats (excel, parquet, orc, avro, numbers) require ``path``.
|
|
47
|
+
``sheet`` selects the worksheet for excel/numbers; ``table`` selects the
|
|
48
|
+
table within a Numbers sheet. Both are ignored by columnar formats.
|
|
45
49
|
"""
|
|
46
50
|
if fmt in BINARY_FORMATS:
|
|
47
51
|
if path is None:
|
|
48
52
|
raise ValueError(f"Format '{fmt}' requires a file path, not text input.")
|
|
49
|
-
return _read_binary(fmt, path)
|
|
53
|
+
return _read_binary(fmt, path, sheet=sheet, table=table)
|
|
50
54
|
if fmt in TEXT_FORMATS:
|
|
51
55
|
if text is None:
|
|
52
56
|
raise ValueError(f"Format '{fmt}' requires text input.")
|
|
@@ -74,11 +78,13 @@ def _read_text(fmt: str, text: str) -> list[dict[str, Any]]:
|
|
|
74
78
|
raise ValueError(f"Unknown text format: {fmt}")
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
def _read_binary(
|
|
81
|
+
def _read_binary(
|
|
82
|
+
fmt: str, path: Path, *, sheet: int = 0, table: int = 0
|
|
83
|
+
) -> list[dict[str, Any]]:
|
|
78
84
|
if fmt == "excel":
|
|
79
85
|
from datoon.readers.excel import read_excel
|
|
80
86
|
|
|
81
|
-
return read_excel(path)
|
|
87
|
+
return read_excel(path, sheet=sheet)
|
|
82
88
|
if fmt == "parquet":
|
|
83
89
|
from datoon.readers.columnar import read_parquet
|
|
84
90
|
|
|
@@ -94,5 +100,5 @@ def _read_binary(fmt: str, path: Path) -> list[dict[str, Any]]:
|
|
|
94
100
|
if fmt == "numbers":
|
|
95
101
|
from datoon.readers.numbers import read_numbers
|
|
96
102
|
|
|
97
|
-
return read_numbers(path)
|
|
103
|
+
return read_numbers(path, sheet=sheet, table=table)
|
|
98
104
|
raise ValueError(f"Unknown binary format: {fmt}")
|
|
@@ -1,15 +1,30 @@
|
|
|
1
|
-
"""XML reader using stdlib xml.etree.ElementTree.
|
|
1
|
+
"""XML reader using stdlib xml.etree.ElementTree.
|
|
2
|
+
|
|
3
|
+
The stdlib parser rejects external entities but expands internal ones, so a DTD
|
|
4
|
+
can smuggle a billion-laughs DoS. datoon may process untrusted payloads, so DTDs
|
|
5
|
+
are refused outright. ``<!DOCTYPE`` is a fixed XML token (no internal
|
|
6
|
+
whitespace), so a literal scan reliably catches every declaration expat would
|
|
7
|
+
process.
|
|
8
|
+
"""
|
|
2
9
|
|
|
3
10
|
from __future__ import annotations
|
|
4
11
|
|
|
12
|
+
import re
|
|
5
13
|
import xml.etree.ElementTree as ET
|
|
6
14
|
from typing import Any
|
|
7
15
|
|
|
8
16
|
from datoon.readers._coerce import coerce_scalar as _coerce
|
|
9
17
|
|
|
18
|
+
_DOCTYPE_RE = re.compile(r"<!DOCTYPE", re.IGNORECASE)
|
|
19
|
+
|
|
10
20
|
|
|
11
21
|
def read_xml(text: str) -> list[dict[str, Any]]:
|
|
12
22
|
"""Parse XML into a list of row dicts from the dominant repeated child element."""
|
|
23
|
+
if _DOCTYPE_RE.search(text):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"XML with a DOCTYPE/DTD is not allowed (entity-expansion risk)."
|
|
26
|
+
)
|
|
27
|
+
|
|
13
28
|
try:
|
|
14
29
|
root = ET.fromstring(text)
|
|
15
30
|
except ET.ParseError as exc:
|
|
@@ -280,3 +280,38 @@ def test_mcp_subcommand_missing_extra_errors(
|
|
|
280
280
|
rc = main(["mcp"])
|
|
281
281
|
assert rc == 1
|
|
282
282
|
assert "mcp" in capsys.readouterr().err
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# --sheet / --table passthrough for spreadsheet formats
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_main_forwards_sheet_and_table_for_binary(
|
|
291
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
292
|
+
tmp_path: Path,
|
|
293
|
+
) -> None:
|
|
294
|
+
captured: dict[str, int] = {}
|
|
295
|
+
|
|
296
|
+
def fake_read_tabular(
|
|
297
|
+
fmt: str,
|
|
298
|
+
*,
|
|
299
|
+
text: str | None = None,
|
|
300
|
+
path: Path | None = None,
|
|
301
|
+
sheet: int = 0,
|
|
302
|
+
table: int = 0,
|
|
303
|
+
) -> list[dict[str, object]]:
|
|
304
|
+
captured["sheet"] = sheet
|
|
305
|
+
captured["table"] = table
|
|
306
|
+
return [{"id": 1, "v": "a"}]
|
|
307
|
+
|
|
308
|
+
monkeypatch.setattr("datoon.cli.read_tabular", fake_read_tabular)
|
|
309
|
+
monkeypatch.setattr("datoon.converter._run_toon_cli", lambda _, **kw: "x\n")
|
|
310
|
+
xlsx = tmp_path / "data.xlsx"
|
|
311
|
+
xlsx.write_bytes(b"")
|
|
312
|
+
|
|
313
|
+
rc = main(
|
|
314
|
+
["--format", "excel", "--sheet", "2", "--table", "1", "--force", str(xlsx)]
|
|
315
|
+
)
|
|
316
|
+
assert rc == 0
|
|
317
|
+
assert captured == {"sheet": 2, "table": 1}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from unittest.mock import MagicMock
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -250,6 +251,26 @@ def test_read_xml_rejects_non_finite() -> None:
|
|
|
250
251
|
assert rows[1]["v"] == "nan"
|
|
251
252
|
|
|
252
253
|
|
|
254
|
+
def test_read_xml_rejects_dtd_billion_laughs() -> None:
|
|
255
|
+
# Internal entity expansion (billion laughs) is a DoS vector; DTDs are refused.
|
|
256
|
+
payload = (
|
|
257
|
+
'<!DOCTYPE lolz [<!ENTITY lol "lol">'
|
|
258
|
+
'<!ENTITY lol2 "&lol;&lol;&lol;">]>'
|
|
259
|
+
"<rows><r>&lol2;</r></rows>"
|
|
260
|
+
)
|
|
261
|
+
with pytest.raises(ValueError, match="DOCTYPE"):
|
|
262
|
+
read_xml(payload)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_read_xml_rejects_external_entity_dtd() -> None:
|
|
266
|
+
payload = (
|
|
267
|
+
'<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'
|
|
268
|
+
"<rows><r>&xxe;</r></rows>"
|
|
269
|
+
)
|
|
270
|
+
with pytest.raises(ValueError, match="DOCTYPE"):
|
|
271
|
+
read_xml(payload)
|
|
272
|
+
|
|
273
|
+
|
|
253
274
|
def test_read_xml_invalid_raises() -> None:
|
|
254
275
|
with pytest.raises(ValueError, match="Invalid XML"):
|
|
255
276
|
read_xml("<not closed>")
|
|
@@ -297,6 +318,35 @@ def test_read_tabular_text_without_text_raises() -> None:
|
|
|
297
318
|
read_tabular(fmt, path=None)
|
|
298
319
|
|
|
299
320
|
|
|
321
|
+
def test_read_tabular_forwards_sheet_to_excel(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
322
|
+
captured: dict[str, int] = {}
|
|
323
|
+
|
|
324
|
+
def fake_read_excel(path: Path, *, sheet: int = 0) -> list[dict[str, int]]:
|
|
325
|
+
captured["sheet"] = sheet
|
|
326
|
+
return [{"a": 1}]
|
|
327
|
+
|
|
328
|
+
monkeypatch.setattr("datoon.readers.excel.read_excel", fake_read_excel)
|
|
329
|
+
read_tabular("excel", path=Path("x.xlsx"), sheet=2)
|
|
330
|
+
assert captured["sheet"] == 2
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_read_tabular_forwards_sheet_and_table_to_numbers(
|
|
334
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
335
|
+
) -> None:
|
|
336
|
+
captured: dict[str, int] = {}
|
|
337
|
+
|
|
338
|
+
def fake_read_numbers(
|
|
339
|
+
path: Path, *, sheet: int = 0, table: int = 0
|
|
340
|
+
) -> list[dict[str, int]]:
|
|
341
|
+
captured["sheet"] = sheet
|
|
342
|
+
captured["table"] = table
|
|
343
|
+
return [{"a": 1}]
|
|
344
|
+
|
|
345
|
+
monkeypatch.setattr("datoon.readers.numbers.read_numbers", fake_read_numbers)
|
|
346
|
+
read_tabular("numbers", path=Path("x.numbers"), sheet=1, table=3)
|
|
347
|
+
assert captured == {"sheet": 1, "table": 3}
|
|
348
|
+
|
|
349
|
+
|
|
300
350
|
# ---------------------------------------------------------------------------
|
|
301
351
|
# Format sets sanity
|
|
302
352
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|