rtflite 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {rtflite-2.2.0 → rtflite-2.3.0}/CHANGELOG.md +18 -0
  2. {rtflite-2.2.0 → rtflite-2.3.0}/PKG-INFO +1 -1
  3. {rtflite-2.2.0 → rtflite-2.3.0}/pyproject.toml +1 -1
  4. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/__init__.py +2 -1
  5. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/assemble.py +102 -0
  6. {rtflite-2.2.0 → rtflite-2.3.0}/.gitignore +0 -0
  7. {rtflite-2.2.0 → rtflite-2.3.0}/.python-version +0 -0
  8. {rtflite-2.2.0 → rtflite-2.3.0}/LICENSE +0 -0
  9. {rtflite-2.2.0 → rtflite-2.3.0}/LICENSES_THIRD_PARTY +0 -0
  10. {rtflite-2.2.0 → rtflite-2.3.0}/README.md +0 -0
  11. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/attributes.py +0 -0
  12. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/convert.py +0 -0
  13. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/core/__init__.py +0 -0
  14. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/core/config.py +0 -0
  15. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/core/constants.py +0 -0
  16. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/HAMD17.parquet +0 -0
  17. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/__init__.py +0 -0
  18. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/adae.parquet +0 -0
  19. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/adsl.parquet +0 -0
  20. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/baseline.parquet +0 -0
  21. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/tbl1.parquet +0 -0
  22. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/tbl2.parquet +0 -0
  23. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/data/tbl3.parquet +0 -0
  24. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/dictionary/__init__.py +0 -0
  25. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/dictionary/color_table.py +0 -0
  26. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/dictionary/libreoffice.py +0 -0
  27. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/dictionary/unicode_latex.py +0 -0
  28. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/encode.py +0 -0
  29. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/encoding/__init__.py +0 -0
  30. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/encoding/base.py +0 -0
  31. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/encoding/engine.py +0 -0
  32. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/encoding/renderer.py +0 -0
  33. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/encoding/unified_encoder.py +0 -0
  34. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/figure.py +0 -0
  35. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/README.md +0 -0
  36. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/__init__.py +0 -0
  37. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/cros/Caladea-Regular.ttf +0 -0
  38. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/cros/Carlito-Regular.ttf +0 -0
  39. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/cros/Gelasio-Regular.ttf +0 -0
  40. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/liberation/LiberationMono-Regular.ttf +0 -0
  41. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/liberation/LiberationSans-Regular.ttf +0 -0
  42. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts/liberation/LiberationSerif-Regular.ttf +0 -0
  43. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/fonts_mapping.py +0 -0
  44. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/input.py +0 -0
  45. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/__init__.py +0 -0
  46. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/core.py +0 -0
  47. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/processor.py +0 -0
  48. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/strategies/__init__.py +0 -0
  49. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/strategies/base.py +0 -0
  50. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/strategies/defaults.py +0 -0
  51. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/strategies/grouping.py +0 -0
  52. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/pagination/strategies/registry.py +0 -0
  53. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/row.py +0 -0
  54. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/rtf/__init__.py +0 -0
  55. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/rtf/syntax.py +0 -0
  56. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/__init__.py +0 -0
  57. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/color_service.py +0 -0
  58. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/document_service.py +0 -0
  59. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/encoding_service.py +0 -0
  60. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/figure_service.py +0 -0
  61. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/grouping_service.py +0 -0
  62. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/services/text_conversion_service.py +0 -0
  63. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/strwidth.py +0 -0
  64. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/text_conversion/__init__.py +0 -0
  65. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/text_conversion/converter.py +0 -0
  66. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/text_conversion/symbols.py +0 -0
  67. {rtflite-2.2.0 → rtflite-2.3.0}/src/rtflite/type_guards.py +0 -0
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## rtflite 2.3.0
4
+
5
+ ### New features
6
+
7
+ - Added `concatenate_docx` function to merge DOCX outputs without manual
8
+ field refreshes, preserving per-section orientation (#160).
9
+
10
+ ### Testing
11
+
12
+ - Added DOCX concatenation coverage and centralized optional dependency
13
+ skip markers for `python-docx` and LibreOffice to keep tests gated
14
+ appropriately (#160).
15
+
16
+ ### Documentation
17
+
18
+ - Updated the assembly article to use `concatenate_docx` in code examples and
19
+ added a reference page for assemble function to the mkdocs site (#160).
20
+
3
21
  ## rtflite 2.2.0
4
22
 
5
23
  ### New features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rtflite
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Lightweight RTF composer for Python
5
5
  Project-URL: Homepage, https://pharmaverse.github.io/rtflite/
6
6
  Project-URL: Documentation, https://pharmaverse.github.io/rtflite/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rtflite"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  description = "Lightweight RTF composer for Python"
5
5
  authors = [
6
6
  { name = "Yilong Zhang", email = "elong0527@gmail.com" },
@@ -1,6 +1,6 @@
1
1
  """rtflite: A Python library for creating RTF documents."""
2
2
 
3
- from .assemble import assemble_docx, assemble_rtf
3
+ from .assemble import assemble_docx, assemble_rtf, concatenate_docx
4
4
  from .attributes import TableAttributes
5
5
  from .convert import LibreOfficeConverter
6
6
  from .core.config import RTFConfiguration
@@ -46,4 +46,5 @@ __all__ = [
46
46
  "LibreOfficeConverter",
47
47
  "assemble_rtf",
48
48
  "assemble_docx",
49
+ "concatenate_docx",
49
50
  ]
@@ -1,6 +1,14 @@
1
1
  """Assemble multiple RTF files into a single RTF or DOCX file."""
2
2
 
3
3
  import os
4
+ from collections.abc import Sequence
5
+ from copy import deepcopy
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING: # pragma: no cover
10
+ from docx.document import Document as DocxDocument
11
+ from docx.section import Section
4
12
 
5
13
  # from .input import RTFPage # Unused
6
14
 
@@ -197,3 +205,97 @@ def _add_field(paragraph, field_code):
197
205
  fldChar = OxmlElement("w:fldChar")
198
206
  fldChar.set(qn("w:fldCharType"), "end")
199
207
  r.append(fldChar)
208
+
209
+
210
+ def concatenate_docx(
211
+ input_files: Sequence[str | os.PathLike[str]],
212
+ output_file: str | os.PathLike[str],
213
+ landscape: bool | Sequence[bool] = False,
214
+ ) -> None:
215
+ """Concatenate DOCX files without relying on Word field toggles.
216
+
217
+ This helper is useful when `RTFDocument.write_docx` already produced DOCX
218
+ files and you need to stitch them together into a single document that can
219
+ be distributed without refreshing fields in Microsoft Word.
220
+
221
+ Args:
222
+ input_files: Ordered collection of DOCX file paths to combine. The
223
+ first document becomes the base; subsequent documents are appended
224
+ as new sections.
225
+ output_file: Path to the combined DOCX file.
226
+ landscape: Whether each appended section should be landscape. Accepts
227
+ a single boolean applied to every section or a list/tuple matching
228
+ ``input_files``.
229
+
230
+ Raises:
231
+ ImportError: If ``python-docx`` is not installed.
232
+ ValueError: If ``input_files`` is empty or the ``landscape`` list length
233
+ does not match ``input_files``.
234
+ FileNotFoundError: If any input file is missing.
235
+ """
236
+ try:
237
+ from docx import Document # type: ignore
238
+ from docx.enum.section import WD_SECTION # type: ignore
239
+ except ImportError as exc:
240
+ raise ImportError(
241
+ "python-docx is required for concatenate_docx. "
242
+ "Install it with: pip install 'rtflite[docx]'"
243
+ ) from exc
244
+
245
+ paths = [Path(path).expanduser() for path in input_files]
246
+ if not paths:
247
+ raise ValueError("Input files list cannot be empty")
248
+
249
+ missing_files = [str(path) for path in paths if not path.exists()]
250
+ if missing_files:
251
+ raise FileNotFoundError(f"Missing files: {', '.join(missing_files)}")
252
+
253
+ orientation_flags = _coerce_landscape_flags(landscape, len(paths))
254
+
255
+ combined_doc = Document(str(paths[0]))
256
+ _set_section_orientation(combined_doc.sections[0], orientation_flags[0])
257
+
258
+ for source_path, is_landscape in zip(paths[1:], orientation_flags[1:], strict=True):
259
+ combined_doc.add_section(WD_SECTION.NEW_PAGE)
260
+ _set_section_orientation(combined_doc.sections[-1], is_landscape)
261
+ _append_document_body(combined_doc, Document(str(source_path)))
262
+
263
+ output_path = Path(output_file).expanduser()
264
+ output_path.parent.mkdir(parents=True, exist_ok=True)
265
+ combined_doc.save(str(output_path))
266
+
267
+
268
+ def _coerce_landscape_flags(
269
+ landscape: bool | Sequence[bool],
270
+ expected_length: int,
271
+ ) -> list[bool]:
272
+ """Normalize the ``landscape`` argument to a list and validate its length."""
273
+ if isinstance(landscape, bool):
274
+ return [landscape] * expected_length
275
+
276
+ flags = list(landscape)
277
+ if len(flags) != expected_length:
278
+ raise ValueError("Length of landscape list must match input files")
279
+
280
+ return flags
281
+
282
+
283
+ def _set_section_orientation(section: "Section", landscape: bool) -> None:
284
+ """Set section orientation and swap dimensions if needed."""
285
+ from docx.enum.section import WD_ORIENT # type: ignore
286
+
287
+ section.orientation = WD_ORIENT.LANDSCAPE if landscape else WD_ORIENT.PORTRAIT
288
+ width, height = section.page_width, section.page_height
289
+ if width is None or height is None:
290
+ return
291
+
292
+ if (landscape and width < height) or (not landscape and width > height):
293
+ section.page_width, section.page_height = height, width
294
+
295
+
296
+ def _append_document_body(target: "DocxDocument", source: "DocxDocument") -> None:
297
+ """Copy body content from ``source`` into ``target`` without section props."""
298
+ for element in list(source.element.body):
299
+ if element.tag.endswith("}sectPr"):
300
+ continue
301
+ target.element.body.append(deepcopy(element))
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