ducpy 3.3.8__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.3.8 → ducpy-3.5.0}/Cargo.lock +1 -1
- ducpy-3.5.0/PKG-INFO +108 -0
- ducpy-3.5.0/README.md +87 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducpy/crate/Cargo.toml +1 -1
- ducpy-3.5.0/packages/ducrs/README.md +54 -0
- {ducpy-3.3.8 → 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.3.8 → ducpy-3.5.0}/packages/ducrs/schema/search.sql +49 -1
- {ducpy-3.3.8 → ducpy-3.5.0}/pyproject.toml +13 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/sql_builder.py +118 -14
- 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.3.8 → 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.3.8 → ducpy-3.5.0}/src/ducpy/serialize.py +45 -1
- ducpy-3.3.8/PKG-INFO +0 -83
- ducpy-3.3.8/README.md +0 -65
- ducpy-3.3.8/packages/ducrs/README.md +0 -52
- ducpy-3.3.8/src/ducpy/search/__init__.py +0 -13
- {ducpy-3.3.8 → ducpy-3.5.0}/Cargo.toml +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/LICENSE +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducpy/crate/src/lib.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/Cargo.toml +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/LICENSE +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/build.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000000_to_3000001.sql +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000001_to_3000002.sql +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000002_to_3000003.sql +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/schema/migrations/3000003_to_3000004.sql +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/schema/version_control.sql +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/api/document.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/api/meta.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/api/mod.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/api/version_control.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/db/bootstrap.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/db/mod.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/db/native.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/db/wasm.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/lib.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/parse.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/serde_utils.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/serialize.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/packages/ducrs/src/types.rs +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/__init__.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/__init__.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/block_instance_builder.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/block_utils.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/element_builders.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/mutate_builder.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/state_builders.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/builders/style_builders.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/classes/DataStateClass.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/classes/ElementsClass.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/classes/__init__.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/enums.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/parse.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/utils/__init__.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/utils/constants.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/utils/convert.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/utils/io.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/utils/mutate_utils.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy/utils/rand_utils.py +0 -0
- {ducpy-3.3.8 → ducpy-3.5.0}/src/ducpy_native/__init__.py +0 -0
ducpy-3.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ducpy
|
|
3
|
+
Version: 3.5.0
|
|
4
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
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
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Summary: The library for the Duc 2D CAD file format
|
|
12
|
+
Author: Jorge Soares
|
|
13
|
+
Author-email: Ducflair <support@ducflair.com>
|
|
14
|
+
License: MIT
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
17
|
+
Project-URL: Documentation, https://ducflair.github.io/duc/reference/python/
|
|
18
|
+
Project-URL: Homepage, https://duc.ducflair.com
|
|
19
|
+
Project-URL: Package Index, https://ducflair.github.io/duc/simple/ducpy/
|
|
20
|
+
Project-URL: Source, https://github.com/ducflair/duc/tree/main/packages/ducpy
|
|
21
|
+
|
|
22
|
+
# ducpy
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<br/>
|
|
26
|
+
<a href="https://duc.ducflair.com" target="_blank"><img width="256px" src="https://cdn.jsdelivr.net/gh/ducflair/assets@main/src/duc/duc-extended.png" /></a>
|
|
27
|
+
<p align="center">2D CAD File Format</p>
|
|
28
|
+
<p align="center" style="align: center;">
|
|
29
|
+
<a href="https://pypi.org/project/ducpy/"><img src="https://shields.io/badge/Pip-blue?logo=Pypi&logoColor=white&style=round-square" alt="Pip" /></a>
|
|
30
|
+
<a href="https://github.com/ducflair/duc/releases"><img src="https://img.shields.io/pypi/v/ducpy?style=round-square&label=latest%20stable" alt="PyPI ducpy@latest release" /></a>
|
|
31
|
+
<a href="https://pypi.org/project/ducpy/"><img src="https://img.shields.io/pypi/dm/ducpy?style=round-square&color=salmon" alt="Downloads" /></a>
|
|
32
|
+
<img src="https://shields.io/badge/Python-ffde57?logo=Python&logoColor=646464&style=round-square" alt="Python" />
|
|
33
|
+
</p>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
The `ducpy` package is the official Python implementation of the `.duc` 2D CAD file format. It is built on top of the [`duc`](../ducrs) Rust crate, exposed as the `ducpy_native` extension. A `.duc` file is a zlib-compressed SQLite database — `ducpy` gives you both a high-level builder DSL and direct low-level access to that schema, plus parsing, serialization, and search helpers.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install ducpy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
or with [`uv`](https://docs.astral.sh/uv/):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv add ducpy
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import ducpy as duc
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API overview
|
|
57
|
+
|
|
58
|
+
`ducpy` is organised around four conceptual entry points, matching the structure of the full [API reference](https://duc.ducflair.com):
|
|
59
|
+
|
|
60
|
+
### Builders API (High-level)
|
|
61
|
+
|
|
62
|
+
The easy way to build and manage `.duc` files. Construct elements, apply styles, manage layers, build blocks, and handle document state through the `duc.builders` module.
|
|
63
|
+
|
|
64
|
+
See the worked examples:
|
|
65
|
+
|
|
66
|
+
- [Element creation](src/examples/element_creation_demo.py) — building rectangles, ellipses, polygons, lines, arrows, text, frames and plots with the fluent builder DSL.
|
|
67
|
+
- [Mutating elements](src/examples/mutation_demo.py) — updating element properties in place and observing version changes.
|
|
68
|
+
- [Document elements](src/examples/document_element_demo.py) and [model elements](src/examples/model_element_demo.py) — for the higher-level container / model element types.
|
|
69
|
+
|
|
70
|
+
### SQL Builder (Low-level)
|
|
71
|
+
|
|
72
|
+
A `.duc` file is a zlib-compressed SQLite database. Use `duc.builders.sql_builder` (`DucSQL`) for direct schema access, bulk queries, and low-level manipulation.
|
|
73
|
+
|
|
74
|
+
See the worked example: [SQL Builder](src/examples/sql_builder_demo.py) — `DucSQL.new()` to create a `.duc` from scratch, `DucSQL(path)` to query an existing one.
|
|
75
|
+
|
|
76
|
+
### Search
|
|
77
|
+
|
|
78
|
+
Query and search elements and files programmatically through the `duc.search` API.
|
|
79
|
+
|
|
80
|
+
### File I/O
|
|
81
|
+
|
|
82
|
+
Read and write `.duc` files using the `duc.parse` and `duc.serialize` modules.
|
|
83
|
+
|
|
84
|
+
See the worked examples:
|
|
85
|
+
|
|
86
|
+
- [Parsing](src/examples/parsing_demo.py) — `parse_duc` / `parse_duc_lazy` / `get_external_file` / `list_external_files`.
|
|
87
|
+
- [Serialization](src/examples/serialization_demo.py) — `serialize_duc` from builder-created elements.
|
|
88
|
+
- [External files](src/examples/external_files_demo.py) — attaching binary blobs (images, PDFs) to a `.duc` document.
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
For detailed documentation, including the full API reference, schema specification, and end-to-end examples, see:
|
|
93
|
+
|
|
94
|
+
- API reference & guides: [duc.ducflair.com](https://duc.ducflair.com)
|
|
95
|
+
- Source-level reference: [docs/](docs/index.rst)
|
|
96
|
+
- Full example index: [docs/examples.rst](docs/examples.rst)
|
|
97
|
+
|
|
98
|
+
## Tools
|
|
99
|
+
|
|
100
|
+
- [Playground](https://scopture.com): Experiment with the `duc` format in a live environment.
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
|
|
104
|
+
Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
|
ducpy-3.5.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# ducpy
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<br/>
|
|
5
|
+
<a href="https://duc.ducflair.com" target="_blank"><img width="256px" src="https://cdn.jsdelivr.net/gh/ducflair/assets@main/src/duc/duc-extended.png" /></a>
|
|
6
|
+
<p align="center">2D CAD File Format</p>
|
|
7
|
+
<p align="center" style="align: center;">
|
|
8
|
+
<a href="https://pypi.org/project/ducpy/"><img src="https://shields.io/badge/Pip-blue?logo=Pypi&logoColor=white&style=round-square" alt="Pip" /></a>
|
|
9
|
+
<a href="https://github.com/ducflair/duc/releases"><img src="https://img.shields.io/pypi/v/ducpy?style=round-square&label=latest%20stable" alt="PyPI ducpy@latest release" /></a>
|
|
10
|
+
<a href="https://pypi.org/project/ducpy/"><img src="https://img.shields.io/pypi/dm/ducpy?style=round-square&color=salmon" alt="Downloads" /></a>
|
|
11
|
+
<img src="https://shields.io/badge/Python-ffde57?logo=Python&logoColor=646464&style=round-square" alt="Python" />
|
|
12
|
+
</p>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
The `ducpy` package is the official Python implementation of the `.duc` 2D CAD file format. It is built on top of the [`duc`](../ducrs) Rust crate, exposed as the `ducpy_native` extension. A `.duc` file is a zlib-compressed SQLite database — `ducpy` gives you both a high-level builder DSL and direct low-level access to that schema, plus parsing, serialization, and search helpers.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install ducpy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or with [`uv`](https://docs.astral.sh/uv/):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add ducpy
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import ducpy as duc
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API overview
|
|
36
|
+
|
|
37
|
+
`ducpy` is organised around four conceptual entry points, matching the structure of the full [API reference](https://duc.ducflair.com):
|
|
38
|
+
|
|
39
|
+
### Builders API (High-level)
|
|
40
|
+
|
|
41
|
+
The easy way to build and manage `.duc` files. Construct elements, apply styles, manage layers, build blocks, and handle document state through the `duc.builders` module.
|
|
42
|
+
|
|
43
|
+
See the worked examples:
|
|
44
|
+
|
|
45
|
+
- [Element creation](src/examples/element_creation_demo.py) — building rectangles, ellipses, polygons, lines, arrows, text, frames and plots with the fluent builder DSL.
|
|
46
|
+
- [Mutating elements](src/examples/mutation_demo.py) — updating element properties in place and observing version changes.
|
|
47
|
+
- [Document elements](src/examples/document_element_demo.py) and [model elements](src/examples/model_element_demo.py) — for the higher-level container / model element types.
|
|
48
|
+
|
|
49
|
+
### SQL Builder (Low-level)
|
|
50
|
+
|
|
51
|
+
A `.duc` file is a zlib-compressed SQLite database. Use `duc.builders.sql_builder` (`DucSQL`) for direct schema access, bulk queries, and low-level manipulation.
|
|
52
|
+
|
|
53
|
+
See the worked example: [SQL Builder](src/examples/sql_builder_demo.py) — `DucSQL.new()` to create a `.duc` from scratch, `DucSQL(path)` to query an existing one.
|
|
54
|
+
|
|
55
|
+
### Search
|
|
56
|
+
|
|
57
|
+
Query and search elements and files programmatically through the `duc.search` API.
|
|
58
|
+
|
|
59
|
+
### File I/O
|
|
60
|
+
|
|
61
|
+
Read and write `.duc` files using the `duc.parse` and `duc.serialize` modules.
|
|
62
|
+
|
|
63
|
+
See the worked examples:
|
|
64
|
+
|
|
65
|
+
- [Parsing](src/examples/parsing_demo.py) — `parse_duc` / `parse_duc_lazy` / `get_external_file` / `list_external_files`.
|
|
66
|
+
- [Serialization](src/examples/serialization_demo.py) — `serialize_duc` from builder-created elements.
|
|
67
|
+
- [External files](src/examples/external_files_demo.py) — attaching binary blobs (images, PDFs) to a `.duc` document.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
For detailed documentation, including the full API reference, schema specification, and end-to-end examples, see:
|
|
72
|
+
|
|
73
|
+
- API reference & guides: [duc.ducflair.com](https://duc.ducflair.com)
|
|
74
|
+
- Source-level reference: [docs/](docs/index.rst)
|
|
75
|
+
- Full example index: [docs/examples.rst](docs/examples.rst)
|
|
76
|
+
|
|
77
|
+
## Tools
|
|
78
|
+
|
|
79
|
+
- [Playground](https://scopture.com): Experiment with the `duc` format in a live environment.
|
|
80
|
+
|
|
81
|
+
## Contributing
|
|
82
|
+
|
|
83
|
+
Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# duc (Rust)
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<br/>
|
|
5
|
+
<a href="https://duc.ducflair.com" target="_blank"><img width="256px" src="https://cdn.jsdelivr.net/gh/ducflair/assets@main/src/duc/duc-extended.png" /></a>
|
|
6
|
+
<p align="center">2D CAD File Format</p>
|
|
7
|
+
<p align="center" style="align: center;">
|
|
8
|
+
<a href="https://crates.io/crates/duc"><img src="https://shields.io/badge/Crates-FFC933?logo=Rust&logoColor=646464&style=round-square" alt="Crates" /></a>
|
|
9
|
+
<a href="https://github.com/ducflair/duc/releases"><img src="https://img.shields.io/crates/v/duc?style=round-square&label=latest%20stable" alt="crates.io duc@latest release" /></a>
|
|
10
|
+
<a href="https://crates.io/crates/duc"><img src="https://img.shields.io/crates/d/duc?style=round-square&color=salmon" alt="Downloads" /></a>
|
|
11
|
+
<img src="https://shields.io/badge/Rust-CE412B?logo=Rust&logoColor=fff&style=round-square" alt="Rust" />
|
|
12
|
+
</p>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
The `duc` crate is the canonical Rust implementation of the `.duc` 2D CAD file format and the **root of the entire Duc ecosystem**. [`ducpy`](https://pypi.org/project/ducpy/), [`ducjs`](https://www.npmjs.com/package/ducjs), and the rest of the toolchain are all built on top of the types, parsers and storage layer defined here.
|
|
16
|
+
|
|
17
|
+
A `.duc` file is a standard zlib compressed **SQLite database file** following the schema in `schema/duc.sql`. This crate wraps that database in a strongly-typed Rust API for reading, writing and manipulating `.duc` documents.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
----
|
|
21
|
+
<br/>
|
|
22
|
+
<br/>
|
|
23
|
+
<br/>
|
|
24
|
+
|
|
25
|
+
## Schema and migrations
|
|
26
|
+
|
|
27
|
+
The canonical SQL lives at `schema/duc.sql` (plus `schema/version_control.sql` and `schema/search.sql`) and is **embedded at compile time** by [`build.rs`](build.rs)
|
|
28
|
+
|
|
29
|
+
Migrations are ran and handled **automatically** upon usage of the library. Each migration is a SQL file named `<from>_to_<to>.sql` in `schema/migrations/`. `build.rs` scans that directory, sorts by `from_version` and emits a static `MIGRATIONS` array. Adding a new migration requires **no Rust changes** — drop the file in and rebuild.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
| Feature | Default | Description |
|
|
34
|
+
|---------|---------|-------------|
|
|
35
|
+
| `default` | ✅ | Native build with bundled SQLite |
|
|
36
|
+
| `opfs` | ❌ | OPFS SAH-pool VFS for `wasm32-unknown-unknown` (browser persistence) |
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
- Full API reference: [docs.rs/duc](https://docs.rs/duc)
|
|
42
|
+
- Format specification, concepts and guides: [duc.ducflair.com](https://duc.ducflair.com)
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
46
|
+
- [Playground](https://ducflair.com/core): Experiment with the `.duc` format in the browser.
|
|
47
|
+
|
|
48
|
+
## Contributing
|
|
49
|
+
|
|
50
|
+
Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
|
|
@@ -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
|
+
]
|
|
@@ -31,12 +31,71 @@ from __future__ import annotations
|
|
|
31
31
|
import os
|
|
32
32
|
import sqlite3
|
|
33
33
|
import tempfile
|
|
34
|
+
import zlib
|
|
34
35
|
from pathlib import Path
|
|
35
|
-
from typing import Any, List, Optional, Union
|
|
36
|
+
from typing import Any, List, Optional, Sequence, Union
|
|
36
37
|
|
|
37
38
|
import ducpy_native
|
|
38
39
|
|
|
39
|
-
__all__ = ["DucSQL"]
|
|
40
|
+
__all__ = ["DucSQL", "quote_sql_identifier"]
|
|
41
|
+
SQLITE_HEADER_MAGIC = b"SQLite format 3\x00"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def quote_sql_identifier(value: str) -> str:
|
|
45
|
+
"""Quote a SQLite identifier such as an attached database alias."""
|
|
46
|
+
if not value:
|
|
47
|
+
raise ValueError("SQLite identifier cannot be empty.")
|
|
48
|
+
return '"' + value.replace('"', '""') + '"'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_sqlite_bytes(data: bytes) -> bool:
|
|
52
|
+
return data.startswith(SQLITE_HEADER_MAGIC)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize_sqlite_image(data: bytes) -> bytes:
|
|
56
|
+
image = bytearray(data)
|
|
57
|
+
if len(image) > 19:
|
|
58
|
+
if image[18] == 2:
|
|
59
|
+
image[18] = 1
|
|
60
|
+
if image[19] == 2:
|
|
61
|
+
image[19] = 1
|
|
62
|
+
return bytes(image)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _inflate_duc_bytes(data: bytes) -> bytes:
|
|
66
|
+
if _is_sqlite_bytes(data):
|
|
67
|
+
return _normalize_sqlite_image(data)
|
|
68
|
+
inflated = zlib.decompress(data, -zlib.MAX_WBITS)
|
|
69
|
+
if not _is_sqlite_bytes(inflated):
|
|
70
|
+
raise ValueError("decompressed .duc payload does not start with a SQLite header")
|
|
71
|
+
return _normalize_sqlite_image(inflated)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _deflate_duc_bytes(data: bytes) -> bytes:
|
|
75
|
+
compressor = zlib.compressobj(level=-1, wbits=-zlib.MAX_WBITS)
|
|
76
|
+
return compressor.compress(data) + compressor.flush()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _write_temp_sqlite(data: bytes) -> str:
|
|
80
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".duc", delete=False)
|
|
81
|
+
try:
|
|
82
|
+
tmp.write(_inflate_duc_bytes(data))
|
|
83
|
+
tmp.close()
|
|
84
|
+
return tmp.name
|
|
85
|
+
except Exception:
|
|
86
|
+
tmp.close()
|
|
87
|
+
os.unlink(tmp.name)
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _sqlite_path_for_duc(path: Union[str, Path]) -> tuple[str, Optional[str]]:
|
|
92
|
+
path = str(path)
|
|
93
|
+
with open(path, "rb") as f:
|
|
94
|
+
data = f.read()
|
|
95
|
+
if _is_sqlite_bytes(data):
|
|
96
|
+
return path, None
|
|
97
|
+
temp_path = _write_temp_sqlite(data)
|
|
98
|
+
return temp_path, temp_path
|
|
40
99
|
|
|
41
100
|
|
|
42
101
|
def _get_current_schema_version() -> int:
|
|
@@ -105,12 +164,14 @@ class DucSQL:
|
|
|
105
164
|
path = str(path)
|
|
106
165
|
if not os.path.exists(path):
|
|
107
166
|
raise FileNotFoundError(f"File not found: {path}")
|
|
108
|
-
|
|
167
|
+
sqlite_path, temp_path = _sqlite_path_for_duc(path)
|
|
168
|
+
self.conn: sqlite3.Connection = sqlite3.connect(sqlite_path)
|
|
109
169
|
self.conn.row_factory = sqlite3.Row
|
|
110
170
|
_apply_pragmas(self.conn)
|
|
111
171
|
_apply_migrations(self.conn)
|
|
112
|
-
self._path: Optional[str] =
|
|
113
|
-
self._temp: Optional[str] =
|
|
172
|
+
self._path: Optional[str] = sqlite_path
|
|
173
|
+
self._temp: Optional[str] = temp_path
|
|
174
|
+
self._attached_temps: list[str] = []
|
|
114
175
|
self._closed = False
|
|
115
176
|
|
|
116
177
|
@classmethod
|
|
@@ -126,29 +187,68 @@ class DucSQL:
|
|
|
126
187
|
inst.conn.executescript(_read_schema_sql())
|
|
127
188
|
inst._path = target if path else None
|
|
128
189
|
inst._temp = None
|
|
190
|
+
inst._attached_temps = []
|
|
129
191
|
inst._closed = False
|
|
130
192
|
return inst
|
|
131
193
|
|
|
132
194
|
@classmethod
|
|
133
195
|
def from_bytes(cls, data: bytes) -> DucSQL:
|
|
134
196
|
"""Open a ``.duc`` from raw bytes (temp file, cleaned up on close)."""
|
|
135
|
-
|
|
197
|
+
temp_path = _write_temp_sqlite(data)
|
|
136
198
|
try:
|
|
137
|
-
tmp.write(data)
|
|
138
|
-
tmp.close()
|
|
139
199
|
inst = object.__new__(cls)
|
|
140
|
-
inst.conn = sqlite3.connect(
|
|
200
|
+
inst.conn = sqlite3.connect(temp_path)
|
|
141
201
|
inst.conn.row_factory = sqlite3.Row
|
|
142
202
|
_apply_pragmas(inst.conn)
|
|
143
203
|
_apply_migrations(inst.conn)
|
|
144
|
-
inst._path =
|
|
145
|
-
inst._temp =
|
|
204
|
+
inst._path = temp_path
|
|
205
|
+
inst._temp = temp_path
|
|
206
|
+
inst._attached_temps = []
|
|
146
207
|
inst._closed = False
|
|
147
208
|
return inst
|
|
148
209
|
except Exception:
|
|
149
|
-
os.unlink(
|
|
210
|
+
os.unlink(temp_path)
|
|
150
211
|
raise
|
|
151
212
|
|
|
213
|
+
@classmethod
|
|
214
|
+
def attach_many(
|
|
215
|
+
cls,
|
|
216
|
+
paths: Sequence[Union[str, Path]],
|
|
217
|
+
aliases: Optional[Sequence[str]] = None,
|
|
218
|
+
read_only: bool = True,
|
|
219
|
+
) -> DucSQL:
|
|
220
|
+
"""Open an in-memory SQLite connection with multiple ``.duc`` files attached.
|
|
221
|
+
|
|
222
|
+
This is useful for querying multiple drawings in one SQL statement. The
|
|
223
|
+
returned ``DucSQL`` owns only the in-memory connection; attached files are
|
|
224
|
+
not deleted on close.
|
|
225
|
+
"""
|
|
226
|
+
if aliases is not None and len(aliases) != len(paths):
|
|
227
|
+
raise ValueError("aliases must have the same length as paths.")
|
|
228
|
+
|
|
229
|
+
inst = object.__new__(cls)
|
|
230
|
+
inst.conn = sqlite3.connect(":memory:")
|
|
231
|
+
inst.conn.row_factory = sqlite3.Row
|
|
232
|
+
inst._path = None
|
|
233
|
+
inst._temp = None
|
|
234
|
+
inst._attached_temps = []
|
|
235
|
+
inst._closed = False
|
|
236
|
+
|
|
237
|
+
for index, path in enumerate(paths):
|
|
238
|
+
alias = aliases[index] if aliases is not None else f"d{index}"
|
|
239
|
+
sqlite_path, temp_path = _sqlite_path_for_duc(path)
|
|
240
|
+
if temp_path:
|
|
241
|
+
inst._attached_temps.append(temp_path)
|
|
242
|
+
inst.conn.execute(
|
|
243
|
+
f"ATTACH DATABASE ? AS {quote_sql_identifier(alias)}",
|
|
244
|
+
(sqlite_path,),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if read_only:
|
|
248
|
+
inst.conn.execute("PRAGMA query_only = ON")
|
|
249
|
+
|
|
250
|
+
return inst
|
|
251
|
+
|
|
152
252
|
# ------------------------------------------------------------------
|
|
153
253
|
# SQL execution
|
|
154
254
|
# ------------------------------------------------------------------
|
|
@@ -186,7 +286,7 @@ class DucSQL:
|
|
|
186
286
|
finally:
|
|
187
287
|
dst.close()
|
|
188
288
|
|
|
189
|
-
def to_bytes(self) -> bytes:
|
|
289
|
+
def to_bytes(self, compressed: bool = False) -> bytes:
|
|
190
290
|
"""Export the database as raw bytes."""
|
|
191
291
|
self.commit()
|
|
192
292
|
tmp = tempfile.NamedTemporaryFile(suffix=".duc", delete=False)
|
|
@@ -196,7 +296,8 @@ class DucSQL:
|
|
|
196
296
|
self.conn.backup(dst)
|
|
197
297
|
dst.close()
|
|
198
298
|
with open(tmp.name, "rb") as f:
|
|
199
|
-
|
|
299
|
+
data = f.read()
|
|
300
|
+
return _deflate_duc_bytes(data) if compressed else data
|
|
200
301
|
finally:
|
|
201
302
|
os.unlink(tmp.name)
|
|
202
303
|
|
|
@@ -210,6 +311,9 @@ class DucSQL:
|
|
|
210
311
|
self._closed = True
|
|
211
312
|
if self._temp and os.path.exists(self._temp):
|
|
212
313
|
os.unlink(self._temp)
|
|
314
|
+
for temp_path in getattr(self, "_attached_temps", []):
|
|
315
|
+
if os.path.exists(temp_path):
|
|
316
|
+
os.unlink(temp_path)
|
|
213
317
|
|
|
214
318
|
def __enter__(self) -> DucSQL:
|
|
215
319
|
return self
|
|
@@ -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
|
+
]
|