Skip to content
Merged

Dev #243

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ducpy/docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

html_theme = "furo"
html_static_path = ['_static']
html_extra_path = ['extra']
html_baseurl = "https://ducflair.github.io/duc/reference/python/"

extensions.append('autoapi.extension')
Expand Down
22 changes: 22 additions & 0 deletions packages/ducpy/docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,25 @@ anything beyond what the high-level builders expose.
.. literalinclude:: ../src/examples/sql_builder_demo.py
:language: python
:linenos:

----

Serialization
-------------

Demonstrates how to serialize builder-created elements directly to a `.duc` file using `duc.serialize_duc`.

.. literalinclude:: ../src/examples/serialization_demo.py
:language: python
:linenos:

----

Parsing
-------

Demonstrates how to parse a `.duc` file or raw binary bytes using `duc.parse_duc`, allowing attribute-style access to the document's content.

.. literalinclude:: ../src/examples/parsing_demo.py
:language: python
:linenos:
4 changes: 4 additions & 0 deletions packages/ducpy/docs/extra/context7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"url": "https://context7.com/websites/ducflair_github_io_duc",
"public_key": "pk_rW7pJ4T4uki1BAr4guCs1"
}
23 changes: 23 additions & 0 deletions packages/ducpy/docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ ducpy
</p>
</p>

Overview
--------

**Builders API (High-level)**
The easy way to build, manage ``.duc`` files.
Construct elements, apply styles, manage layers, build blocks,
and handle document state with the
:doc:`builders <autoapi/ducpy/builders/index>` module.

**SQL Builder (Low-level)**
A ``.duc`` file is a zlib-compressed SQLite database. Use the
:doc:`sql_builder <autoapi/ducpy/builders/sql_builder/index>`
for direct schema access, bulk queries, and low-level manipulation.

**Search**
Query/search elements and files programmatically via the
:doc:`search <autoapi/ducpy/search/index>` API.

**File I/O**
Read and write ``.duc`` files using the
:doc:`parse <autoapi/ducpy/parse/index>` and
:doc:`serialize <autoapi/ducpy/serialize/index>` modules.

.. toctree::
:maxdepth: 2
:caption: Contents:
Expand Down
29 changes: 21 additions & 8 deletions packages/ducpy/src/ducpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
"""Main module for duc_py package.
"""Python library for the DUC 2D CAD file format.

Import usage examples:
import ducpy as duc

duc.classes.SomeClass
duc.parse_duc(source)
duc.serialize_duc(name=..., elements=...)
duc.utils.some_utility
Usage::
``import ducpy as duc``

Builders API (High-level):
The easy way to build, manage ``.duc`` files.
Construct elements, apply styles, manage layers, build blocks,
and handle document state with the ``duc.builders`` module.

SQL Builder (Low-level):
A ``.duc`` file is a zlib-compressed SQLite database. Use
``duc.builders.sql_builder`` for direct schema access, bulk
queries, and low-level manipulation.

Search:
Query/search elements and files programmatically via the
``duc.search`` API.

File I/O:
Read and write ``.duc`` files using the ``duc.parse``
and ``duc.serialize`` modules.
"""

from .builders import *
Expand Down
19 changes: 16 additions & 3 deletions packages/ducpy/src/ducpy/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,29 @@ def _read_bytes(source: Union[bytes, bytearray, BinaryIO, str]) -> bytes:
def parse_duc(source: Union[bytes, bytearray, BinaryIO, str]) -> DucData:
"""Parse a ``.duc`` file into a :class:`DucData` dict.

This function reads a raw `.duc` binary blob or file path and parses it using
the Rust native extension. It returns a specialized dictionary (`DucData`)
that allows attribute-style access to the parsed properties (e.g. `data.elements[0].id`),
using `snake_case` keys instead of the internal `camelCase` format.

Parameters
----------
source : bytes | file | str
Raw bytes, an open binary file, or a path to a ``.duc`` file.
Raw bytes, an open binary file, or a string path to a ``.duc`` file.

Returns
-------
DucData
Attribute-accessible dict matching the ExportedDataState schema with
snake_case keys.
An attribute-accessible dictionary matching the internal `ExportedDataState`
schema with snake_case keys. Common keys include `elements`, `global_state`,
`local_state`, and `version_graph`.

Examples
--------
>>> data = duc.parse_duc("path/to/file.duc")
>>> data = duc.parse_duc(binary_data)
>>> print(f"Found {len(data.elements)} elements")
>>> print(f"First element type: {data.elements[0].type}")
"""
buf = _read_bytes(source)
raw = ducpy_native.parse_duc(buf)
Expand Down
45 changes: 42 additions & 3 deletions packages/ducpy/src/ducpy/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,48 @@ def serialize_duc(
layers: Optional[list] = None,
external_files: Optional[list] = None,
) -> bytes:
"""Serialize elements to ``.duc`` format.
Element instances and state dataclasses are automatically converted to the
camelCase dicts expected by the Rust native module.
"""Serialize elements and document state to raw ``.duc`` binary format.

This function accepts lists of elements created via the `ducpy.builders` API
(e.g., `ElementBuilder`) and serializes them into the compressed format
expected by `.duc` files. Element instances and state dataclasses are
automatically converted to the camelCase dicts expected by the Rust native module.

Parameters
----------
name : str
The document name or identifier (used to populate the `source` field).
thumbnail : Optional[bytes], default=None
Raw PNG bytes representing a thumbnail of the document.
dictionary : Optional[list], default=None
List of Key-Value string pairs for dictionary entries.
elements : Optional[list], default=None
A list of elements (e.g., created via `ElementBuilder`) to include.
duc_local_state : Any, default=None
A `DucLocalState` object representing viewport state (pan, zoom, etc).
duc_global_state : Any, default=None
A `DucGlobalState` object representing document-wide settings.
version_graph : Any, default=None
Version history metadata of the document.
blocks : Optional[list], default=None
List of block definitions.
block_instances : Optional[list], default=None
List of block instances.
block_collections : Optional[list], default=None
List of block collections (libraries).
groups : Optional[list], default=None
List of element groups.
regions : Optional[list], default=None
List of boolean regions.
layers : Optional[list], default=None
List of document layers.
external_files : Optional[list], default=None
List of external files (e.g., embedded images or PDFs).

Returns
-------
bytes
The raw `.duc` binary data, ready to be written to a file.
"""
thumb = bytes(thumbnail) if thumbnail is not None else None

Expand Down
68 changes: 68 additions & 0 deletions packages/ducpy/src/examples/parsing_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
Example demonstrating how to parse a .duc file using the parsing API.

This demo shows how to read a `.duc` binary blob or file path and access
the parsed data using attribute-style access via DucData.
"""

import os
import tempfile
import ducpy as duc

def main():
print("Parsing Demo")
print("=" * 30)

# First, let's create a temporary .duc file to parse
from ducpy.builders.style_builders import create_fill_and_stroke_style, create_solid_content
elements = [
duc.ElementBuilder()
.at_position(10, 20)
.with_size(100, 50)
.with_label("Parsed Rectangle")
.with_styles(create_fill_and_stroke_style(
fill_content=create_solid_content("#FF6B6B"),
stroke_content=create_solid_content("#2C3E50"),
stroke_width=2.0
))
.build_rectangle()
.build()
]
duc_bytes = duc.serialize_duc(name="parsing_example", elements=elements)

with tempfile.NamedTemporaryFile(suffix=".duc", delete=False) as tmp:
tmp.write(duc_bytes)
tmp_path = tmp.name

print("1. Parsing a .duc file from a file path...")

# You can pass a string path directly to parse_duc
parsed_data = duc.parse_duc(tmp_path)

print(f" Document Source: {parsed_data.source}")
print(f" Parsed {len(parsed_data.elements)} elements.")

print("\n2. Accessing element attributes (snake_case)...")

# Element properties are accessible via dot-notation with snake_case keys
# because parse_duc returns a DucData object.
first_element = parsed_data.elements[0]
print(f" Element ID: {first_element.id}")
print(f" Element Type: {first_element.type}")
print(f" Element Label: {first_element.label}")
print(f" Element Position: (X: {first_element.x}, Y: {first_element.y})")

print("\n3. Parsing directly from raw bytes...")

# You can also pass raw bytes directly to parse_duc
parsed_from_bytes = duc.parse_duc(duc_bytes)
print(f" Parsed successfully from bytes. Found {len(parsed_from_bytes.elements)} elements.")

# Clean up the temporary file
os.unlink(tmp_path)

print("\n✅ Parsing demo complete!")

if __name__ == "__main__":
main()
68 changes: 68 additions & 0 deletions packages/ducpy/src/examples/serialization_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
Example demonstrating how to serialize elements created by the Builder API into a .duc file.

This demo shows the correct pattern for taking in-memory python elements
and writing them to a raw `.duc` binary blob.
"""

import os
import ducpy as duc
from ducpy.builders.style_builders import create_fill_and_stroke_style, create_solid_content

def main():
print("Serialization Demo")
print("=" * 30)

print("1. Creating elements via Builder API...")
elements = []

# Create some basic elements
rect = (duc.ElementBuilder()
.at_position(0, 0)
.with_size(100, 50)
.with_label("Sample Rectangle")
.with_styles(create_fill_and_stroke_style(
fill_content=create_solid_content("#FF6B6B"),
stroke_content=create_solid_content("#2C3E50"),
stroke_width=2.0
))
.build_rectangle()
.build())
elements.append(rect)

ellipse = (duc.ElementBuilder()
.at_position(120, 0)
.with_size(60, 40)
.with_label("Sample Ellipse")
.with_styles(create_fill_and_stroke_style(
fill_content=create_solid_content("#4ECDC4"),
stroke_content=create_solid_content("#34495E"),
stroke_width=1.5
))
.build_ellipse()
.build())
elements.append(ellipse)

print(f" Created {len(elements)} elements.")

print("2. Serializing to .duc format...")
# NOTE: The serialize_duc function takes keyword arguments for elements,
# blocks, global state, etc. and bridges to the Rust native backend.
duc_bytes = duc.serialize_duc(
name="serialization_example",
elements=elements
)

print(f" Successfully serialized {len(duc_bytes)} bytes.")

# You would typically write this to a file
# output_file = "example.duc"
# with open(output_file, "wb") as f:
# f.write(duc_bytes)
# print(f"3. Saved to {output_file}")

print("\n✅ Serialization demo complete!")

if __name__ == "__main__":
main()
Loading
Loading