Skip to content

Commit 4a3272e

Browse files
authored
Merge pull request #259 from ducflair/dev
ci: remove legacy logic for ducpy ci
2 parents 7db58eb + ae6afad commit 4a3272e

12 files changed

Lines changed: 357 additions & 113 deletions

File tree

.github/workflows/release-ducpy.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ jobs:
9898
TAG=$(git describe --tags --abbrev=0 --match 'ducpy@*')
9999
VERSION="${TAG#ducpy@}"
100100
echo "Building Pyodide wheel for version ${VERSION}"
101-
uv run python scripts/sync_schema.py
102101
RUSTUP_TOOLCHAIN=nightly SETUPTOOLS_SCM_PRETEND_VERSION="${VERSION}" pyodide build --outdir dist
103102
104103
- name: Verify wheel platform tag

packages/ducjs/README.md

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,13 @@ For detailed documentation, including all available types and utility functions,
2020

2121
## Tools
2222

23-
- [Playground](https://ducflair.com/core): Experiment with the `duc` format in a live environment.
23+
- [Playground](https://scopture.com/w/hollow): Experiment with the `duc` format in a live environment.
2424
- [Documentation](https://duc.ducflair.com): Comprehensive guides and API references.
2525

2626
## Contributing
2727

28-
At the moment we are not accepting contributions to this package. However, we welcome feedback and suggestions for future improvements. Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
28+
Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
2929

3030
## License
3131

32-
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
33-
34-
35-
## Commit Message Guidelines
36-
37-
To ensure smooth releases with semantic-release, please follow [these guidelines](https://semantic-release.gitbook.io/semantic-release#how-does-it-work).
38-
39-
---
40-
41-
*The duc format and libraries are constantly evolving, aiming to set new standards in the 2D CAD industry. Be a part of this transformation and help shape the future of design technology!*
32+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.

packages/ducpdf/README.md

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,13 @@ For detailed documentation, including all available types and utility functions,
2323

2424
## Tools
2525

26-
- [Playground](https://ducflair.com/core): Experiment with the `duc` format in a live environment.
26+
- [Playground](https://scopture.com/w/hollow): Experiment with the `duc` format in a live environment.
2727
- [Documentation](https://duc.ducflair.com): Comprehensive guides and API references.
2828

2929
## Contributing
3030

31-
At the moment we are not accepting contributions to this package. However, we welcome feedback and suggestions for future improvements. Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
31+
Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
3232

3333
## License
3434

35-
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
36-
37-
38-
## Commit Message Guidelines
39-
40-
To ensure smooth releases with semantic-release, please follow [these guidelines](https://semantic-release.gitbook.io/semantic-release#how-does-it-work).
41-
42-
---
43-
44-
*The duc format and libraries are constantly evolving, aiming to set new standards in the 2D CAD industry. Be a part of this transformation and help shape the future of design technology!*
35+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.

packages/ducpdf/src/duc2pdf/README.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,10 @@ For detailed documentation, including all available types and utility functions,
6565

6666
## Tools
6767

68-
- [Playground](https://ducflair.com/core): Experiment with the `duc` format in a live environment.
68+
- [Playground](https://scopture.com/w/hollow): Experiment with the `duc` format in a live environment.
6969
- [Documentation](https://duc.ducflair.com): Comprehensive guides and API references.
7070

7171

7272
## License
7373

74-
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
75-
76-
## Commit Message Guidelines
77-
78-
To ensure smooth releases with semantic-release, please follow [these guidelines](https://semantic-release.gitbook.io/semantic-release#how-does-it-work).
79-
80-
---
81-
82-
*The duc format and libraries are constantly evolving, aiming to set new standards in the 2D CAD industry. Be a part of this transformation and help shape the future of design technology!*
74+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.

packages/ducpy/README.md

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,76 @@
1212
</p>
1313
</p>
1414

15-
16-
The `ducpy` package provides a robust Python implementation for the `duc` 2D CAD file format, a cornerstone of our advanced design system. Designed for professionals seeking precision and efficiency, this package ensures seamless integration and manipulation of `duc` files within your Python projects.
17-
18-
## Introduction
19-
20-
The `ducpy` package offers comprehensive Python types and helper functions to work effortlessly with the `duc` CAD file format. Built with efficiency and performance in mind, this package enables you to parse, validate, and manipulate `duc` files with ease.
21-
22-
## Features
23-
24-
- **Python Support:** Strongly-typed interfaces for all aspects of the `duc` file format, ensuring type safety and IDE support.
25-
- **Utility Functions:** Easy-to-use functions for parsing, validating, and manipulating `duc` files.
26-
- **Extensible Architecture:** Designed to integrate seamlessly with other `ducflair` packages and your custom tools.
27-
- **Performance Optimized:** Efficient processing to handle complex CAD data with ease.
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.
2816

2917
## Installation
3018

31-
Install the package using pip:
32-
3319
```bash
3420
pip install ducpy
3521
```
3622

37-
## Usage
23+
or with [`uv`](https://docs.astral.sh/uv/):
24+
25+
```bash
26+
uv add ducpy
27+
```
28+
29+
## Quick start
3830

3931
```python
4032
import ducpy as duc
4133
```
4234

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+
4369
## Documentation
4470

45-
For detailed documentation, including all available types and utility functions, visit our [Documentation](https://duc.ducflair.com).
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)
4676

4777
## Tools
4878

4979
- [Playground](https://scopture.com): Experiment with the `duc` format in a live environment.
5080

5181
## Contributing
5282

53-
At the moment we are not accepting contributions to this package. However, we welcome feedback and suggestions for future improvements. Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
83+
Feel free to open an issue or contact us at [Ducflair Support](https://www.ducflair.com/support).
5484

5585
## License
5686

57-
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.
58-
59-
## Commit Message Guidelines
60-
61-
To ensure smooth releases with semantic-release, please follow [these guidelines](https://semantic-release.gitbook.io/semantic-release#how-does-it-work).
62-
63-
---
64-
65-
*The duc format and libraries are constantly evolving, aiming to set new standards in the 2D CAD industry. Be a part of this transformation and help shape the future of design technology!*
87+
This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details.

packages/ducpy/src/ducpy/builders/sql_builder.py

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,71 @@
3131
import os
3232
import sqlite3
3333
import tempfile
34+
import zlib
3435
from pathlib import Path
35-
from typing import Any, List, Optional, Union
36+
from typing import Any, List, Optional, Sequence, Union
3637

3738
import ducpy_native
3839

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
4099

41100

42101
def _get_current_schema_version() -> int:
@@ -105,12 +164,14 @@ def __init__(self, path: Union[str, Path]):
105164
path = str(path)
106165
if not os.path.exists(path):
107166
raise FileNotFoundError(f"File not found: {path}")
108-
self.conn: sqlite3.Connection = sqlite3.connect(path)
167+
sqlite_path, temp_path = _sqlite_path_for_duc(path)
168+
self.conn: sqlite3.Connection = sqlite3.connect(sqlite_path)
109169
self.conn.row_factory = sqlite3.Row
110170
_apply_pragmas(self.conn)
111171
_apply_migrations(self.conn)
112-
self._path: Optional[str] = path
113-
self._temp: Optional[str] = None
172+
self._path: Optional[str] = sqlite_path
173+
self._temp: Optional[str] = temp_path
174+
self._attached_temps: list[str] = []
114175
self._closed = False
115176

116177
@classmethod
@@ -126,29 +187,68 @@ def new(cls, path: Union[str, Path, None] = None) -> DucSQL:
126187
inst.conn.executescript(_read_schema_sql())
127188
inst._path = target if path else None
128189
inst._temp = None
190+
inst._attached_temps = []
129191
inst._closed = False
130192
return inst
131193

132194
@classmethod
133195
def from_bytes(cls, data: bytes) -> DucSQL:
134196
"""Open a ``.duc`` from raw bytes (temp file, cleaned up on close)."""
135-
tmp = tempfile.NamedTemporaryFile(suffix=".duc", delete=False)
197+
temp_path = _write_temp_sqlite(data)
136198
try:
137-
tmp.write(data)
138-
tmp.close()
139199
inst = object.__new__(cls)
140-
inst.conn = sqlite3.connect(tmp.name)
200+
inst.conn = sqlite3.connect(temp_path)
141201
inst.conn.row_factory = sqlite3.Row
142202
_apply_pragmas(inst.conn)
143203
_apply_migrations(inst.conn)
144-
inst._path = tmp.name
145-
inst._temp = tmp.name
204+
inst._path = temp_path
205+
inst._temp = temp_path
206+
inst._attached_temps = []
146207
inst._closed = False
147208
return inst
148209
except Exception:
149-
os.unlink(tmp.name)
210+
os.unlink(temp_path)
150211
raise
151212

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+
152252
# ------------------------------------------------------------------
153253
# SQL execution
154254
# ------------------------------------------------------------------
@@ -186,7 +286,7 @@ def save(self, path: Union[str, Path, None] = None) -> None:
186286
finally:
187287
dst.close()
188288

189-
def to_bytes(self) -> bytes:
289+
def to_bytes(self, compressed: bool = False) -> bytes:
190290
"""Export the database as raw bytes."""
191291
self.commit()
192292
tmp = tempfile.NamedTemporaryFile(suffix=".duc", delete=False)
@@ -196,7 +296,8 @@ def to_bytes(self) -> bytes:
196296
self.conn.backup(dst)
197297
dst.close()
198298
with open(tmp.name, "rb") as f:
199-
return f.read()
299+
data = f.read()
300+
return _deflate_duc_bytes(data) if compressed else data
200301
finally:
201302
os.unlink(tmp.name)
202303

@@ -210,6 +311,9 @@ def close(self) -> None:
210311
self._closed = True
211312
if self._temp and os.path.exists(self._temp):
212313
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)
213317

214318
def __enter__(self) -> DucSQL:
215319
return self

0 commit comments

Comments
 (0)