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.
- {ducpy-3.4.0 → ducpy-3.5.0}/Cargo.lock +1 -1
- {ducpy-3.4.0 → ducpy-3.5.0}/PKG-INFO +5 -1
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducpy/crate/Cargo.toml +1 -1
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/duc.sql +1 -1
- ducpy-3.5.0/packages/ducrs/schema/migrations/3000004_to_3000005.sql +51 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/search.sql +49 -1
- {ducpy-3.4.0 → ducpy-3.5.0}/pyproject.toml +13 -0
- ducpy-3.5.0/src/ducpy/search/__init__.py +19 -0
- ducpy-3.5.0/src/ducpy/search/image_ocr.py +79 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/search/search_elements.py +398 -89
- ducpy-3.5.0/src/ducpy/search/search_external_files.py +608 -0
- ducpy-3.5.0/src/ducpy/search/search_pdf.py +95 -0
- ducpy-3.4.0/src/ducpy/search/__init__.py +0 -13
- {ducpy-3.4.0 → ducpy-3.5.0}/Cargo.toml +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/LICENSE +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/README.md +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducpy/crate/src/lib.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/Cargo.toml +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/LICENSE +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/README.md +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/build.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/schema/version_control.sql +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/document.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/meta.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/mod.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/api/version_control.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/bootstrap.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/mod.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/native.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/db/wasm.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/lib.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/parse.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/serde_utils.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/serialize.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/packages/ducrs/src/types.rs +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/block_instance_builder.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/block_utils.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/element_builders.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/mutate_builder.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/sql_builder.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/state_builders.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/builders/style_builders.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/classes/DataStateClass.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/classes/ElementsClass.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/classes/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/enums.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/parse.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/serialize.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/__init__.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/constants.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/convert.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/io.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/mutate_utils.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy/utils/rand_utils.py +0 -0
- {ducpy-3.4.0 → ducpy-3.5.0}/src/ducpy_native/__init__.py +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ducpy
|
|
3
|
-
Version: 3.
|
|
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,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 =
|
|
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
|