consync 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,71 @@
1
+ """JSON renderer — writes constants to a structured JSON file.
2
+
3
+ Output format:
4
+ {
5
+ "_meta": {
6
+ "generator": "consync",
7
+ "source": "constants.xlsx",
8
+ "synced": "2026-05-05T14:30:00"
9
+ },
10
+ "constants": [
11
+ {
12
+ "name": "R_SENSE",
13
+ "value": 1.9999999999910001,
14
+ "unit": "Ohm",
15
+ "description": "Current sense resistor"
16
+ },
17
+ ...
18
+ ]
19
+ }
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+
28
+ from consync.models import Constant, MappingConfig
29
+ from consync.renderers import register
30
+
31
+
32
+ @register("json")
33
+ def render_json(
34
+ constants: list[Constant],
35
+ filepath: str | Path,
36
+ *,
37
+ config: MappingConfig | None = None,
38
+ **kwargs,
39
+ ) -> None:
40
+ """Render constants as a JSON file with metadata."""
41
+ filepath = Path(filepath)
42
+ source_name = config.source if config else kwargs.get("source", "unknown")
43
+ prefix = config.prefix if config else kwargs.get("prefix", "")
44
+
45
+ output = {
46
+ "_meta": {
47
+ "generator": "consync",
48
+ "source": source_name,
49
+ "synced": datetime.now().isoformat(timespec="seconds"),
50
+ },
51
+ "constants": [],
52
+ }
53
+
54
+ for c in constants:
55
+ name = prefix + c.name
56
+ if config and config.uppercase_names:
57
+ name = name.upper()
58
+
59
+ entry: dict = {"name": name, "value": c.value}
60
+ if c.unit:
61
+ entry["unit"] = c.unit
62
+ if c.description:
63
+ entry["description"] = c.description
64
+
65
+ output["constants"].append(entry)
66
+
67
+ filepath.parent.mkdir(parents=True, exist_ok=True)
68
+ filepath.write_text(
69
+ json.dumps(output, indent=2, ensure_ascii=False) + "\n",
70
+ encoding="utf-8",
71
+ )
@@ -0,0 +1,84 @@
1
+ """Python constants renderer — generates a Python module with typed constants.
2
+
3
+ Output format:
4
+ # AUTO-GENERATED by consync — do not edit by hand.
5
+ # Source: constants.xlsx | Synced: 2026-05-05 14:30:00
6
+
7
+ R_SENSE: float = 1.9999999999910001 # Ohm — Current sense resistor
8
+ R_PULLUP: int = 4706 # Ohm — I2C pull-up resistor
9
+ C_FILTER: float = 4.7832940000000000e-07 # F — Input filter capacitor
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+ from consync.models import Constant, MappingConfig
18
+ from consync.precision import format_float
19
+ from consync.renderers import register
20
+
21
+
22
+ @register("python")
23
+ def render_python(
24
+ constants: list[Constant],
25
+ filepath: str | Path,
26
+ *,
27
+ config: MappingConfig | None = None,
28
+ **kwargs,
29
+ ) -> None:
30
+ """Render constants as a Python module with type annotations."""
31
+ filepath = Path(filepath)
32
+ precision = config.precision if config else kwargs.get("precision", 17)
33
+ source_name = config.source if config else kwargs.get("source", "unknown")
34
+ prefix = config.prefix if config else kwargs.get("prefix", "")
35
+
36
+ lines = [
37
+ f'"""AUTO-GENERATED by consync — do not edit by hand."""',
38
+ f"# Source: {source_name} | Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
39
+ f"# Regenerate: consync sync",
40
+ f"",
41
+ ]
42
+
43
+ # Calculate alignment
44
+ names = [prefix + c.name for c in constants]
45
+ max_name = max((len(n) for n in names), default=10)
46
+
47
+ for c in constants:
48
+ name = prefix + c.name
49
+ if config and config.uppercase_names:
50
+ name = name.upper()
51
+
52
+ # Build inline comment
53
+ parts = [p for p in (c.unit, c.description) if p]
54
+ comment = f" # {' — '.join(parts)}" if parts else ""
55
+
56
+ # Format value with type annotation
57
+ if isinstance(c.value, list):
58
+ # Array → list[int] / list[float] / list[str]
59
+ if c.value and isinstance(c.value[0], int):
60
+ type_hint = "list[int]"
61
+ val_str = repr(c.value)
62
+ elif c.value and isinstance(c.value[0], float):
63
+ type_hint = "list[float]"
64
+ val_str = "[" + ", ".join(format_float(v, precision) for v in c.value) + "]"
65
+ else:
66
+ type_hint = "list[str]"
67
+ val_str = repr(c.value)
68
+ elif isinstance(c.value, int):
69
+ type_hint = "int"
70
+ val_str = str(c.value)
71
+ elif isinstance(c.value, float):
72
+ type_hint = "float"
73
+ val_str = format_float(c.value, precision)
74
+ else:
75
+ type_hint = "str"
76
+ val_str = repr(c.value)
77
+
78
+ decl = f"{name}: {type_hint}"
79
+ lines.append(f"{decl:<{max_name + 8}} = {val_str}{comment}")
80
+
81
+ lines.append("") # trailing newline
82
+
83
+ filepath.parent.mkdir(parents=True, exist_ok=True)
84
+ filepath.write_text("\n".join(lines), encoding="utf-8")
@@ -0,0 +1,90 @@
1
+ """Rust constants renderer — generates const declarations.
2
+
3
+ Output format:
4
+ //! AUTO-GENERATED by consync — do not edit by hand.
5
+ //! Source: constants.xlsx | Synced: 2026-05-05 14:30:00
6
+
7
+ /// Current sense resistor (Ohm)
8
+ pub const R_SENSE: f64 = 1.9999999999910001;
9
+
10
+ /// I2C pull-up resistor (Ohm)
11
+ pub const R_PULLUP: f64 = 4706.0;
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+
19
+ from consync.models import Constant, MappingConfig
20
+ from consync.precision import format_float
21
+ from consync.renderers import register
22
+
23
+
24
+ @register("rust")
25
+ def render_rust(
26
+ constants: list[Constant],
27
+ filepath: str | Path,
28
+ *,
29
+ config: MappingConfig | None = None,
30
+ **kwargs,
31
+ ) -> None:
32
+ """Render constants as Rust const declarations."""
33
+ filepath = Path(filepath)
34
+ precision = config.precision if config else kwargs.get("precision", 17)
35
+ source_name = config.source if config else kwargs.get("source", "unknown")
36
+ prefix = config.prefix if config else kwargs.get("prefix", "")
37
+
38
+ lines = [
39
+ f"//! AUTO-GENERATED by consync — do not edit by hand.",
40
+ f"//! Source: {source_name} | Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
41
+ f"//! Regenerate: consync sync",
42
+ f"",
43
+ ]
44
+
45
+ for c in constants:
46
+ name = prefix + c.name
47
+ if config and config.uppercase_names:
48
+ name = name.upper()
49
+
50
+ # Doc comment
51
+ parts = [p for p in (c.description, f"({c.unit})" if c.unit else "") if p]
52
+ if parts:
53
+ lines.append(f"/// {' '.join(parts)}")
54
+
55
+ # Type and value
56
+ if isinstance(c.value, list):
57
+ # Array → pub const X: [i64; N] = [1, 2, 3];
58
+ if c.value and isinstance(c.value[0], int):
59
+ type_name = "i64"
60
+ vals = ", ".join(str(v) for v in c.value)
61
+ elif c.value and isinstance(c.value[0], float):
62
+ type_name = "f64"
63
+ formatted = []
64
+ for v in c.value:
65
+ s = format_float(v, precision)
66
+ if "." not in s and "e" not in s and "E" not in s:
67
+ s += ".0"
68
+ formatted.append(s)
69
+ vals = ", ".join(formatted)
70
+ else:
71
+ # String arrays — use &str
72
+ type_name = "&str"
73
+ vals = ", ".join(f'"{v}"' for v in c.value)
74
+ lines.append(f"pub const {name}: [{type_name}; {len(c.value)}] = [{vals}];")
75
+ elif isinstance(c.value, int):
76
+ type_name = "i64"
77
+ val_str = str(c.value)
78
+ lines.append(f"pub const {name}: {type_name} = {val_str};")
79
+ else:
80
+ type_name = "f64"
81
+ val_str = format_float(c.value, precision)
82
+ # Rust requires decimal point or scientific notation for f64 literals
83
+ if "." not in val_str and "e" not in val_str and "E" not in val_str:
84
+ val_str += ".0"
85
+ lines.append(f"pub const {name}: {type_name} = {val_str};")
86
+
87
+ lines.append("")
88
+
89
+ filepath.parent.mkdir(parents=True, exist_ok=True)
90
+ filepath.write_text("\n".join(lines), encoding="utf-8")
@@ -0,0 +1,89 @@
1
+ """Verilog/SystemVerilog renderer — generates parameter declarations.
2
+
3
+ Output format (Verilog):
4
+ // AUTO-GENERATED by consync — do not edit by hand.
5
+ // Source: constants.xlsx | Synced: 2026-05-05 14:30:00
6
+
7
+ parameter real R_SENSE = 1.9999999999910001; // Ohm — Current sense resistor
8
+ parameter real R_PULLUP = 4706.0; // Ohm — I2C pull-up resistor
9
+ parameter real FREQ_SWITCH = 299872.93847293; // Hz — Switching frequency
10
+
11
+ Output format (module wrapper):
12
+ module design_params;
13
+ parameter real R_SENSE = 1.9999999999910001;
14
+ ...
15
+ endmodule
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+
23
+ from consync.models import Constant, MappingConfig
24
+ from consync.precision import format_float
25
+ from consync.renderers import register
26
+
27
+
28
+ @register("verilog")
29
+ def render_verilog(
30
+ constants: list[Constant],
31
+ filepath: str | Path,
32
+ *,
33
+ config: MappingConfig | None = None,
34
+ **kwargs,
35
+ ) -> None:
36
+ """Render constants as Verilog/SystemVerilog parameters."""
37
+ filepath = Path(filepath)
38
+ precision = config.precision if config else kwargs.get("precision", 17)
39
+ source_name = config.source if config else kwargs.get("source", "unknown")
40
+ prefix = config.prefix if config else kwargs.get("prefix", "")
41
+ module_name = config.module_name if config else kwargs.get("module_name", "")
42
+
43
+ lines = [
44
+ f"// AUTO-GENERATED by consync — do not edit by hand.",
45
+ f"// Source: {source_name} | Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
46
+ f"// Regenerate: consync sync",
47
+ f"",
48
+ ]
49
+
50
+ indent = ""
51
+ if module_name:
52
+ lines.append(f"module {module_name};")
53
+ lines.append("")
54
+ indent = " "
55
+
56
+ # Calculate alignment
57
+ names = [prefix + c.name for c in constants]
58
+ name_width = max((len(n) for n in names), default=20) + 2
59
+
60
+ for c in constants:
61
+ name = prefix + c.name
62
+ if config and config.uppercase_names:
63
+ name = name.upper()
64
+
65
+ # Verilog types
66
+ if isinstance(c.value, int):
67
+ type_kw = "integer"
68
+ val_str = str(c.value)
69
+ else:
70
+ type_kw = "real"
71
+ val_str = format_float(c.value, precision)
72
+ # Verilog real literals need a decimal point
73
+ if "." not in val_str and "e" not in val_str and "E" not in val_str:
74
+ val_str += ".0"
75
+
76
+ # Comment
77
+ parts = [p for p in (c.unit, c.description) if p]
78
+ comment = f" // {' — '.join(parts)}" if parts else ""
79
+
80
+ lines.append(f"{indent}parameter {type_kw} {name:<{name_width}} = {val_str};{comment}")
81
+
82
+ if module_name:
83
+ lines.append("")
84
+ lines.append("endmodule")
85
+
86
+ lines.append("")
87
+
88
+ filepath.parent.mkdir(parents=True, exist_ok=True)
89
+ filepath.write_text("\n".join(lines), encoding="utf-8")
@@ -0,0 +1,94 @@
1
+ """VHDL renderer — generates constant declarations in a package.
2
+
3
+ Output format:
4
+ -- AUTO-GENERATED by consync — do not edit by hand.
5
+ -- Source: constants.xlsx | Synced: 2026-05-05 14:30:00
6
+
7
+ library ieee;
8
+ use ieee.std_logic_1164.all;
9
+ use ieee.math_real.all;
10
+
11
+ package hw_constants is
12
+ constant R_SENSE : real := 1.9999999999910001; -- Ohm | Current sense resistor
13
+ constant R_PULLUP : real := 4706.0; -- Ohm | I2C pull-up resistor
14
+ constant FREQ_SWITCH : real := 299872.93847293; -- Hz | Switching frequency
15
+ end package hw_constants;
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+
23
+ from consync.models import Constant, MappingConfig
24
+ from consync.precision import format_float
25
+ from consync.renderers import register
26
+
27
+
28
+ @register("vhdl")
29
+ def render_vhdl(
30
+ constants: list[Constant],
31
+ filepath: str | Path,
32
+ *,
33
+ config: MappingConfig | None = None,
34
+ **kwargs,
35
+ ) -> None:
36
+ """Render constants as a VHDL package."""
37
+ filepath = Path(filepath)
38
+ precision = config.precision if config else kwargs.get("precision", 17)
39
+ source_name = config.source if config else kwargs.get("source", "unknown")
40
+ prefix = config.prefix if config else kwargs.get("prefix", "")
41
+ module_name = config.module_name if config else kwargs.get("module_name", "")
42
+
43
+ # Package name: from module_name or derive from filename
44
+ pkg_name = module_name or filepath.stem.replace("-", "_").replace(".", "_")
45
+
46
+ lines = [
47
+ f"-- AUTO-GENERATED by consync — do not edit by hand.",
48
+ f"-- Source: {source_name} | Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
49
+ f"-- Regenerate: consync sync",
50
+ f"",
51
+ f"library ieee;",
52
+ f"use ieee.std_logic_1164.all;",
53
+ f"use ieee.math_real.all;",
54
+ f"",
55
+ f"package {pkg_name} is",
56
+ f"",
57
+ ]
58
+
59
+ # Calculate alignment
60
+ names = [prefix + c.name for c in constants]
61
+ name_width = max((len(n) for n in names), default=20) + 2
62
+
63
+ for c in constants:
64
+ name = prefix + c.name
65
+ if config and config.uppercase_names:
66
+ name = name.upper()
67
+
68
+ # VHDL types
69
+ if isinstance(c.value, int):
70
+ type_name = "integer"
71
+ val_str = str(c.value)
72
+ else:
73
+ type_name = "real"
74
+ val_str = format_float(c.value, precision)
75
+ # VHDL real literals need a decimal point
76
+ if "." not in val_str and "e" not in val_str and "E" not in val_str:
77
+ val_str += ".0"
78
+
79
+ # Comment
80
+ parts = [p for p in (c.unit, c.description) if p]
81
+ comment = f" -- {' | '.join(parts)}" if parts else ""
82
+
83
+ lines.append(
84
+ f" constant {name:<{name_width}} : {type_name} := {val_str};{comment}"
85
+ )
86
+
87
+ lines += [
88
+ f"",
89
+ f"end package {pkg_name};",
90
+ f"",
91
+ ]
92
+
93
+ filepath.parent.mkdir(parents=True, exist_ok=True)
94
+ filepath.write_text("\n".join(lines), encoding="utf-8")
consync/state.py ADDED
@@ -0,0 +1,76 @@
1
+ """State management — hash-based change detection for sync.
2
+
3
+ Tracks MD5 hashes of constant data (name+value pairs) to detect which
4
+ side changed since last sync. This enables smart bidirectional sync:
5
+
6
+ - If only source changed → sync source → target
7
+ - If only target changed → sync target → source
8
+ - If both changed → conflict (resolve per on_conflict setting)
9
+ - If neither changed → skip (already in sync)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ from pathlib import Path
17
+
18
+ from consync.models import Constant
19
+
20
+
21
+ def compute_hash(constants: list[Constant]) -> str:
22
+ """Compute a stable hash of constant name+value pairs.
23
+
24
+ Only hashes names and values (not unit/description) since those
25
+ are the semantically meaningful content for sync detection.
26
+ """
27
+ key = json.dumps(
28
+ {c.name: c.value for c in constants},
29
+ sort_keys=True,
30
+ default=str,
31
+ )
32
+ return hashlib.md5(key.encode()).hexdigest()
33
+
34
+
35
+ class SyncState:
36
+ """Persistent sync state stored in .consync.state.json."""
37
+
38
+ def __init__(self, state_file: Path):
39
+ self.state_file = state_file
40
+ self._data: dict = {}
41
+ self._load()
42
+
43
+ def _load(self):
44
+ if self.state_file.exists():
45
+ try:
46
+ self._data = json.loads(self.state_file.read_text())
47
+ except (json.JSONDecodeError, OSError):
48
+ self._data = {}
49
+ else:
50
+ self._data = {}
51
+
52
+ def _save(self):
53
+ self.state_file.write_text(json.dumps(self._data, indent=2) + "\n")
54
+
55
+ def get_hash(self, mapping_key: str, side: str) -> str | None:
56
+ """Get the stored hash for a mapping side ('source' or 'target')."""
57
+ entry = self._data.get(mapping_key, {})
58
+ return entry.get(side)
59
+
60
+ def set_hash(self, mapping_key: str, source_hash: str, target_hash: str):
61
+ """Update stored hashes after a successful sync."""
62
+ self._data[mapping_key] = {
63
+ "source": source_hash,
64
+ "target": target_hash,
65
+ }
66
+ self._save()
67
+
68
+ def mapping_key(self, source: str, target: str) -> str:
69
+ """Generate a stable key for a source/target pair."""
70
+ return f"{source}::{target}"
71
+
72
+ def clear(self):
73
+ """Reset all state."""
74
+ self._data = {}
75
+ if self.state_file.exists():
76
+ self.state_file.unlink()