ducpy 3.3.5__tar.gz → 3.3.6__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.5 → ducpy-3.3.6}/Cargo.lock +1 -1
- {ducpy-3.3.5 → ducpy-3.3.6}/PKG-INFO +1 -1
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducpy/crate/Cargo.toml +1 -1
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/parse.rs +34 -5
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/serialize.rs +22 -8
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/types.rs +2 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/tests/common/mod.rs +1 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/pyproject.toml +4 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/schema/duc.sql +12 -2
- ducpy-3.3.6/schema/migrations/3000003_to_3000004.sql +41 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/__init__.py +1 -1
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/element_builders.py +54 -44
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/classes/ElementsClass.py +8 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/serialize.py +243 -2
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/utils/__init__.py +2 -1
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/utils/convert.py +47 -3
- ducpy-3.3.5/schema/migrations/3000003_to_3000004.sql +0 -20
- {ducpy-3.3.5 → ducpy-3.3.6}/Cargo.toml +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/LICENSE +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/README.md +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducpy/crate/src/lib.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/.gitignore +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/Cargo.toml +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/LICENSE +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/README.md +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/build.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/package.json +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/release.config.cjs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/api/document.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/api/meta.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/api/mod.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/api/version_control.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/db/bootstrap.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/db/mod.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/db/native.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/db/wasm.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/lib.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/src/serde_utils.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/sst-env.d.ts +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/tests/.gitignore +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/tests/parser_assets.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/packages/ducrs/tests/synthetic_roundtrip.rs +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/schema/migrations/3000000_to_3000001.sql +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/schema/migrations/3000001_to_3000002.sql +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/schema/migrations/3000002_to_3000003.sql +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/schema/search.sql +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/schema/version_control.sql +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/_version.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/__init__.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/block_instance_builder.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/block_utils.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/mutate_builder.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/sql_builder.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/state_builders.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/builders/style_builders.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/classes/DataStateClass.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/classes/__init__.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/enums.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/parse.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/search/__init__.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/search/search_elements.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/utils/constants.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/utils/io.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/utils/mutate_utils.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy/utils/rand_utils.py +0 -0
- {ducpy-3.3.5 → ducpy-3.3.6}/src/ducpy_native/__init__.py +0 -0
|
@@ -1460,17 +1460,20 @@ fn read_pdf_element(conn: &Connection, base: DucElementBase) -> ParseResult<DucE
|
|
|
1460
1460
|
}
|
|
1461
1461
|
|
|
1462
1462
|
fn read_doc_element(conn: &Connection, base: DucElementBase) -> ParseResult<DucElementEnum> {
|
|
1463
|
+
let id = base.id.clone();
|
|
1463
1464
|
let (file_id, grid_config) = read_document_grid_config(conn, &base.id)?;
|
|
1464
1465
|
let mut stmt = conn.prepare_cached(
|
|
1465
1466
|
"SELECT text FROM element_doc WHERE element_id = ?1"
|
|
1466
1467
|
)?;
|
|
1467
1468
|
let text: String = stmt.query_row(params![base.id], |row| row.get(0))?;
|
|
1469
|
+
let referenced_file_ids = read_doc_referenced_file_ids(conn, &id)?;
|
|
1468
1470
|
Ok(DucElementEnum::DucDocElement(DucDocElement {
|
|
1469
1471
|
base,
|
|
1470
1472
|
style: DucDocStyle {},
|
|
1471
1473
|
text,
|
|
1472
1474
|
grid_config,
|
|
1473
1475
|
file_id,
|
|
1476
|
+
referenced_file_ids,
|
|
1474
1477
|
}))
|
|
1475
1478
|
}
|
|
1476
1479
|
|
|
@@ -1512,11 +1515,7 @@ fn read_model_element(conn: &Connection, base: DucElementBase) -> ParseResult<Du
|
|
|
1512
1515
|
Ok((row.get::<_, Option<String>>(0)?, row.get::<_, Option<String>>(1)?, row.get::<_, Option<Vec<u8>>>(2)?))
|
|
1513
1516
|
})?;
|
|
1514
1517
|
|
|
1515
|
-
let
|
|
1516
|
-
"SELECT file_id FROM model_element_files WHERE element_id = ?1 ORDER BY sort_order"
|
|
1517
|
-
)?;
|
|
1518
|
-
let file_ids: Vec<String> = f_stmt.query_map(params![id], |row| row.get(0))?
|
|
1519
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
1518
|
+
let file_ids = read_element_file_ids(conn, &id)?;
|
|
1520
1519
|
|
|
1521
1520
|
let viewer_state = read_model_viewer_state(conn, &id)?;
|
|
1522
1521
|
|
|
@@ -1525,6 +1524,36 @@ fn read_model_element(conn: &Connection, base: DucElementBase) -> ParseResult<Du
|
|
|
1525
1524
|
}))
|
|
1526
1525
|
}
|
|
1527
1526
|
|
|
1527
|
+
fn read_element_file_ids(conn: &Connection, element_id: &str) -> ParseResult<Vec<String>> {
|
|
1528
|
+
let mut stmt = conn.prepare_cached(
|
|
1529
|
+
"SELECT file_id FROM model_element_files WHERE element_id = ?1 ORDER BY sort_order"
|
|
1530
|
+
)?;
|
|
1531
|
+
let file_ids = stmt.query_map(params![element_id], |row| row.get(0))?
|
|
1532
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
1533
|
+
Ok(file_ids)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
fn read_doc_referenced_file_ids(conn: &Connection, element_id: &str) -> ParseResult<Vec<String>> {
|
|
1537
|
+
if !table_exists(conn, "doc_element_referenced_files")? {
|
|
1538
|
+
return Ok(Vec::new());
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
let mut stmt = conn.prepare_cached(
|
|
1542
|
+
"SELECT file_id FROM doc_element_referenced_files WHERE element_id = ?1 ORDER BY sort_order"
|
|
1543
|
+
)?;
|
|
1544
|
+
let file_ids = stmt.query_map(params![element_id], |row| row.get(0))?
|
|
1545
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
1546
|
+
Ok(file_ids)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
fn table_exists(conn: &Connection, table_name: &str) -> ParseResult<bool> {
|
|
1550
|
+
Ok(conn.query_row(
|
|
1551
|
+
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)",
|
|
1552
|
+
params![table_name],
|
|
1553
|
+
|row| row.get::<_, i64>(0),
|
|
1554
|
+
)? != 0)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1528
1557
|
fn read_model_viewer_state(conn: &Connection, element_id: &str) -> ParseResult<Option<Viewer3DState>> {
|
|
1529
1558
|
let mut stmt = conn.prepare_cached(
|
|
1530
1559
|
"SELECT
|
|
@@ -456,6 +456,7 @@ fn write_element_wrapper(tx: &Transaction, wrapper: &ElementWrapper) -> Serializ
|
|
|
456
456
|
"INSERT INTO element_doc (element_id, text) VALUES (?1, ?2)",
|
|
457
457
|
params![e.base.id, e.text],
|
|
458
458
|
)?;
|
|
459
|
+
write_doc_referenced_file_ids(tx, &e.base.id, &e.referenced_file_ids)?;
|
|
459
460
|
}
|
|
460
461
|
DucElementEnum::DucTableElement(e) => {
|
|
461
462
|
write_base_element(tx, "table", &e.base)?;
|
|
@@ -834,14 +835,7 @@ fn write_model_element(tx: &Transaction, e: &DucModelElement) -> SerializeResult
|
|
|
834
835
|
params![e.base.id, e.model_type, e.code, e.thumbnail],
|
|
835
836
|
)?;
|
|
836
837
|
|
|
837
|
-
|
|
838
|
-
let mut stmt = tx.prepare_cached(
|
|
839
|
-
"INSERT INTO model_element_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
|
|
840
|
-
)?;
|
|
841
|
-
for (i, fid) in e.file_ids.iter().enumerate() {
|
|
842
|
-
stmt.execute(params![e.base.id, fid, i as i32])?;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
838
|
+
write_element_file_ids(tx, &e.base.id, &e.file_ids)?;
|
|
845
839
|
|
|
846
840
|
if let Some(ref vs) = e.viewer_state {
|
|
847
841
|
write_model_viewer_state(tx, &e.base.id, vs)?;
|
|
@@ -850,6 +844,26 @@ fn write_model_element(tx: &Transaction, e: &DucModelElement) -> SerializeResult
|
|
|
850
844
|
Ok(())
|
|
851
845
|
}
|
|
852
846
|
|
|
847
|
+
fn write_element_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String]) -> SerializeResult<()> {
|
|
848
|
+
let mut stmt = tx.prepare_cached(
|
|
849
|
+
"INSERT INTO model_element_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
|
|
850
|
+
)?;
|
|
851
|
+
for (i, fid) in file_ids.iter().enumerate() {
|
|
852
|
+
stmt.execute(params![element_id, fid, i as i32])?;
|
|
853
|
+
}
|
|
854
|
+
Ok(())
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
fn write_doc_referenced_file_ids(tx: &Transaction, element_id: &str, file_ids: &[String]) -> SerializeResult<()> {
|
|
858
|
+
let mut stmt = tx.prepare_cached(
|
|
859
|
+
"INSERT INTO doc_element_referenced_files (element_id, file_id, sort_order) VALUES (?1, ?2, ?3)"
|
|
860
|
+
)?;
|
|
861
|
+
for (i, fid) in file_ids.iter().enumerate() {
|
|
862
|
+
stmt.execute(params![element_id, fid, i as i32])?;
|
|
863
|
+
}
|
|
864
|
+
Ok(())
|
|
865
|
+
}
|
|
866
|
+
|
|
853
867
|
fn write_model_viewer_state(tx: &Transaction, element_id: &str, vs: &Viewer3DState) -> SerializeResult<()> {
|
|
854
868
|
let cam = &vs.camera;
|
|
855
869
|
let disp = &vs.display;
|
|
@@ -1045,6 +1045,8 @@ pub struct DucDocElement {
|
|
|
1045
1045
|
pub text: String,
|
|
1046
1046
|
pub grid_config: DocumentGridConfig,
|
|
1047
1047
|
pub file_id: Option<String>,
|
|
1048
|
+
#[serde(default)]
|
|
1049
|
+
pub referenced_file_ids: Vec<String>,
|
|
1048
1050
|
}
|
|
1049
1051
|
|
|
1050
1052
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
@@ -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 = 3000004;
|
|
5
5
|
PRAGMA journal_mode = WAL;
|
|
6
6
|
PRAGMA foreign_keys = ON;
|
|
7
7
|
PRAGMA synchronous = NORMAL;
|
|
@@ -702,6 +702,16 @@ CREATE TABLE element_doc (
|
|
|
702
702
|
text TEXT NOT NULL DEFAULT ''
|
|
703
703
|
); -- no WITHOUT ROWID: implicit integer rowid needed for FTS5 content sync
|
|
704
704
|
|
|
705
|
+
-- External files referenced by a doc element's Typst source (images, data files, etc.).
|
|
706
|
+
CREATE TABLE doc_element_referenced_files (
|
|
707
|
+
element_id TEXT NOT NULL REFERENCES element_doc(element_id) ON DELETE CASCADE,
|
|
708
|
+
file_id TEXT NOT NULL,
|
|
709
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
710
|
+
PRIMARY KEY (element_id, file_id)
|
|
711
|
+
) WITHOUT ROWID;
|
|
712
|
+
|
|
713
|
+
CREATE INDEX idx_doc_referenced_files_file ON doc_element_referenced_files(file_id);
|
|
714
|
+
|
|
705
715
|
-- Table element. Source of truth is the linked xlsx file.
|
|
706
716
|
CREATE TABLE element_table (
|
|
707
717
|
element_id TEXT PRIMARY KEY REFERENCES elements(id) ON DELETE CASCADE,
|
|
@@ -844,4 +854,4 @@ CREATE INDEX idx_external_file_revisions_file ON external_file_revisions(file_id
|
|
|
844
854
|
CREATE TABLE external_file_revision_data (
|
|
845
855
|
revision_id TEXT PRIMARY KEY REFERENCES external_file_revisions(id) ON DELETE CASCADE,
|
|
846
856
|
data BLOB NOT NULL
|
|
847
|
-
) WITHOUT ROWID;
|
|
857
|
+
) WITHOUT ROWID;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Migration: 3000003 → 3000004
|
|
2
|
+
-- Rename element_model.svg_path (TEXT) to thumbnail (BLOB), and add dedicated
|
|
3
|
+
-- references for external files used by doc element Typst source.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE element_model_new (
|
|
6
|
+
element_id TEXT PRIMARY KEY REFERENCES elements(id) ON DELETE CASCADE,
|
|
7
|
+
model_type TEXT,
|
|
8
|
+
code TEXT,
|
|
9
|
+
thumbnail BLOB
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
INSERT INTO element_model_new (element_id, model_type, code, thumbnail)
|
|
13
|
+
SELECT element_id, model_type, code, CAST(svg_path AS BLOB)
|
|
14
|
+
FROM element_model;
|
|
15
|
+
|
|
16
|
+
DROP TABLE element_model;
|
|
17
|
+
|
|
18
|
+
ALTER TABLE element_model_new RENAME TO element_model;
|
|
19
|
+
|
|
20
|
+
CREATE INDEX idx_element_model_type ON element_model(model_type);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS doc_element_referenced_files (
|
|
23
|
+
element_id TEXT NOT NULL REFERENCES element_doc(element_id) ON DELETE CASCADE,
|
|
24
|
+
file_id TEXT NOT NULL,
|
|
25
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
PRIMARY KEY (element_id, file_id)
|
|
27
|
+
) WITHOUT ROWID;
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_doc_referenced_files_file ON doc_element_referenced_files(file_id);
|
|
30
|
+
|
|
31
|
+
-- Defensive cleanup for prerelease files that may have stored doc references in
|
|
32
|
+
-- model_element_files before this dedicated table existed.
|
|
33
|
+
INSERT OR IGNORE INTO doc_element_referenced_files (element_id, file_id, sort_order)
|
|
34
|
+
SELECT mf.element_id, mf.file_id, mf.sort_order
|
|
35
|
+
FROM model_element_files mf
|
|
36
|
+
INNER JOIN element_doc d ON d.element_id = mf.element_id;
|
|
37
|
+
|
|
38
|
+
DELETE FROM model_element_files
|
|
39
|
+
WHERE element_id IN (SELECT element_id FROM element_doc);
|
|
40
|
+
|
|
41
|
+
PRAGMA user_version = 3000004;
|
|
@@ -27,6 +27,6 @@ from .classes import *
|
|
|
27
27
|
from .enums import *
|
|
28
28
|
from .parse import (DucData, get_external_file, list_external_files, parse_duc,
|
|
29
29
|
parse_duc_lazy)
|
|
30
|
-
from .serialize import DUC_SCHEMA_VERSION, serialize_duc
|
|
30
|
+
from .serialize import DUC_SCHEMA_VERSION, DucSerializationValidationError, serialize_duc
|
|
31
31
|
from .search import *
|
|
32
32
|
from .utils import *
|
|
@@ -55,45 +55,46 @@ def _create_element_wrapper(element_class, base_params, element_params, explicit
|
|
|
55
55
|
base_params.setdefault('region_ids', [])
|
|
56
56
|
base_params.setdefault('instance_id', None)
|
|
57
57
|
|
|
58
|
-
base_params.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
58
|
+
if base_params.get('styles') is None:
|
|
59
|
+
base_params['styles'] = DucElementStylesBase(
|
|
60
|
+
roundness=0.0,
|
|
61
|
+
stroke=[ElementStroke(
|
|
62
|
+
content=ElementContentBase(
|
|
63
|
+
preference=None,
|
|
64
|
+
src="",
|
|
65
|
+
visible=True,
|
|
66
|
+
opacity=1.0,
|
|
67
|
+
tiling=None,
|
|
68
|
+
hatch=None,
|
|
69
|
+
image_filter=DucImageFilter(brightness=1.0, contrast=1.0)
|
|
70
|
+
),
|
|
71
|
+
width=DEFAULT_STROKE_WIDTH,
|
|
72
|
+
style=StrokeStyle(
|
|
73
|
+
preference=STROKE_PREFERENCE.SOLID,
|
|
74
|
+
dash=None,
|
|
75
|
+
dash_line_override=None,
|
|
76
|
+
cap=None,
|
|
77
|
+
join=None,
|
|
78
|
+
dash_cap=None,
|
|
79
|
+
miter_limit=None
|
|
80
|
+
),
|
|
81
|
+
placement=STROKE_PLACEMENT.CENTER,
|
|
82
|
+
stroke_sides=None
|
|
83
|
+
)],
|
|
84
|
+
background=[ElementBackground(
|
|
85
|
+
content=ElementContentBase(
|
|
86
|
+
preference=None,
|
|
87
|
+
src="",
|
|
88
|
+
visible=True,
|
|
89
|
+
opacity=1.0,
|
|
90
|
+
tiling=None,
|
|
91
|
+
hatch=None,
|
|
92
|
+
image_filter=DucImageFilter(brightness=1.0, contrast=1.0)
|
|
93
|
+
)
|
|
94
|
+
)],
|
|
95
|
+
opacity=1.0,
|
|
96
|
+
blending=None
|
|
97
|
+
)
|
|
97
98
|
|
|
98
99
|
base_element = DucElementBase(**base_params)
|
|
99
100
|
|
|
@@ -168,8 +169,9 @@ def _create_element_wrapper(element_class, base_params, element_params, explicit
|
|
|
168
169
|
base=base_element,
|
|
169
170
|
style=element_params.get('style') or DucDocStyle(),
|
|
170
171
|
text=element_params.get('text', ""),
|
|
171
|
-
grid_config=element_params.get('grid_config'
|
|
172
|
+
grid_config=element_params.get('grid_config') or default_grid_config,
|
|
172
173
|
file_id=element_params.get('file_id'),
|
|
174
|
+
referenced_file_ids=element_params.get('referenced_file_ids') or [],
|
|
173
175
|
)
|
|
174
176
|
elif element_class == DucFrameElement:
|
|
175
177
|
stack_base = element_params.get('stack_base')
|
|
@@ -698,6 +700,8 @@ class DocElementBuilder(ElementSpecificBuilder):
|
|
|
698
700
|
self.extra["style"] = style; return self
|
|
699
701
|
def with_file_id(self, file_id: str):
|
|
700
702
|
self.extra["file_id"] = file_id; return self
|
|
703
|
+
def with_referenced_file_ids(self, referenced_file_ids: List[str]):
|
|
704
|
+
self.extra["referenced_file_ids"] = referenced_file_ids; return self
|
|
701
705
|
def with_grid_config(self, grid_config: DocumentGridConfig):
|
|
702
706
|
self.extra["grid_config"] = grid_config; return self
|
|
703
707
|
|
|
@@ -707,6 +711,7 @@ class DocElementBuilder(ElementSpecificBuilder):
|
|
|
707
711
|
"style": self.extra.get('style'),
|
|
708
712
|
"text": self.extra.get('text', ""),
|
|
709
713
|
"file_id": self.extra.get('file_id'),
|
|
714
|
+
"referenced_file_ids": self.extra.get('referenced_file_ids', []),
|
|
710
715
|
"grid_config": self.extra.get('grid_config'),
|
|
711
716
|
}
|
|
712
717
|
return _create_element_wrapper(DucDocElement, base_params, element_params,
|
|
@@ -727,11 +732,12 @@ class EmbeddableElementBuilder(ElementSpecificBuilder):
|
|
|
727
732
|
|
|
728
733
|
class ModelElementBuilder(ElementSpecificBuilder):
|
|
729
734
|
def with_model_type(self, model_type: str):
|
|
735
|
+
valid_types = {"python", "ifc", "dxf", "dwg", "step", "stl"}
|
|
736
|
+
if model_type not in valid_types:
|
|
737
|
+
raise ValueError(f"Invalid model_type: {model_type}. Allowed types: {sorted(list(valid_types))}")
|
|
730
738
|
self.extra["model_type"] = model_type; return self
|
|
731
739
|
def with_code(self, code: str):
|
|
732
740
|
self.extra["code"] = code; return self
|
|
733
|
-
def with_svg_path(self, svg_path: str):
|
|
734
|
-
self.extra["svg_path"] = svg_path; return self
|
|
735
741
|
def with_file_ids(self, file_ids: List[str]):
|
|
736
742
|
self.extra["file_ids"] = file_ids; return self
|
|
737
743
|
def with_viewer_state(self, viewer_state):
|
|
@@ -739,11 +745,15 @@ class ModelElementBuilder(ElementSpecificBuilder):
|
|
|
739
745
|
|
|
740
746
|
def build(self) -> ElementWrapper:
|
|
741
747
|
base_params = self.base.__dict__.copy()
|
|
748
|
+
model_type = self.extra.get('model_type', 'python')
|
|
749
|
+
valid_types = {"python", "ifc", "dxf", "dwg", "step", "stl"}
|
|
750
|
+
if model_type not in valid_types:
|
|
751
|
+
raise ValueError(f"Invalid model_type: {model_type}. Allowed types: {sorted(list(valid_types))}")
|
|
752
|
+
|
|
742
753
|
element_params = {
|
|
743
754
|
"file_ids": self.extra.get('file_ids', []),
|
|
744
|
-
"model_type":
|
|
755
|
+
"model_type": model_type,
|
|
745
756
|
"code": self.extra.get('code'),
|
|
746
|
-
"svg_path": self.extra.get('svg_path'),
|
|
747
757
|
"viewer_state": self.extra.get('viewer_state'),
|
|
748
758
|
}
|
|
749
759
|
return _create_element_wrapper(DucModelElement, base_params, element_params,
|
|
@@ -393,6 +393,7 @@ class DucDocElement:
|
|
|
393
393
|
text: str
|
|
394
394
|
grid_config: DocumentGridConfig
|
|
395
395
|
file_id: Optional[str] = None
|
|
396
|
+
referenced_file_ids: Optional[List[str]] = None
|
|
396
397
|
|
|
397
398
|
@dataclass
|
|
398
399
|
class DucTableElement:
|
|
@@ -482,6 +483,13 @@ class DucModelElement:
|
|
|
482
483
|
thumbnail: Optional[bytes] = None
|
|
483
484
|
viewer_state: Optional[Viewer3DState] = None
|
|
484
485
|
|
|
486
|
+
def __post_init__(self):
|
|
487
|
+
valid_types = {"python", "ifc", "dxf", "dwg", "step", "stl"}
|
|
488
|
+
if self.model_type is None:
|
|
489
|
+
self.model_type = "python"
|
|
490
|
+
if self.model_type not in valid_types:
|
|
491
|
+
raise ValueError(f"Invalid model_type: {self.model_type}. Allowed types: {sorted(list(valid_types))}")
|
|
492
|
+
|
|
485
493
|
|
|
486
494
|
# =============== BLOCK DEFINITIONS ===============
|
|
487
495
|
|
|
@@ -7,9 +7,12 @@ from __future__ import annotations
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
10
13
|
from dataclasses import asdict, is_dataclass
|
|
11
14
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
16
|
|
|
14
17
|
import ducpy_native
|
|
15
18
|
from ducpy.utils.convert import (deep_snake_to_camel, snake_to_camel,
|
|
@@ -18,6 +21,14 @@ from ducpy.utils.convert import (deep_snake_to_camel, snake_to_camel,
|
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
class DucSerializationValidationError(ValueError):
|
|
25
|
+
"""Raised when embedded element code fails validation before serialization."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, failures: List[str]):
|
|
28
|
+
self.failures = failures
|
|
29
|
+
super().__init__("Embedded code validation failed:\n" + "\n".join(f"- {failure}" for failure in failures))
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
def _find_schema_file() -> Path | None:
|
|
22
33
|
env_path = Path(os.environ["DUC_SCHEMA_DIR"]) if "DUC_SCHEMA_DIR" in os.environ else None
|
|
23
34
|
if env_path is not None:
|
|
@@ -200,6 +211,218 @@ def _convert_list(items: Optional[list]) -> Optional[list]:
|
|
|
200
211
|
return [to_serializable(item) for item in items]
|
|
201
212
|
|
|
202
213
|
|
|
214
|
+
def _element_label(element: Dict[str, Any]) -> str:
|
|
215
|
+
element_id = element.get("id") or element.get("elementId") or "unknown"
|
|
216
|
+
element_type = element.get("type") or "element"
|
|
217
|
+
return f"{element_type} {element_id}"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _find_revision(revisions: Any, active_revision_id: str) -> Optional[Dict[str, Any]]:
|
|
221
|
+
if not isinstance(revisions, dict):
|
|
222
|
+
return None
|
|
223
|
+
if active_revision_id in revisions:
|
|
224
|
+
return revisions[active_revision_id]
|
|
225
|
+
|
|
226
|
+
# Try simple camelCase match fallback since deep_snake_to_camel converts keys
|
|
227
|
+
camel_id = snake_to_camel(active_revision_id)
|
|
228
|
+
if camel_id in revisions:
|
|
229
|
+
return revisions[camel_id]
|
|
230
|
+
|
|
231
|
+
for r in revisions.values():
|
|
232
|
+
if isinstance(r, dict):
|
|
233
|
+
r_id = r.get("id") or r.get("key")
|
|
234
|
+
if r_id == active_revision_id or r_id == camel_id:
|
|
235
|
+
return r
|
|
236
|
+
|
|
237
|
+
if revisions:
|
|
238
|
+
first_val = next(iter(revisions.values()))
|
|
239
|
+
if isinstance(first_val, dict):
|
|
240
|
+
return first_val
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _write_python_external_files(
|
|
246
|
+
project_dir: Path,
|
|
247
|
+
files_meta: Optional[Dict[str, Any]],
|
|
248
|
+
files_data: Optional[Dict[str, bytes]],
|
|
249
|
+
) -> Dict[str, str]:
|
|
250
|
+
resolved_paths: Dict[str, str] = {}
|
|
251
|
+
if not files_meta or not files_data:
|
|
252
|
+
return resolved_paths
|
|
253
|
+
|
|
254
|
+
for file_id, meta in files_meta.items():
|
|
255
|
+
if not isinstance(meta, dict):
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
active_revision_id = meta.get("activeRevisionId") or meta.get("active_revision_id")
|
|
259
|
+
revisions = meta.get("revisions") or {}
|
|
260
|
+
revision = _find_revision(revisions, active_revision_id) if active_revision_id else None
|
|
261
|
+
if not active_revision_id or not isinstance(revision, dict):
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
data = files_data.get(active_revision_id)
|
|
265
|
+
if data is None:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
source_name = revision.get("sourceName") or revision.get("source_name") or "file"
|
|
269
|
+
safe_name = re.sub(r"[^a-zA-Z0-9._-]", "_", str(source_name).strip() or "file")
|
|
270
|
+
target = project_dir / "scopture-files" / str(file_id) / safe_name
|
|
271
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
target.write_bytes(bytes(data))
|
|
273
|
+
resolved_paths[file_id] = str(target.resolve())
|
|
274
|
+
|
|
275
|
+
return resolved_paths
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _run_python_validation(
|
|
279
|
+
code: str,
|
|
280
|
+
label: str,
|
|
281
|
+
timeout_seconds: Optional[float],
|
|
282
|
+
files_meta: Optional[Dict[str, Any]] = None,
|
|
283
|
+
files_data: Optional[Dict[str, bytes]] = None,
|
|
284
|
+
) -> Optional[str]:
|
|
285
|
+
with tempfile.TemporaryDirectory(prefix="ducpy-model-") as tmpdir:
|
|
286
|
+
tmp_path = Path(tmpdir)
|
|
287
|
+
resolved_files = _write_python_external_files(tmp_path, files_meta, files_data)
|
|
288
|
+
|
|
289
|
+
import json
|
|
290
|
+
resolved_files_json = json.dumps(resolved_files)
|
|
291
|
+
|
|
292
|
+
header = f"""
|
|
293
|
+
import json
|
|
294
|
+
import os
|
|
295
|
+
import sys
|
|
296
|
+
|
|
297
|
+
_RESOLVED_EXTERNAL_FILES = json.loads({repr(resolved_files_json)})
|
|
298
|
+
|
|
299
|
+
def resolve_external_file(file_id):
|
|
300
|
+
if file_id in _RESOLVED_EXTERNAL_FILES:
|
|
301
|
+
return _RESOLVED_EXTERNAL_FILES[file_id]
|
|
302
|
+
raise FileNotFoundError(f"External file '{{file_id}}' not found in validation sandbox.")
|
|
303
|
+
|
|
304
|
+
globals()["resolve_external_file"] = resolve_external_file
|
|
305
|
+
import builtins
|
|
306
|
+
builtins.resolve_external_file = resolve_external_file
|
|
307
|
+
"""
|
|
308
|
+
script_path = tmp_path / "model.py"
|
|
309
|
+
script_path.write_text(header + "\n" + code, encoding="utf-8")
|
|
310
|
+
|
|
311
|
+
run_kwargs = {
|
|
312
|
+
"cwd": tmpdir,
|
|
313
|
+
"text": True,
|
|
314
|
+
"stdout": subprocess.PIPE,
|
|
315
|
+
"stderr": subprocess.PIPE,
|
|
316
|
+
"check": False,
|
|
317
|
+
}
|
|
318
|
+
if timeout_seconds is not None:
|
|
319
|
+
run_kwargs["timeout"] = timeout_seconds
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
completed = subprocess.run(
|
|
323
|
+
[sys.executable, str(script_path)],
|
|
324
|
+
**run_kwargs,
|
|
325
|
+
)
|
|
326
|
+
except subprocess.TimeoutExpired as exc:
|
|
327
|
+
return f"{label}: Python validation timed out after {timeout_seconds:g}s"
|
|
328
|
+
except OSError as exc:
|
|
329
|
+
return f"{label}: failed to start Python validation: {exc}"
|
|
330
|
+
|
|
331
|
+
if completed.returncode == 0:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
output = (completed.stderr or completed.stdout or "Python process exited with an error").strip()
|
|
335
|
+
return f"{label}: Python validation failed\n{output}"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _write_typst_external_files(project_dir: Path, files_meta: Optional[Dict[str, Any]], files_data: Optional[Dict[str, bytes]]) -> None:
|
|
339
|
+
if not files_meta or not files_data:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
for file_id, meta in files_meta.items():
|
|
343
|
+
if not isinstance(meta, dict):
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
active_revision_id = meta.get("activeRevisionId") or meta.get("active_revision_id")
|
|
347
|
+
revisions = meta.get("revisions") or {}
|
|
348
|
+
revision = _find_revision(revisions, active_revision_id) if active_revision_id else None
|
|
349
|
+
if not active_revision_id or not isinstance(revision, dict):
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
data = files_data.get(active_revision_id)
|
|
353
|
+
if data is None:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
source_name = revision.get("sourceName") or revision.get("source_name") or "file"
|
|
357
|
+
safe_name = re.sub(r"[^a-zA-Z0-9._-]", "_", str(source_name).strip() or "file")
|
|
358
|
+
target = project_dir / "scopture-files" / str(file_id) / safe_name
|
|
359
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
target.write_bytes(bytes(data))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _run_typst_validation(
|
|
364
|
+
code: str,
|
|
365
|
+
label: str,
|
|
366
|
+
timeout_seconds: float,
|
|
367
|
+
files_meta: Optional[Dict[str, Any]],
|
|
368
|
+
files_data: Optional[Dict[str, bytes]],
|
|
369
|
+
) -> Optional[str]:
|
|
370
|
+
try:
|
|
371
|
+
import typst # type: ignore[import-not-found]
|
|
372
|
+
except ModuleNotFoundError:
|
|
373
|
+
return f"{label}: Typst validation requires the 'typst' package. Install it with 'pip install typst'."
|
|
374
|
+
|
|
375
|
+
with tempfile.TemporaryDirectory(prefix="ducpy-typst-") as tmpdir:
|
|
376
|
+
project_dir = Path(tmpdir)
|
|
377
|
+
main_path = project_dir / "main.typ"
|
|
378
|
+
main_path.write_text(code.replace('"/scopture-files/', '"scopture-files/'), encoding="utf-8")
|
|
379
|
+
_write_typst_external_files(project_dir, files_meta, files_data)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
try:
|
|
383
|
+
typst.compile(str(main_path), format="pdf")
|
|
384
|
+
except TypeError:
|
|
385
|
+
typst.compile(str(main_path))
|
|
386
|
+
except Exception as exc:
|
|
387
|
+
return f"{label}: Typst validation failed\n{exc}"
|
|
388
|
+
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _validate_embedded_code(
|
|
393
|
+
elements: List[Any],
|
|
394
|
+
files_meta: Optional[Dict[str, Any]],
|
|
395
|
+
files_data: Optional[Dict[str, bytes]],
|
|
396
|
+
timeout_seconds: float,
|
|
397
|
+
) -> None:
|
|
398
|
+
failures: List[str] = []
|
|
399
|
+
|
|
400
|
+
for element in elements:
|
|
401
|
+
if not isinstance(element, dict):
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
element_type = element.get("type")
|
|
405
|
+
label = _element_label(element)
|
|
406
|
+
|
|
407
|
+
if element_type == "model":
|
|
408
|
+
code = element.get("code")
|
|
409
|
+
if isinstance(code, str) and code.strip():
|
|
410
|
+
error = _run_python_validation(code, label, timeout_seconds, files_meta, files_data)
|
|
411
|
+
if error:
|
|
412
|
+
failures.append(error)
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
if element_type == "doc":
|
|
416
|
+
text = element.get("text")
|
|
417
|
+
if isinstance(text, str) and text.strip():
|
|
418
|
+
error = _run_typst_validation(text, label, timeout_seconds, files_meta, files_data)
|
|
419
|
+
if error:
|
|
420
|
+
failures.append(error)
|
|
421
|
+
|
|
422
|
+
if failures:
|
|
423
|
+
raise DucSerializationValidationError(failures)
|
|
424
|
+
|
|
425
|
+
|
|
203
426
|
def serialize_duc(
|
|
204
427
|
name: str,
|
|
205
428
|
thumbnail: Optional[bytes] = None,
|
|
@@ -215,6 +438,8 @@ def serialize_duc(
|
|
|
215
438
|
regions: Optional[list] = None,
|
|
216
439
|
layers: Optional[list] = None,
|
|
217
440
|
external_files: Optional[list] = None,
|
|
441
|
+
validate_embedded_code: bool = True,
|
|
442
|
+
validation_timeout_seconds: Optional[float] = None,
|
|
218
443
|
) -> bytes:
|
|
219
444
|
"""Serialize elements and document state to raw ``.duc`` binary format.
|
|
220
445
|
|
|
@@ -253,6 +478,12 @@ def serialize_duc(
|
|
|
253
478
|
List of document layers.
|
|
254
479
|
external_files : Optional[list], default=None
|
|
255
480
|
List of external files (e.g., embedded images or PDFs).
|
|
481
|
+
validate_embedded_code : bool, default=True
|
|
482
|
+
Validate model Python code and document source before native serialization.
|
|
483
|
+
This is intended for server-side CPython usage and raises
|
|
484
|
+
DucSerializationValidationError with per-element diagnostics on failure.
|
|
485
|
+
validation_timeout_seconds : float, optional, default=None
|
|
486
|
+
Timeout used for each embedded code validation step. If None, no timeout is applied.
|
|
256
487
|
|
|
257
488
|
Returns
|
|
258
489
|
-------
|
|
@@ -263,12 +494,22 @@ def serialize_duc(
|
|
|
263
494
|
|
|
264
495
|
files_meta, files_data = _convert_external_files(external_files)
|
|
265
496
|
|
|
497
|
+
serialized_elements = [_element_to_camel(e) for e in (elements or [])]
|
|
498
|
+
|
|
499
|
+
if validate_embedded_code:
|
|
500
|
+
_validate_embedded_code(
|
|
501
|
+
serialized_elements,
|
|
502
|
+
files_meta,
|
|
503
|
+
files_data,
|
|
504
|
+
validation_timeout_seconds,
|
|
505
|
+
)
|
|
506
|
+
|
|
266
507
|
data: Dict[str, Any] = {
|
|
267
508
|
"type": "duc",
|
|
268
509
|
"version": DUC_SCHEMA_VERSION,
|
|
269
510
|
"source": f"ducpy_{name}",
|
|
270
511
|
"thumbnail": thumb,
|
|
271
|
-
"elements":
|
|
512
|
+
"elements": serialized_elements,
|
|
272
513
|
"blocks": _convert_list(blocks) or [],
|
|
273
514
|
"blockInstances": _convert_list(block_instances) or [],
|
|
274
515
|
"blockCollections": _convert_list(block_collections) or [],
|
|
@@ -60,7 +60,7 @@ def deep_snake_to_camel(obj: Any) -> Any:
|
|
|
60
60
|
if is_dataclass(obj) and not isinstance(obj, type):
|
|
61
61
|
return deep_snake_to_camel(asdict(obj))
|
|
62
62
|
if isinstance(obj, bytes):
|
|
63
|
-
return
|
|
63
|
+
return obj
|
|
64
64
|
return obj
|
|
65
65
|
|
|
66
66
|
|
|
@@ -80,7 +80,7 @@ def to_serializable(obj: Any) -> Any:
|
|
|
80
80
|
|
|
81
81
|
Handles:
|
|
82
82
|
- Dataclass instances → dict (recursively)
|
|
83
|
-
- bytes →
|
|
83
|
+
- bytes → bytes (native serializer accepts PyBytes for binary fields)
|
|
84
84
|
- Nested base/styles/linear_base/stack_element_base → flattened
|
|
85
85
|
- snake_case keys → camelCase keys
|
|
86
86
|
"""
|
|
@@ -97,5 +97,49 @@ def to_serializable(obj: Any) -> Any:
|
|
|
97
97
|
if isinstance(obj, list):
|
|
98
98
|
return [to_serializable(item) for item in obj]
|
|
99
99
|
if isinstance(obj, bytes):
|
|
100
|
-
return
|
|
100
|
+
return obj
|
|
101
101
|
return obj
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def extract_embedded_code(func) -> str:
|
|
105
|
+
"""Extracts the body of a Python function as a clean standalone script.
|
|
106
|
+
|
|
107
|
+
This is highly useful for defining CAD/BIM model generation code as actual,
|
|
108
|
+
syntax-highlighted Python functions inside examples/tests, and then serializing
|
|
109
|
+
them to DUC.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
func: The Python function/callable to extract the body from.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The body of the function as a clean, dedented string.
|
|
116
|
+
"""
|
|
117
|
+
import inspect
|
|
118
|
+
import textwrap
|
|
119
|
+
|
|
120
|
+
if not callable(func):
|
|
121
|
+
raise TypeError(f"extract_embedded_code expects a callable, got {type(func).__name__}")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
source = inspect.getsource(func)
|
|
125
|
+
except OSError as exc:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Could not retrieve source code for {func.__name__ if hasattr(func, '__name__') else func}. "
|
|
128
|
+
"Ensure the function is defined in a physical file rather than dynamically."
|
|
129
|
+
) from exc
|
|
130
|
+
|
|
131
|
+
lines = source.splitlines()
|
|
132
|
+
|
|
133
|
+
# Locate the starting 'def ' statement (skipping decorators)
|
|
134
|
+
try:
|
|
135
|
+
def_idx = next(i for i, line in enumerate(lines) if line.strip().startswith("def "))
|
|
136
|
+
except StopIteration:
|
|
137
|
+
# If no 'def ' was found (e.g. lambda), return the whole source dedented
|
|
138
|
+
return textwrap.dedent(source)
|
|
139
|
+
|
|
140
|
+
body_lines = lines[def_idx + 1:]
|
|
141
|
+
|
|
142
|
+
# Reconstruct and dedent the body
|
|
143
|
+
body_content = "\n".join(body_lines)
|
|
144
|
+
return textwrap.dedent(body_content)
|
|
145
|
+
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
-- Migration: 3000003 → 3000004
|
|
2
|
-
-- Rename element_model.svg_path (TEXT) to thumbnail (BLOB).
|
|
3
|
-
-- SQLite does not support ALTER COLUMN, so we recreate the table.
|
|
4
|
-
|
|
5
|
-
CREATE TABLE element_model_new (
|
|
6
|
-
element_id TEXT PRIMARY KEY REFERENCES elements(id) ON DELETE CASCADE,
|
|
7
|
-
model_type TEXT,
|
|
8
|
-
code TEXT,
|
|
9
|
-
thumbnail BLOB
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
INSERT INTO element_model_new (element_id, model_type, code, thumbnail)
|
|
13
|
-
SELECT element_id, model_type, code, CAST(svg_path AS BLOB)
|
|
14
|
-
FROM element_model;
|
|
15
|
-
|
|
16
|
-
DROP TABLE element_model;
|
|
17
|
-
|
|
18
|
-
ALTER TABLE element_model_new RENAME TO element_model;
|
|
19
|
-
|
|
20
|
-
CREATE INDEX idx_element_model_type ON element_model(model_type);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|