ducpy 3.3.6__tar.gz → 3.3.8__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.
Files changed (66) hide show
  1. {ducpy-3.3.6 → ducpy-3.3.8}/Cargo.lock +1 -1
  2. {ducpy-3.3.6 → ducpy-3.3.8}/PKG-INFO +1 -1
  3. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducpy/crate/Cargo.toml +1 -1
  4. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducpy/crate/src/lib.rs +45 -0
  5. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/Cargo.toml +9 -0
  6. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/build.rs +20 -16
  7. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/db/bootstrap.rs +14 -0
  8. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/db/mod.rs +1 -1
  9. {ducpy-3.3.6 → ducpy-3.3.8}/pyproject.toml +0 -2
  10. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/sql_builder.py +16 -56
  11. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/state_builders.py +3 -3
  12. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/serialize.py +3 -72
  13. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/utils/convert.py +11 -3
  14. ducpy-3.3.8/src/ducpy_native/__init__.py +28 -0
  15. ducpy-3.3.6/packages/ducrs/.gitignore +0 -21
  16. ducpy-3.3.6/packages/ducrs/package.json +0 -13
  17. ducpy-3.3.6/packages/ducrs/release.config.cjs +0 -31
  18. ducpy-3.3.6/packages/ducrs/sst-env.d.ts +0 -10
  19. ducpy-3.3.6/packages/ducrs/tests/.gitignore +0 -2
  20. ducpy-3.3.6/packages/ducrs/tests/common/mod.rs +0 -1009
  21. ducpy-3.3.6/packages/ducrs/tests/parser_assets.rs +0 -227
  22. ducpy-3.3.6/packages/ducrs/tests/synthetic_roundtrip.rs +0 -25
  23. ducpy-3.3.6/src/ducpy/_version.py +0 -3
  24. ducpy-3.3.6/src/ducpy_native/__init__.py +0 -19
  25. {ducpy-3.3.6 → ducpy-3.3.8}/Cargo.toml +0 -0
  26. {ducpy-3.3.6 → ducpy-3.3.8}/LICENSE +0 -0
  27. {ducpy-3.3.6 → ducpy-3.3.8}/README.md +0 -0
  28. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/LICENSE +0 -0
  29. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/README.md +0 -0
  30. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/duc.sql +0 -0
  31. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/migrations/3000000_to_3000001.sql +0 -0
  32. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/migrations/3000001_to_3000002.sql +0 -0
  33. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/migrations/3000002_to_3000003.sql +0 -0
  34. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/migrations/3000003_to_3000004.sql +0 -0
  35. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/search.sql +0 -0
  36. {ducpy-3.3.6 → ducpy-3.3.8/packages/ducrs}/schema/version_control.sql +0 -0
  37. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/api/document.rs +0 -0
  38. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/api/meta.rs +0 -0
  39. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/api/mod.rs +0 -0
  40. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/api/version_control.rs +0 -0
  41. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/db/native.rs +0 -0
  42. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/db/wasm.rs +0 -0
  43. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/lib.rs +0 -0
  44. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/parse.rs +0 -0
  45. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/serde_utils.rs +0 -0
  46. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/serialize.rs +0 -0
  47. {ducpy-3.3.6 → ducpy-3.3.8}/packages/ducrs/src/types.rs +0 -0
  48. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/__init__.py +0 -0
  49. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/__init__.py +0 -0
  50. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/block_instance_builder.py +0 -0
  51. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/block_utils.py +0 -0
  52. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/element_builders.py +0 -0
  53. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/mutate_builder.py +0 -0
  54. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/builders/style_builders.py +0 -0
  55. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/classes/DataStateClass.py +0 -0
  56. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/classes/ElementsClass.py +0 -0
  57. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/classes/__init__.py +0 -0
  58. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/enums.py +0 -0
  59. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/parse.py +0 -0
  60. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/search/__init__.py +0 -0
  61. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/search/search_elements.py +0 -0
  62. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/utils/__init__.py +0 -0
  63. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/utils/constants.py +0 -0
  64. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/utils/io.py +0 -0
  65. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/utils/mutate_utils.py +0 -0
  66. {ducpy-3.3.6 → ducpy-3.3.8}/src/ducpy/utils/rand_utils.py +0 -0
@@ -412,7 +412,7 @@ dependencies = [
412
412
 
413
413
  [[package]]
414
414
  name = "ducpy-native"
415
- version = "3.3.6"
415
+ version = "3.3.8"
416
416
  dependencies = [
417
417
  "duc",
418
418
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducpy
3
- Version: 3.3.6
3
+ Version: 3.3.8
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Requires-Dist: nanoid>=2.0.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ducpy-native"
3
- version = "3.3.6"
3
+ version = "3.3.8"
4
4
  edition = "2021"
5
5
  description = "Python native extension for the duc file format (used by ducpy)"
6
6
  publish = false
@@ -58,6 +58,45 @@ fn list_external_files(py: Python<'_>, buf: &[u8]) -> PyResult<PyObject> {
58
58
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))
59
59
  }
60
60
 
61
+ /// Returns the current DUC schema version as a semver string (e.g. "3.0.0").
62
+ #[pyfunction]
63
+ fn get_schema_version() -> PyResult<String> {
64
+ Ok(duc::db::bootstrap::CURRENT_SCHEMA_VERSION_SEMVER.into())
65
+ }
66
+
67
+ /// Returns the raw integer schema version (e.g. 3000000).
68
+ #[pyfunction]
69
+ fn get_schema_version_int() -> PyResult<i64> {
70
+ Ok(duc::db::bootstrap::current_schema_version_int())
71
+ }
72
+
73
+ /// Returns the canonical `duc.sql` schema string.
74
+ #[pyfunction]
75
+ fn get_duc_schema_sql() -> PyResult<String> {
76
+ Ok(duc::db::bootstrap::DUC_SCHEMA_SQL.into())
77
+ }
78
+
79
+ /// Returns the `version_control.sql` schema string.
80
+ #[pyfunction]
81
+ fn get_version_control_schema_sql() -> PyResult<String> {
82
+ Ok(duc::db::bootstrap::VERSION_CONTROL_SCHEMA_SQL.into())
83
+ }
84
+
85
+ /// Returns the `search.sql` schema string.
86
+ #[pyfunction]
87
+ fn get_search_schema_sql() -> PyResult<String> {
88
+ Ok(duc::db::bootstrap::SEARCH_SCHEMA_SQL.into())
89
+ }
90
+
91
+ /// Returns all migrations as a list of `(from_version, to_version, sql)` tuples.
92
+ #[pyfunction]
93
+ fn get_migrations(_py: Python<'_>) -> PyResult<Vec<(i64, i64, String)>> {
94
+ Ok(duc::db::bootstrap::MIGRATIONS
95
+ .iter()
96
+ .map(|(f, t, sql)| (*f, *t, sql.to_string()))
97
+ .collect())
98
+ }
99
+
61
100
  /// Native duc file format operations.
62
101
  #[pymodule]
63
102
  fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -66,5 +105,11 @@ fn ducpy_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
66
105
  m.add_function(wrap_pyfunction!(serialize_duc, m)?)?;
67
106
  m.add_function(wrap_pyfunction!(get_external_file, m)?)?;
68
107
  m.add_function(wrap_pyfunction!(list_external_files, m)?)?;
108
+ m.add_function(wrap_pyfunction!(get_schema_version, m)?)?;
109
+ m.add_function(wrap_pyfunction!(get_schema_version_int, m)?)?;
110
+ m.add_function(wrap_pyfunction!(get_duc_schema_sql, m)?)?;
111
+ m.add_function(wrap_pyfunction!(get_version_control_schema_sql, m)?)?;
112
+ m.add_function(wrap_pyfunction!(get_search_schema_sql, m)?)?;
113
+ m.add_function(wrap_pyfunction!(get_migrations, m)?)?;
69
114
  Ok(())
70
115
  }
@@ -7,6 +7,15 @@ license = "MIT"
7
7
  authors = ["Ducflair support@ducflair.com"]
8
8
  repository = "https://github.com/ducflair/duc/tree/main/packages/ducrs"
9
9
  readme = "README.md"
10
+ include = [
11
+ "src/**",
12
+ "build.rs",
13
+ "Cargo.toml",
14
+ "README.md",
15
+ "LICENSE",
16
+ "schema/*.sql",
17
+ "schema/migrations/*.sql",
18
+ ]
10
19
 
11
20
  [dependencies]
12
21
  base64 = "0.21.5"
@@ -25,6 +25,7 @@ fn decode_user_version_to_semver(user_version: u32) -> String {
25
25
  }
26
26
 
27
27
  fn find_schema_dir(manifest_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
28
+ // 1. Explicit override.
28
29
  if let Ok(path) = env::var("DUC_SCHEMA_DIR") {
29
30
  let candidate = PathBuf::from(path);
30
31
  if candidate.join("duc.sql").is_file() {
@@ -32,22 +33,25 @@ fn find_schema_dir(manifest_dir: &Path) -> Result<PathBuf, Box<dyn std::error::E
32
33
  }
33
34
  }
34
35
 
35
- for ancestor in manifest_dir.ancestors() {
36
- for candidate in [
37
- ancestor.join("schema"),
38
- ancestor.join("packages").join("ducpy").join("schema"),
39
- ] {
40
- if candidate.join("duc.sql").is_file() {
41
- return Ok(candidate);
42
- }
43
- }
36
+ // 2. Schema inside the crate directory (symlink in dev, real files after
37
+ // `cargo publish` dereferences the symlink into the tarball).
38
+ let bundled = manifest_dir.join("schema");
39
+ if bundled.join("duc.sql").is_file() {
40
+ return Ok(bundled);
44
41
  }
45
42
 
46
- Err(format!(
47
- "Could not locate schema/duc.sql from {}. Set DUC_SCHEMA_DIR to a directory containing duc.sql.",
48
- manifest_dir.display()
49
- )
50
- .into())
43
+ // 3. Local workspace: walk up to the repo root `schema/`.
44
+ let source = manifest_dir
45
+ .ancestors()
46
+ .map(|a| a.join("schema"))
47
+ .find(|p| p.join("duc.sql").is_file())
48
+ .ok_or_else(|| {
49
+ format!(
50
+ "Could not locate schema/duc.sql from {}. Set DUC_SCHEMA_DIR to a directory containing duc.sql.",
51
+ manifest_dir.display()
52
+ )
53
+ })?;
54
+ Ok(source)
51
55
  }
52
56
 
53
57
  fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -106,7 +110,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
106
110
  let mut reg = String::from(
107
111
  "// Auto-generated by build.rs — do not edit by hand.\n\
108
112
  // Each entry: (from_version, to_version, sql).\n\
109
- pub(crate) static MIGRATIONS: &[(i64, i64, &str)] = &[\n",
113
+ pub static MIGRATIONS: &[(i64, i64, &str)] = &[\n",
110
114
  );
111
115
  for (from, to, sql) in &migrations {
112
116
  reg.push_str(&format!(" ({from}i64, {to}i64, \""));
@@ -149,4 +153,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
149
153
  println!("cargo:rerun-if-changed=build.rs");
150
154
 
151
155
  Ok(())
152
- }
156
+ }
@@ -32,10 +32,24 @@ const CURRENT_VERSION: i64 = {
32
32
  val
33
33
  };
34
34
 
35
+ /// Runtime accessor for the compile-time schema version integer.
36
+ pub fn current_schema_version_int() -> i64 {
37
+ CURRENT_VERSION
38
+ }
39
+
35
40
  // Migration registry auto-generated by build.rs from `schema/migrations/*.sql`.
36
41
  // Each entry is (from_version, to_version, sql). Sorted ascending by from_version.
37
42
  include!(concat!(env!("OUT_DIR"), "/migrations_registry.rs"));
38
43
 
44
+ // ── Public re-exports for downstream crates ─────────────────────────────
45
+
46
+ /// Current schema version as a semver string (generated at build time).
47
+ pub const CURRENT_SCHEMA_VERSION_SEMVER: &str = env!("DUC_SCHEMA_VERSION");
48
+
49
+ pub const DUC_SCHEMA_SQL: &str = DUC_SCHEMA;
50
+ pub const VERSION_CONTROL_SCHEMA_SQL: &str = VERSION_CONTROL_SCHEMA;
51
+ pub const SEARCH_SCHEMA_SQL: &str = SEARCH_SCHEMA;
52
+
39
53
  /// Per-connection PRAGMAs that must be re-applied on every open (they are
40
54
  /// either ephemeral or take effect only after the first statement on the
41
55
  /// connection).
@@ -3,7 +3,7 @@
3
3
  //! Call [`open_file`] or [`open_memory`] instead of calling `rusqlite::Connection`
4
4
  //! directly. Internally they dispatch to the correct backend for each compile target.
5
5
 
6
- mod bootstrap;
6
+ pub mod bootstrap;
7
7
 
8
8
  #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
9
9
  mod native;
@@ -34,8 +34,6 @@ python-packages = ["ducpy", "ducpy_native"]
34
34
  module-name = "ducpy_native"
35
35
  include = [
36
36
  { path = "LICENSE", format = "sdist" },
37
- { path = "schema/**/*", format = "sdist" },
38
- { path = "schema/**/*", format = "wheel" }
39
37
  ]
40
38
  manifest-path = "packages/ducpy/crate/Cargo.toml"
41
39
  python-source = "src"
@@ -29,67 +29,33 @@ Usage::
29
29
  from __future__ import annotations
30
30
 
31
31
  import os
32
- import re
33
32
  import sqlite3
34
33
  import tempfile
35
34
  from pathlib import Path
36
35
  from typing import Any, List, Optional, Union
37
36
 
38
- __all__ = ["DucSQL"]
39
-
40
-
41
- _MIGRATION_RE = re.compile(r"^(\d+)_to_(\d+)$")
37
+ import ducpy_native
42
38
 
43
-
44
- def _find_schema_dir() -> Optional[Path]:
45
- env_path = os.environ.get("DUC_SCHEMA_DIR")
46
- if env_path:
47
- candidate = Path(env_path)
48
- if (candidate / "duc.sql").exists():
49
- return candidate
50
-
51
- current = Path(__file__).resolve()
52
- for parent in current.parents:
53
- candidate = parent / "schema"
54
- if (candidate / "duc.sql").exists():
55
- return candidate
56
- return None
39
+ __all__ = ["DucSQL"]
57
40
 
58
41
 
59
42
  def _get_current_schema_version() -> int:
60
- """Read the target user_version from duc.sqlmirrors Rust's CURRENT_VERSION."""
61
- schema_dir = _find_schema_dir()
62
- if schema_dir is None:
63
- return 0
64
- duc_sql = (schema_dir / "duc.sql").read_text(encoding="utf-8")
65
- m = re.search(r"PRAGMA\s+user_version\s*=\s*(\d+)", duc_sql, re.IGNORECASE)
66
- return int(m.group(1)) if m else 0
43
+ """Current schema version integer (e.g. 3000000) from the Rust crate."""
44
+ return ducpy_native.get_schema_version_int()
67
45
 
68
46
 
69
47
  def _read_migrations() -> list[tuple[int, int, str]]:
70
- """Load all migration SQL files from schema/migrations/, sorted by from_version."""
71
- schema_dir = _find_schema_dir()
72
- if schema_dir is None:
73
- return []
74
- migrations_dir = schema_dir / "migrations"
75
- if not migrations_dir.exists():
76
- return []
77
- result: list[tuple[int, int, str]] = []
78
- for path in sorted(migrations_dir.glob("*.sql")):
79
- m = _MIGRATION_RE.match(path.stem)
80
- if m:
81
- result.append((int(m.group(1)), int(m.group(2)), path.read_text(encoding="utf-8")))
82
- result.sort(key=lambda x: x[0])
83
- return result
48
+ """All migrations as (from_version, to_version, sql) — from the Rust crate."""
49
+ raw = ducpy_native.get_migrations()
50
+ # get_migrations returns list of tuples with i64 values
51
+ return [(int(f), int(t), sql) for f, t, sql in raw]
84
52
 
85
53
 
86
54
  def _apply_migrations(conn: sqlite3.Connection) -> None:
87
55
  """Walk the migration chain until user_version reaches the current schema version.
88
56
 
89
- Mirrors the migration logic in Rust's ``bootstrap.rs``:
90
- reads ``schema/migrations/{from}_to_{to}.sql`` files in order and executes
91
- them until ``PRAGMA user_version`` matches the version declared in ``duc.sql``.
92
- Safe to call on already-current or brand-new databases.
57
+ Mirrors the migration logic in Rust's ``bootstrap.rs``. Safe to call on
58
+ already-current or brand-new databases.
93
59
  """
94
60
  user_version: int = conn.execute("PRAGMA user_version").fetchone()[0]
95
61
  if user_version == 0:
@@ -112,18 +78,12 @@ def _apply_migrations(conn: sqlite3.Connection) -> None:
112
78
 
113
79
 
114
80
  def _read_schema_sql() -> str:
115
- schema_dir = _find_schema_dir()
116
- if schema_dir is None:
117
- raise FileNotFoundError(
118
- "Could not locate schema/duc.sql. "
119
- "Ensure you are running from within the DUC repository."
120
- )
121
- parts: list[str] = []
122
- for filename in ("duc.sql", "version_control.sql", "search.sql"):
123
- path = schema_dir / filename
124
- if path.exists():
125
- parts.append(path.read_text(encoding="utf-8"))
126
- return "\n".join(parts)
81
+ """Concatenate the three schema SQL strings shipped by the Rust crate."""
82
+ return "\n".join([
83
+ ducpy_native.get_duc_schema_sql(),
84
+ ducpy_native.get_version_control_schema_sql(),
85
+ ducpy_native.get_search_schema_sql(),
86
+ ])
127
87
 
128
88
 
129
89
  def _apply_pragmas(conn: sqlite3.Connection) -> None:
@@ -446,9 +446,9 @@ def create_local_state_from_base(base: BaseStateParams, **kwargs) -> DucLocalSta
446
446
  current_item_stroke=kwargs.get('current_item_stroke', None),
447
447
  current_item_background=kwargs.get('current_item_background', None),
448
448
  current_item_opacity=kwargs.get('current_item_opacity', 1.0),
449
- current_item_font_family=kwargs.get('current_item_font_family', "Virgil"),
449
+ current_item_font_family=kwargs.get('current_item_font_family', "Roboto Mono"),
450
450
  current_item_font_size=kwargs.get('current_item_font_size', 20.0),
451
- current_item_text_align=kwargs.get('current_item_text_align', None),
451
+ current_item_text_align=kwargs.get('current_item_text_align', TEXT_ALIGN.LEFT),
452
452
  current_item_roundness=kwargs.get('current_item_roundness', 0.0),
453
453
  current_item_start_line_head=kwargs.get('current_item_start_line_head', None),
454
454
  current_item_end_line_head=kwargs.get('current_item_end_line_head', None),
@@ -457,7 +457,7 @@ def create_local_state_from_base(base: BaseStateParams, **kwargs) -> DucLocalSta
457
457
  objects_snap_mode_enabled=kwargs.get('objects_snap_mode_enabled', True),
458
458
  grid_mode_enabled=kwargs.get('grid_mode_enabled', True),
459
459
  outline_mode_enabled=kwargs.get('outline_mode_enabled', False),
460
- manual_save_mode=kwargs.get('manual_save_mode', None),
460
+ manual_save_mode=kwargs.get('manual_save_mode', False),
461
461
  decimal_places=kwargs.get('decimal_places', 2),
462
462
  )
463
463
 
@@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple
16
16
 
17
17
  import ducpy_native
18
18
  from ducpy.utils.convert import (deep_snake_to_camel, snake_to_camel,
19
- to_serializable)
19
+ to_serializable, _flatten_dict)
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
@@ -29,61 +29,8 @@ class DucSerializationValidationError(ValueError):
29
29
  super().__init__("Embedded code validation failed:\n" + "\n".join(f"- {failure}" for failure in failures))
30
30
 
31
31
 
32
- def _find_schema_file() -> Path | None:
33
- env_path = Path(os.environ["DUC_SCHEMA_DIR"]) if "DUC_SCHEMA_DIR" in os.environ else None
34
- if env_path is not None:
35
- candidate = env_path / "duc.sql"
36
- if candidate.exists():
37
- return candidate
38
-
39
- current = Path(__file__).resolve()
40
- for parent in current.parents:
41
- candidate = parent / "schema" / "duc.sql"
42
- if candidate.exists():
43
- return candidate
44
-
45
- return None
46
-
47
-
48
- def _decode_user_version_to_semver(user_version: int) -> str:
49
- """Decode sqlite-style schema user_version to semver.
50
-
51
- Encoding convention:
52
- major * 1_000_000 + minor * 1_000 + patch
53
- """
54
- if user_version < 0:
55
- return "0.0.0"
56
-
57
- major = user_version // 1_000_000
58
- minor = (user_version % 1_000_000) // 1_000
59
- patch = user_version % 1_000
60
- return f"{major}.{minor}.{patch}"
61
-
62
-
63
- def _read_schema_version_fallback() -> str:
64
- """Resolve schema version directly from repository `schema/duc.sql`.
65
-
66
- This is used when `ducpy._version` isn't available (for example, in clean
67
- CI environments before setup-time generation has run).
68
- """
69
- try:
70
- schema_path = _find_schema_file()
71
- if schema_path is None:
72
- return "0.0.0"
73
- content = schema_path.read_text(encoding="utf-8")
74
- match = re.search(r"PRAGMA\s+user_version\s*=\s*(\d+)\s*;", content)
75
- if match:
76
- return _decode_user_version_to_semver(int(match.group(1)))
77
- except Exception as exc: # pragma: no cover - defensive fallback for CI/runtime variance
78
- logger.warning("Failed to resolve schema version fallback from duc.sql: %s", exc)
79
-
80
- return "0.0.0"
81
-
82
-
83
- try:
84
- from ducpy._version import DUC_SCHEMA_VERSION
85
- except ModuleNotFoundError:
86
- DUC_SCHEMA_VERSION = _read_schema_version_fallback()
32
+ # Single source of truth for the schema version — comes from the Rust crate.
33
+ DUC_SCHEMA_VERSION = ducpy_native.get_schema_version()
87
34
 
88
35
  # Map Python element class names → Rust serde type tag strings.
89
36
  _ELEMENT_CLASS_TO_TYPE: Dict[str, str] = {
@@ -104,22 +51,6 @@ _ELEMENT_CLASS_TO_TYPE: Dict[str, str] = {
104
51
  "DucModelElement": "model",
105
52
  }
106
53
 
107
- # Keys in the asdict() output that should be flattened (merged into the parent),
108
- # mirroring Rust's #[serde(flatten)] on base / styles / linear_base / stack_element_base.
109
- _FLATTEN_KEYS = frozenset({"base", "styles", "linear_base", "stack_element_base"})
110
-
111
-
112
- def _flatten_dict(d: dict) -> dict:
113
- """Recursively flatten nested dicts whose key is in ``_FLATTEN_KEYS``."""
114
- result: dict = {}
115
- for k, v in d.items():
116
- if k in _FLATTEN_KEYS and isinstance(v, dict):
117
- result.update(_flatten_dict(v))
118
- else:
119
- result[k] = v
120
- return result
121
-
122
-
123
54
  def _element_to_camel(wrapper_or_element: Any) -> dict:
124
55
  """Convert an element (or ElementWrapper) to the camelCase dict Rust expects."""
125
56
  el = wrapper_or_element
@@ -65,11 +65,19 @@ def deep_snake_to_camel(obj: Any) -> Any:
65
65
 
66
66
 
67
67
  def _flatten_dict(d: dict) -> dict:
68
- """Recursively flatten keys that Rust serde #[serde(flatten)] would flatten."""
68
+ """Recursively flatten keys that Rust serde #[serde(flatten)] would flatten.
69
+
70
+ Walks the entire dict so that nested flatten keys (e.g. ``stack_element_base``
71
+ -> ``stack_base`` -> ``styles``) are all merged into the appropriate level.
72
+ """
69
73
  result: dict = {}
70
74
  for k, v in d.items():
71
- if k in _FLATTEN_KEYS and isinstance(v, dict):
72
- result.update(_flatten_dict(v))
75
+ if isinstance(v, dict):
76
+ v = _flatten_dict(v)
77
+ if k in _FLATTEN_KEYS:
78
+ result.update(v)
79
+ else:
80
+ result[k] = v
73
81
  else:
74
82
  result[k] = v
75
83
  return result
@@ -0,0 +1,28 @@
1
+ """Python shim for the compiled :mod:`ducpy_native` extension.
2
+
3
+ When built with maturin (``module-name = "ducpy_native"`` and
4
+ ``python-source = "src"``), the compiled extension is emitted as the
5
+ submodule ``ducpy_native.ducpy_native``. Re-export the extension symbols at the
6
+ package root so existing imports like ``import ducpy_native`` continue to work.
7
+ """
8
+
9
+ from .ducpy_native import (get_external_file, # type: ignore[attr-defined]
10
+ list_external_files, parse_duc, parse_duc_lazy,
11
+ serialize_duc, get_schema_version,
12
+ get_schema_version_int, get_duc_schema_sql,
13
+ get_version_control_schema_sql,
14
+ get_search_schema_sql, get_migrations)
15
+
16
+ __all__ = [
17
+ "parse_duc",
18
+ "parse_duc_lazy",
19
+ "serialize_duc",
20
+ "get_external_file",
21
+ "list_external_files",
22
+ "get_schema_version",
23
+ "get_schema_version_int",
24
+ "get_duc_schema_sql",
25
+ "get_version_control_schema_sql",
26
+ "get_search_schema_sql",
27
+ "get_migrations",
28
+ ]
@@ -1,21 +0,0 @@
1
- # Generated by Cargo
2
- # will have compiled files and executables
3
- debug/
4
- target/
5
-
6
- # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7
- # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8
- Cargo.lock
9
-
10
- # These are backup files generated by rustfmt
11
- **/*.rs.bk
12
-
13
- # MSVC Windows builds of rustc generate these, which store debugging information
14
- *.pdb
15
-
16
- # RustRover
17
- # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18
- # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19
- # and can be added to the global gitignore or merged into this file. For a more nuclear
20
- # option (not recommended) you can uncomment the following to ignore the entire idea folder.
21
- #.idea/
@@ -1,13 +0,0 @@
1
- {
2
- "name": "ducrs",
3
- "version": "0.0.0-development",
4
- "private": true,
5
- "scripts": {
6
- "build": "cargo build --release",
7
- "test": "cargo test",
8
- "semantic-release": "semantic-release"
9
- },
10
- "devDependencies": {
11
- "semantic-release": "^24.1.2"
12
- }
13
- }
@@ -1,31 +0,0 @@
1
- module.exports = {
2
- branches: ["main", { name: "next", prerelease: true }, { name: "dev", prerelease: true }],
3
- plugins: [
4
- [
5
- require.resolve("../../scripts/semrel-path-filter.cjs"),
6
- {
7
- paths: [
8
- "packages/ducrs"
9
- ],
10
- analyzer: { preset: "conventionalcommits" },
11
- notes: { preset: "conventionalcommits" },
12
- },
13
- ],
14
- [
15
- "@semantic-release/github",
16
- ],
17
- [
18
- "@semantic-release/exec",
19
- {
20
- // Prepare step: Set the crate version and build the project
21
- prepareCmd:
22
- "cargo set-version ${nextRelease.version} && cargo build --release",
23
-
24
- // Publish step: Publish the crate to crates.io
25
- publishCmd:
26
- "cargo publish --allow-dirty --token ${process.env.CARGO_REGISTRY_TOKEN}",
27
- },
28
- ],
29
- ],
30
- tagFormat: "ducrs@${version}",
31
- };
@@ -1,10 +0,0 @@
1
- /* This file is auto-generated by SST. Do not edit. */
2
- /* tslint:disable */
3
- /* eslint-disable */
4
- /* deno-fmt-ignore-file */
5
- /* biome-ignore-all lint: auto-generated */
6
-
7
- /// <reference path="../../sst-env.d.ts" />
8
-
9
- import "sst"
10
- export {}
@@ -1,2 +0,0 @@
1
- inputs
2
- output