licos-dev-sdk 0.1.0__py3-none-any.whl
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.
- licos_dev_sdk/__init__.py +57 -0
- licos_dev_sdk/_utils.py +34 -0
- licos_dev_sdk/archive.py +34 -0
- licos_dev_sdk/chart.py +80 -0
- licos_dev_sdk/data.py +56 -0
- licos_dev_sdk/diagram.py +22 -0
- licos_dev_sdk/document.py +98 -0
- licos_dev_sdk/image.py +60 -0
- licos_dev_sdk/presentation.py +103 -0
- licos_dev_sdk/spreadsheet.py +75 -0
- licos_dev_sdk/web.py +61 -0
- licos_dev_sdk-0.1.0.dist-info/METADATA +17 -0
- licos_dev_sdk-0.1.0.dist-info/RECORD +14 -0
- licos_dev_sdk-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""LICOS Dev SDK — file generation toolkit.
|
|
2
|
+
|
|
3
|
+
Modules with heavy native dependencies (weasyprint, graphviz, matplotlib)
|
|
4
|
+
are lazy-imported to avoid errors in minimal environments (e.g., node-base).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name: str):
|
|
9
|
+
"""Lazy import — only load modules when accessed."""
|
|
10
|
+
_map = {
|
|
11
|
+
# data
|
|
12
|
+
"create_json": ("data", "create_json"),
|
|
13
|
+
"create_xml": ("data", "create_xml"),
|
|
14
|
+
"create_yaml": ("data", "create_yaml"),
|
|
15
|
+
# archive
|
|
16
|
+
"create_zip": ("archive", "create_zip"),
|
|
17
|
+
"create_tar_gz": ("archive", "create_tar_gz"),
|
|
18
|
+
# image
|
|
19
|
+
"create_qrcode": ("image", "create_qrcode"),
|
|
20
|
+
"create_barcode": ("image", "create_barcode"),
|
|
21
|
+
"create_watermark": ("image", "create_watermark"),
|
|
22
|
+
# web
|
|
23
|
+
"create_html": ("web", "create_html"),
|
|
24
|
+
"create_markdown": ("web", "create_markdown"),
|
|
25
|
+
"markdown_to_html": ("web", "markdown_to_html"),
|
|
26
|
+
# document
|
|
27
|
+
"create_pdf": ("document", "create_pdf"),
|
|
28
|
+
"create_docx": ("document", "create_docx"),
|
|
29
|
+
# spreadsheet
|
|
30
|
+
"create_xlsx": ("spreadsheet", "create_xlsx"),
|
|
31
|
+
"create_csv": ("spreadsheet", "create_csv"),
|
|
32
|
+
# chart
|
|
33
|
+
"create_chart": ("chart", "create_chart"),
|
|
34
|
+
# diagram
|
|
35
|
+
"create_diagram": ("diagram", "create_diagram"),
|
|
36
|
+
# presentation
|
|
37
|
+
"create_pptx": ("presentation", "create_pptx"),
|
|
38
|
+
}
|
|
39
|
+
if name in _map:
|
|
40
|
+
mod_name, attr = _map[name]
|
|
41
|
+
import importlib
|
|
42
|
+
mod = importlib.import_module(f".{mod_name}", __name__)
|
|
43
|
+
return getattr(mod, attr)
|
|
44
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"create_json", "create_xml", "create_yaml",
|
|
49
|
+
"create_zip", "create_tar_gz",
|
|
50
|
+
"create_qrcode", "create_barcode", "create_watermark",
|
|
51
|
+
"create_html", "create_markdown", "markdown_to_html",
|
|
52
|
+
"create_pdf", "create_docx",
|
|
53
|
+
"create_xlsx", "create_csv",
|
|
54
|
+
"create_chart",
|
|
55
|
+
"create_diagram",
|
|
56
|
+
"create_pptx",
|
|
57
|
+
]
|
licos_dev_sdk/_utils.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared utilities for output path resolution and file naming."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_output_dir(output_dir: str | None = None) -> Path:
|
|
11
|
+
"""Resolve the output directory.
|
|
12
|
+
|
|
13
|
+
Priority: explicit arg > $LICOS_WORKSPACE_PATH > cwd
|
|
14
|
+
"""
|
|
15
|
+
if output_dir:
|
|
16
|
+
p = Path(output_dir)
|
|
17
|
+
else:
|
|
18
|
+
env = os.environ.get("LICOS_WORKSPACE_PATH", "")
|
|
19
|
+
p = Path(env) if env else Path.cwd()
|
|
20
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
return p
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_output_path(filename: str, ext: str, output_dir: str | None = None) -> Path:
|
|
25
|
+
"""Build the full output path, auto-adding extension and deduplicating."""
|
|
26
|
+
d = resolve_output_dir(output_dir)
|
|
27
|
+
# Ensure ext starts with dot
|
|
28
|
+
if not ext.startswith("."):
|
|
29
|
+
ext = f".{ext}"
|
|
30
|
+
path = d / f"{filename}{ext}"
|
|
31
|
+
if path.exists():
|
|
32
|
+
ts = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
33
|
+
path = d / f"{filename}_{ts}{ext}"
|
|
34
|
+
return path
|
licos_dev_sdk/archive.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Archive generation — ZIP, TAR.GZ."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tarfile
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ._utils import resolve_output_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_zip(source_paths: list[str], filename: str, *, output_dir: str | None = None) -> str:
|
|
13
|
+
"""Create a ZIP archive from a list of files/directories. Returns absolute path."""
|
|
14
|
+
path = resolve_output_path(filename, ".zip", output_dir)
|
|
15
|
+
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
16
|
+
for src in source_paths:
|
|
17
|
+
p = Path(src)
|
|
18
|
+
if p.is_dir():
|
|
19
|
+
for child in p.rglob("*"):
|
|
20
|
+
if child.is_file():
|
|
21
|
+
zf.write(child, child.relative_to(p.parent))
|
|
22
|
+
elif p.is_file():
|
|
23
|
+
zf.write(p, p.name)
|
|
24
|
+
return str(path.resolve())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_tar_gz(source_paths: list[str], filename: str, *, output_dir: str | None = None) -> str:
|
|
28
|
+
"""Create a TAR.GZ archive from a list of files/directories. Returns absolute path."""
|
|
29
|
+
path = resolve_output_path(filename, ".tar.gz", output_dir)
|
|
30
|
+
with tarfile.open(path, "w:gz") as tf:
|
|
31
|
+
for src in source_paths:
|
|
32
|
+
p = Path(src)
|
|
33
|
+
tf.add(p, arcname=p.name)
|
|
34
|
+
return str(path.resolve())
|
licos_dev_sdk/chart.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
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())
|
licos_dev_sdk/data.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Data file generation — JSON, XML, YAML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import csv as csv_mod
|
|
7
|
+
import io
|
|
8
|
+
from typing import Any
|
|
9
|
+
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
10
|
+
from xml.dom.minidom import parseString
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from ._utils import resolve_output_path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_json(data: Any, filename: str, *, output_dir: str | None = None, indent: int = 2) -> str:
|
|
18
|
+
"""Write data as a formatted JSON file. Returns absolute path."""
|
|
19
|
+
path = resolve_output_path(filename, ".json", output_dir)
|
|
20
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=indent), encoding="utf-8")
|
|
21
|
+
return str(path.resolve())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_yaml(data: Any, filename: str, *, output_dir: str | None = None) -> str:
|
|
25
|
+
"""Write data as a YAML file. Returns absolute path."""
|
|
26
|
+
path = resolve_output_path(filename, ".yaml", output_dir)
|
|
27
|
+
path.write_text(yaml.dump(data, allow_unicode=True, default_flow_style=False, sort_keys=False), encoding="utf-8")
|
|
28
|
+
return str(path.resolve())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_xml(data: dict, filename: str, *, output_dir: str | None = None, root_tag: str = "root") -> str:
|
|
32
|
+
"""Write a dict as an XML file. Returns absolute path."""
|
|
33
|
+
path = resolve_output_path(filename, ".xml", output_dir)
|
|
34
|
+
|
|
35
|
+
def _dict_to_xml(parent: Element, d: dict | list | Any) -> None:
|
|
36
|
+
if isinstance(d, dict):
|
|
37
|
+
for key, val in d.items():
|
|
38
|
+
child = SubElement(parent, str(key))
|
|
39
|
+
_dict_to_xml(child, val)
|
|
40
|
+
elif isinstance(d, list):
|
|
41
|
+
for item in d:
|
|
42
|
+
child = SubElement(parent, "item")
|
|
43
|
+
_dict_to_xml(child, item)
|
|
44
|
+
else:
|
|
45
|
+
parent.text = str(d)
|
|
46
|
+
|
|
47
|
+
root = Element(root_tag)
|
|
48
|
+
_dict_to_xml(root, data)
|
|
49
|
+
raw = tostring(root, encoding="unicode")
|
|
50
|
+
pretty = parseString(raw).toprettyxml(indent=" ")
|
|
51
|
+
# Remove extra xml declaration line
|
|
52
|
+
lines = pretty.split("\n")
|
|
53
|
+
if lines and lines[0].startswith("<?xml"):
|
|
54
|
+
lines[0] = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
55
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
56
|
+
return str(path.resolve())
|
licos_dev_sdk/diagram.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Diagram generation — Graphviz DOT → PNG/SVG."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import graphviz
|
|
6
|
+
|
|
7
|
+
from ._utils import resolve_output_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_diagram(dot_source: str, filename: str, *,
|
|
11
|
+
output_dir: str | None = None, format: str = "png") -> str:
|
|
12
|
+
"""Render a Graphviz DOT source to an image. Returns absolute path.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
dot_source: DOT language string (e.g., 'digraph { A -> B }')
|
|
16
|
+
format: "png" | "svg" | "pdf"
|
|
17
|
+
"""
|
|
18
|
+
path = resolve_output_path(filename, f".{format}", output_dir)
|
|
19
|
+
src = graphviz.Source(dot_source)
|
|
20
|
+
# graphviz renders to {filename}.{format} — render to parent dir with stem
|
|
21
|
+
rendered = src.render(filename=str(path.with_suffix("")), format=format, cleanup=True)
|
|
22
|
+
return str(path.resolve())
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Document generation — PDF, DOCX."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mistune
|
|
6
|
+
from weasyprint import HTML
|
|
7
|
+
from docx import Document
|
|
8
|
+
from docx.shared import Pt, Inches
|
|
9
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
10
|
+
|
|
11
|
+
from ._utils import resolve_output_path
|
|
12
|
+
|
|
13
|
+
_PDF_CSS = """
|
|
14
|
+
@page { size: {page_size}; margin: 2cm; }
|
|
15
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans CJK SC", sans-serif;
|
|
16
|
+
font-size: 11pt; line-height: 1.6; color: #333; }
|
|
17
|
+
h1 { font-size: 22pt; margin-top: 0; }
|
|
18
|
+
h2 { font-size: 16pt; }
|
|
19
|
+
h3 { font-size: 13pt; }
|
|
20
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
21
|
+
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; }
|
|
22
|
+
th { background: #f0f0f0; font-weight: 600; }
|
|
23
|
+
code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px; font-size: 10pt; }
|
|
24
|
+
pre { background: #f4f4f4; padding: 12px; border-radius: 4px; overflow-x: auto; }
|
|
25
|
+
blockquote { border-left: 3px solid #ccc; margin-left: 0; padding-left: 1em; color: #666; }
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_pdf(content: str, filename: str, *, content_type: str = "markdown",
|
|
30
|
+
output_dir: str | None = None, page_size: str = "A4") -> str:
|
|
31
|
+
"""Generate a PDF file from Markdown or HTML. Returns absolute path."""
|
|
32
|
+
path = resolve_output_path(filename, ".pdf", output_dir)
|
|
33
|
+
html_body = mistune.html(content) if content_type == "markdown" else content
|
|
34
|
+
css = _PDF_CSS.replace("{page_size}", page_size)
|
|
35
|
+
full_html = f"<html><head><style>{css}</style></head><body>{html_body}</body></html>"
|
|
36
|
+
HTML(string=full_html).write_pdf(str(path))
|
|
37
|
+
return str(path.resolve())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_docx(content: str, filename: str, *, content_type: str = "markdown",
|
|
41
|
+
output_dir: str | None = None, font_name: str = "Arial", font_size: int = 11) -> str:
|
|
42
|
+
"""Generate a DOCX file from Markdown or HTML. Returns absolute path.
|
|
43
|
+
|
|
44
|
+
Converts Markdown to a simple DOCX with headings, paragraphs, and lists.
|
|
45
|
+
For complex HTML, consider generating PDF instead.
|
|
46
|
+
"""
|
|
47
|
+
path = resolve_output_path(filename, ".docx", output_dir)
|
|
48
|
+
|
|
49
|
+
if content_type == "html":
|
|
50
|
+
# For HTML input, convert to markdown-like text first
|
|
51
|
+
import re
|
|
52
|
+
text = re.sub(r"<[^>]+>", "", content)
|
|
53
|
+
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
|
54
|
+
else:
|
|
55
|
+
lines = content.split("\n")
|
|
56
|
+
|
|
57
|
+
doc = Document()
|
|
58
|
+
# Set default font
|
|
59
|
+
style = doc.styles["Normal"]
|
|
60
|
+
style.font.name = font_name
|
|
61
|
+
style.font.size = Pt(font_size)
|
|
62
|
+
|
|
63
|
+
for line in lines:
|
|
64
|
+
stripped = line.strip()
|
|
65
|
+
if not stripped:
|
|
66
|
+
continue
|
|
67
|
+
# Headings
|
|
68
|
+
if stripped.startswith("######"):
|
|
69
|
+
doc.add_heading(stripped.lstrip("#").strip(), level=6)
|
|
70
|
+
elif stripped.startswith("#####"):
|
|
71
|
+
doc.add_heading(stripped.lstrip("#").strip(), level=5)
|
|
72
|
+
elif stripped.startswith("####"):
|
|
73
|
+
doc.add_heading(stripped.lstrip("#").strip(), level=4)
|
|
74
|
+
elif stripped.startswith("###"):
|
|
75
|
+
doc.add_heading(stripped.lstrip("#").strip(), level=3)
|
|
76
|
+
elif stripped.startswith("##"):
|
|
77
|
+
doc.add_heading(stripped.lstrip("#").strip(), level=2)
|
|
78
|
+
elif stripped.startswith("#"):
|
|
79
|
+
doc.add_heading(stripped.lstrip("#").strip(), level=1)
|
|
80
|
+
# Unordered list
|
|
81
|
+
elif stripped.startswith("- ") or stripped.startswith("* "):
|
|
82
|
+
doc.add_paragraph(stripped[2:], style="List Bullet")
|
|
83
|
+
# Ordered list
|
|
84
|
+
elif len(stripped) > 2 and stripped[0].isdigit() and stripped[1] in (".", ")"):
|
|
85
|
+
doc.add_paragraph(stripped[2:].strip(), style="List Number")
|
|
86
|
+
# Blockquote
|
|
87
|
+
elif stripped.startswith("> "):
|
|
88
|
+
p = doc.add_paragraph(stripped[2:])
|
|
89
|
+
p.paragraph_format.left_indent = Inches(0.5)
|
|
90
|
+
p.style.font.italic = True
|
|
91
|
+
# Horizontal rule
|
|
92
|
+
elif stripped in ("---", "***", "___"):
|
|
93
|
+
doc.add_paragraph("─" * 50)
|
|
94
|
+
else:
|
|
95
|
+
doc.add_paragraph(stripped)
|
|
96
|
+
|
|
97
|
+
doc.save(str(path))
|
|
98
|
+
return str(path.resolve())
|
licos_dev_sdk/image.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Image generation — QR code, barcode, watermark."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import qrcode
|
|
8
|
+
import barcode as barcode_lib
|
|
9
|
+
from barcode.writer import ImageWriter
|
|
10
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
11
|
+
|
|
12
|
+
from ._utils import resolve_output_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_qrcode(data: str, filename: str, *, output_dir: str | None = None, size: int = 300) -> str:
|
|
16
|
+
"""Generate a QR code PNG image. Returns absolute path."""
|
|
17
|
+
path = resolve_output_path(filename, ".png", output_dir)
|
|
18
|
+
qr = qrcode.QRCode(box_size=10, border=2)
|
|
19
|
+
qr.add_data(data)
|
|
20
|
+
qr.make(fit=True)
|
|
21
|
+
img = qr.make_image(fill_color="black", back_color="white")
|
|
22
|
+
img = img.resize((size, size))
|
|
23
|
+
img.save(str(path))
|
|
24
|
+
return str(path.resolve())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_barcode(data: str, filename: str, *, output_dir: str | None = None, barcode_type: str = "code128") -> str:
|
|
28
|
+
"""Generate a barcode PNG image. Returns absolute path."""
|
|
29
|
+
path = resolve_output_path(filename, ".png", output_dir)
|
|
30
|
+
bc_class = barcode_lib.get_barcode_class(barcode_type)
|
|
31
|
+
bc = bc_class(data, writer=ImageWriter())
|
|
32
|
+
# python-barcode appends extension, save without it
|
|
33
|
+
saved = bc.save(str(path.with_suffix("")))
|
|
34
|
+
return str(Path(saved).resolve())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_watermark(text: str, filename: str, *, output_dir: str | None = None,
|
|
38
|
+
width: int = 800, height: int = 600, opacity: float = 0.3) -> str:
|
|
39
|
+
"""Generate a semi-transparent watermark PNG image. Returns absolute path."""
|
|
40
|
+
path = resolve_output_path(filename, ".png", output_dir)
|
|
41
|
+
img = Image.new("RGBA", (width, height), (255, 255, 255, 0))
|
|
42
|
+
draw = ImageDraw.Draw(img)
|
|
43
|
+
|
|
44
|
+
# Use default font, scale size to image
|
|
45
|
+
font_size = max(width, height) // 10
|
|
46
|
+
try:
|
|
47
|
+
font = ImageFont.truetype("arial.ttf", font_size)
|
|
48
|
+
except (OSError, IOError):
|
|
49
|
+
font = ImageFont.load_default()
|
|
50
|
+
|
|
51
|
+
alpha = int(255 * opacity)
|
|
52
|
+
fill = (128, 128, 128, alpha)
|
|
53
|
+
|
|
54
|
+
# Tile the watermark text diagonally
|
|
55
|
+
for y in range(-height, height * 2, font_size * 3):
|
|
56
|
+
for x in range(-width, width * 2, font_size * len(text)):
|
|
57
|
+
draw.text((x, y), text, fill=fill, font=font)
|
|
58
|
+
|
|
59
|
+
img.save(str(path), "PNG")
|
|
60
|
+
return str(path.resolve())
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Presentation generation — PPTX from Markdown."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pptx import Presentation
|
|
6
|
+
from pptx.util import Inches, Pt, Emu
|
|
7
|
+
from pptx.dml.color import RGBColor
|
|
8
|
+
from pptx.enum.text import PP_ALIGN
|
|
9
|
+
|
|
10
|
+
from ._utils import resolve_output_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_pptx(content: str, filename: str, *, content_type: str = "markdown",
|
|
14
|
+
output_dir: str | None = None) -> str:
|
|
15
|
+
"""Generate a PPTX file from Markdown. Returns absolute path.
|
|
16
|
+
|
|
17
|
+
Slides are separated by `---` in Markdown.
|
|
18
|
+
First `#` heading becomes the slide title; remaining content becomes bullet points.
|
|
19
|
+
"""
|
|
20
|
+
path = resolve_output_path(filename, ".pptx", output_dir)
|
|
21
|
+
prs = Presentation()
|
|
22
|
+
prs.slide_width = Inches(13.333)
|
|
23
|
+
prs.slide_height = Inches(7.5)
|
|
24
|
+
|
|
25
|
+
if content_type == "html":
|
|
26
|
+
# Strip tags for simple conversion
|
|
27
|
+
import re
|
|
28
|
+
content = re.sub(r"<[^>]+>", "", content)
|
|
29
|
+
|
|
30
|
+
slides_text = content.split("\n---\n") if "\n---\n" in content else content.split("\n---")
|
|
31
|
+
|
|
32
|
+
for slide_md in slides_text:
|
|
33
|
+
lines = [l for l in slide_md.strip().split("\n") if l.strip()]
|
|
34
|
+
if not lines:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
|
38
|
+
|
|
39
|
+
# Extract title (first # heading)
|
|
40
|
+
title_text = ""
|
|
41
|
+
body_lines = []
|
|
42
|
+
for line in lines:
|
|
43
|
+
stripped = line.strip()
|
|
44
|
+
if not title_text and stripped.startswith("#"):
|
|
45
|
+
title_text = stripped.lstrip("#").strip()
|
|
46
|
+
else:
|
|
47
|
+
body_lines.append(stripped)
|
|
48
|
+
|
|
49
|
+
# Add title
|
|
50
|
+
if title_text:
|
|
51
|
+
left = Inches(0.8)
|
|
52
|
+
top = Inches(0.5)
|
|
53
|
+
width = Inches(11.7)
|
|
54
|
+
height = Inches(1.2)
|
|
55
|
+
txBox = slide.shapes.add_textbox(left, top, width, height)
|
|
56
|
+
tf = txBox.text_frame
|
|
57
|
+
tf.word_wrap = True
|
|
58
|
+
p = tf.paragraphs[0]
|
|
59
|
+
p.text = title_text
|
|
60
|
+
p.font.size = Pt(36)
|
|
61
|
+
p.font.bold = True
|
|
62
|
+
p.font.color.rgb = RGBColor(0x1C, 0x28, 0x33)
|
|
63
|
+
|
|
64
|
+
# Add body content
|
|
65
|
+
if body_lines:
|
|
66
|
+
left = Inches(0.8)
|
|
67
|
+
top = Inches(2.0)
|
|
68
|
+
width = Inches(11.7)
|
|
69
|
+
height = Inches(4.5)
|
|
70
|
+
txBox = slide.shapes.add_textbox(left, top, width, height)
|
|
71
|
+
tf = txBox.text_frame
|
|
72
|
+
tf.word_wrap = True
|
|
73
|
+
|
|
74
|
+
for i, line in enumerate(body_lines):
|
|
75
|
+
if i == 0:
|
|
76
|
+
p = tf.paragraphs[0]
|
|
77
|
+
else:
|
|
78
|
+
p = tf.add_paragraph()
|
|
79
|
+
|
|
80
|
+
# Bullet points
|
|
81
|
+
if line.startswith("- ") or line.startswith("* "):
|
|
82
|
+
p.text = line[2:]
|
|
83
|
+
p.level = 0
|
|
84
|
+
p.space_before = Pt(6)
|
|
85
|
+
elif line.startswith(" - ") or line.startswith(" * "):
|
|
86
|
+
p.text = line[4:]
|
|
87
|
+
p.level = 1
|
|
88
|
+
p.space_before = Pt(4)
|
|
89
|
+
# Sub-headings
|
|
90
|
+
elif line.startswith("##"):
|
|
91
|
+
p.text = line.lstrip("#").strip()
|
|
92
|
+
p.font.size = Pt(24)
|
|
93
|
+
p.font.bold = True
|
|
94
|
+
p.space_before = Pt(12)
|
|
95
|
+
continue
|
|
96
|
+
else:
|
|
97
|
+
p.text = line
|
|
98
|
+
|
|
99
|
+
p.font.size = Pt(18)
|
|
100
|
+
p.font.color.rgb = RGBColor(0x33, 0x33, 0x33)
|
|
101
|
+
|
|
102
|
+
prs.save(str(path))
|
|
103
|
+
return str(path.resolve())
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Spreadsheet generation — XLSX, CSV."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from openpyxl import Workbook
|
|
10
|
+
from openpyxl.styles import Font, PatternFill, Alignment
|
|
11
|
+
|
|
12
|
+
from ._utils import resolve_output_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_xlsx(data: list[dict] | list[list], filename: str, *,
|
|
16
|
+
output_dir: str | None = None, sheet_name: str = "Sheet1",
|
|
17
|
+
header_color: str = "4472C4") -> str:
|
|
18
|
+
"""Generate an XLSX file from data. Returns absolute path.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: List of dicts (keys as headers) or 2D list (first row as headers).
|
|
22
|
+
"""
|
|
23
|
+
path = resolve_output_path(filename, ".xlsx", output_dir)
|
|
24
|
+
wb = Workbook()
|
|
25
|
+
ws = wb.active
|
|
26
|
+
ws.title = sheet_name
|
|
27
|
+
|
|
28
|
+
header_fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
|
|
29
|
+
header_font = Font(bold=True, color="FFFFFF")
|
|
30
|
+
|
|
31
|
+
if data and isinstance(data[0], dict):
|
|
32
|
+
headers = list(data[0].keys())
|
|
33
|
+
ws.append(headers)
|
|
34
|
+
for row in data:
|
|
35
|
+
ws.append([row.get(h) for h in headers])
|
|
36
|
+
elif data and isinstance(data[0], list):
|
|
37
|
+
for row in data:
|
|
38
|
+
ws.append(row)
|
|
39
|
+
else:
|
|
40
|
+
wb.save(str(path))
|
|
41
|
+
return str(path.resolve())
|
|
42
|
+
|
|
43
|
+
# Style header row
|
|
44
|
+
for cell in ws[1]:
|
|
45
|
+
cell.fill = header_fill
|
|
46
|
+
cell.font = header_font
|
|
47
|
+
cell.alignment = Alignment(horizontal="center")
|
|
48
|
+
|
|
49
|
+
# Auto-width columns
|
|
50
|
+
for col in ws.columns:
|
|
51
|
+
max_len = 0
|
|
52
|
+
col_letter = col[0].column_letter
|
|
53
|
+
for cell in col:
|
|
54
|
+
val = str(cell.value) if cell.value is not None else ""
|
|
55
|
+
max_len = max(max_len, len(val))
|
|
56
|
+
ws.column_dimensions[col_letter].width = min(max_len + 4, 50)
|
|
57
|
+
|
|
58
|
+
wb.save(str(path))
|
|
59
|
+
return str(path.resolve())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_csv(data: list[dict] | list[list], filename: str, *, output_dir: str | None = None) -> str:
|
|
63
|
+
"""Generate a CSV file from data. Returns absolute path."""
|
|
64
|
+
path = resolve_output_path(filename, ".csv", output_dir)
|
|
65
|
+
|
|
66
|
+
with open(path, "w", newline="", encoding="utf-8-sig") as f:
|
|
67
|
+
if data and isinstance(data[0], dict):
|
|
68
|
+
writer = csv.DictWriter(f, fieldnames=data[0].keys())
|
|
69
|
+
writer.writeheader()
|
|
70
|
+
writer.writerows(data)
|
|
71
|
+
elif data and isinstance(data[0], list):
|
|
72
|
+
writer = csv.writer(f)
|
|
73
|
+
writer.writerows(data)
|
|
74
|
+
|
|
75
|
+
return str(path.resolve())
|
licos_dev_sdk/web.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Web file generation — HTML, Markdown."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mistune
|
|
6
|
+
from jinja2 import Template
|
|
7
|
+
|
|
8
|
+
from ._utils import resolve_output_path
|
|
9
|
+
|
|
10
|
+
_DEFAULT_HTML_TEMPLATE = """\
|
|
11
|
+
<!DOCTYPE html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
+
<title>{{ title }}</title>
|
|
17
|
+
<style>
|
|
18
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
19
|
+
max-width: 800px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #333; }
|
|
20
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
21
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
22
|
+
th { background-color: #f4f4f4; }
|
|
23
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
|
|
24
|
+
pre { background: #f4f4f4; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
|
25
|
+
blockquote { border-left: 4px solid #ddd; margin-left: 0; padding-left: 1rem; color: #666; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
{{ body }}
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def markdown_to_html(markdown_content: str) -> str:
|
|
36
|
+
"""Convert Markdown to HTML fragment."""
|
|
37
|
+
return mistune.html(markdown_content)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_html(content: str, filename: str, *, output_dir: str | None = None,
|
|
41
|
+
template: str | None = None, content_type: str = "markdown") -> str:
|
|
42
|
+
"""Generate an HTML file. Returns absolute path.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
content: Markdown or raw HTML content.
|
|
46
|
+
content_type: "markdown" (default) or "html".
|
|
47
|
+
template: Custom Jinja2 template string. Must contain {{ body }}.
|
|
48
|
+
"""
|
|
49
|
+
path = resolve_output_path(filename, ".html", output_dir)
|
|
50
|
+
body = markdown_to_html(content) if content_type == "markdown" else content
|
|
51
|
+
tmpl = Template(template or _DEFAULT_HTML_TEMPLATE)
|
|
52
|
+
html = tmpl.render(title=filename, body=body)
|
|
53
|
+
path.write_text(html, encoding="utf-8")
|
|
54
|
+
return str(path.resolve())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_markdown(content: str, filename: str, *, output_dir: str | None = None) -> str:
|
|
58
|
+
"""Write a Markdown file. Returns absolute path."""
|
|
59
|
+
path = resolve_output_path(filename, ".md", output_dir)
|
|
60
|
+
path.write_text(content, encoding="utf-8")
|
|
61
|
+
return str(path.resolve())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: licos-dev-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LICOS file generation SDK — PDF, DOCX, XLSX, PPTX, charts, diagrams, QR codes, and more
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: graphviz>=0.20
|
|
7
|
+
Requires-Dist: jinja2>=3.1
|
|
8
|
+
Requires-Dist: matplotlib>=3.9
|
|
9
|
+
Requires-Dist: mistune>=3.0
|
|
10
|
+
Requires-Dist: openpyxl>=3.1
|
|
11
|
+
Requires-Dist: pillow>=10.0
|
|
12
|
+
Requires-Dist: python-barcode>=0.15
|
|
13
|
+
Requires-Dist: python-docx>=1.1
|
|
14
|
+
Requires-Dist: python-pptx>=1.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: qrcode[pil]>=8.0
|
|
17
|
+
Requires-Dist: weasyprint>=62.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
licos_dev_sdk/__init__.py,sha256=nHEiiYPMRKT_sIZRT4eQ6VhFou2ItmByoXV1_Ay7fAA,2015
|
|
2
|
+
licos_dev_sdk/_utils.py,sha256=Vrx3op7BK0LXBubv9Cx7Dxl6XcF2xq9Wp1KOA514SlI,1017
|
|
3
|
+
licos_dev_sdk/archive.py,sha256=xn3E30jD5tOjougRd8glyTmV2--__rMliLi20q-gDX0,1251
|
|
4
|
+
licos_dev_sdk/chart.py,sha256=Dd6xFNw6m_NSpEJe-ILxBDbsGXmhkemMQL9Mce5wizo,2819
|
|
5
|
+
licos_dev_sdk/data.py,sha256=p0CRcVFsBykz3eW333Z_EPWOgm2ANWpNhztnus3_Cu0,2117
|
|
6
|
+
licos_dev_sdk/diagram.py,sha256=3Dv3SDvzLXVYEKpGMIDwTe5WtQ4qaCBd6hYOgfz6Ico,790
|
|
7
|
+
licos_dev_sdk/document.py,sha256=07GHtcbuARcPd-QZwJNkA59J6ZT0HPJ6H5o-6McYyos,4045
|
|
8
|
+
licos_dev_sdk/image.py,sha256=B-TgF4cMVPyEIoz8M1Zee7tFQLBXf3foikebucfypiE,2235
|
|
9
|
+
licos_dev_sdk/presentation.py,sha256=HRVfq6wf-DwQlFlFeWpZ-OtzX24MLXpo6d0mM88TRLw,3462
|
|
10
|
+
licos_dev_sdk/spreadsheet.py,sha256=n4htL_GN--zaBQ_iiHLvWpykGXExKxo6W6NekOIJfEo,2420
|
|
11
|
+
licos_dev_sdk/web.py,sha256=IPZIcbgpaEBwKdpRiKcceIAxkh9jQmemqJHQ-jB7Oes,2193
|
|
12
|
+
licos_dev_sdk-0.1.0.dist-info/METADATA,sha256=oPu-sejeZt8vvOzUff0ga2Y88zdrqdwquTS0ap8uVZg,544
|
|
13
|
+
licos_dev_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
14
|
+
licos_dev_sdk-0.1.0.dist-info/RECORD,,
|