dot-parser 2.1.1__tar.gz → 2.1.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 (75) hide show
  1. {dot_parser-2.1.1 → dot_parser-2.1.2}/CHANGELOG.md +12 -1
  2. {dot_parser-2.1.1 → dot_parser-2.1.2}/PKG-INFO +1 -1
  3. {dot_parser-2.1.1 → dot_parser-2.1.2}/pyproject.toml +1 -1
  4. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/chunking.py +35 -25
  5. {dot_parser-2.1.1 → dot_parser-2.1.2}/tests/test_chunking.py +25 -0
  6. {dot_parser-2.1.1 → dot_parser-2.1.2}/uv.lock +1 -1
  7. {dot_parser-2.1.1 → dot_parser-2.1.2}/.gitignore +0 -0
  8. {dot_parser-2.1.1 → dot_parser-2.1.2}/.gitlab-ci.yml +0 -0
  9. {dot_parser-2.1.1 → dot_parser-2.1.2}/.pre-commit-config.yaml +0 -0
  10. {dot_parser-2.1.1 → dot_parser-2.1.2}/.python-version +0 -0
  11. {dot_parser-2.1.1 → dot_parser-2.1.2}/AUTHORS.md +0 -0
  12. {dot_parser-2.1.1 → dot_parser-2.1.2}/CONTRIBUTING.md +0 -0
  13. {dot_parser-2.1.1 → dot_parser-2.1.2}/DCO +0 -0
  14. {dot_parser-2.1.1 → dot_parser-2.1.2}/LICENSE.md +0 -0
  15. {dot_parser-2.1.1 → dot_parser-2.1.2}/README.md +0 -0
  16. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/README.md +0 -0
  17. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/input/broken_encoding.pdf +0 -0
  18. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/input/image_only.pdf +0 -0
  19. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/input/native_equations.pdf +0 -0
  20. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/input/native_simple.pdf +0 -0
  21. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/input/native_tables.pdf +0 -0
  22. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_docling_broken_encoding.md +0 -0
  23. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_docling_image_only.md +0 -0
  24. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_docling_native_equations.md +0 -0
  25. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_docling_native_simple.md +0 -0
  26. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_docling_native_tables.md +0 -0
  27. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_llama_broken_encoding.md +0 -0
  28. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_llama_image_only.md +0 -0
  29. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_llama_native_equations.md +0 -0
  30. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_llama_native_simple.md +0 -0
  31. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_llama_native_tables.md +0 -0
  32. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_mistral_batch_broken_encoding.md +0 -0
  33. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_mistral_batch_image_only.md +0 -0
  34. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_mistral_batch_native_equations.md +0 -0
  35. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_mistral_batch_native_simple.md +0 -0
  36. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_mistral_batch_native_tables.md +0 -0
  37. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_broken_encoding.md +0 -0
  38. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_image_only.md +0 -0
  39. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_native_equations.md +0 -0
  40. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_native_simple.md +0 -0
  41. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_native_tables.md +0 -0
  42. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_ocr_broken_encoding.md +0 -0
  43. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_ocr_image_only.md +0 -0
  44. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_ocr_native_equations.md +0 -0
  45. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_ocr_native_simple.md +0 -0
  46. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/md/out_pymu_ocr_native_tables.md +0 -0
  47. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/output/results.md +0 -0
  48. {dot_parser-2.1.1 → dot_parser-2.1.2}/benchmark/run.py +0 -0
  49. {dot_parser-2.1.1 → dot_parser-2.1.2}/docs/DESIGN.md +0 -0
  50. {dot_parser-2.1.1 → dot_parser-2.1.2}/docs/DEVELOPMENT.md +0 -0
  51. {dot_parser-2.1.1 → dot_parser-2.1.2}/docs/IMAGE_EXTRACTION.md +0 -0
  52. {dot_parser-2.1.1 → dot_parser-2.1.2}/docs/PUBLISHING.md +0 -0
  53. {dot_parser-2.1.1 → dot_parser-2.1.2}/docs/VERSIONING.md +0 -0
  54. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/__init__.py +0 -0
  55. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/backends/__init__.py +0 -0
  56. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/backends/_base.py +0 -0
  57. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/backends/docling.py +0 -0
  58. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/backends/llama.py +0 -0
  59. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/backends/mistral.py +0 -0
  60. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/backends/pymu.py +0 -0
  61. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/docx_images.py +0 -0
  62. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/docx_markdown.py +0 -0
  63. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/image_utils.py +0 -0
  64. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/images.py +0 -0
  65. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/markdown_utils.py +0 -0
  66. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/models.py +0 -0
  67. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/parsers.py +0 -0
  68. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/pricing.py +0 -0
  69. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/tokens.py +0 -0
  70. {dot_parser-2.1.1 → dot_parser-2.1.2}/src/dot_parser/vlms.py +0 -0
  71. {dot_parser-2.1.1 → dot_parser-2.1.2}/tests/test_docx_markdown.py +0 -0
  72. {dot_parser-2.1.1 → dot_parser-2.1.2}/tests/test_images.py +0 -0
  73. {dot_parser-2.1.1 → dot_parser-2.1.2}/tests/test_mistral_batch_logging.py +0 -0
  74. {dot_parser-2.1.1 → dot_parser-2.1.2}/tests/test_parsers.py +0 -0
  75. {dot_parser-2.1.1 → dot_parser-2.1.2}/tests/test_tokens.py +0 -0
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.1.2] - 2026-10-27
11
+
12
+ ### Fixed
13
+
14
+ - `chunk()` keeps every chunk within `max_tokens`. A paragraph larger than
15
+ the limit on its own (a long table, text without blank lines) is now split
16
+ instead of kept whole, and an undersized chunk is absorbed into its neighbour
17
+ only when the result still fits: a long run of heading-only sections (a table
18
+ of contents, an index) no longer piles up into a single chunk.
19
+
10
20
  ## [2.1.1] - 2026-09-30
11
21
 
12
22
  ### Changed
@@ -90,7 +100,8 @@ this release.
90
100
  - `parse_with_images(backend=...)` accepts DOCX- and PPTX-capable backends in
91
101
  its type signature, matching what the function already supported at runtime.
92
102
 
93
- [Unreleased]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser/-/compare/v2.1.1...main
103
+ [Unreleased]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser/-/compare/v2.1.2...main
104
+ [2.1.2]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser/-/compare/v2.1.1...v2.1.2
94
105
  [2.1.1]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser/-/compare/v2.1.0...v2.1.1
95
106
  [2.1.0]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser/-/compare/v2.0.0...v2.1.0
96
107
  [2.0.0]: https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser/-/tags/v2.0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dot-parser
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: Document-to-markdown parser and chunker for RAG pipelines
5
5
  Project-URL: Homepage, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser
6
6
  Project-URL: Repository, https://gitlab.com/deepika6190303/deepika-open-toolbox/dot-parser
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dot-parser"
3
- version = "2.1.1"
3
+ version = "2.1.2"
4
4
  description = "Document-to-markdown parser and chunker for RAG pipelines"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Kannon For Deep Tech", email = "louis.letarnec@deepika.ai" }]
@@ -3,6 +3,7 @@
3
3
 
4
4
  import re
5
5
  import statistics
6
+ from typing import cast
6
7
 
7
8
  import semchunk
8
9
 
@@ -63,17 +64,16 @@ def _split_oversized(section: dict, max_tokens: int) -> list[dict]:
63
64
  body = section["body"]
64
65
  path = section["section_path"]
65
66
 
66
- full_text = f"{heading}\n\n{body}" if heading else body
67
- if count_tokens(full_text) <= max_tokens:
67
+ prefix = f"{heading}\n\n" if heading else ""
68
+ if count_tokens(prefix + body) <= max_tokens:
68
69
  return [section]
69
70
 
70
71
  # Try paragraph splitting first
71
72
  paragraphs = re.split(r"\n\n+", body)
72
73
  if len(paragraphs) > 1:
73
- sub_chunks = _group_paragraphs(paragraphs, max_tokens, heading)
74
+ sub_chunks = _group_paragraphs(paragraphs, max_tokens, prefix)
74
75
  else:
75
- # Semantic splitting via semchunk
76
- sub_chunks = semchunk.chunk(body, chunk_size=max_tokens, token_counter=count_tokens)
76
+ sub_chunks = _split_text(body, max_tokens, prefix)
77
77
 
78
78
  results = []
79
79
  for i, text in enumerate(sub_chunks):
@@ -96,31 +96,37 @@ def _split_oversized(section: dict, max_tokens: int) -> list[dict]:
96
96
  return results
97
97
 
98
98
 
99
- def _group_paragraphs(paragraphs: list[str], max_tokens: int, heading: str | None) -> list[str]:
100
- """Group paragraphs into chunks that fit within max_tokens."""
99
+ def _split_text(text: str, max_tokens: int, prefix: str) -> list[str]:
100
+ """Split text semantically (semchunk) into pieces that fit within max_tokens
101
+ even with `prefix` (the heading the first piece carries) before them.
102
+ semchunk returns a bare list unless asked for offsets."""
103
+ pieces = semchunk.chunk(
104
+ text, chunk_size=max_tokens, token_counter=lambda t: count_tokens(prefix + t)
105
+ )
106
+ return cast("list[str]", pieces)
107
+
108
+
109
+ def _group_paragraphs(paragraphs: list[str], max_tokens: int, prefix: str) -> list[str]:
110
+ """Group paragraphs into chunks that fit within max_tokens, the first one
111
+ with `prefix` (its heading) before it. A paragraph too large on its own is
112
+ split semantically first."""
101
113
  groups: list[str] = []
102
114
  current: list[str] = []
103
- current_tokens = 0
104
115
 
105
- # Account for heading in first group
106
- heading_tokens = count_tokens(heading + "\n\n") if heading else 0
107
- first_group = True
116
+ def fits(texts: list[str]) -> bool:
117
+ lead = "" if groups else prefix
118
+ return count_tokens(lead + "\n\n".join(texts)) <= max_tokens
108
119
 
109
120
  for para in paragraphs:
110
121
  para = para.strip()
111
122
  if not para:
112
123
  continue
113
- para_tokens = count_tokens(para)
114
- overhead = heading_tokens if first_group and not current else 0
115
-
116
- if current and current_tokens + overhead + para_tokens > max_tokens:
117
- groups.append("\n\n".join(current))
118
- current = [para]
119
- current_tokens = para_tokens
120
- first_group = False
121
- else:
122
- current.append(para)
123
- current_tokens += para_tokens
124
+ pieces = [para] if fits([para]) else _split_text(para, max_tokens, prefix)
125
+ for piece in pieces:
126
+ if current and not fits([*current, piece]):
127
+ groups.append("\n\n".join(current))
128
+ current = []
129
+ current.append(piece)
124
130
 
125
131
  if current:
126
132
  groups.append("\n\n".join(current))
@@ -168,15 +174,19 @@ def _merge_small(sections: list[dict], max_tokens: int) -> list[dict]:
168
174
 
169
175
 
170
176
  def _absorb_undersized(sections: list[dict], max_tokens: int) -> list[dict]:
171
- """Absorb chunks smaller than 15% of max_tokens into neighbors."""
177
+ """Absorb chunks smaller than 15% of max_tokens into neighbors, when the
178
+ result still fits within max_tokens."""
172
179
  threshold = max_tokens * 0.15
173
180
  result: list[dict] = []
174
181
 
182
+ def fits(first: str, second: str) -> bool:
183
+ return count_tokens(first + "\n\n" + second) <= max_tokens
184
+
175
185
  for section in sections:
176
186
  text = _section_text(section)
177
187
  tokens = count_tokens(text)
178
188
 
179
- if tokens < threshold and result:
189
+ if tokens < threshold and result and fits(_section_text(result[-1]), text):
180
190
  # Append to previous
181
191
  prev = result[-1]
182
192
  prev_text = _section_text(prev)
@@ -192,7 +202,7 @@ def _absorb_undersized(sections: list[dict], max_tokens: int) -> list[dict]:
192
202
  # Check if first chunk is undersized and can be prepended to next
193
203
  if len(result) >= 2:
194
204
  first_text = _section_text(result[0])
195
- if count_tokens(first_text) < threshold:
205
+ if count_tokens(first_text) < threshold and fits(first_text, _section_text(result[1])):
196
206
  second = result[1]
197
207
  second_text = _section_text(second)
198
208
  result[1] = {
@@ -1,7 +1,10 @@
1
1
  # SPDX-FileCopyrightText: Kannon For Deep Tech
2
2
  # SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
+ import pytest
5
+
4
6
  from dot_parser import Chunk, chunk
7
+ from dot_parser.tokens import count_tokens
5
8
 
6
9
 
7
10
  class TestChunkBasic:
@@ -62,6 +65,28 @@ class TestSplitOversized:
62
65
  assert c.section_path == ["# Big"]
63
66
 
64
67
 
68
+ class TestMaxTokensBound:
69
+ @pytest.mark.parametrize("merge", [True, False])
70
+ def test_an_oversized_paragraph_among_others_is_split(self, merge):
71
+ md = "# Big\n\nIntro.\n\n" + "word " * 2000 + "\n\nOutro."
72
+ chunks = chunk(md, max_tokens=64, merge=merge)
73
+ assert all(count_tokens(c.content) <= 64 for c in chunks)
74
+ assert chunks[0].heading == "# Big"
75
+
76
+ @pytest.mark.parametrize("merge", [True, False])
77
+ def test_a_table_without_blank_lines_is_split(self, merge):
78
+ table = "\n".join(f"| row {i} | 0.{i:04d} | 12.5 |" for i in range(500))
79
+ md = f"# Results\n\nThe table below.\n\n{table}\n\nEnd."
80
+ chunks = chunk(md, max_tokens=64, merge=merge)
81
+ assert all(count_tokens(c.content) <= 64 for c in chunks)
82
+
83
+ def test_a_run_of_heading_only_sections_stays_within_the_bound(self):
84
+ md = "# Index\n\n" + "\n\n".join(f"## Entry {i}" for i in range(300))
85
+ chunks = chunk(md, max_tokens=64)
86
+ assert all(count_tokens(c.content) <= 64 for c in chunks)
87
+ assert "## Entry 299" in chunks[-1].content
88
+
89
+
65
90
  class TestMerge:
66
91
  def test_small_sections_merged(self):
67
92
  md = "# A\n\n## B\n\nShort.\n\n## C\n\nAlso short."
@@ -654,7 +654,7 @@ standard = [
654
654
 
655
655
  [[package]]
656
656
  name = "dot-parser"
657
- version = "2.1.1"
657
+ version = "2.1.2"
658
658
  source = { editable = "." }
659
659
  dependencies = [
660
660
  { name = "beautifulsoup4" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes