evolutiondb-notes-sync 0.1.0__tar.gz → 0.1.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.
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/PKG-INFO +1 -1
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/evolutiondb_notes_sync.egg-info/PKG-INFO +1 -1
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/notes_sync/state.py +19 -3
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/notes_sync/sync.py +47 -25
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/pyproject.toml +1 -1
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/evolutiondb_notes_sync.egg-info/SOURCES.txt +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/evolutiondb_notes_sync.egg-info/dependency_links.txt +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/evolutiondb_notes_sync.egg-info/entry_points.txt +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/evolutiondb_notes_sync.egg-info/requires.txt +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/evolutiondb_notes_sync.egg-info/top_level.txt +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/notes_sync/__init__.py +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/notes_sync/__main__.py +0 -0
- {evolutiondb_notes_sync-0.1.0 → evolutiondb_notes_sync-0.1.1}/setup.cfg +0 -0
|
@@ -110,7 +110,23 @@ class MemoryStore:
|
|
|
110
110
|
f"('{_e(self.namespace)}','{_e(WATERMARK_KEY)}','{_e(value)}')"))
|
|
111
111
|
|
|
112
112
|
def put_record(self, key: str, record: Dict) -> None:
|
|
113
|
+
# MEMORY PUT acts as upsert for small payloads but reports a
|
|
114
|
+
# spurious "duplicate key" error for larger ones; clearing the
|
|
115
|
+
# existing row first sidesteps that path. The DELETE is cheap
|
|
116
|
+
# and harmless when no row exists yet.
|
|
113
117
|
value = json.dumps(record, ensure_ascii=False)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
def run(cur):
|
|
119
|
+
cur.execute(
|
|
120
|
+
f"DELETE FROM __mem_{self.store} "
|
|
121
|
+
f"WHERE mem_namespace = '{_e(self.namespace)}' "
|
|
122
|
+
f"AND mem_key = '{_e(key)}'")
|
|
123
|
+
cur.execute(
|
|
124
|
+
f"MEMORY PUT INTO {self.store} VALUES "
|
|
125
|
+
f"('{_e(self.namespace)}','{_e(key)}','{_e(value)}')")
|
|
126
|
+
try:
|
|
127
|
+
self._retry(run)
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
print(f"[notes-sync] put_record FAILED key={key!r} "
|
|
130
|
+
f"value_len={len(value)} err={exc}",
|
|
131
|
+
file=sys.stderr, flush=True)
|
|
132
|
+
raise
|
|
@@ -95,11 +95,18 @@ def _html_to_text(body_html: str) -> str:
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def iter_notes() -> Iterable[Dict]:
|
|
98
|
-
"""Spawn osascript and stream JSON notes line by line.
|
|
98
|
+
"""Spawn osascript and stream JSON notes line by line.
|
|
99
|
+
|
|
100
|
+
JXA's `console.log` writes to stderr, not stdout, so we merge the
|
|
101
|
+
two streams. Any non-JSON line (genuine osascript errors) falls
|
|
102
|
+
through the JSONDecodeError branch and is ignored. Without the
|
|
103
|
+
merge, the stderr pipe fills (~64KB) on the first oversized note
|
|
104
|
+
and osascript blocks forever.
|
|
105
|
+
"""
|
|
99
106
|
try:
|
|
100
107
|
proc = subprocess.Popen(
|
|
101
108
|
["/usr/bin/osascript", "-l", "JavaScript", "-e", _JXA],
|
|
102
|
-
stdout=subprocess.PIPE, stderr=subprocess.
|
|
109
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
103
110
|
text=True, bufsize=1)
|
|
104
111
|
except FileNotFoundError:
|
|
105
112
|
print("[notes-sync] /usr/bin/osascript not found — Notes "
|
|
@@ -126,12 +133,7 @@ def iter_notes() -> Iterable[Dict]:
|
|
|
126
133
|
proc.stdout.close()
|
|
127
134
|
except Exception:
|
|
128
135
|
pass
|
|
129
|
-
|
|
130
|
-
if rc != 0 and proc.stderr is not None:
|
|
131
|
-
err = proc.stderr.read().strip()
|
|
132
|
-
if err:
|
|
133
|
-
print(f"[notes-sync] osascript stderr: {err[:400]}",
|
|
134
|
-
file=sys.stderr, flush=True)
|
|
136
|
+
proc.wait(timeout=600)
|
|
135
137
|
|
|
136
138
|
|
|
137
139
|
# ---------------------------------------------------------------- #
|
|
@@ -161,12 +163,27 @@ class Config:
|
|
|
161
163
|
"mcp_mem")
|
|
162
164
|
|
|
163
165
|
|
|
166
|
+
# EvolutionDB caps a single SQL statement at 8KB. The MEMORY PUT
|
|
167
|
+
# wrapper, JSON envelope (title/folder/timestamps/tags), and SQL
|
|
168
|
+
# single-quote doubling can roughly double the on-wire byte count
|
|
169
|
+
# against the raw text. We leave generous headroom so a note full
|
|
170
|
+
# of quotes or multi-byte UTF-8 still fits in one statement.
|
|
171
|
+
_MAX_TEXT_CHARS = 3000
|
|
172
|
+
|
|
173
|
+
|
|
164
174
|
def _build_record(n: Dict) -> Optional[Dict]:
|
|
165
175
|
title = (n.get("name") or "").strip() or "(untitled)"
|
|
166
176
|
text = _html_to_text(n.get("body") or "")
|
|
167
177
|
if not text and title == "(untitled)":
|
|
168
178
|
return None
|
|
179
|
+
truncated = False
|
|
180
|
+
if len(text) > _MAX_TEXT_CHARS:
|
|
181
|
+
text = text[:_MAX_TEXT_CHARS]
|
|
182
|
+
truncated = True
|
|
169
183
|
modified = n.get("modified") or n.get("created") or ""
|
|
184
|
+
tags = ["notes", "apple-notes"]
|
|
185
|
+
if truncated:
|
|
186
|
+
tags.append("truncated")
|
|
170
187
|
return {
|
|
171
188
|
"fact": f"Note \"{title}\": {text[:240]}",
|
|
172
189
|
"source": "notes",
|
|
@@ -177,7 +194,8 @@ def _build_record(n: Dict) -> Optional[Dict]:
|
|
|
177
194
|
"note_id": n.get("id") or "",
|
|
178
195
|
"modified_at": modified,
|
|
179
196
|
"created_at": n.get("created") or "",
|
|
180
|
-
"
|
|
197
|
+
"truncated": truncated,
|
|
198
|
+
"tags": tags,
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
|
|
@@ -202,23 +220,27 @@ def sync_once(cfg: Config, *, since_iso: Optional[str],
|
|
|
202
220
|
floor = wm or since_iso
|
|
203
221
|
highest = floor or ""
|
|
204
222
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
223
|
+
for raw in iter_notes():
|
|
224
|
+
rec = _build_record(raw)
|
|
225
|
+
if not rec:
|
|
226
|
+
counters["skipped"] += 1
|
|
227
|
+
continue
|
|
228
|
+
if floor and rec["modified_at"] <= floor:
|
|
229
|
+
continue
|
|
230
|
+
if store:
|
|
231
|
+
try:
|
|
214
232
|
store.put_record(_note_key(rec), rec)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
233
|
+
except Exception as exc: # noqa: BLE001
|
|
234
|
+
# One bad row should not abort the whole run; the next
|
|
235
|
+
# invocation will retry. Track and move on.
|
|
236
|
+
print(f"[notes-sync] note write failed "
|
|
237
|
+
f"(title={rec['title'][:60]!r}): {exc}",
|
|
238
|
+
file=sys.stderr, flush=True)
|
|
239
|
+
counters["errors"] += 1
|
|
240
|
+
continue
|
|
241
|
+
counters["notes"] += 1
|
|
242
|
+
if rec["modified_at"] > highest:
|
|
243
|
+
highest = rec["modified_at"]
|
|
222
244
|
|
|
223
245
|
if store and highest and highest != (wm or ""):
|
|
224
246
|
store.set_watermark_iso(highest)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|