licos-dev-sdk 0.3.2__tar.gz → 0.3.3__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 (26) hide show
  1. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/PKG-INFO +1 -1
  2. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/pyproject.toml +1 -1
  3. licos_dev_sdk-0.3.3/src/licos_dev_sdk/chart.py +114 -0
  4. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_document_spreadsheet.py +28 -1
  5. licos_dev_sdk-0.3.2/src/licos_dev_sdk/chart.py +0 -80
  6. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/.gitignore +0 -0
  7. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/__init__.py +0 -0
  8. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/_utils.py +0 -0
  9. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/archive.py +0 -0
  10. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/data.py +0 -0
  11. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/diagram.py +0 -0
  12. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document.py +0 -0
  13. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document_common.py +0 -0
  14. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document_docx.py +0 -0
  15. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/document_docx_layout.py +0 -0
  16. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/image.py +0 -0
  17. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/model.py +0 -0
  18. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/observability.py +0 -0
  19. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/pdf_inspect.py +0 -0
  20. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/presentation.py +0 -0
  21. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/spreadsheet.py +0 -0
  22. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
  23. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/src/licos_dev_sdk/web.py +0 -0
  24. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_model.py +0 -0
  25. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_observability.py +0 -0
  26. {licos_dev_sdk-0.3.2 → licos_dev_sdk-0.3.3}/tests/test_output_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-dev-sdk
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: LICOS Dev SDK - file generation and model capability clients
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: beautifulsoup4>=4.12
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-sdk"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "LICOS Dev SDK - file generation and model capability clients"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -0,0 +1,114 @@
1
+ """Chart generation — bar, line, pie, scatter, heatmap via matplotlib."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+
7
+ import matplotlib
8
+ matplotlib.use("Agg") # Non-interactive backend
9
+ from matplotlib import font_manager
10
+ import matplotlib.pyplot as plt
11
+ import numpy as np
12
+
13
+ from ._utils import resolve_output_path
14
+
15
+
16
+ _CJK_FONT_CANDIDATES = (
17
+ "Noto Sans CJK SC",
18
+ "Noto Sans SC",
19
+ "Microsoft YaHei",
20
+ "SimHei",
21
+ "WenQuanYi Zen Hei",
22
+ "Source Han Sans CN",
23
+ )
24
+
25
+
26
+ @lru_cache(maxsize=1)
27
+ def _resolve_cjk_font() -> str:
28
+ installed_fonts = {font.name.casefold(): font.name for font in font_manager.fontManager.ttflist}
29
+ for candidate in _CJK_FONT_CANDIDATES:
30
+ installed_name = installed_fonts.get(candidate.casefold())
31
+ if installed_name:
32
+ return installed_name
33
+ choices = ", ".join(_CJK_FONT_CANDIDATES)
34
+ raise RuntimeError(
35
+ "Chinese chart font is unavailable. Install a CJK font before generating charts "
36
+ f"(supported families: {choices})."
37
+ )
38
+
39
+
40
+ def create_chart(chart_type: str, data: dict, filename: str, *,
41
+ output_dir: str | None = None, format: str = "png",
42
+ title: str = "", width: int = 800, height: int = 600) -> str:
43
+ """Generate a chart image (PNG/SVG). Returns absolute path.
44
+
45
+ Args:
46
+ chart_type: "bar" | "line" | "pie" | "scatter" | "heatmap"
47
+ data: {
48
+ "labels": ["A", "B", "C"],
49
+ "datasets": [{"label": "Series 1", "values": [10, 20, 30]}]
50
+ }
51
+ For pie: only first dataset is used.
52
+ For heatmap: data["matrix"] = [[1,2],[3,4]], data["x_labels"], data["y_labels"]
53
+ """
54
+ path = resolve_output_path(filename, f".{format}", output_dir)
55
+ dpi = 100
56
+ font_name = _resolve_cjk_font()
57
+ chart_style = {
58
+ "font.family": "sans-serif",
59
+ "font.sans-serif": [font_name],
60
+ "axes.unicode_minus": False,
61
+ }
62
+ with matplotlib.rc_context(chart_style):
63
+ fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi)
64
+
65
+ labels = data.get("labels", [])
66
+ datasets = data.get("datasets", [])
67
+
68
+ if chart_type == "bar":
69
+ x = np.arange(len(labels))
70
+ n = len(datasets)
71
+ bar_width = 0.8 / max(n, 1)
72
+ for i, ds in enumerate(datasets):
73
+ offset = (i - n / 2 + 0.5) * bar_width
74
+ ax.bar(x + offset, ds["values"], bar_width, label=ds.get("label", ""))
75
+ ax.set_xticks(x)
76
+ ax.set_xticklabels(labels)
77
+ if n > 1:
78
+ ax.legend()
79
+
80
+ elif chart_type == "line":
81
+ for ds in datasets:
82
+ ax.plot(labels, ds["values"], marker="o", label=ds.get("label", ""))
83
+ if len(datasets) > 1:
84
+ ax.legend()
85
+
86
+ elif chart_type == "pie":
87
+ values = datasets[0]["values"] if datasets else []
88
+ ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
89
+ ax.axis("equal")
90
+
91
+ elif chart_type == "scatter":
92
+ for ds in datasets:
93
+ x_vals = ds.get("x", list(range(len(ds["values"]))))
94
+ ax.scatter(x_vals, ds["values"], label=ds.get("label", ""), alpha=0.7)
95
+ if len(datasets) > 1:
96
+ ax.legend()
97
+
98
+ elif chart_type == "heatmap":
99
+ matrix = np.array(data.get("matrix", [[]]))
100
+ im = ax.imshow(matrix, cmap="YlOrRd", aspect="auto")
101
+ fig.colorbar(im, ax=ax)
102
+ if "x_labels" in data:
103
+ ax.set_xticks(range(len(data["x_labels"])))
104
+ ax.set_xticklabels(data["x_labels"])
105
+ if "y_labels" in data:
106
+ ax.set_yticks(range(len(data["y_labels"])))
107
+ ax.set_yticklabels(data["y_labels"])
108
+
109
+ if title:
110
+ ax.set_title(title)
111
+ fig.tight_layout()
112
+ fig.savefig(str(path), format=format, bbox_inches="tight")
113
+ plt.close(fig)
114
+ return str(path.resolve())
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import sys
4
4
  import tempfile
5
5
  import unittest
6
+ import warnings
6
7
  from pathlib import Path
7
8
 
8
9
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
@@ -11,10 +12,36 @@ from docx import Document
11
12
  from openpyxl import load_workbook
12
13
  from PIL import Image
13
14
 
14
- from licos_dev_sdk import create_docx, create_docx_from_template, create_xlsx_workbook
15
+ from licos_dev_sdk import create_chart, create_docx, create_docx_from_template, create_xlsx_workbook
15
16
 
16
17
 
17
18
  class DocumentSpreadsheetTests(unittest.TestCase):
19
+ def test_create_chart_renders_chinese_text_without_missing_glyphs(self) -> None:
20
+ with tempfile.TemporaryDirectory() as tmp, warnings.catch_warnings(record=True) as caught:
21
+ warnings.simplefilter("always")
22
+ output = create_chart(
23
+ "bar",
24
+ {
25
+ "labels": ["华东地区", "华中地区", "华南地区"],
26
+ "datasets": [
27
+ {"label": "预计收入(万元)", "values": [4800, 3200, 2600]},
28
+ {"label": "项目ROI(%)", "values": [18, 23, 16]},
29
+ ],
30
+ },
31
+ "chinese-chart",
32
+ output_dir=tmp,
33
+ title="重点行业客户投资回报分析",
34
+ )
35
+
36
+ self.assertTrue(Path(output).is_file())
37
+ self.assertGreater(Path(output).stat().st_size, 10_000)
38
+ missing_glyph_warnings = [
39
+ str(item.message)
40
+ for item in caught
41
+ if "Glyph" in str(item.message) and "missing from font" in str(item.message)
42
+ ]
43
+ self.assertEqual(missing_glyph_warnings, [])
44
+
18
45
  def test_create_docx_resolves_assets_from_explicit_base_path(self) -> None:
19
46
  with tempfile.TemporaryDirectory() as tmp:
20
47
  tmp_path = Path(tmp)
@@ -1,80 +0,0 @@
1
- """Chart generation — bar, line, pie, scatter, heatmap via matplotlib."""
2
-
3
- from __future__ import annotations
4
-
5
- import matplotlib
6
- matplotlib.use("Agg") # Non-interactive backend
7
- import matplotlib.pyplot as plt
8
- import numpy as np
9
-
10
- from ._utils import resolve_output_path
11
-
12
-
13
- def create_chart(chart_type: str, data: dict, filename: str, *,
14
- output_dir: str | None = None, format: str = "png",
15
- title: str = "", width: int = 800, height: int = 600) -> str:
16
- """Generate a chart image (PNG/SVG). Returns absolute path.
17
-
18
- Args:
19
- chart_type: "bar" | "line" | "pie" | "scatter" | "heatmap"
20
- data: {
21
- "labels": ["A", "B", "C"],
22
- "datasets": [{"label": "Series 1", "values": [10, 20, 30]}]
23
- }
24
- For pie: only first dataset is used.
25
- For heatmap: data["matrix"] = [[1,2],[3,4]], data["x_labels"], data["y_labels"]
26
- """
27
- path = resolve_output_path(filename, f".{format}", output_dir)
28
- dpi = 100
29
- fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi)
30
-
31
- labels = data.get("labels", [])
32
- datasets = data.get("datasets", [])
33
-
34
- if chart_type == "bar":
35
- x = np.arange(len(labels))
36
- n = len(datasets)
37
- bar_width = 0.8 / max(n, 1)
38
- for i, ds in enumerate(datasets):
39
- offset = (i - n / 2 + 0.5) * bar_width
40
- ax.bar(x + offset, ds["values"], bar_width, label=ds.get("label", ""))
41
- ax.set_xticks(x)
42
- ax.set_xticklabels(labels)
43
- if n > 1:
44
- ax.legend()
45
-
46
- elif chart_type == "line":
47
- for ds in datasets:
48
- ax.plot(labels, ds["values"], marker="o", label=ds.get("label", ""))
49
- if len(datasets) > 1:
50
- ax.legend()
51
-
52
- elif chart_type == "pie":
53
- values = datasets[0]["values"] if datasets else []
54
- ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90)
55
- ax.axis("equal")
56
-
57
- elif chart_type == "scatter":
58
- for ds in datasets:
59
- x_vals = ds.get("x", list(range(len(ds["values"]))))
60
- ax.scatter(x_vals, ds["values"], label=ds.get("label", ""), alpha=0.7)
61
- if len(datasets) > 1:
62
- ax.legend()
63
-
64
- elif chart_type == "heatmap":
65
- matrix = np.array(data.get("matrix", [[]]))
66
- im = ax.imshow(matrix, cmap="YlOrRd", aspect="auto")
67
- fig.colorbar(im, ax=ax)
68
- if "x_labels" in data:
69
- ax.set_xticks(range(len(data["x_labels"])))
70
- ax.set_xticklabels(data["x_labels"])
71
- if "y_labels" in data:
72
- ax.set_yticks(range(len(data["y_labels"])))
73
- ax.set_yticklabels(data["y_labels"])
74
-
75
- if title:
76
- ax.set_title(title)
77
- fig.tight_layout()
78
- fig.savefig(str(path), format=format, bbox_inches="tight")
79
- plt.close(fig)
80
- return str(path.resolve())
File without changes