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.
Files changed (51) hide show
  1. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/PKG-INFO +6 -3
  2. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/README.md +5 -2
  3. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/anydoc_kit.py +7 -0
  5. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/rc_docparser_kit.py +110 -2
  6. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/.gitignore +0 -0
  7. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/__init__.py +0 -0
  8. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/base_service.py +0 -0
  9. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/chat_service.py +0 -0
  10. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/embedding_service.py +0 -0
  11. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/exceptions.py +0 -0
  12. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/failover.py +0 -0
  13. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/image_service.py +0 -0
  14. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/model_builder.py +0 -0
  15. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/model_client.py +0 -0
  16. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/pool.py +0 -0
  17. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/profiles.py +0 -0
  18. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/rerank_service.py +0 -0
  19. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/runner.py +0 -0
  20. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/ai/types.py +0 -0
  21. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/common_kit.py +0 -0
  22. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/config/__init__.py +0 -0
  23. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/config/kit_config.py +0 -0
  24. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/config/log_config.py +0 -0
  25. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/__init__.py +0 -0
  26. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/article_models.py +0 -0
  27. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/web_article_kit.py +0 -0
  28. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/crawl/wx_article_kit.py +0 -0
  29. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/__init__.py +0 -0
  30. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/base_db_kit.py +0 -0
  31. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/crud.py +0 -0
  32. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/engine.py +0 -0
  33. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/exceptions.py +0 -0
  34. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/models/__init__.py +0 -0
  35. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/models/base.py +0 -0
  36. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/mysql_kit.py +0 -0
  37. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/pg_kit.py +0 -0
  38. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/query.py +0 -0
  39. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/session.py +0 -0
  40. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/sqlite_kit.py +0 -0
  41. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/db/transaction.py +0 -0
  42. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/__init__.py +0 -0
  43. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/docx_kit.py +0 -0
  44. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/marker_kit.py +0 -0
  45. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/md2doc_kit.py +0 -0
  46. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/docs/mineru_kit.py +0 -0
  47. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/file_kit.py +0 -0
  48. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/laya_kit.py +0 -0
  49. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/license_kit.py +0 -0
  50. {sdpy_kit-0.5.0 → sdpy_kit-0.5.1}/kit/redis_kit.py +0 -0
  51. {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.0
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
- > 会退化为 GFM 表格;页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
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
- > 会退化为 GFM 表格;页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
976
+ > 前提:中文标题依赖 `w:outlineLvl` 或「标题 N」样式(都没有时退化为正文);合并单元格表格会重建为
977
+ > 等宽 GFM(覆盖位留空,不输出 HTML `<table>`);页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
975
978
 
976
979
  ## kit.crawl.wx_article_kit —— 微信公众号文章抓取存储
977
980
 
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.5.0"
26
+ __version__ = "0.5.1"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -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
- - 已知限制:页眉页脚与脚注不提取;合并单元格的复杂表格退化为 GFM 表格
45
- (跨行跨列丢失);未带 `w:numPr` 的列表样式(如 List Bullet)按普通段落输出。
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