sdpy-kit 0.5.0__tar.gz → 0.5.1__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.
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/PKG-INFO +6 -3
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/README.md +5 -2
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/__init__.py +1 -1
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/anydoc_kit.py +7 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/rc_docparser_kit.py +110 -2
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/.gitignore +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/base_service.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/chat_service.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/profiles.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/runner.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/types.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/common_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/config/kit_config.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/__init__.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/article_models.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/web_article_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/wx_article_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/__init__.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/base_db_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/crud.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/engine.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/query.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/session.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/sqlite_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/__init__.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/docx_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/marker_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/md2doc_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/mineru_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/file_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/laya_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/license_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sdpy-kit
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
|
|
5
5
|
Author-email: Clark <changhongyuan@126.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -881,6 +881,9 @@ md = AnyDocKit.convert_to_md("scan.pdf", ocr="hosted") # 需 FIRECRAWL_API_KEY
|
|
|
881
881
|
|
|
882
882
|
`AnyDocResult(success, markdown, file_path, source_format, error)`
|
|
883
883
|
|
|
884
|
+
> 异常:沿用上游 `anydoc.ConvertError` 体系(`MalformedError` / `NeedsOcrError` 等子类可直接捕获),
|
|
885
|
+
> `AnyDocError` 是该基类的别名;安全版 `convert()` 会把它们封装为 `AnyDocResult.error`。
|
|
886
|
+
|
|
884
887
|
---
|
|
885
888
|
|
|
886
889
|
## kit.docs.docx_kit —— Word 模板动态填充
|
|
@@ -1046,8 +1049,8 @@ if result.success:
|
|
|
1046
1049
|
| `convert(src_path, output_dir=None, *, ...) -> RcDocParserResult` | 安全版:失败封装为结果对象 |
|
|
1047
1050
|
|
|
1048
1051
|
> 产物:`<out>/images/img-NNN-<sha1 前 10>.<ext>`(按出现顺序、内容哈希命名);无图不建空目录。
|
|
1049
|
-
> 前提:中文标题依赖 `w:outlineLvl` 或「标题 N
|
|
1050
|
-
>
|
|
1052
|
+
> 前提:中文标题依赖 `w:outlineLvl` 或「标题 N」样式(都没有时退化为正文);合并单元格表格会重建为
|
|
1053
|
+
> 等宽 GFM(覆盖位留空,不输出 HTML `<table>`);页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
|
|
1051
1054
|
|
|
1052
1055
|
## kit.crawl.wx_article_kit —— 微信公众号文章抓取存储
|
|
1053
1056
|
|
|
@@ -805,6 +805,9 @@ md = AnyDocKit.convert_to_md("scan.pdf", ocr="hosted") # 需 FIRECRAWL_API_KEY
|
|
|
805
805
|
|
|
806
806
|
`AnyDocResult(success, markdown, file_path, source_format, error)`
|
|
807
807
|
|
|
808
|
+
> 异常:沿用上游 `anydoc.ConvertError` 体系(`MalformedError` / `NeedsOcrError` 等子类可直接捕获),
|
|
809
|
+
> `AnyDocError` 是该基类的别名;安全版 `convert()` 会把它们封装为 `AnyDocResult.error`。
|
|
810
|
+
|
|
808
811
|
---
|
|
809
812
|
|
|
810
813
|
## kit.docs.docx_kit —— Word 模板动态填充
|
|
@@ -970,8 +973,8 @@ if result.success:
|
|
|
970
973
|
| `convert(src_path, output_dir=None, *, ...) -> RcDocParserResult` | 安全版:失败封装为结果对象 |
|
|
971
974
|
|
|
972
975
|
> 产物:`<out>/images/img-NNN-<sha1 前 10>.<ext>`(按出现顺序、内容哈希命名);无图不建空目录。
|
|
973
|
-
> 前提:中文标题依赖 `w:outlineLvl` 或「标题 N
|
|
974
|
-
>
|
|
976
|
+
> 前提:中文标题依赖 `w:outlineLvl` 或「标题 N」样式(都没有时退化为正文);合并单元格表格会重建为
|
|
977
|
+
> 等宽 GFM(覆盖位留空,不输出 HTML `<table>`);页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
|
|
975
978
|
|
|
976
979
|
## kit.crawl.wx_article_kit —— 微信公众号文章抓取存储
|
|
977
980
|
|
|
@@ -39,6 +39,10 @@ Author: Clark Chang
|
|
|
39
39
|
依赖:
|
|
40
40
|
- `firecrawl-anydoc`(extra `anydoc`,导入名 `anydoc`)+ `pikepdf`;
|
|
41
41
|
未安装时导入报错并提示安装命令。
|
|
42
|
+
|
|
43
|
+
异常:
|
|
44
|
+
- 沿用上游 `anydoc.ConvertError` 体系(不包装,保留 `MalformedError` /
|
|
45
|
+
`NeedsOcrError` 等子类可捕获性);`AnyDocError` 是该基类的别名。
|
|
42
46
|
"""
|
|
43
47
|
|
|
44
48
|
import io
|
|
@@ -58,6 +62,9 @@ from kit.config.log_config import get_default_logger
|
|
|
58
62
|
|
|
59
63
|
logger = get_default_logger(__name__)
|
|
60
64
|
|
|
65
|
+
# 异常别名:沿用 anydoc 的异常体系,不包装(保留上游子类可捕获性)
|
|
66
|
+
AnyDocError = anydoc.ConvertError
|
|
67
|
+
|
|
61
68
|
# kit 维护的扩展名白名单(用于本地快速校验与错误提示)。
|
|
62
69
|
# 注意:这是 anydoc 实际支持面的子集——anydoc 另支持 docm / pptm / ppsx / ppsm /
|
|
63
70
|
# xlsm / xlsb / pps / pot / odt / ods / odp / rtf / epub 等。
|
|
@@ -35,14 +35,17 @@ Author: Clark Chang
|
|
|
35
35
|
图片引用统一改写为 `images/<文件名>`(库在 Windows 上输出反斜杠相对路径)。
|
|
36
36
|
- 图片:库按出现顺序产出 `img-NNN-<sha1 前 10>.<ext>`,本模块复制到 `images/`;
|
|
37
37
|
`extract_images=False` 时不落图片并删除 md 中的图片引用行。
|
|
38
|
+
- 表格:用 python-docx 把每张表重建为**等宽 GFM 网格**,合并单元格的覆盖位留空
|
|
39
|
+
(库原生产物会把同一单元格文本复制到每个覆盖列/行);表格数量不匹配时保留原产物。
|
|
38
40
|
- 临时副本与解析工作目录均创建在产物目录内的临时目录,结束后清理,不修改源文件。
|
|
39
41
|
|
|
40
42
|
前提条件:
|
|
41
43
|
- 纯本地解析:不需要 GPU、模型权重、OCR、GTK 等系统库。
|
|
42
44
|
- 中文标题依赖 `w:outlineLvl` 或「标题 N」样式;两者都没有(仅靠字号/加粗)时
|
|
43
45
|
无法还原层级。
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
+
- 已知限制:页眉页脚与脚注不提取;合并单元格按「覆盖位留空」展平(span 语义不再标出),
|
|
47
|
+
单元格内嵌图片/公式只保留文字;未带 `w:numPr` 的列表样式(如 List Bullet)
|
|
48
|
+
按普通段落输出。
|
|
46
49
|
|
|
47
50
|
依赖:
|
|
48
51
|
- `rc-docparser>=0.2.2`(`pip install sdpy-kit[docx-parse]`);
|
|
@@ -64,6 +67,7 @@ try:
|
|
|
64
67
|
from docx import Document
|
|
65
68
|
from docx.enum.style import WD_STYLE_TYPE
|
|
66
69
|
from docx.oxml.ns import qn
|
|
70
|
+
from docx.text.paragraph import Paragraph
|
|
67
71
|
except ImportError as e:
|
|
68
72
|
raise ImportError(
|
|
69
73
|
"rc_docparser_kit 需要可选依赖,请安装:pip install sdpy-kit[docx-parse]"
|
|
@@ -89,6 +93,9 @@ _CAPTION_LIKE_RE = re.compile(
|
|
|
89
93
|
# 标题文本长度上限(超过视为误设大纲级别的正文/图注)
|
|
90
94
|
_MAX_HEADING_CHARS = 60
|
|
91
95
|
|
|
96
|
+
# GFM 表格行(用于定位库输出中的表格块)
|
|
97
|
+
_TABLE_LINE_PREFIX = "|"
|
|
98
|
+
|
|
92
99
|
|
|
93
100
|
class RcDocParserError(Exception):
|
|
94
101
|
"""docx 解析异常(依赖调用失败 / 产物缺失 / 落盘失败)"""
|
|
@@ -257,6 +264,105 @@ class RcDocParserKit:
|
|
|
257
264
|
]
|
|
258
265
|
return "\n".join(kept)
|
|
259
266
|
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# 表格重建(合并单元格 → 等宽 GFM)
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _tc_text(tc) -> str:
|
|
273
|
+
"""从 `w:tc` 取文本:段落用空格连接,转义竖线(GFM 单元格不能换行)"""
|
|
274
|
+
parts = [Paragraph(p, None).text.strip() for p in tc.findall(qn("w:p"))]
|
|
275
|
+
text = " ".join(part for part in parts if part)
|
|
276
|
+
return text.replace("|", "\\|")
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def _table_grid(cls, table) -> list[list[str]]:
|
|
280
|
+
"""把 docx 表格重建为等宽矩形网格(合并格的覆盖位留空)
|
|
281
|
+
|
|
282
|
+
直接读 OOXML:每个 `w:tr` 的 `w:tc` 列表是「逻辑单元格」(不含网格展开),
|
|
283
|
+
`w:gridSpan` 给出横向跨度,`w:vMerge`(无 val 或 `continue`)表示纵向续接。
|
|
284
|
+
横向与纵向的覆盖位一律留空,避免同一单元格文本被复制到每一列/每一行。
|
|
285
|
+
"""
|
|
286
|
+
rows: list[list[str]] = []
|
|
287
|
+
for tr in table._tbl.tr_lst:
|
|
288
|
+
cells: list[str] = []
|
|
289
|
+
for tc in tr.tc_lst:
|
|
290
|
+
tc_pr = tc.tcPr
|
|
291
|
+
span = 1
|
|
292
|
+
if tc_pr is not None and tc_pr.gridSpan is not None:
|
|
293
|
+
span = max(1, int(tc_pr.gridSpan.val))
|
|
294
|
+
continued = False
|
|
295
|
+
if tc_pr is not None and tc_pr.vMerge is not None:
|
|
296
|
+
continued = tc_pr.vMerge.val in (None, "continue")
|
|
297
|
+
cells.append("" if continued else cls._tc_text(tc))
|
|
298
|
+
cells.extend([""] * (span - 1))
|
|
299
|
+
rows.append(cells)
|
|
300
|
+
|
|
301
|
+
width = max((len(row) for row in rows), default=0)
|
|
302
|
+
return [row + [""] * (width - len(row)) for row in rows]
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _render_gfm_table(rows: list[list[str]]) -> list[str]:
|
|
306
|
+
"""把矩形网格渲染为 GFM 表格行(首行作表头)"""
|
|
307
|
+
if not rows:
|
|
308
|
+
return []
|
|
309
|
+
width = max(len(row) for row in rows)
|
|
310
|
+
lines = [
|
|
311
|
+
"| " + " | ".join(rows[0]) + " |",
|
|
312
|
+
"| " + " | ".join(["---"] * width) + " |",
|
|
313
|
+
]
|
|
314
|
+
for row in rows[1:]:
|
|
315
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
316
|
+
return lines
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def _rebuild_tables(cls, markdown: str, src: Path) -> str:
|
|
320
|
+
"""用 python-docx 重建的等宽 GFM 表格替换库输出的表格块
|
|
321
|
+
|
|
322
|
+
库按 `row.cells` 取文本,合并单元格会重复出现在每个覆盖格位;这里按顺序
|
|
323
|
+
一一替换。表格数量不一致、或读取 docx 失败时**保留原产物**并 warning,
|
|
324
|
+
不影响整体转换。
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
doc = Document(str(src))
|
|
328
|
+
except Exception as e: # noqa: BLE001 - 表格重建失败不应影响正文
|
|
329
|
+
logger.warning(f"读取 docx 表格失败,保留库输出: {e}")
|
|
330
|
+
return markdown
|
|
331
|
+
if not doc.tables:
|
|
332
|
+
return markdown
|
|
333
|
+
|
|
334
|
+
lines = markdown.splitlines()
|
|
335
|
+
blocks: list[tuple[int, int]] = []
|
|
336
|
+
index = 0
|
|
337
|
+
while index < len(lines):
|
|
338
|
+
if lines[index].lstrip().startswith(_TABLE_LINE_PREFIX):
|
|
339
|
+
start = index
|
|
340
|
+
while index < len(lines) and lines[index].lstrip().startswith(
|
|
341
|
+
_TABLE_LINE_PREFIX
|
|
342
|
+
):
|
|
343
|
+
index += 1
|
|
344
|
+
blocks.append((start, index))
|
|
345
|
+
else:
|
|
346
|
+
index += 1
|
|
347
|
+
|
|
348
|
+
if len(blocks) != len(doc.tables):
|
|
349
|
+
logger.warning(
|
|
350
|
+
f"表格数量不匹配(md {len(blocks)} / docx {len(doc.tables)}),"
|
|
351
|
+
f"保留原始表格"
|
|
352
|
+
)
|
|
353
|
+
return markdown
|
|
354
|
+
|
|
355
|
+
rebuilt: list[str] = []
|
|
356
|
+
cursor = 0
|
|
357
|
+
for (start, end), table in zip(blocks, doc.tables):
|
|
358
|
+
rebuilt.extend(lines[cursor:start])
|
|
359
|
+
rebuilt.extend(cls._render_gfm_table(cls._table_grid(table)))
|
|
360
|
+
cursor = end
|
|
361
|
+
rebuilt.extend(lines[cursor:])
|
|
362
|
+
|
|
363
|
+
suffix = "\n" if markdown.endswith("\n") else ""
|
|
364
|
+
return "\n".join(rebuilt) + suffix
|
|
365
|
+
|
|
260
366
|
@staticmethod
|
|
261
367
|
def _outline_level(paragraph) -> int | None:
|
|
262
368
|
"""读取段落的 `w:outlineLvl`(0 基)并转为 Heading 级别,无则 None"""
|
|
@@ -374,6 +480,7 @@ class RcDocParserKit:
|
|
|
374
480
|
mapping=mapping,
|
|
375
481
|
extract_images=extract_images,
|
|
376
482
|
)
|
|
483
|
+
markdown = cls._rebuild_tables(markdown, src)
|
|
377
484
|
md_path = out_dir / f"{src.stem}.md"
|
|
378
485
|
md_path.write_text(markdown, encoding="utf-8")
|
|
379
486
|
finally:
|
|
@@ -445,6 +552,7 @@ class RcDocParserKit:
|
|
|
445
552
|
markdown = cls._render(
|
|
446
553
|
raw_md, images, src.stem, mapping=mapping, extract_images=True
|
|
447
554
|
)
|
|
555
|
+
markdown = cls._rebuild_tables(markdown, src)
|
|
448
556
|
logger.info(f"docx 解析成功: {src.name}({len(markdown)} 字符)")
|
|
449
557
|
return markdown
|
|
450
558
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|