consync 2.2.3__tar.gz → 2.3.1__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.
- {consync-2.2.3 → consync-2.3.1}/PKG-INFO +1 -1
- {consync-2.2.3 → consync-2.3.1}/consync/__init__.py +1 -1
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/c_struct_table.py +74 -23
- {consync-2.2.3 → consync-2.3.1}/consync/state.py +32 -4
- {consync-2.2.3 → consync-2.3.1}/consync/watcher.py +59 -24
- {consync-2.2.3 → consync-2.3.1}/pyproject.toml +1 -1
- consync-2.3.1/tests/test_audit_sync.py +669 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_c_struct_table.py +120 -1
- {consync-2.2.3 → consync-2.3.1}/.github/CODEOWNERS +0 -0
- {consync-2.2.3 → consync-2.3.1}/.github/copilot-instructions.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/.github/dependabot.yml +0 -0
- {consync-2.2.3 → consync-2.3.1}/.github/workflows/ci.yml +0 -0
- {consync-2.2.3 → consync-2.3.1}/.github/workflows/codeql.yml +0 -0
- {consync-2.2.3 → consync-2.3.1}/.github/workflows/publish.yml +0 -0
- {consync-2.2.3 → consync-2.3.1}/.github/workflows/release.yml +0 -0
- {consync-2.2.3 → consync-2.3.1}/.gitignore +0 -0
- {consync-2.2.3 → consync-2.3.1}/CLAUDE.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/CONTRIBUTING.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/FAQ.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/LICENSE +0 -0
- {consync-2.2.3 → consync-2.3.1}/README.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/SECURITY.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/TODO.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/assets/demo.gif +0 -0
- {consync-2.2.3 → consync-2.3.1}/assets/demo.tape +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/backup.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/cli.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/config.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/hooks.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/lock.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/logging_config.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/models.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/__init__.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/c_header.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/c_struct_table.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/csv_parser.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/json_parser.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/toml_parser.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/parsers/xlsx.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/precision.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/__init__.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/c_header.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/csharp.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/csv_renderer.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/json_renderer.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/python_const.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/rust_const.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/verilog.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/renderers/vhdl.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/sync.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/consync/validators.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/examples/fpga/.consync.yaml +0 -0
- {consync-2.2.3 → consync-2.3.1}/examples/fpga/design_params.csv +0 -0
- {consync-2.2.3 → consync-2.3.1}/examples/hardware/.consync.yaml +0 -0
- {consync-2.2.3 → consync-2.3.1}/examples/hardware/constants.csv +0 -0
- {consync-2.2.3 → consync-2.3.1}/examples/multilang/.consync.yaml +0 -0
- {consync-2.2.3 → consync-2.3.1}/examples/multilang/constants.json +0 -0
- {consync-2.2.3 → consync-2.3.1}/npm/.npmrc +0 -0
- {consync-2.2.3 → consync-2.3.1}/npm/LICENSE +0 -0
- {consync-2.2.3 → consync-2.3.1}/npm/README.md +0 -0
- {consync-2.2.3 → consync-2.3.1}/npm/bin/consync.js +0 -0
- {consync-2.2.3 → consync-2.3.1}/npm/package.json +0 -0
- {consync-2.2.3 → consync-2.3.1}/npm/scripts/install.js +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/__init__.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_arrays.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_bidirectional.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_cli.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_comprehensive_sync.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_embedded.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_parsers.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_precision.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_renderers.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_safety.py +0 -0
- {consync-2.2.3 → consync-2.3.1}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: consync
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Bidirectional sync between spreadsheets and source code constants — with full decimal precision.
|
|
5
5
|
Project-URL: Homepage, https://github.com/naveenkumarbaskaran/consync
|
|
6
6
|
Project-URL: Repository, https://github.com/naveenkumarbaskaran/consync
|
|
@@ -41,7 +41,12 @@ def _sanitize_label(label: str) -> str:
|
|
|
41
41
|
def _format_numeric(value: float | int, original_raw: str = "") -> str:
|
|
42
42
|
"""Format a numeric value back to C literal, preserving style of original.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Preserves:
|
|
45
|
+
1. Original exponent for scientific notation (E-3 stays E-3)
|
|
46
|
+
2. Lowercase/uppercase 'e'/'E'
|
|
47
|
+
3. Mantissa format (with/without decimal point)
|
|
48
|
+
4. Precision (number of decimal digits)
|
|
49
|
+
5. Exponent digit count (E-03 vs E-3)
|
|
45
50
|
"""
|
|
46
51
|
if isinstance(value, int):
|
|
47
52
|
if original_raw and original_raw.strip().startswith(("0x", "0X")):
|
|
@@ -55,42 +60,88 @@ def _format_numeric(value: float | int, original_raw: str = "") -> str:
|
|
|
55
60
|
return f"{value}{suffix}"
|
|
56
61
|
|
|
57
62
|
# Float value
|
|
58
|
-
suffix = "F"
|
|
63
|
+
suffix = "F" # default
|
|
59
64
|
if original_raw:
|
|
60
|
-
stripped = original_raw.strip()
|
|
61
|
-
if
|
|
62
|
-
suffix =
|
|
65
|
+
stripped = original_raw.strip()
|
|
66
|
+
if stripped.endswith("f"):
|
|
67
|
+
suffix = "f"
|
|
68
|
+
elif stripped.endswith("F"):
|
|
69
|
+
suffix = "F"
|
|
70
|
+
elif stripped.endswith("L") or stripped.endswith("l"):
|
|
71
|
+
suffix = stripped[-1]
|
|
63
72
|
else:
|
|
64
73
|
suffix = ""
|
|
65
74
|
|
|
66
75
|
# Check if original used scientific notation
|
|
67
|
-
if original_raw and ("e" in original_raw
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if original_raw and ("e" in original_raw or "E" in original_raw):
|
|
77
|
+
# Determine case of exponent character
|
|
78
|
+
use_lowercase = "e" in original_raw and "E" not in original_raw
|
|
79
|
+
exp_char = "e" if use_lowercase else "E"
|
|
80
|
+
|
|
81
|
+
# Extract original exponent value
|
|
82
|
+
orig_no_suffix = original_raw.strip().rstrip("fFlLuU")
|
|
83
|
+
exp_match = re.search(r"[eE]([+-]?\d+)", orig_no_suffix)
|
|
84
|
+
if exp_match:
|
|
85
|
+
orig_exponent = int(exp_match.group(1))
|
|
86
|
+
|
|
87
|
+
# Handle negative values
|
|
88
|
+
sign = ""
|
|
89
|
+
abs_value = value
|
|
90
|
+
if value < 0:
|
|
91
|
+
sign = "-"
|
|
92
|
+
abs_value = abs(value)
|
|
93
|
+
|
|
94
|
+
# Calculate new mantissa preserving the original exponent
|
|
95
|
+
# value = mantissa * 10^exponent => mantissa = value / 10^exponent
|
|
96
|
+
if abs_value == 0:
|
|
97
|
+
new_mantissa = 0.0
|
|
78
98
|
else:
|
|
79
|
-
|
|
80
|
-
else:
|
|
81
|
-
decimal_digits = 2
|
|
99
|
+
new_mantissa = abs_value / (10 ** orig_exponent)
|
|
82
100
|
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
# Determine mantissa format from original
|
|
102
|
+
mantissa_part = orig_no_suffix.split("e")[0].split("E")[0]
|
|
103
|
+
mantissa_part = mantissa_part.lstrip("+-")
|
|
104
|
+
|
|
105
|
+
if "." in mantissa_part:
|
|
106
|
+
decimal_digits = len(mantissa_part.split(".")[1])
|
|
107
|
+
mantissa_str = f"{new_mantissa:.{decimal_digits}f}"
|
|
108
|
+
else:
|
|
109
|
+
# No decimal in original (like "1E-2")
|
|
110
|
+
mantissa_str = f"{int(round(new_mantissa))}"
|
|
111
|
+
|
|
112
|
+
# Preserve exponent format (E-03 vs E-3, leading zeros)
|
|
113
|
+
exp_match_full = re.search(r"[eE]([+-]?)(\d+)", orig_no_suffix)
|
|
114
|
+
if exp_match_full:
|
|
115
|
+
exp_sign_char = exp_match_full.group(1)
|
|
116
|
+
exp_digit_count = len(exp_match_full.group(2))
|
|
117
|
+
if orig_exponent < 0:
|
|
118
|
+
exp_sign_str = "-"
|
|
119
|
+
elif exp_sign_char == "+":
|
|
120
|
+
exp_sign_str = "+"
|
|
121
|
+
else:
|
|
122
|
+
exp_sign_str = ""
|
|
123
|
+
exp_abs = abs(orig_exponent)
|
|
124
|
+
exp_str = f"{exp_sign_str}{exp_abs:0{exp_digit_count}d}"
|
|
125
|
+
else:
|
|
126
|
+
exp_str = f"{orig_exponent:+d}"
|
|
127
|
+
|
|
128
|
+
formatted = f"{sign}{mantissa_str}{exp_char}{exp_str}"
|
|
129
|
+
return f"{formatted}{suffix}"
|
|
130
|
+
else:
|
|
131
|
+
# Fallback (shouldn't happen if we got here)
|
|
132
|
+
formatted = f"{value:E}"
|
|
133
|
+
return f"{formatted}{suffix}"
|
|
85
134
|
else:
|
|
86
|
-
# Regular float notation
|
|
135
|
+
# Regular float notation (no scientific notation in original)
|
|
87
136
|
if original_raw:
|
|
88
137
|
orig_stripped = original_raw.strip().rstrip("fFlL")
|
|
89
138
|
if "." in orig_stripped:
|
|
90
139
|
decimal_digits = len(orig_stripped.split(".")[1])
|
|
91
140
|
formatted = f"{value:.{decimal_digits}f}"
|
|
92
141
|
else:
|
|
93
|
-
formatted =
|
|
142
|
+
formatted = (
|
|
143
|
+
str(int(round(value))) if value == int(value) else str(value)
|
|
144
|
+
)
|
|
94
145
|
else:
|
|
95
146
|
formatted = str(value)
|
|
96
147
|
return f"{formatted}{suffix}"
|
|
@@ -18,17 +18,45 @@ from pathlib import Path
|
|
|
18
18
|
from consync.models import Constant
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _normalize_value(v):
|
|
22
|
+
"""Normalize a constant value for stable hashing.
|
|
23
|
+
|
|
24
|
+
Ensures int/float equivalence: C produces 1.0 (float) for ``1.0F``,
|
|
25
|
+
but Excel stores it as integer 1. Without normalization these hash
|
|
26
|
+
differently, causing perpetual "out of sync" after every round-trip.
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- int/float that are whole numbers → canonical float (``1.0``)
|
|
30
|
+
- list values → each element normalized recursively
|
|
31
|
+
- strings → unchanged
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(v, list):
|
|
34
|
+
return [_normalize_value(x) for x in v]
|
|
35
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
36
|
+
return float(v)
|
|
37
|
+
return v
|
|
38
|
+
|
|
39
|
+
|
|
21
40
|
def compute_hash(constants: list[Constant]) -> str:
|
|
22
41
|
"""Compute a stable hash of constant name+value pairs.
|
|
23
42
|
|
|
24
43
|
Only hashes names and values (not unit/description) since those
|
|
25
44
|
are the semantically meaningful content for sync detection.
|
|
45
|
+
|
|
46
|
+
Uses a sorted list of (name, value) tuples instead of a dict to
|
|
47
|
+
correctly handle duplicate constant names (e.g., multi-variant
|
|
48
|
+
struct tables where Motor_X__R_Phase appears once per variant).
|
|
49
|
+
A dict would silently discard all but the last duplicate, making
|
|
50
|
+
edits to earlier variants invisible to change detection.
|
|
51
|
+
|
|
52
|
+
Numeric values are normalized to float so that ``int 1`` and
|
|
53
|
+
``float 1.0`` hash identically — this is critical for xlsx
|
|
54
|
+
round-trips where Excel drops the ``.0`` from whole numbers.
|
|
26
55
|
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
sort_keys=True,
|
|
30
|
-
default=str,
|
|
56
|
+
pairs = sorted(
|
|
57
|
+
(c.name, _normalize_value(c.value)) for c in constants
|
|
31
58
|
)
|
|
59
|
+
key = json.dumps(pairs, default=str)
|
|
32
60
|
return hashlib.md5(key.encode()).hexdigest()
|
|
33
61
|
|
|
34
62
|
|
|
@@ -6,6 +6,11 @@ No `brew install` required — pure Python.
|
|
|
6
6
|
Event handling:
|
|
7
7
|
- Changes during debounce are QUEUED, not dropped
|
|
8
8
|
- After debounce expires, all queued events are coalesced into one sync
|
|
9
|
+
- Write suppression prevents ping-pong loops (sync-written files are
|
|
10
|
+
ignored for a short window so they don't re-trigger sync)
|
|
11
|
+
- Direction is NEVER forced — the state engine auto-detects which side
|
|
12
|
+
changed using stored hashes, which correctly returns "already in sync"
|
|
13
|
+
for files that were just written by the sync itself
|
|
9
14
|
- Lock conflicts trigger automatic retry after a short delay
|
|
10
15
|
- Errors are logged but the watcher continues (resilient)
|
|
11
16
|
- On startup, a full sync is run to recover from any drift
|
|
@@ -29,6 +34,10 @@ logger = logging.getLogger(__name__)
|
|
|
29
34
|
LOCK_RETRY_ATTEMPTS = 3
|
|
30
35
|
LOCK_RETRY_DELAY = 2.0 # seconds between retries
|
|
31
36
|
|
|
37
|
+
# Write suppression: ignore events on files we just wrote for this duration (seconds).
|
|
38
|
+
# Must be longer than filesystem event propagation but shorter than user interaction.
|
|
39
|
+
WRITE_SUPPRESSION_WINDOW = 2.0
|
|
40
|
+
|
|
32
41
|
|
|
33
42
|
def start_watcher(
|
|
34
43
|
config_path: str | Path | None = None,
|
|
@@ -84,10 +93,14 @@ def start_watcher(
|
|
|
84
93
|
click.echo(f"[startup] ⚠️ Startup sync failed: {e} — continuing in watch mode.\n")
|
|
85
94
|
|
|
86
95
|
# ── Event queue (thread-safe) — changes are QUEUED, never dropped ──
|
|
87
|
-
pending_changes: dict[Path, str] = {} # path →
|
|
96
|
+
pending_changes: dict[Path, str] = {} # path → info about what changed
|
|
88
97
|
pending_lock = threading.Lock()
|
|
89
98
|
last_sync_time: float = 0
|
|
90
99
|
|
|
100
|
+
# ── Write suppression — tracks files recently written by sync ──
|
|
101
|
+
recently_written: dict[Path, float] = {} # path → timestamp of sync write
|
|
102
|
+
written_lock = threading.Lock()
|
|
103
|
+
|
|
91
104
|
class SyncHandler(FileSystemEventHandler):
|
|
92
105
|
def on_modified(self, event):
|
|
93
106
|
if event.is_directory:
|
|
@@ -97,22 +110,22 @@ def start_watcher(
|
|
|
97
110
|
if changed_path not in watch_files:
|
|
98
111
|
return
|
|
99
112
|
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
+
# Write suppression: skip events caused by sync's own writes
|
|
114
|
+
with written_lock:
|
|
115
|
+
written_time = recently_written.get(changed_path)
|
|
116
|
+
if written_time and (time.time() - written_time) < WRITE_SUPPRESSION_WINDOW:
|
|
117
|
+
logger.debug(
|
|
118
|
+
"Suppressed event for %s (written %.1fs ago by sync)",
|
|
119
|
+
changed_path.name,
|
|
120
|
+
time.time() - written_time,
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Queue the event (never drop). Don't force direction — let
|
|
125
|
+
# the state engine auto-detect using stored hashes.
|
|
113
126
|
with pending_lock:
|
|
114
|
-
pending_changes[changed_path] =
|
|
115
|
-
logger.debug("Queued: %s
|
|
127
|
+
pending_changes[changed_path] = changed_path.name
|
|
128
|
+
logger.debug("Queued: %s", changed_path.name)
|
|
116
129
|
|
|
117
130
|
def _process_queue():
|
|
118
131
|
"""Process all pending changes in one coalesced sync."""
|
|
@@ -125,24 +138,25 @@ def start_watcher(
|
|
|
125
138
|
queued = dict(pending_changes)
|
|
126
139
|
pending_changes.clear()
|
|
127
140
|
|
|
128
|
-
# Determine overall force direction (if all queued point same way)
|
|
129
|
-
directions = set(queued.values())
|
|
130
|
-
if len(directions) == 1:
|
|
131
|
-
force = directions.pop()
|
|
132
|
-
else:
|
|
133
|
-
force = None # mixed — let engine auto-detect
|
|
134
|
-
|
|
135
141
|
rel_names = [p.name for p in queued.keys()]
|
|
136
142
|
timestamp = time.strftime("%H:%M:%S")
|
|
137
143
|
click.echo(f"[{timestamp}] 📝 {', '.join(rel_names)} changed — syncing...")
|
|
138
144
|
|
|
145
|
+
# Never force direction — let the state engine auto-detect which
|
|
146
|
+
# side changed using stored hashes. This prevents ping-pong:
|
|
147
|
+
# after sync writes file B, B's hash matches the stored hash,
|
|
148
|
+
# so the next cycle correctly detects "already in sync".
|
|
149
|
+
|
|
139
150
|
# Retry on lock conflict
|
|
140
151
|
for attempt in range(LOCK_RETRY_ATTEMPTS):
|
|
141
152
|
try:
|
|
142
|
-
reports = sync(config_path=config_path
|
|
153
|
+
reports = sync(config_path=config_path)
|
|
143
154
|
for r in reports:
|
|
144
155
|
if r.result in (SyncResult.SYNCED_SOURCE_TO_TARGET, SyncResult.SYNCED_TARGET_TO_SOURCE):
|
|
145
156
|
click.echo(f" ✅ {r.message}")
|
|
157
|
+
# Register written files for suppression so their
|
|
158
|
+
# filesystem events don't re-trigger another sync.
|
|
159
|
+
_suppress_written_files(r, config_dir)
|
|
146
160
|
elif r.result == SyncResult.ALREADY_IN_SYNC:
|
|
147
161
|
pass # silent
|
|
148
162
|
elif r.result == SyncResult.ERROR:
|
|
@@ -158,6 +172,27 @@ def start_watcher(
|
|
|
158
172
|
logger.error("Watch sync failed: %s", e)
|
|
159
173
|
return
|
|
160
174
|
|
|
175
|
+
def _suppress_written_files(report, conf_dir):
|
|
176
|
+
"""Mark files written by sync for suppression so their events are ignored."""
|
|
177
|
+
now = time.time()
|
|
178
|
+
with written_lock:
|
|
179
|
+
# Determine which file was written based on sync direction
|
|
180
|
+
if report.result == SyncResult.SYNCED_SOURCE_TO_TARGET:
|
|
181
|
+
# source → target: target was written
|
|
182
|
+
written_path = (conf_dir / report.target).resolve()
|
|
183
|
+
recently_written[written_path] = now
|
|
184
|
+
elif report.result == SyncResult.SYNCED_TARGET_TO_SOURCE:
|
|
185
|
+
# target → source: source was written
|
|
186
|
+
written_path = (conf_dir / report.source).resolve()
|
|
187
|
+
recently_written[written_path] = now
|
|
188
|
+
|
|
189
|
+
# Prune old entries to prevent memory leak
|
|
190
|
+
with written_lock:
|
|
191
|
+
cutoff = now - WRITE_SUPPRESSION_WINDOW * 3
|
|
192
|
+
stale = [p for p, t in recently_written.items() if t < cutoff]
|
|
193
|
+
for p in stale:
|
|
194
|
+
del recently_written[p]
|
|
195
|
+
|
|
161
196
|
observer = Observer()
|
|
162
197
|
handler = SyncHandler()
|
|
163
198
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "consync"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.1"
|
|
8
8
|
description = "Bidirectional sync between spreadsheets and source code constants — with full decimal precision."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|