tmjlite 0.2.8__py3-none-win_amd64.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.
tmjlite/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ TMJLite Python Driver
3
+ Cross-platform embedded database engine
4
+
5
+ Usage:
6
+ import tmjlite
7
+
8
+ db = tmjlite.connect("mydb.tmjp")
9
+ db.execute("CREATE TABLE users (name TEXT, age INT);")
10
+ db.execute("INSERT INTO users VALUES ('Alice', 30);")
11
+
12
+ for row in db.query("SELECT * FROM users;"):
13
+ print(row["name"], row["age"])
14
+
15
+ with db.transaction():
16
+ db.execute("INSERT INTO users VALUES ('Bob', 25);")
17
+
18
+ db.close()
19
+ """
20
+
21
+ from tmjlite.driver import (
22
+ connect,
23
+ connect_shadow,
24
+ connect_shared,
25
+ open_branch,
26
+ Connection,
27
+ QueryResult,
28
+ Row,
29
+ ShadowConnection,
30
+ SharedConnection,
31
+ )
32
+
33
+ __version__ = "0.2.8"
34
+ __all__ = [
35
+ "connect", "connect_shared", "connect_shadow", "open_branch",
36
+ "Connection", "SharedConnection", "ShadowConnection", "QueryResult", "Row",
37
+ ]
Binary file
tmjlite/driver.py ADDED
@@ -0,0 +1,595 @@
1
+ """
2
+ TMJLite Python driver — loads libtmjlite_ffi via ctypes.
3
+ """
4
+
5
+ import ctypes
6
+ import os
7
+ import platform
8
+ import sys
9
+ from contextlib import contextmanager
10
+ from pathlib import Path
11
+
12
+
13
+ def _find_library():
14
+ """Locate the TMJLite shared library."""
15
+ system = platform.system()
16
+ if system == "Darwin":
17
+ name = "libtmjlite_ffi.dylib"
18
+ elif system == "Linux":
19
+ name = "libtmjlite_ffi.so"
20
+ elif system == "Windows":
21
+ name = "tmjlite_ffi.dll"
22
+ else:
23
+ raise OSError(f"unsupported platform: {system}")
24
+
25
+ search_paths = []
26
+
27
+ env_path = os.environ.get("TMJLITE_LIB_PATH")
28
+ if env_path:
29
+ search_paths.append(Path(env_path))
30
+
31
+ driver_dir = Path(__file__).resolve().parent
32
+
33
+ # Lib nativa embutida no pacote (_lib/<os>-<arch>/)
34
+ machine = platform.machine().lower()
35
+ arch = "arm64" if machine in ("arm64", "aarch64") else \
36
+ "x64" if machine in ("x86_64", "amd64") else machine
37
+ os_slug = {"Darwin": "darwin", "Linux": "linux", "Windows": "win32"}.get(system, system.lower())
38
+ search_paths.append(driver_dir / "_lib" / f"{os_slug}-{arch}" / name)
39
+
40
+ search_paths.extend([
41
+ driver_dir / name,
42
+ driver_dir.parent / name,
43
+ driver_dir.parent / "lib" / name,
44
+ ])
45
+
46
+ repo_root = driver_dir.parent.parent.parent
47
+ search_paths.extend([
48
+ repo_root / "target" / "release" / name,
49
+ repo_root / "target" / "debug" / name,
50
+ ])
51
+
52
+ for p in search_paths:
53
+ if p.is_file():
54
+ return str(p)
55
+
56
+ raise OSError(
57
+ f"cannot find {name}. Set TMJLITE_LIB_PATH or build with: "
58
+ f"cargo build --release -p tmjlite-ffi"
59
+ )
60
+
61
+
62
+ def _load_lib():
63
+ path = _find_library()
64
+ lib = ctypes.cdll.LoadLibrary(path)
65
+
66
+ # Lifecycle
67
+ lib.tmjlite_open.argtypes = [ctypes.c_char_p]
68
+ lib.tmjlite_open.restype = ctypes.c_void_p
69
+
70
+ lib.tmjlite_open_memory.argtypes = []
71
+ lib.tmjlite_open_memory.restype = ctypes.c_void_p
72
+
73
+ lib.tmjlite_close.argtypes = [ctypes.c_void_p]
74
+ lib.tmjlite_close.restype = None
75
+
76
+ # Execute
77
+ lib.tmjlite_exec.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
78
+ lib.tmjlite_exec.restype = ctypes.c_int
79
+
80
+ # Query
81
+ lib.tmjlite_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
82
+ lib.tmjlite_query.restype = ctypes.c_void_p
83
+
84
+ # Result accessors
85
+ lib.tmjlite_result_column_count.argtypes = [ctypes.c_void_p]
86
+ lib.tmjlite_result_column_count.restype = ctypes.c_int
87
+
88
+ lib.tmjlite_result_column_name.argtypes = [ctypes.c_void_p, ctypes.c_int]
89
+ lib.tmjlite_result_column_name.restype = ctypes.c_char_p
90
+
91
+ lib.tmjlite_result_row_count.argtypes = [ctypes.c_void_p]
92
+ lib.tmjlite_result_row_count.restype = ctypes.c_int
93
+
94
+ lib.tmjlite_result_value.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
95
+ lib.tmjlite_result_value.restype = ctypes.c_char_p
96
+
97
+ lib.tmjlite_result_message.argtypes = [ctypes.c_void_p]
98
+ lib.tmjlite_result_message.restype = ctypes.c_char_p
99
+
100
+ lib.tmjlite_result_free.argtypes = [ctypes.c_void_p]
101
+ lib.tmjlite_result_free.restype = None
102
+
103
+ # Error
104
+ lib.tmjlite_errmsg.argtypes = [ctypes.c_void_p]
105
+ lib.tmjlite_errmsg.restype = ctypes.c_char_p
106
+
107
+ # Save
108
+ lib.tmjlite_save.argtypes = [ctypes.c_void_p]
109
+ lib.tmjlite_save.restype = ctypes.c_int
110
+
111
+ lib.tmjlite_save_as.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
112
+ lib.tmjlite_save_as.restype = ctypes.c_int
113
+
114
+ # Shared (thread-safe) API
115
+ lib.tmjlite_open_shared.argtypes = [ctypes.c_char_p]
116
+ lib.tmjlite_open_shared.restype = ctypes.c_void_p
117
+
118
+ lib.tmjlite_close_shared.argtypes = [ctypes.c_void_p]
119
+ lib.tmjlite_close_shared.restype = None
120
+
121
+ lib.tmjlite_exec_shared.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
122
+ lib.tmjlite_exec_shared.restype = ctypes.c_int
123
+
124
+ lib.tmjlite_query_shared.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
125
+ lib.tmjlite_query_shared.restype = ctypes.c_void_p
126
+
127
+ lib.tmjlite_errmsg_shared.argtypes = [ctypes.c_void_p]
128
+ lib.tmjlite_errmsg_shared.restype = ctypes.c_char_p
129
+
130
+ # Version
131
+ lib.tmjlite_version.argtypes = []
132
+ lib.tmjlite_version.restype = ctypes.c_char_p
133
+
134
+ # Shadow / branch (git-merge) API
135
+ lib.tmjlite_shadow_open.argtypes = [ctypes.c_char_p]
136
+ lib.tmjlite_shadow_open.restype = ctypes.c_void_p
137
+ lib.tmjlite_shadow_open_branch.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
138
+ lib.tmjlite_shadow_open_branch.restype = ctypes.c_void_p
139
+ lib.tmjlite_shadow_close.argtypes = [ctypes.c_void_p]
140
+ lib.tmjlite_shadow_close.restype = None
141
+ lib.tmjlite_shadow_exec.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
142
+ lib.tmjlite_shadow_exec.restype = ctypes.c_int
143
+ lib.tmjlite_shadow_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
144
+ lib.tmjlite_shadow_query.restype = ctypes.c_void_p
145
+ lib.tmjlite_shadow_commit.argtypes = [ctypes.c_void_p]
146
+ lib.tmjlite_shadow_commit.restype = ctypes.c_longlong
147
+ lib.tmjlite_shadow_refresh.argtypes = [ctypes.c_void_p]
148
+ lib.tmjlite_shadow_refresh.restype = ctypes.c_int
149
+ lib.tmjlite_shadow_pending.argtypes = [ctypes.c_void_p]
150
+ lib.tmjlite_shadow_pending.restype = ctypes.c_longlong
151
+ lib.tmjlite_shadow_seal.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
152
+ lib.tmjlite_shadow_seal.restype = ctypes.c_longlong
153
+ lib.tmjlite_shadow_unsealed.argtypes = [ctypes.c_void_p]
154
+ lib.tmjlite_shadow_unsealed.restype = ctypes.c_longlong
155
+ lib.tmjlite_shadow_set_merge_policy.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
156
+ lib.tmjlite_shadow_set_merge_policy.restype = ctypes.c_int
157
+ lib.tmjlite_shadow_conflicts.argtypes = [ctypes.c_void_p]
158
+ lib.tmjlite_shadow_conflicts.restype = ctypes.c_longlong
159
+ lib.tmjlite_shadow_errmsg.argtypes = [ctypes.c_void_p]
160
+ lib.tmjlite_shadow_errmsg.restype = ctypes.c_char_p
161
+
162
+ return lib
163
+
164
+
165
+ _lib = None
166
+
167
+
168
+ def _get_lib():
169
+ global _lib
170
+ if _lib is None:
171
+ _lib = _load_lib()
172
+ return _lib
173
+
174
+
175
+ class TMJLiteError(Exception):
176
+ """Error from the TMJLite engine."""
177
+ pass
178
+
179
+
180
+ class Row:
181
+ """A single row from a query result."""
182
+
183
+ __slots__ = ("_columns", "_values")
184
+
185
+ def __init__(self, columns, values):
186
+ self._columns = columns
187
+ self._values = values
188
+
189
+ def __getitem__(self, key):
190
+ if isinstance(key, int):
191
+ return self._values[key]
192
+ if isinstance(key, str):
193
+ try:
194
+ idx = self._columns.index(key)
195
+ except ValueError:
196
+ raise KeyError(f"column '{key}' not found")
197
+ return self._values[idx]
198
+ raise TypeError(f"indices must be int or str, not {type(key).__name__}")
199
+
200
+ def __len__(self):
201
+ return len(self._values)
202
+
203
+ def __repr__(self):
204
+ pairs = ", ".join(f"{c}={v!r}" for c, v in zip(self._columns, self._values))
205
+ return f"Row({pairs})"
206
+
207
+ def keys(self):
208
+ return list(self._columns)
209
+
210
+ def values(self):
211
+ return list(self._values)
212
+
213
+ def items(self):
214
+ return list(zip(self._columns, self._values))
215
+
216
+ def to_dict(self):
217
+ return dict(zip(self._columns, self._values))
218
+
219
+
220
+ class QueryResult:
221
+ """Result of a SELECT query."""
222
+
223
+ __slots__ = ("columns", "rows")
224
+
225
+ def __init__(self, columns, rows):
226
+ self.columns = columns
227
+ self.rows = rows
228
+
229
+ def __len__(self):
230
+ return len(self.rows)
231
+
232
+ def __iter__(self):
233
+ return iter(self.rows)
234
+
235
+ def __getitem__(self, index):
236
+ return self.rows[index]
237
+
238
+ def __bool__(self):
239
+ return len(self.rows) > 0
240
+
241
+ def __repr__(self):
242
+ return f"QueryResult(columns={self.columns}, rows={len(self.rows)})"
243
+
244
+ def to_dicts(self):
245
+ return [row.to_dict() for row in self.rows]
246
+
247
+
248
+ def _parse_value(raw_str):
249
+ """Convert a string value from the C API to a Python type."""
250
+ if raw_str is None or raw_str == "NULL":
251
+ return None
252
+ if raw_str == "true":
253
+ return True
254
+ if raw_str == "false":
255
+ return False
256
+ try:
257
+ return int(raw_str)
258
+ except ValueError:
259
+ pass
260
+ try:
261
+ return float(raw_str)
262
+ except ValueError:
263
+ pass
264
+ return raw_str
265
+
266
+
267
+ class Connection:
268
+ """Connection to a TMJLite database."""
269
+
270
+ def __init__(self, handle):
271
+ self._lib = _get_lib()
272
+ self._handle = handle
273
+
274
+ def execute(self, sql):
275
+ """Execute a non-query SQL statement. Returns rows affected info as a string."""
276
+ rc = self._lib.tmjlite_exec(self._handle, sql.encode("utf-8"))
277
+ if rc != 0:
278
+ err = self._lib.tmjlite_errmsg(self._handle)
279
+ msg = err.decode("utf-8") if err else "unknown error"
280
+ raise TMJLiteError(msg)
281
+
282
+ def query(self, sql):
283
+ """Execute a SELECT and return a QueryResult."""
284
+ result_ptr = self._lib.tmjlite_query(self._handle, sql.encode("utf-8"))
285
+ if not result_ptr:
286
+ err = self._lib.tmjlite_errmsg(self._handle)
287
+ msg = err.decode("utf-8") if err else "unknown error"
288
+ raise TMJLiteError(msg)
289
+
290
+ try:
291
+ col_count = self._lib.tmjlite_result_column_count(result_ptr)
292
+ row_count = self._lib.tmjlite_result_row_count(result_ptr)
293
+
294
+ columns = []
295
+ for i in range(col_count):
296
+ name = self._lib.tmjlite_result_column_name(result_ptr, i)
297
+ columns.append(name.decode("utf-8") if name else "")
298
+
299
+ rows = []
300
+ for r in range(row_count):
301
+ values = []
302
+ for c in range(col_count):
303
+ raw = self._lib.tmjlite_result_value(result_ptr, r, c)
304
+ val_str = raw.decode("utf-8") if raw else None
305
+ values.append(_parse_value(val_str))
306
+ rows.append(Row(columns, values))
307
+
308
+ return QueryResult(columns, rows)
309
+ finally:
310
+ self._lib.tmjlite_result_free(result_ptr)
311
+
312
+ def begin(self):
313
+ """Start a transaction."""
314
+ self.execute("BEGIN")
315
+
316
+ def commit(self):
317
+ """Commit the current transaction."""
318
+ self.execute("COMMIT")
319
+
320
+ def rollback(self):
321
+ """Roll back the current transaction."""
322
+ self.execute("ROLLBACK")
323
+
324
+ @contextmanager
325
+ def transaction(self):
326
+ """Context manager: auto-commits on success, rolls back on exception."""
327
+ self.begin()
328
+ try:
329
+ yield
330
+ self.commit()
331
+ except Exception:
332
+ self.rollback()
333
+ raise
334
+
335
+ def save(self):
336
+ """Explicitly save to the database file."""
337
+ rc = self._lib.tmjlite_save(self._handle)
338
+ if rc != 0:
339
+ err = self._lib.tmjlite_errmsg(self._handle)
340
+ msg = err.decode("utf-8") if err else "unknown error"
341
+ raise TMJLiteError(msg)
342
+
343
+ def save_as(self, path):
344
+ """Save to a new file path."""
345
+ rc = self._lib.tmjlite_save_as(self._handle, path.encode("utf-8"))
346
+ if rc != 0:
347
+ err = self._lib.tmjlite_errmsg(self._handle)
348
+ msg = err.decode("utf-8") if err else "unknown error"
349
+ raise TMJLiteError(msg)
350
+
351
+ def close(self):
352
+ """Close the database connection."""
353
+ if self._handle:
354
+ self._lib.tmjlite_close(self._handle)
355
+ self._handle = None
356
+
357
+ def __enter__(self):
358
+ return self
359
+
360
+ def __exit__(self, *args):
361
+ self.close()
362
+
363
+ def __del__(self):
364
+ self.close()
365
+
366
+
367
+ def _read_query_result(lib, result_ptr):
368
+ """Materializa um TmjResult* em QueryResult e libera o ponteiro."""
369
+ try:
370
+ col_count = lib.tmjlite_result_column_count(result_ptr)
371
+ row_count = lib.tmjlite_result_row_count(result_ptr)
372
+ columns = []
373
+ for i in range(col_count):
374
+ name = lib.tmjlite_result_column_name(result_ptr, i)
375
+ columns.append(name.decode("utf-8") if name else "")
376
+ rows = []
377
+ for r in range(row_count):
378
+ values = []
379
+ for c in range(col_count):
380
+ raw = lib.tmjlite_result_value(result_ptr, r, c)
381
+ val_str = raw.decode("utf-8") if raw else None
382
+ values.append(_parse_value(val_str))
383
+ rows.append(Row(columns, values))
384
+ return QueryResult(columns, rows)
385
+ finally:
386
+ lib.tmjlite_result_free(result_ptr)
387
+
388
+
389
+ class SharedConnection:
390
+ """
391
+ Conexão thread-safe (MVCC): um único handle pode ser usado por N threads —
392
+ SELECTs rodam em snapshot em paralelo entre si e com o escritor; escritas
393
+ serializam e são duráveis ao retornar. Cada statement é auto-commit
394
+ (BEGIN/COMMIT não são suportados aqui; use Connection para transações).
395
+
396
+ Multi-processo continua exclusivo: abrir o mesmo arquivo em outro processo
397
+ falha com "database is locked by another process".
398
+
399
+ db = tmjlite.connect_shared("app.tmjp")
400
+ with ThreadPoolExecutor(8) as pool:
401
+ pool.map(lambda a: db.execute(...), agencies)
402
+ """
403
+
404
+ def __init__(self, handle):
405
+ self._lib = _get_lib()
406
+ self._handle = handle
407
+
408
+ def _raise_last(self):
409
+ err = self._lib.tmjlite_errmsg_shared(self._handle)
410
+ raise TMJLiteError(err.decode("utf-8") if err else "unknown error")
411
+
412
+ def execute(self, sql):
413
+ rc = self._lib.tmjlite_exec_shared(self._handle, sql.encode("utf-8"))
414
+ if rc != 0:
415
+ self._raise_last()
416
+
417
+ def query(self, sql):
418
+ result_ptr = self._lib.tmjlite_query_shared(self._handle, sql.encode("utf-8"))
419
+ if not result_ptr:
420
+ self._raise_last()
421
+ return _read_query_result(self._lib, result_ptr)
422
+
423
+ def close(self):
424
+ if self._handle:
425
+ self._lib.tmjlite_close_shared(self._handle)
426
+ self._handle = None
427
+
428
+ def __enter__(self):
429
+ return self
430
+
431
+ def __exit__(self, *args):
432
+ self.close()
433
+
434
+ def __del__(self):
435
+ self.close()
436
+
437
+
438
+ def connect_shared(path):
439
+ """Abre uma SharedConnection (thread-safe) para um arquivo .tmjp."""
440
+ lib = _get_lib()
441
+ handle = lib.tmjlite_open_shared(path.encode("utf-8"))
442
+ if not handle:
443
+ raise TMJLiteError(
444
+ f"failed to open database: {path} (em uso por outro processo?)"
445
+ )
446
+ return SharedConnection(handle)
447
+
448
+
449
+ class ShadowConnection:
450
+ """
451
+ Sessão shadow (git-merge): SQL normal sobre um snapshot do banco + as
452
+ próprias escritas; cada INSERT/UPDATE/DELETE vira uma op num `.tmjv` ao
453
+ lado do arquivo, e `commit()` faz o merge por chave primária no banco
454
+ principal (lock de milissegundos). N processos coexistem no mesmo arquivo
455
+ sem se bloquear — o que os vizinhos gravaram aparece após o merge deles
456
+ e um `refresh()`. DML-only: DDL e BEGIN/COMMIT são rejeitados.
457
+
458
+ Aberta com `open_branch(path, nome)` vira uma **branch nomeada** e
459
+ persistente (`.tmjb`): `seal()` grava um commit (autor/mensagem); só ops
460
+ seladas entram no `commit()`, que consome a branch.
461
+
462
+ with tmjlite.connect_shadow("app.tmjp") as sess:
463
+ sess.execute("INSERT INTO listing (agency_id, external_id) VALUES ('ag', 'e1');")
464
+ applied = sess.commit() # merge no main
465
+
466
+ br = tmjlite.open_branch("app.tmjp", "import-2026-09")
467
+ br.execute("UPDATE listing SET seen = 1 WHERE external_id = 'e1';")
468
+ br.seal("primeira leva", author="thiago")
469
+ br.commit()
470
+ """
471
+
472
+ def __init__(self, handle):
473
+ self._lib = _get_lib()
474
+ self._handle = handle
475
+
476
+ def _raise_last(self):
477
+ err = self._lib.tmjlite_shadow_errmsg(self._handle)
478
+ raise TMJLiteError(err.decode("utf-8") if err else "unknown error")
479
+
480
+ def execute(self, sql):
481
+ """INSERT/UPDATE/DELETE viram ops da sessão; SELECT roda no overlay."""
482
+ if self._lib.tmjlite_shadow_exec(self._handle, sql.encode("utf-8")) != 0:
483
+ self._raise_last()
484
+
485
+ def query(self, sql):
486
+ """SELECT sobre snapshot + próprias escritas."""
487
+ result_ptr = self._lib.tmjlite_shadow_query(self._handle, sql.encode("utf-8"))
488
+ if not result_ptr:
489
+ self._raise_last()
490
+ return _read_query_result(self._lib, result_ptr)
491
+
492
+ def commit(self):
493
+ """Merge no banco principal. Retorna o nº de ops aplicadas. Numa
494
+ branch, só ops seladas; ops soltas depois do último `seal` → erro."""
495
+ n = self._lib.tmjlite_shadow_commit(self._handle)
496
+ if n < 0:
497
+ self._raise_last()
498
+ return n
499
+
500
+ def refresh(self):
501
+ """Re-snapshota o main (vê merges dos vizinhos) mantendo as próprias ops."""
502
+ if self._lib.tmjlite_shadow_refresh(self._handle) != 0:
503
+ self._raise_last()
504
+
505
+ @property
506
+ def pending_ops(self):
507
+ """Ops registradas e ainda não mergeadas."""
508
+ return self._lib.tmjlite_shadow_pending(self._handle)
509
+
510
+ def seal(self, message, author=None):
511
+ """Branch nomeada: sela as ops não commitadas num commit. Retorna o nº de ops."""
512
+ author = author or os.environ.get("TMJLITE_AUTHOR") or os.environ.get("USER") or "python"
513
+ n = self._lib.tmjlite_shadow_seal(
514
+ self._handle, author.encode("utf-8"), (message or "").encode("utf-8")
515
+ )
516
+ if n < 0:
517
+ self._raise_last()
518
+ return n
519
+
520
+ @property
521
+ def unsealed_ops(self):
522
+ """Ops registradas depois do último `seal` (0 em sessão anônima)."""
523
+ return self._lib.tmjlite_shadow_unsealed(self._handle)
524
+
525
+ def set_merge_policy(self, policy, table=None):
526
+ """Política de conflito do commit: 'lww' | 'error' | 'field_merge' | 'manual'.
527
+ `table=None` define o default da sessão; senão só aquela tabela."""
528
+ t = table.encode("utf-8") if table else None
529
+ if self._lib.tmjlite_shadow_set_merge_policy(self._handle, t, policy.encode("utf-8")) != 0:
530
+ self._raise_last()
531
+
532
+ def conflicts(self):
533
+ """Dry-run do commit: descrição dos conflitos que o merge encontraria."""
534
+ n = self._lib.tmjlite_shadow_conflicts(self._handle)
535
+ if n < 0:
536
+ self._raise_last()
537
+ if n == 0:
538
+ return []
539
+ err = self._lib.tmjlite_shadow_errmsg(self._handle)
540
+ return err.decode("utf-8").split("\n") if err else []
541
+
542
+ def close(self):
543
+ """Fecha a sessão. Ops não mergeadas ficam no disco (mergeáveis depois)."""
544
+ if self._handle:
545
+ self._lib.tmjlite_shadow_close(self._handle)
546
+ self._handle = None
547
+
548
+ def __enter__(self):
549
+ return self
550
+
551
+ def __exit__(self, *args):
552
+ self.close()
553
+
554
+ def __del__(self):
555
+ self.close()
556
+
557
+
558
+ def connect_shadow(path):
559
+ """Abre uma sessão shadow anônima (escrita multi-processo por merge)."""
560
+ lib = _get_lib()
561
+ handle = lib.tmjlite_shadow_open(path.encode("utf-8"))
562
+ if not handle:
563
+ raise TMJLiteError(f"failed to open shadow session on: {path}")
564
+ return ShadowConnection(handle)
565
+
566
+
567
+ def open_branch(path, name):
568
+ """Abre (ou cria) a branch nomeada `name` em `<path>.shadows/<name>.tmjb`."""
569
+ lib = _get_lib()
570
+ handle = lib.tmjlite_shadow_open_branch(path.encode("utf-8"), name.encode("utf-8"))
571
+ if not handle:
572
+ raise TMJLiteError(f"failed to open branch '{name}' on: {path}")
573
+ return ShadowConnection(handle)
574
+
575
+
576
+ def connect(path=":memory:"):
577
+ """
578
+ Open a TMJLite database connection.
579
+
580
+ Args:
581
+ path: File path, or ":memory:" for an in-memory database.
582
+
583
+ Returns:
584
+ Connection object.
585
+ """
586
+ lib = _get_lib()
587
+ if path == ":memory:":
588
+ handle = lib.tmjlite_open_memory()
589
+ else:
590
+ handle = lib.tmjlite_open(path.encode("utf-8"))
591
+
592
+ if not handle:
593
+ raise TMJLiteError(f"failed to open database: {path}")
594
+
595
+ return Connection(handle)
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: tmjlite
3
+ Version: 0.2.8
4
+ Summary: TMJLite embedded SQL database driver — git-native branches, single-file .tmjp, native engine included
5
+ Home-page: https://tmjlite.com.br
6
+ Author: TMJ Sistemas de Informação
7
+ License: MIT
8
+ Project-URL: Homepage, https://tmjlite.com.br
9
+ Project-URL: Documentation, https://tmjlite.com.br/manual.html
10
+ Project-URL: Repository, https://github.com/TMJacometti/TMJPlatform
11
+ Project-URL: Changelog, https://github.com/TMJacometti/TMJPlatform/blob/main/CHANGELOG.md
12
+ Keywords: database,embedded,sql,tmjlite,branches
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Rust
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Database :: Database Engines/Servers
20
+ Classifier: Operating System :: MacOS
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Classifier: Operating System :: Microsoft :: Windows
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ Dynamic: home-page
26
+ Dynamic: requires-python
27
+
28
+ # tmjlite (Python)
29
+
30
+ Driver Python do **TMJLite** — banco de dados SQL embarcado, em um único
31
+ arquivo `.tmjp`, com **branches de dados git-native**. O motor nativo (Rust)
32
+ já vem dentro do wheel: nada para instalar além do `pip`.
33
+
34
+ ```bash
35
+ pip install tmjlite
36
+ ```
37
+
38
+ Plataformas: macOS (Apple Silicon), Linux x86-64, Windows x86-64 · Python ≥ 3.8.
39
+
40
+ ```python
41
+ import tmjlite
42
+
43
+ db = tmjlite.connect("app.tmjp")
44
+ db.execute("CREATE TABLE users (id PK, name TEXT, age INT);")
45
+ db.execute("INSERT INTO users (name, age) VALUES ('Alice', 30);")
46
+ for row in db.query("SELECT name, age FROM users WHERE age > 18;"):
47
+ print(row["name"], row["age"])
48
+
49
+ with db.transaction():
50
+ db.execute("INSERT INTO users (name, age) VALUES ('Bob', 25);")
51
+ db.close()
52
+ ```
53
+
54
+ Branches — trabalhe numa cópia lógica e faça merge no arquivo principal,
55
+ enquanto a aplicação continua enxergando o banco intocado:
56
+
57
+ ```python
58
+ b = tmjlite.open_branch("app.tmjp", "importacao")
59
+ b.execute("INSERT INTO users (name, age) VALUES ('Carol', 41);")
60
+ b.seal("primeira leva", author="thiago") # commit na branch
61
+ applied = b.commit() # merge no app.tmjp
62
+ print(f"{applied} linha(s) aplicadas")
63
+ ```
64
+
65
+ Vários processos ao mesmo tempo? `tmjlite.connect_shadow(path)` dá a cada
66
+ worker uma sessão própria; `commit()` mergeia por chave primária.
67
+
68
+ - Manual: <https://tmjlite.com.br/manual.html>
69
+ - Branches e concorrência (guia + FAQ): <https://tmjlite.com.br/branches.html>
70
+ - Exemplo completo (sistema de cadastro): <https://tmjlite.com.br/examples.html>
71
+ - Changelog: <https://github.com/TMJacometti/TMJPlatform/blob/main/CHANGELOG.md>
72
+
73
+ Licença MIT · TMJ Sistemas de Informação.
@@ -0,0 +1,7 @@
1
+ tmjlite/__init__.py,sha256=YWXB2_j2AV3pnykwDadBgylZ8qd1h905IHXEt-NUsqY,817
2
+ tmjlite/driver.py,sha256=dNZ0HCZb1IY7Iyr-0SobkDpCB0qyNxPDaX5LtMiC-_8,19636
3
+ tmjlite/_lib/win32-x64/tmjlite_ffi.dll,sha256=jXB3FJR17biXfpeMTqaT08lZ_aRviViNlbfdhOvHS0k,1639936
4
+ tmjlite-0.2.8.dist-info/METADATA,sha256=0bVNsAcvMZ8OpUatATkS8bUezW10v-N3lezKeDkBYDg,2792
5
+ tmjlite-0.2.8.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
6
+ tmjlite-0.2.8.dist-info/top_level.txt,sha256=XZC3Qcm1WAGoYeD6HoglS68FZFLPpPfqhb-m7kxsfWk,8
7
+ tmjlite-0.2.8.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1 @@
1
+ tmjlite