ducpy 3.4.0__tar.gz → 3.5.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.
Files changed (61) hide show
  1. {ducpy-3.4.0 → ducpy-3.5.0}/Cargo.lock +1 -1
  2. {ducpy-3.4.0 → ducpy-3.5.0}/PKG-INFO +5 -1
  3. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducpy/crate/Cargo.toml +1 -1
  4. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/duc.sql +1 -1
  5. ducpy-3.5.0/packages/ducrs/schema/migrations/3000004_to_3000005.sql +51 -0
  6. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/search.sql +49 -1
  7. {ducpy-3.4.0 → ducpy-3.5.0}/pyproject.toml +13 -0
  8. ducpy-3.5.0/src/ducpy/search/__init__.py +19 -0
  9. ducpy-3.5.0/src/ducpy/search/image_ocr.py +79 -0
  10. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/search/search_elements.py +398 -89
  11. ducpy-3.5.0/src/ducpy/search/search_external_files.py +608 -0
  12. ducpy-3.5.0/src/ducpy/search/search_pdf.py +95 -0
  13. ducpy-3.4.0/src/ducpy/search/__init__.py +0 -13
  14. {ducpy-3.4.0 → ducpy-3.5.0}/Cargo.toml +0 -0
  15. {ducpy-3.4.0 → ducpy-3.5.0}/LICENSE +0 -0
  16. {ducpy-3.4.0 → ducpy-3.5.0}/README.md +0 -0
  17. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducpy/crate/src/lib.rs +0 -0
  18. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/Cargo.toml +0 -0
  19. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/LICENSE +0 -0
  20. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/README.md +0 -0
  21. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/build.rs +0 -0
  22. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
  23. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +0 -0
  24. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
  25. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
  26. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/version_control.sql +0 -0
  27. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/document.rs +0 -0
  28. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/meta.rs +0 -0
  29. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/mod.rs +0 -0
  30. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/version_control.rs +0 -0
  31. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/bootstrap.rs +0 -0
  32. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/mod.rs +0 -0
  33. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/native.rs +0 -0
  34. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/wasm.rs +0 -0
  35. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/lib.rs +0 -0
  36. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/parse.rs +0 -0
  37. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/serde_utils.rs +0 -0
  38. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/serialize.rs +0 -0
  39. {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/types.rs +0 -0
  40. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/__init__.py +0 -0
  41. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/__init__.py +0 -0
  42. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/block_instance_builder.py +0 -0
  43. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/block_utils.py +0 -0
  44. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/element_builders.py +0 -0
  45. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/mutate_builder.py +0 -0
  46. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/sql_builder.py +0 -0
  47. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/state_builders.py +0 -0
  48. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/style_builders.py +0 -0
  49. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/classes/DataStateClass.py +0 -0
  50. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/classes/ElementsClass.py +0 -0
  51. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/classes/__init__.py +0 -0
  52. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/enums.py +0 -0
  53. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/parse.py +0 -0
  54. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/serialize.py +0 -0
  55. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/__init__.py +0 -0
  56. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/constants.py +0 -0
  57. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/convert.py +0 -0
  58. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/io.py +0 -0
  59. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/mutate_utils.py +0 -0
  60. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/rand_utils.py +0 -0
  61. {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy_native/__init__.py +0 -0
@@ -412,7 +412,7 @@ dependencies = [
412
412
 
413
413
  [[package]]
414
414
  name = "ducpy-native"
415
- version = "3.4.0"
415
+ version = "3.5.0"
416
416
  dependencies = [
417
417
  "duc",
418
418
  "pyo3",
@@ -1,8 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducpy
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Requires-Dist: nanoid>=2.0.0
6
+ Requires-Dist: pypdf>=4.2.0
7
+ Requires-Dist: rapidocr-onnxruntime>=1.3.24 ; extra == 'ocr'
8
+ Requires-Dist: numpy>=1.20.0 ; extra == 'ocr'
9
+ Provides-Extra: ocr
6
10
  License-File: LICENSE
7
11
  Summary: The library for the Duc 2D CAD file format
8
12
  Author: Jorge Soares
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ducpy-native"
3
- version = "3.4.0"
3
+ version = "3.5.0"
4
4
  edition = "2021"
5
5
  description = "Python native extension for the duc file format (used by ducpy)"
6
6
  publish = false
@@ -1,7 +1,7 @@
1
1
  -- "DUC_" in ASCII
2
2
  -- Apply in order: duc.sql → version_control.sql → search.sql
3
3
  PRAGMA application_id = 1146569567;
4
- PRAGMA user_version = 3000004;
4
+ PRAGMA user_version = 3000005;
5
5
  PRAGMA journal_mode = WAL;
6
6
  PRAGMA foreign_keys = ON;
7
7
  PRAGMA synchronous = NORMAL;
@@ -0,0 +1,51 @@
1
+ -- Migration: 3000003 → 3000004
2
+ -- Add searchable index for extracted external-file text (PDF content search).
3
+
4
+ CREATE TABLE IF NOT EXISTS external_file_text_index (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ file_id TEXT NOT NULL,
7
+ revision_id TEXT NOT NULL,
8
+ mime_type TEXT NOT NULL,
9
+ extracted_text TEXT NOT NULL DEFAULT '',
10
+ has_ocr INTEGER NOT NULL DEFAULT 0,
11
+ updated INTEGER NOT NULL,
12
+ UNIQUE (file_id, revision_id)
13
+ );
14
+
15
+ CREATE INDEX IF NOT EXISTS idx_external_file_text_index_file_id
16
+ ON external_file_text_index(file_id);
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_external_file_text_index_revision_id
19
+ ON external_file_text_index(revision_id);
20
+
21
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_external_file_text USING fts5(
22
+ extracted_text,
23
+ content='external_file_text_index',
24
+ content_rowid='id',
25
+ tokenize='unicode61 remove_diacritics 2',
26
+ prefix='2 3 4 5 6 7 8 9 10'
27
+ );
28
+
29
+ CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ai
30
+ AFTER INSERT ON external_file_text_index BEGIN
31
+ INSERT INTO search_external_file_text(rowid, extracted_text)
32
+ VALUES (NEW.id, NEW.extracted_text);
33
+ END;
34
+
35
+ CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ad
36
+ AFTER DELETE ON external_file_text_index BEGIN
37
+ INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
38
+ VALUES ('delete', OLD.id, OLD.extracted_text);
39
+ END;
40
+
41
+ CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_au
42
+ AFTER UPDATE OF extracted_text ON external_file_text_index BEGIN
43
+ INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
44
+ VALUES ('delete', OLD.id, OLD.extracted_text);
45
+ INSERT INTO search_external_file_text(rowid, extracted_text)
46
+ VALUES (NEW.id, NEW.extracted_text);
47
+ END;
48
+
49
+ INSERT INTO search_external_file_text(search_external_file_text) VALUES ('rebuild');
50
+
51
+ PRAGMA user_version = 3000005;
@@ -5,7 +5,8 @@
5
5
  -- Triggers keep the FTS index automatically in sync on every INSERT/UPDATE/DELETE.
6
6
  -- No application-level sync needed.
7
7
  --
8
- -- Depends on: duc.sql (elements, element_text, element_doc, element_model, blocks)
8
+ -- Depends on: duc.sql (elements, element_text, element_doc, element_model, blocks,
9
+ -- external_files, external_file_revisions, external_file_revision_data, document_grid_config)
9
10
  --
10
11
  -- Query examples:
11
12
  -- SELECT rowid, rank FROM search_elements WHERE search_elements MATCH 'motor';
@@ -129,6 +130,52 @@ CREATE TRIGGER IF NOT EXISTS trg_blocks_ai AFTER INSERT ON blocks BEGIN
129
130
  INSERT INTO search_blocks(rowid, label, description)
130
131
  VALUES (NEW.rowid, NEW.label, NEW.description);
131
132
  END;
133
+
134
+ -- Extracted text index for external file revisions (used for PDF content search).
135
+ CREATE TABLE IF NOT EXISTS external_file_text_index (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ file_id TEXT NOT NULL,
138
+ revision_id TEXT NOT NULL,
139
+ mime_type TEXT NOT NULL,
140
+ extracted_text TEXT NOT NULL DEFAULT '',
141
+ has_ocr INTEGER NOT NULL DEFAULT 0,
142
+ updated INTEGER NOT NULL,
143
+ UNIQUE (file_id, revision_id)
144
+ );
145
+
146
+ CREATE INDEX IF NOT EXISTS idx_external_file_text_index_file_id
147
+ ON external_file_text_index(file_id);
148
+
149
+ CREATE INDEX IF NOT EXISTS idx_external_file_text_index_revision_id
150
+ ON external_file_text_index(revision_id);
151
+
152
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_external_file_text USING fts5(
153
+ extracted_text,
154
+ content='external_file_text_index',
155
+ content_rowid='id',
156
+ tokenize='unicode61 remove_diacritics 2',
157
+ prefix='2 3 4 5 6 7 8 9 10'
158
+ );
159
+
160
+ CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ai
161
+ AFTER INSERT ON external_file_text_index BEGIN
162
+ INSERT INTO search_external_file_text(rowid, extracted_text)
163
+ VALUES (NEW.id, NEW.extracted_text);
164
+ END;
165
+
166
+ CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_ad
167
+ AFTER DELETE ON external_file_text_index BEGIN
168
+ INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
169
+ VALUES ('delete', OLD.id, OLD.extracted_text);
170
+ END;
171
+
172
+ CREATE TRIGGER IF NOT EXISTS trg_external_file_text_index_au
173
+ AFTER UPDATE OF extracted_text ON external_file_text_index BEGIN
174
+ INSERT INTO search_external_file_text(search_external_file_text, rowid, extracted_text)
175
+ VALUES ('delete', OLD.id, OLD.extracted_text);
176
+ INSERT INTO search_external_file_text(rowid, extracted_text)
177
+ VALUES (NEW.id, NEW.extracted_text);
178
+ END;
132
179
  CREATE TRIGGER IF NOT EXISTS trg_blocks_ad AFTER DELETE ON blocks BEGIN
133
180
  INSERT INTO search_blocks(search_blocks, rowid, label, description)
134
181
  VALUES ('delete', OLD.rowid, OLD.label, OLD.description);
@@ -146,3 +193,4 @@ INSERT INTO search_element_text(search_element_text) VALUES ('rebuild');
146
193
  INSERT INTO search_element_doc(search_element_doc) VALUES ('rebuild');
147
194
  INSERT INTO search_element_model(search_element_model) VALUES ('rebuild');
148
195
  INSERT INTO search_blocks(search_blocks) VALUES ('rebuild');
196
+ INSERT INTO search_external_file_text(search_external_file_text) VALUES ('rebuild');
@@ -6,6 +6,7 @@ readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "nanoid>=2.0.0",
9
+ "pypdf>=4.2.0"
9
10
  ]
10
11
 
11
12
  license = { text = "MIT" }
@@ -19,6 +20,12 @@ classifiers = [
19
20
  "License :: OSI Approved :: MIT License",
20
21
  ]
21
22
 
23
+ [project.optional-dependencies]
24
+ ocr = [
25
+ "rapidocr-onnxruntime>=1.3.24",
26
+ "numpy>=1.20.0",
27
+ ]
28
+
22
29
  [project.urls]
23
30
  "Homepage" = "https://duc.ducflair.com"
24
31
  "Source" = "https://github.com/ducflair/duc/tree/main/packages/ducpy"
@@ -44,11 +51,14 @@ where = ["src"]
44
51
 
45
52
  [dependency-groups]
46
53
  dev = [
54
+ "ducpy[ocr]",
55
+ "maturin>=1.0,<2.0",
47
56
  "furo>=2024.8.6",
48
57
  "sphinx>=8.1.3",
49
58
  "sphinx-autoapi>=3.6.0",
50
59
  "pytest>=8.3.5",
51
60
  "rich>=13.0.0",
61
+ "numpy>=1.20.0",
52
62
  "typst>=0.14.9",
53
63
  "build123d>=0.10.0",
54
64
  "ocp-vscode>=3.4.0",
@@ -60,3 +70,6 @@ dev = [
60
70
  testpaths = ["src/tests"]
61
71
  python_files = "test_*.py"
62
72
  addopts = "-vv"
73
+ markers = [
74
+ "slow: marks tests as slow (OCR-heavy, etc.)",
75
+ ]
@@ -0,0 +1,19 @@
1
+ """Search helpers for DUC SQLite databases."""
2
+
3
+ from .search_elements import (
4
+ DucElementSearchResult,
5
+ DucFileSearchResult,
6
+ DucSearchResponse,
7
+ DucSearchResult,
8
+ ExternalFileSearchTarget,
9
+ search_duc_elements,
10
+ )
11
+
12
+ __all__ = [
13
+ "DucElementSearchResult",
14
+ "DucFileSearchResult",
15
+ "DucSearchResponse",
16
+ "DucSearchResult",
17
+ "ExternalFileSearchTarget",
18
+ "search_duc_elements",
19
+ ]
@@ -0,0 +1,79 @@
1
+ """Server-side image OCR helpers for DUC search.
2
+
3
+ Uses ``rapidocr-onnxruntime`` when the ``ocr`` extra is installed, otherwise
4
+ OCR is gracefully skipped.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import logging
11
+ from functools import lru_cache
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _RAPID_OCR_ENGINE: Any | None = None
17
+
18
+
19
+ @lru_cache(maxsize=1)
20
+ def server_side_ocr_available() -> bool:
21
+ """Return whether the ``ocr`` extra (RapidOCR) is installed and usable."""
22
+
23
+ try:
24
+ from rapidocr_onnxruntime import RapidOCR # type: ignore
25
+ from PIL import Image # type: ignore # noqa: F401
26
+ except Exception as exc:
27
+ logger.debug("OCR extra (rapidocr-onnxruntime) unavailable: %s", exc)
28
+ return False
29
+
30
+ return True
31
+
32
+
33
+ def _get_rapid_engine() -> Any:
34
+ global _RAPID_OCR_ENGINE
35
+ if _RAPID_OCR_ENGINE is None:
36
+ from rapidocr_onnxruntime import RapidOCR # type: ignore
37
+
38
+ _RAPID_OCR_ENGINE = RapidOCR()
39
+ return _RAPID_OCR_ENGINE
40
+
41
+
42
+ def extract_image_text_with_ocr(image_bytes: bytes, *, ocr_language: str) -> tuple[str, bool]:
43
+ """OCR an image using the ``ocr`` extra, or skip when unavailable."""
44
+
45
+ if not server_side_ocr_available():
46
+ return "", False
47
+
48
+ try:
49
+ import numpy as np # type: ignore
50
+ from PIL import Image # type: ignore
51
+ except Exception as exc:
52
+ logger.debug("Image OCR dependencies unavailable: %s", exc)
53
+ return "", False
54
+
55
+ try:
56
+ with Image.open(io.BytesIO(image_bytes)) as image_obj:
57
+ image_input = np.array(image_obj.convert("RGB"))
58
+
59
+ engine = _get_rapid_engine()
60
+ result, _elapsed = engine(image_input)
61
+ if not result:
62
+ return "", False
63
+
64
+ lines: list[str] = []
65
+ for row in result:
66
+ if isinstance(row, (tuple, list)) and len(row) >= 2:
67
+ value = str(row[1] or "").strip()
68
+ if value:
69
+ lines.append(value)
70
+ text = "\n".join(lines)
71
+ except Exception as exc:
72
+ logger.debug("Failed to OCR image bytes: %s", exc)
73
+ return "", False
74
+
75
+ return text, bool(text)
76
+
77
+
78
+ _server_side_ocr_available = server_side_ocr_available
79
+ _extract_image_text_with_ocr = extract_image_text_with_ocr