excelipy 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 @@
1
+ recursive-include excelipy *
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: excelipy
3
+ Version: 0.1.0
4
+ Summary: Wrapper around xlsxwriter to improve usability
5
+ Requires-Python: >=3.9.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pandas>=2.2.3
8
+ Requires-Dist: pydantic>=2.11.3
9
+ Requires-Dist: xlsxwriter>=3.2.2
10
+
11
+ # Excelipy
12
+
13
+ [![codecov](https://codecov.io/gh/choinhet/excelipy/graph/badge.svg?token=${CODECOV_TOKEN})](https://codecov.io/gh/choinhet/excelipy)
@@ -0,0 +1,3 @@
1
+ # Excelipy
2
+
3
+ [![codecov](https://codecov.io/gh/choinhet/excelipy/graph/badge.svg?token=${CODECOV_TOKEN})](https://codecov.io/gh/choinhet/excelipy)
@@ -0,0 +1,22 @@
1
+ __all__ = [
2
+ "Style",
3
+ "Component",
4
+ "Fill",
5
+ "Text",
6
+ "Table",
7
+ "Sheet",
8
+ "Excel",
9
+ "save",
10
+ ]
11
+
12
+ from excelipy.models import (
13
+ Style,
14
+ Component,
15
+ Fill,
16
+ Text,
17
+ Table,
18
+ Sheet,
19
+ Excel,
20
+ )
21
+
22
+ from excelipy.service import save
@@ -0,0 +1,15 @@
1
+ PROP_MAP = dict(
2
+ align="align",
3
+ valign="valign",
4
+ font_size="font_size",
5
+ font_color="font_color",
6
+ font_family="font_name",
7
+ bold="bold",
8
+ border="border",
9
+ border_left="left",
10
+ border_right="right",
11
+ border_top="top",
12
+ border_bottom="bottom",
13
+ border_color="border_color",
14
+ background="bg_color",
15
+ )
@@ -0,0 +1,74 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import pandas as pd
5
+
6
+ import excelipy as ep
7
+
8
+
9
+ def main():
10
+ df = pd.DataFrame(
11
+ {
12
+ "testing": [1, 2, 3],
13
+ "tested": ["Yay", "Thanks", "Bud"],
14
+ }
15
+ )
16
+
17
+ sheets = [
18
+ ep.Sheet(
19
+ name="Hello!",
20
+ components=[
21
+ ep.Text(
22
+ text="This is my table",
23
+ style=ep.Style(bold=True),
24
+ width=4,
25
+ ),
26
+ ep.Fill(
27
+ width=4,
28
+ style=ep.Style(background="#D0D0D0"),
29
+ ),
30
+ ep.Table(
31
+ data=df,
32
+ header_style=ep.Style(
33
+ bold=True,
34
+ border=5,
35
+ border_color="#F02932",
36
+ ),
37
+ body_style=ep.Style(font_size=18),
38
+ column_style={
39
+ "testing": ep.Style(
40
+ font_size=10,
41
+ align="center",
42
+ ),
43
+ },
44
+ column_width={
45
+ "tested": 20,
46
+ },
47
+ row_style={
48
+ 1: ep.Style(
49
+ border=2,
50
+ border_color="#F02932",
51
+ )
52
+ },
53
+ style=ep.Style(padding=1),
54
+ ).with_stripes(pattern="even"),
55
+ ],
56
+ style=ep.Style(
57
+ font_size=14,
58
+ font_family="Times New Roman",
59
+ padding=1,
60
+ ),
61
+ ),
62
+ ]
63
+
64
+ excel = ep.Excel(
65
+ path=Path("filename.xlsx"),
66
+ sheets=sheets,
67
+ )
68
+
69
+ ep.save(excel)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ logging.basicConfig(level=logging.DEBUG)
74
+ main()
@@ -0,0 +1,123 @@
1
+ from pathlib import Path
2
+ from typing import Dict, Optional, Sequence, Literal
3
+
4
+ import pandas as pd
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Style(BaseModel):
9
+ class Config:
10
+ frozen = True
11
+
12
+ align: Optional[
13
+ Literal[
14
+ "left",
15
+ "center",
16
+ "right",
17
+ "fill",
18
+ "justify",
19
+ "center_across",
20
+ "distributed",
21
+ ]
22
+ ] = Field(default=None)
23
+ valign: Optional[
24
+ Literal[
25
+ "top",
26
+ "vcenter",
27
+ "bottom",
28
+ "vcenter",
29
+ "bottom",
30
+ "vjustify",
31
+ ]
32
+ ] = Field(default=None)
33
+ padding: Optional[int] = Field(default=None)
34
+ padding_left: Optional[int] = Field(default=None)
35
+ padding_right: Optional[int] = Field(default=None)
36
+ padding_top: Optional[int] = Field(default=None)
37
+ padding_bottom: Optional[int] = Field(default=None)
38
+ font_size: Optional[int] = Field(default=None)
39
+ font_color: Optional[str] = Field(default=None)
40
+ font_family: Optional[str] = Field(default=None)
41
+ bold: Optional[bool] = Field(default=None)
42
+ border: Optional[int] = Field(default=None)
43
+ border_left: Optional[int] = Field(default=None)
44
+ border_right: Optional[int] = Field(default=None)
45
+ border_top: Optional[int] = Field(default=None)
46
+ border_bottom: Optional[int] = Field(default=None)
47
+ border_color: Optional[str] = Field(default=None)
48
+ background: Optional[str] = Field(default=None)
49
+
50
+ def merge(self, other: "Style") -> "Style":
51
+ self_dict = self.model_dump(exclude_none=True)
52
+ other_dict = other.model_dump(exclude_none=True)
53
+ self_dict.update(other_dict)
54
+ return self.model_validate(self_dict)
55
+
56
+ def pl(self) -> int:
57
+ return self.padding_left or self.padding or 0
58
+
59
+ def pt(self) -> int:
60
+ return self.padding_top or self.padding or 0
61
+
62
+ def pr(self) -> int:
63
+ return self.padding_right or self.padding or 0
64
+
65
+ def pb(self) -> int:
66
+ return self.padding_bottom or self.padding or 0
67
+
68
+
69
+ class Component(BaseModel):
70
+ style: Style = Field(default_factory=Style)
71
+
72
+ class Config:
73
+ arbitrary_types_allowed = True
74
+
75
+
76
+ class Text(Component):
77
+ text: str
78
+ width: int = Field(default=1)
79
+ height: int = Field(default=1)
80
+
81
+
82
+ class Fill(Component):
83
+ width: int = Field(default=1)
84
+ height: int = Field(default=1)
85
+
86
+
87
+ class Table(Component):
88
+ data: pd.DataFrame
89
+ header_style: Style = Field(default_factory=Style)
90
+ body_style: Style = Field(default_factory=Style)
91
+ column_style: Dict[str, Style] = Field(default_factory=dict)
92
+ column_width: Dict[str, int] = Field(default_factory=dict)
93
+ row_style: Dict[int, Style] = Field(default_factory=dict)
94
+ max_col_width: Optional[int] = Field(default=None)
95
+ header_filters: bool = Field(default=True)
96
+
97
+ def with_stripes(
98
+ self,
99
+ color: str = "#D0D0D0",
100
+ pattern: Literal["even", "odd"] = "odd",
101
+ ) -> "Table":
102
+ return self.model_copy(
103
+ update=dict(
104
+ row_style={
105
+ idx: self.row_style.get(idx, Style()).merge(Style(background=color))
106
+ if (pattern == "odd" and idx % 2 != 0)
107
+ or (pattern == "even" and idx % 2 == 0)
108
+ else self.row_style.get(idx, Style())
109
+ for idx in range(self.data.shape[0])
110
+ }
111
+ )
112
+ )
113
+
114
+
115
+ class Sheet(BaseModel):
116
+ name: str
117
+ components: Sequence[Component] = Field(default_factory=list)
118
+ style: Style = Field(default_factory=Style)
119
+
120
+
121
+ class Excel(BaseModel):
122
+ path: Path
123
+ sheets: Sequence[Sheet] = Field(default_factory=list)
@@ -0,0 +1,65 @@
1
+ import logging
2
+ from typing import Tuple
3
+
4
+ import xlsxwriter
5
+ from xlsxwriter.workbook import Workbook, Worksheet
6
+
7
+ from excelipy.models import Component, Excel, Fill, Style, Table, Text
8
+ from excelipy.writers import (
9
+ write_fill,
10
+ write_table,
11
+ write_text,
12
+ )
13
+
14
+ log = logging.getLogger("excelipy")
15
+
16
+
17
+ def write_component(
18
+ workbook: Workbook,
19
+ worksheet: Worksheet,
20
+ component: Component,
21
+ default_style: Style,
22
+ origin: Tuple[int, int] = (0, 0),
23
+ ) -> Tuple[int, int]:
24
+ writing_map = {
25
+ Table: write_table,
26
+ Text: write_text,
27
+ Fill: write_fill,
28
+ }
29
+ render_func = writing_map.get(type(component))
30
+ if render_func is None:
31
+ return 0, 0
32
+ return render_func(
33
+ workbook,
34
+ worksheet,
35
+ component,
36
+ default_style,
37
+ origin,
38
+ )
39
+
40
+
41
+ def save(excel: Excel):
42
+ workbook = xlsxwriter.Workbook(excel.path)
43
+ log.debug("Workbook opened")
44
+ for sheet in excel.sheets:
45
+ origin = (
46
+ sheet.style.pl(),
47
+ sheet.style.pt(),
48
+ )
49
+ worksheet = workbook.add_worksheet(sheet.name)
50
+ for component in sheet.components:
51
+ cur_origin = (
52
+ origin[0] + component.style.pl(),
53
+ origin[1] + component.style.pt(),
54
+ )
55
+ x, y = write_component(
56
+ workbook,
57
+ worksheet,
58
+ component,
59
+ sheet.style,
60
+ cur_origin,
61
+ )
62
+ origin = origin[0], origin[1] + y
63
+
64
+ workbook.close()
65
+ log.debug("Workbook closed")
@@ -0,0 +1,33 @@
1
+ from typing import Dict, Sequence
2
+
3
+ from xlsxwriter.workbook import Format, Workbook
4
+
5
+ from excelipy.const import PROP_MAP
6
+ from excelipy.models import Style
7
+
8
+ cached_styles: Dict[Style, Format] = {}
9
+
10
+
11
+ def _process_single(workbook: Workbook, style: Style) -> Format:
12
+ style_dict = style.model_dump(exclude_none=True)
13
+ style_map = {
14
+ mapped_prop: value
15
+ for property, value in style_dict.items()
16
+ if (mapped_prop := PROP_MAP.get(property)) is not None
17
+ }
18
+ format = workbook.add_format(style_map)
19
+ cached_styles[style] = format
20
+ return format
21
+
22
+
23
+ def process_style(
24
+ workbook: Workbook,
25
+ styles: Sequence[Style],
26
+ ) -> Format:
27
+ cur_style = Style()
28
+ for style in styles:
29
+ cur_style = cur_style.merge(style)
30
+ if cur_style in cached_styles:
31
+ return cached_styles[cur_style]
32
+ format = _process_single(workbook, cur_style)
33
+ return format
@@ -0,0 +1,9 @@
1
+ __all__ = [
2
+ "write_table",
3
+ "write_fill",
4
+ "write_text",
5
+ ]
6
+
7
+ from excelipy.writers.fill import write_fill
8
+ from excelipy.writers.table import write_table
9
+ from excelipy.writers.text import write_text
@@ -0,0 +1,34 @@
1
+ import logging
2
+ from typing import Tuple
3
+
4
+ from xlsxwriter.workbook import Workbook, Worksheet
5
+
6
+ from excelipy.models import Fill, Style
7
+ from excelipy.style import process_style
8
+
9
+ log = logging.getLogger("excelipy")
10
+
11
+
12
+ def write_fill(
13
+ workbook: Workbook,
14
+ worksheet: Worksheet,
15
+ component: Fill,
16
+ default_style: Style,
17
+ origin: Tuple[int, int] = (0, 0),
18
+ ) -> Tuple[int, int]:
19
+ log.debug(f"Writing fill at {origin}")
20
+ worksheet.merge_range(
21
+ origin[1],
22
+ origin[0],
23
+ origin[1] + component.height - 1,
24
+ origin[0] + component.width - 1,
25
+ "",
26
+ process_style(
27
+ workbook,
28
+ [
29
+ default_style,
30
+ component.style,
31
+ ],
32
+ ),
33
+ )
34
+ return component.width, component.height
@@ -0,0 +1,105 @@
1
+ import logging
2
+ from typing import Tuple
3
+
4
+ from xlsxwriter.workbook import Workbook, Worksheet
5
+
6
+ from excelipy.models import Style, Table
7
+ from excelipy.style import process_style
8
+
9
+ log = logging.getLogger("excelipy")
10
+
11
+ DEFAULT_FONT_SIZE = 11
12
+ SCALING_FACTOR = 1
13
+ BASE_PADDING = 2
14
+
15
+
16
+ def get_auto_width(
17
+ header: str,
18
+ component: Table,
19
+ default_style: Style,
20
+ ) -> int:
21
+ header_len = len(header)
22
+ col_len = component.data[header].apply(str).apply(len).max()
23
+ max_len = max(header_len, col_len)
24
+ max_font_size = max(
25
+ (
26
+ component.header_style.font_size
27
+ or default_style.font_size
28
+ or DEFAULT_FONT_SIZE
29
+ ),
30
+ (
31
+ component.column_style.get(header, Style()).font_size
32
+ or component.body_style.font_size
33
+ or default_style.font_size
34
+ or DEFAULT_FONT_SIZE
35
+ ),
36
+ (
37
+ max(
38
+ s.font_size
39
+ or component.body_style.font_size
40
+ or default_style.font_size
41
+ or DEFAULT_FONT_SIZE
42
+ for s in component.row_style.values()
43
+ )
44
+ ),
45
+ )
46
+ font_factor = max_font_size / DEFAULT_FONT_SIZE
47
+ return SCALING_FACTOR * font_factor * max_len + BASE_PADDING
48
+
49
+
50
+ def write_table(
51
+ workbook: Workbook,
52
+ worksheet: Worksheet,
53
+ component: Table,
54
+ default_style: Style,
55
+ origin: Tuple[int, int] = (0, 0),
56
+ ) -> Tuple[int, int]:
57
+ x_size = component.data.shape[1]
58
+ y_size = component.data.shape[0]
59
+
60
+ header_format = process_style(workbook, [default_style, component.header_style])
61
+ for col_idx, header in enumerate(component.data.columns):
62
+ worksheet.write(
63
+ origin[1],
64
+ origin[0] + col_idx,
65
+ header,
66
+ header_format,
67
+ )
68
+ set_width = component.column_width.get(header)
69
+ if set_width:
70
+ estimated_width = set_width
71
+ else:
72
+ estimated_width = get_auto_width(header, component, default_style)
73
+ worksheet.set_column(origin[1], origin[0] + col_idx, int(estimated_width))
74
+
75
+ if component.header_filters:
76
+ worksheet.autofilter(
77
+ origin[1],
78
+ origin[0],
79
+ origin[1],
80
+ origin[0] + len(list(component.data.columns)) - 1,
81
+ )
82
+
83
+ for col_idx, col in enumerate(component.data.columns):
84
+ col_style = component.column_style.get(col)
85
+ for row_idx, (_, row) in enumerate(component.data.iterrows()):
86
+ row_style = component.row_style.get(row_idx)
87
+ non_none = filter(
88
+ None,
89
+ [
90
+ default_style,
91
+ component.body_style,
92
+ col_style,
93
+ row_style,
94
+ ],
95
+ )
96
+ current_format = process_style(workbook, list(non_none))
97
+ cell = row[col]
98
+ worksheet.write(
99
+ origin[1] + row_idx + 1,
100
+ origin[0] + col_idx,
101
+ cell,
102
+ current_format,
103
+ )
104
+
105
+ return x_size, y_size
@@ -0,0 +1,41 @@
1
+ import logging
2
+ from typing import Tuple
3
+
4
+ from xlsxwriter.workbook import Workbook, Worksheet
5
+
6
+ from excelipy.models import Style, Text
7
+ from excelipy.style import process_style
8
+
9
+ log = logging.getLogger("excelipy")
10
+
11
+
12
+ def write_text(
13
+ workbook: Workbook,
14
+ worksheet: Worksheet,
15
+ component: Text,
16
+ default_style: Style,
17
+ origin: Tuple[int, int] = (0, 0),
18
+ ) -> Tuple[int, int]:
19
+ log.debug(f"Writing text at {origin}")
20
+
21
+ worksheet.merge_range(
22
+ origin[1],
23
+ origin[0],
24
+ origin[1] + component.height - 1,
25
+ origin[0] + component.width - 1,
26
+ "",
27
+ )
28
+
29
+ worksheet.write(
30
+ origin[1],
31
+ origin[0],
32
+ component.text,
33
+ process_style(
34
+ workbook,
35
+ [
36
+ default_style,
37
+ component.style,
38
+ ],
39
+ ),
40
+ )
41
+ return component.width, component.height
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: excelipy
3
+ Version: 0.1.0
4
+ Summary: Wrapper around xlsxwriter to improve usability
5
+ Requires-Python: >=3.9.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pandas>=2.2.3
8
+ Requires-Dist: pydantic>=2.11.3
9
+ Requires-Dist: xlsxwriter>=3.2.2
10
+
11
+ # Excelipy
12
+
13
+ [![codecov](https://codecov.io/gh/choinhet/excelipy/graph/badge.svg?token=${CODECOV_TOKEN})](https://codecov.io/gh/choinhet/excelipy)
@@ -0,0 +1,19 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ excelipy/__init__.py
5
+ excelipy/const.py
6
+ excelipy/main.py
7
+ excelipy/models.py
8
+ excelipy/service.py
9
+ excelipy/style.py
10
+ excelipy.egg-info/PKG-INFO
11
+ excelipy.egg-info/SOURCES.txt
12
+ excelipy.egg-info/dependency_links.txt
13
+ excelipy.egg-info/requires.txt
14
+ excelipy.egg-info/top_level.txt
15
+ excelipy/writers/__init__.py
16
+ excelipy/writers/fill.py
17
+ excelipy/writers/table.py
18
+ excelipy/writers/text.py
19
+ test/test_load.py
@@ -0,0 +1,3 @@
1
+ pandas>=2.2.3
2
+ pydantic>=2.11.3
3
+ xlsxwriter>=3.2.2
@@ -0,0 +1 @@
1
+ excelipy
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "excelipy"
3
+ version = "0.1.0"
4
+ description = "Wrapper around xlsxwriter to improve usability"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9.10"
7
+
8
+ dependencies = ["pandas>=2.2.3", "pydantic>=2.11.3", "xlsxwriter>=3.2.2"]
9
+
10
+ [tool.uv]
11
+ dev-dependencies = [
12
+ "pytest>=8.3.3",
13
+ "pytest-cov>=4.1.0",
14
+ "coverage>=7.6.1",
15
+ "pytest-mock>=3.14.0",
16
+ "pytest-asyncio>=0.25.3",
17
+ "debugpy>=1.8.13",
18
+ "pyright>=1.1.398",
19
+ ]
20
+
21
+ [tool.setuptools]
22
+ packages = ["excelipy"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ def test_load():
2
+ assert True