gost-docx 0.1.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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.3
2
+ Name: gost-docx
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Dmitry Klimov
6
+ Author-email: Dmitry Klimov <dmitry.v.klimov@gmail.com>
7
+ Requires-Dist: pandas>=2.3.2,<3.0.0
8
+ Requires-Dist: python-docx>=1.2.0
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
File without changes
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "gost-docx"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Dmitry Klimov", email = "dmitry.v.klimov@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "pandas>=2.3.2,<3.0.0",
12
+ "python-docx>=1.2.0",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.10.8,<0.11.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.build-backend]
20
+ module-name = "gost"
@@ -0,0 +1,3 @@
1
+ from .word_builder import WordBuilder
2
+
3
+ __all__ = ["WordBuilder"]
@@ -0,0 +1,24 @@
1
+ from gost.index.head_counter import HeadCounter
2
+ from gost.index.counter import Counter
3
+ from gost.elements.head import Head
4
+ from gost.elements.image import Image
5
+ from gost.elements.text import Text
6
+
7
+
8
+ class ElementFactory:
9
+ def __init__(self, character: str) -> None:
10
+ self.__image_counter = Counter()
11
+ self.__table_counter = Counter()
12
+ self.__head_counter = HeadCounter()
13
+ self.__character = character
14
+
15
+ def create_text(self, text: str) -> Text:
16
+ return Text(text)
17
+
18
+ def create_head(self, use_numbers: bool, text: str, level: int = 1) -> Head:
19
+ number = self.__head_counter.get_next(level)
20
+ return Head(number, use_numbers, text, level)
21
+
22
+ def create_image(self, path: str, alt: str) -> Image:
23
+ number = self.__image_counter.get_next()
24
+ return Image(number, alt, path)
File without changes
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ from docx.document import Document
5
+
6
+
7
+ class ElementBase(ABC):
8
+ @abstractmethod
9
+ def render(self, document: Document) -> None:
10
+ pass
11
+
12
+
13
+ class NumberedElement(ElementBase, ABC):
14
+ def __init__(self, number: str) -> None:
15
+ self.number = number
@@ -0,0 +1,32 @@
1
+ from docx.document import Document
2
+
3
+ from gost.elements.element import NumberedElement
4
+
5
+
6
+ class Head(NumberedElement):
7
+ def __init__(
8
+ self,
9
+ number: str,
10
+ use_number: bool,
11
+ text: str,
12
+ level: int = 1,
13
+ ) -> None:
14
+ super().__init__(number)
15
+ self.text = text
16
+ self.level = level
17
+ self.use_number = use_number
18
+
19
+ def render(self, document: Document) -> None:
20
+ text = self.__prepare_render_text()
21
+ document.add_heading(text, level=self.level)
22
+
23
+ def __prepare_render_text(self) -> str:
24
+ if self.use_number:
25
+ text = f"{self.use_number} {self.text}"
26
+ else:
27
+ text = f"{self.text}"
28
+
29
+ if self.level == 1:
30
+ text = self.text.upper()
31
+
32
+ return text
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+
3
+ from docx.document import Document
4
+ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
5
+ from docx.shared import Cm, Pt
6
+
7
+ from gost.elements.element import NumberedElement
8
+
9
+
10
+ class Image(NumberedElement):
11
+ def __init__(
12
+ self,
13
+ number: str,
14
+ path: str,
15
+ alt: str,
16
+ ) -> None:
17
+ """
18
+ Создает изображение с подписью «Рисунок number — alt».
19
+ """
20
+ super().__init__(number)
21
+ self.alt = alt
22
+ self.path = Path(path)
23
+
24
+ def render(self, document: Document) -> None:
25
+ if self.path.exists():
26
+ document.add_picture(str(self.path), width=Cm(16.5))
27
+ last_paragraph = document.paragraphs[-1]
28
+ last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
29
+ last_paragraph.paragraph_format.first_line_indent = Cm(0)
30
+ else:
31
+ document.add_paragraph(f"[Изображение не найдено: {self.path}]")
32
+
33
+ caption = document.add_paragraph()
34
+ caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
35
+ caption.paragraph_format.first_line_indent = Cm(0)
36
+ caption.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
37
+ caption.paragraph_format.space_after = Pt(6)
38
+ run = caption.add_run(f"Рисунок {self.number} – {self.alt}")
39
+ run.font.size = Pt(14)
@@ -0,0 +1,93 @@
1
+ from typing import Optional
2
+
3
+ import pandas as pd
4
+ from docx.document import Document
5
+ from docx.enum.table import WD_TABLE_ALIGNMENT
6
+ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
7
+ from docx.shared import Cm, Pt
8
+
9
+ from gost.elements.element import NumberedElement
10
+ from gost.utils import format_value
11
+
12
+
13
+ class Table(NumberedElement):
14
+ def __init__(
15
+ self,
16
+ number: str,
17
+ df: pd.DataFrame,
18
+ float_format: Optional[str] = ".1f",
19
+ title: Optional[str] = None,
20
+ show_header: bool = True,
21
+ show_index: bool = True,
22
+ bold_header: bool = False,
23
+ bold_index: bool = False,
24
+ ) -> None:
25
+ """Создает таблицу из DataFrame с подписью «Таблица number — title».
26
+
27
+ Подпись добавляется только при наличии title, при этом счётчик таблиц увеличивается.
28
+
29
+ Args:
30
+ df: Данные для отображения в таблице.
31
+ float_format: Формат вещественных чисел (например, «.1f»). None — без форматирования.
32
+ title: Заголовок таблицы. Если None, подпись и счётчик не добавляются.
33
+ show_header: Добавлять ли строку с именами столбцов.
34
+ show_index: Добавлять ли столбец с индексом DataFrame.
35
+ bold_header: Выделять ли заголовки столбцов жирным.
36
+ bold_index: Выделять ли значения индекса жирным.
37
+
38
+ Returns:
39
+ Текущий экземпляр WordBuilder для цепочки вызовов.
40
+ """
41
+ super().__init__(number)
42
+ self.df = df
43
+ self.float_format = float_format
44
+ self.title = title
45
+ self.show_header = show_header
46
+ self.show_index = show_index
47
+ self.bold_header = bold_header
48
+ self.bold_index = bold_index
49
+
50
+ def render(self, document: Document) -> None:
51
+ if self.title is not None:
52
+ caption = document.add_paragraph()
53
+ caption.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
54
+ caption.paragraph_format.first_line_indent = Cm(0)
55
+ caption.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
56
+ caption.paragraph_format.space_after = Pt(6)
57
+ run = caption.add_run(f"Таблица – {self.number} {self.title}")
58
+ run.font.size = Pt(14)
59
+
60
+ header_rows = 1 if self.show_header else 0
61
+ rows = len(self.df) + header_rows
62
+ index_offset = 1 if self.show_index else 0
63
+ cols = len(self.df.columns) + index_offset
64
+
65
+ table = document.add_table(rows=rows, cols=cols)
66
+ table.style = "Table Grid"
67
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
68
+
69
+ if self.show_header:
70
+ if self.show_index:
71
+ table.cell(0, 0).text = ""
72
+ for j, col_name in enumerate(self.df.columns):
73
+ cell = table.cell(0, j + index_offset)
74
+ cell.text = ""
75
+ run = cell.paragraphs[0].add_run(format_value(col_name, self.float_format))
76
+ run.bold = self.bold_header
77
+
78
+ for i, (idx, row) in enumerate(self.df.iterrows()):
79
+ row_idx = i + header_rows
80
+ if self.show_index:
81
+ idx_cell = table.cell(row_idx, 0)
82
+ idx_cell.text = ""
83
+ run = idx_cell.paragraphs[0].add_run(str(idx))
84
+ run.bold = self.bold_index
85
+ for j, val in enumerate(row):
86
+ table.cell(row_idx, j + index_offset).text = format_value(val, self.float_format)
87
+
88
+ for row in table.rows:
89
+ for cell in row.cells:
90
+ for para in cell.paragraphs:
91
+ para.style = "Table Text"
92
+
93
+ document.add_paragraph()
@@ -0,0 +1,13 @@
1
+ from docx.document import Document
2
+
3
+ from gost.elements.element import ElementBase
4
+ from gost.utils import render_bold
5
+
6
+
7
+ class Text(ElementBase):
8
+ def __init__(self, text: str) -> None:
9
+ self.text = text
10
+
11
+ def render(self, document: Document) -> None:
12
+ paragraph = document.add_paragraph()
13
+ render_bold(paragraph, self.text)
File without changes
@@ -0,0 +1,45 @@
1
+ class Counter:
2
+ def __init__(self, initial: int = 1) -> None:
3
+ self.__initial = initial - 1
4
+ self.current: int = self.__initial
5
+
6
+ def get_next(self) -> int:
7
+ self.current = self.current + 1
8
+ return self.current
9
+
10
+ def reset(self):
11
+ self.current = self.__initial
12
+
13
+
14
+ class HeadCounter:
15
+ def __init__(self) -> None:
16
+ self.__levels_counter: dict[int, Counter] = {
17
+ 1: Counter(),
18
+ 2: Counter(),
19
+ 3: Counter(),
20
+ 4: Counter(),
21
+ }
22
+
23
+ def get_next(self, head_level: int) -> str:
24
+ self.__levels_counter[head_level].get_next()
25
+
26
+ result_number = str(self.__levels_counter[1].current)
27
+ if head_level > 1:
28
+ for level in range(2, head_level + 1):
29
+ result_number = result_number + '.' + str(self.__levels_counter[level].current)
30
+
31
+ return result_number
32
+
33
+ def get_current_chapter(self) -> int:
34
+ return self.__levels_counter[1].current
35
+
36
+ def reset(self):
37
+ for level in self.__levels_counter:
38
+ self.__levels_counter[level].reset()
39
+
40
+ def __get_currents(self) -> list[int]:
41
+ currents: list[int] = []
42
+ for level in self.__levels_counter:
43
+ currents.append(self.__levels_counter[level].current)
44
+
45
+ return currents
@@ -0,0 +1,48 @@
1
+ from gost.index.counter import Counter, HeadCounter
2
+ from gost.index.index_type import IndexType
3
+
4
+
5
+ class IndexManager:
6
+ def __init__(
7
+ self,
8
+ character: str | None = None,
9
+ index_type: IndexType = IndexType.CONTINUOUS
10
+ ) -> None:
11
+ self.__character = character
12
+ self.__index_type = index_type
13
+
14
+ self.__head_counter = HeadCounter()
15
+ self.__image_counter = Counter()
16
+ self.__table_counter = Counter()
17
+
18
+ def get_head_index(self, level: int) -> str:
19
+ index = self.__head_counter.get_next(level)
20
+
21
+ if self.__character:
22
+ index = self.__add_character(index)
23
+
24
+ if level == 0 and self.__index_type == IndexType.CHAPTER_RELATIVE:
25
+ self.__reset_counters()
26
+ return index
27
+
28
+ def get_image_index(self):
29
+ index = str(self.__image_counter.get_next())
30
+ if self.__index_type == IndexType.CHAPTER_RELATIVE:
31
+ index = self.__add_chapter(index)
32
+
33
+ if self.__character:
34
+ index = self.__add_character(index)
35
+
36
+ return index
37
+
38
+ def __add_chapter(self, index: str) -> str:
39
+ current_chapter = self.__head_counter.get_current_chapter()
40
+ index = f"{current_chapter}.{index}"
41
+ return index
42
+
43
+ def __add_character(self, index: str) -> str:
44
+ return f"{self.__character}.{index}"
45
+
46
+ def __reset_counters(self):
47
+ self.__image_counter.reset()
48
+ self.__head_counter.reset()
@@ -0,0 +1,11 @@
1
+ from enum import Enum, auto
2
+
3
+
4
+ class IndexType(Enum):
5
+ """Режим нумерации объектов документа (рисунков, таблиц, формул)."""
6
+
7
+ CONTINUOUS = auto()
8
+ """Сквозная нумерация по всему документу: 1, 2, 3, ..."""
9
+
10
+ CHAPTER_RELATIVE = auto()
11
+ """Нумерация относительно номера текущей главы: 1.1, 1.2, 2.1, ..."""
@@ -0,0 +1,81 @@
1
+ from docx.document import Document
2
+ from docx.enum.style import WD_STYLE_TYPE
3
+ from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
4
+ from docx.oxml.ns import qn
5
+ from docx.shared import Cm, Pt, RGBColor
6
+
7
+
8
+ def apply_gost_styles(doc: Document) -> None:
9
+ """Configure GOST 7.32-2001 paragraph and heading styles on *doc* in-place."""
10
+ _apply_section(doc)
11
+ _apply_normal_style(doc)
12
+ _apply_table_text_style(doc)
13
+ _apply_heading_styles(doc)
14
+
15
+ def _apply_section(doc: Document) -> None:
16
+ # Page margins per ГОСТ 7.32-2001: left ≥ 30mm, right ≥ 10mm, top ≥ 20mm, bottom ≥ 20mm
17
+ section = doc.sections[0]
18
+ section.left_margin = Cm(3.0)
19
+ section.right_margin = Cm(1.5)
20
+ section.top_margin = Cm(2.0)
21
+ section.bottom_margin = Cm(2.0)
22
+
23
+ def _apply_normal_style(doc: Document) -> None:
24
+ normal = doc.styles["Normal"]
25
+ normal.font.name = "Times New Roman"
26
+ normal.font.size = Pt(14)
27
+ normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
28
+ normal.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
29
+ normal.paragraph_format.first_line_indent = Cm(1.25)
30
+ normal.paragraph_format.space_before = Pt(0)
31
+ normal.paragraph_format.space_after = Pt(0)
32
+
33
+
34
+ def _apply_table_text_style(doc: Document) -> None:
35
+ table_text = doc.styles.add_style("Table Text", WD_STYLE_TYPE.PARAGRAPH)
36
+ table_text.font.name = "Times New Roman"
37
+ table_text.font.size = Pt(12)
38
+ table_text.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
39
+ table_text.paragraph_format.first_line_indent = Cm(0)
40
+ table_text.paragraph_format.space_before = Pt(0)
41
+ table_text.paragraph_format.space_after = Pt(0)
42
+
43
+
44
+ def _apply_heading_styles(doc: Document) -> None:
45
+ # (style_name, centered, font_size, space_before)
46
+ heading_configs = [
47
+ ("Heading 1", True, Pt(14), Pt(0)),
48
+ ("Heading 2", False, Pt(14), Pt(0)),
49
+ ("Heading 3", False, Pt(14), Pt(0)),
50
+ ("Heading 4", False, Pt(14), Pt(0)),
51
+ ]
52
+ for style_name, centered, font_size, space_before in heading_configs:
53
+ s = doc.styles[style_name]
54
+ s.font.name = "Times New Roman"
55
+ _remove_theme_font_overrides(s)
56
+ s.font.size = font_size
57
+ s.font.bold = True
58
+ s.font.italic = False
59
+ s.font.color.rgb = RGBColor(0, 0, 0)
60
+ s.paragraph_format.alignment = (
61
+ WD_ALIGN_PARAGRAPH.CENTER if centered else WD_ALIGN_PARAGRAPH.LEFT
62
+ )
63
+ s.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
64
+ s.paragraph_format.first_line_indent = Cm(0) if centered else Cm(1.25)
65
+ s.paragraph_format.space_before = space_before
66
+ s.paragraph_format.space_after = Pt(0)
67
+ _remove_bottom_border(s)
68
+
69
+
70
+ def _remove_theme_font_overrides(style) -> None:
71
+ rPr = style.element.get_or_add_rPr()
72
+ rFonts = rPr.find(qn("w:rFonts"))
73
+ if rFonts is not None:
74
+ for attr in [qn("w:asciiTheme"), qn("w:hAnsiTheme"), qn("w:eastAsiaTheme"), qn("w:cstheme")]:
75
+ rFonts.attrib.pop(attr, None)
76
+
77
+
78
+ def _remove_bottom_border(style) -> None:
79
+ pPr = style.element.get_or_add_pPr()
80
+ for pBdr in pPr.findall(qn("w:pBdr")):
81
+ pPr.remove(pBdr)
@@ -0,0 +1,21 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+
5
+ def format_value(val, float_format: Optional[str]) -> str:
6
+ if isinstance(val, float):
7
+ if float_format:
8
+ return f"{val:{float_format}}"
9
+ return str(val)
10
+ return str(val)
11
+
12
+
13
+ def render_bold(paragraph, text: str) -> None:
14
+ """Parse **bold** fragments into Word runs."""
15
+ parts = re.split(r"(\*\*[^*]+\*\*)", text)
16
+ for part in parts:
17
+ if part.startswith("**") and part.endswith("**"):
18
+ run = paragraph.add_run(part[2:-2])
19
+ run.bold = True
20
+ else:
21
+ paragraph.add_run(part)
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+
3
+ from docx import Document
4
+
5
+ from .elements.element import ElementBase
6
+ from .styles import apply_gost_styles
7
+
8
+
9
+ class WordBuilder:
10
+ def __init__(self) -> None:
11
+ """
12
+ Инициализирует документ с полями по ГОСТ 7.32-2001 и применяет стили.
13
+ """
14
+ self.__document = Document()
15
+ self.__is_rendered = False
16
+ self.__figure_counter = 0
17
+ self.__table_counter = 0
18
+ self.__elements: list[ElementBase] = []
19
+
20
+ apply_gost_styles(self.__document)
21
+
22
+ def add_element(self, element: ElementBase) -> None:
23
+ self.__elements.append(element)
24
+
25
+ def save(self, path: Path) -> None:
26
+ """Сохраняет документ в файл формата .docx.
27
+
28
+ Args:
29
+ path: Путь к итоговому файлу.
30
+ """
31
+ if not self.__is_rendered:
32
+ self.__render_elements()
33
+ self.__is_rendered = True
34
+
35
+ self.__document.save(str(path))
36
+
37
+ def __render_elements(self):
38
+ for element in self.__elements:
39
+ element.render(self.__document)