mdb-cli 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.
@@ -304,3 +304,6 @@ docs/assets/interactive-demo.js
304
304
 
305
305
  # mdb databases (tool-managed)
306
306
  .mdb/
307
+
308
+ # mdb lock sidecar files
309
+ *.md.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Markdown table workflows w/ SQLite powers ✨
5
5
  Project-URL: Homepage, https://atomanoid.github.io/mdb/
6
6
  Project-URL: Repository, https://github.com/atomanoid/mdb
@@ -89,15 +89,16 @@ Place inline query markers above tables in your markdown file using the followin
89
89
  | `🌀` | feed | Include the table below me as part of the dataset |
90
90
  | `💎` | tap | Execute my SQL query on the dataset and render the results as the table below me (table auto-generated if absent) |
91
91
 
92
- > Under the hood, datasets are ingested into SQLite backing databases on which SQL queries are actually run.
92
+ > Under the hood, datasets are dynamically backed by SQLite databases on which SQL queries are actually run 🚀
93
93
 
94
94
  ### Subcommands
95
95
 
96
- | Subcommand | Behavior |
97
- | ------------------------- | ----------------------------------------------------------------------------------------------------------- |
98
- | `mdb pull <sql-query>` | process all `🌀` markers first (pulls), then outputs SQL query results in CSV format |
99
- | `mdb push` | process all `🌀` markers first (pulls), then all `💎` markers after (pushes) |
100
- | `mdb push <sql-mutation>` | process all `🌀` markers first (pulls), execute the mutation, then all `🌀` and `💎` markers after (pushes) |
96
+ | Subcommand | Behavior |
97
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
98
+ | `mdb push` | process all `🌀` markers first (pulls), then all `💎` markers after (pushes) reports only changed markers by default |
99
+ | `mdb push <sql-mutation>` | process all `🌀` markers first (pulls), execute the mutation, then all `🌀` and `💎` markers after (pushes) |
100
+ | `mdb pull <sql-query>` | process all `🌀` markers first (pulls), then outputs SQL query results in CSV format |
101
+ | `mdb status` | inspects all `🌀` and `💎` markers, then reports metadata and warnings |
101
102
 
102
103
  ### Example
103
104
 
@@ -181,7 +182,9 @@ We can also mutate data directly. Running:
181
182
  mdb push "UPDATE snacks SET qty = qty + 20 WHERE name = 'Takis'"
182
183
  ```
183
184
 
184
- The 🌀 (feed) marker again ingests the snacks data into the dataset, applies the UPDATE, then both 🌀 (feed) and 💎 (tap) markers push fresh results back:
185
+ The 🌀 (feed) marker again ingests the snacks data into the dataset, applies the UPDATE, then both 🌀 (feed) and 💎 (tap) markers push fresh results back out.
186
+
187
+ So then in the updated `snacks.md`:
185
188
 
186
189
  ```markdown
187
190
  ## Inventory
@@ -206,11 +209,29 @@ The 🌀 (feed) marker again ingests the snacks data into the dataset, applies t
206
209
 
207
210
  Takis got restocked to 27 and dropped off the low-stock list — all from a single command.
208
211
 
212
+ ---
213
+
214
+ We can also inspect the current landscape of markers. Running:
215
+
216
+ ```bash
217
+ mdb status
218
+ ```
219
+
220
+ We'll get a snapshot of the available data:
221
+
222
+ ```
223
+ OK 🌀 snacks (3 cols, 4 rows) [snacks.md:5]
224
+ OK 💎 SELECT name, qty FROM snacks WHERE qty < 10 ORDER BY qty (2 cols, 2 rows) [snacks.md:15]
225
+ Inspected 1 files, 2 markers: 0 problems
226
+ ```
227
+
228
+ 🌀 (feed) and 💎 (tap) markers show table dimensions, query text — and any problems (missing tables, conflicts) are flagged.
229
+
209
230
  ## Agent Support
210
231
 
211
232
  `mdb-cli` includes a built-in agent skill. Install it:
212
233
 
213
- ```
234
+ ```bash
214
235
  mdb init # default: .mdb/skills/mdb/SKILL.md
215
236
  mdb init .claude/skills # Claude Code: .claude/skills/mdb/SKILL.md
216
237
  mdb init .opencode/skills # OpenCode: .opencode/skills/mdb/SKILL.md
@@ -218,3 +239,7 @@ mdb init path/to/agent/skills # any agent: path/to/agent/skills/mdb/SKILL.md
218
239
  ```
219
240
 
220
241
  Within a session, invoke the `/mdb` slash command to access a comprehensive reference covering marker syntax, subcommand workflows, templates for common operations, and an error reference guide. The skill provides everything an AI coding assistant needs to construct and debug mdb markers without leaving the editor.
242
+
243
+ ## Configuration
244
+
245
+ `mdb init` also generates a `.mdbrc` configuration file in the project root. Edit it to set project-level defaults for CLI flags (`include`, `compaction`, `verbose`). CLI flags always override `.mdbrc` values. See [CLI Usage](https://atomanoid.github.io/mdb/cli-usage/) for details.
@@ -61,15 +61,16 @@ Place inline query markers above tables in your markdown file using the followin
61
61
  | `🌀` | feed | Include the table below me as part of the dataset |
62
62
  | `💎` | tap | Execute my SQL query on the dataset and render the results as the table below me (table auto-generated if absent) |
63
63
 
64
- > Under the hood, datasets are ingested into SQLite backing databases on which SQL queries are actually run.
64
+ > Under the hood, datasets are dynamically backed by SQLite databases on which SQL queries are actually run 🚀
65
65
 
66
66
  ### Subcommands
67
67
 
68
- | Subcommand | Behavior |
69
- | ------------------------- | ----------------------------------------------------------------------------------------------------------- |
70
- | `mdb pull <sql-query>` | process all `🌀` markers first (pulls), then outputs SQL query results in CSV format |
71
- | `mdb push` | process all `🌀` markers first (pulls), then all `💎` markers after (pushes) |
72
- | `mdb push <sql-mutation>` | process all `🌀` markers first (pulls), execute the mutation, then all `🌀` and `💎` markers after (pushes) |
68
+ | Subcommand | Behavior |
69
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
70
+ | `mdb push` | process all `🌀` markers first (pulls), then all `💎` markers after (pushes) reports only changed markers by default |
71
+ | `mdb push <sql-mutation>` | process all `🌀` markers first (pulls), execute the mutation, then all `🌀` and `💎` markers after (pushes) |
72
+ | `mdb pull <sql-query>` | process all `🌀` markers first (pulls), then outputs SQL query results in CSV format |
73
+ | `mdb status` | inspects all `🌀` and `💎` markers, then reports metadata and warnings |
73
74
 
74
75
  ### Example
75
76
 
@@ -153,7 +154,9 @@ We can also mutate data directly. Running:
153
154
  mdb push "UPDATE snacks SET qty = qty + 20 WHERE name = 'Takis'"
154
155
  ```
155
156
 
156
- The 🌀 (feed) marker again ingests the snacks data into the dataset, applies the UPDATE, then both 🌀 (feed) and 💎 (tap) markers push fresh results back:
157
+ The 🌀 (feed) marker again ingests the snacks data into the dataset, applies the UPDATE, then both 🌀 (feed) and 💎 (tap) markers push fresh results back out.
158
+
159
+ So then in the updated `snacks.md`:
157
160
 
158
161
  ```markdown
159
162
  ## Inventory
@@ -178,11 +181,29 @@ The 🌀 (feed) marker again ingests the snacks data into the dataset, applies t
178
181
 
179
182
  Takis got restocked to 27 and dropped off the low-stock list — all from a single command.
180
183
 
184
+ ---
185
+
186
+ We can also inspect the current landscape of markers. Running:
187
+
188
+ ```bash
189
+ mdb status
190
+ ```
191
+
192
+ We'll get a snapshot of the available data:
193
+
194
+ ```
195
+ OK 🌀 snacks (3 cols, 4 rows) [snacks.md:5]
196
+ OK 💎 SELECT name, qty FROM snacks WHERE qty < 10 ORDER BY qty (2 cols, 2 rows) [snacks.md:15]
197
+ Inspected 1 files, 2 markers: 0 problems
198
+ ```
199
+
200
+ 🌀 (feed) and 💎 (tap) markers show table dimensions, query text — and any problems (missing tables, conflicts) are flagged.
201
+
181
202
  ## Agent Support
182
203
 
183
204
  `mdb-cli` includes a built-in agent skill. Install it:
184
205
 
185
- ```
206
+ ```bash
186
207
  mdb init # default: .mdb/skills/mdb/SKILL.md
187
208
  mdb init .claude/skills # Claude Code: .claude/skills/mdb/SKILL.md
188
209
  mdb init .opencode/skills # OpenCode: .opencode/skills/mdb/SKILL.md
@@ -190,3 +211,7 @@ mdb init path/to/agent/skills # any agent: path/to/agent/skills/mdb/SKILL.md
190
211
  ```
191
212
 
192
213
  Within a session, invoke the `/mdb` slash command to access a comprehensive reference covering marker syntax, subcommand workflows, templates for common operations, and an error reference guide. The skill provides everything an AI coding assistant needs to construct and debug mdb markers without leaving the editor.
214
+
215
+ ## Configuration
216
+
217
+ `mdb init` also generates a `.mdbrc` configuration file in the project root. Edit it to set project-level defaults for CLI flags (`include`, `compaction`, `verbose`). CLI flags always override `.mdbrc` values. See [CLI Usage](https://atomanoid.github.io/mdb/cli-usage/) for details.
@@ -0,0 +1,198 @@
1
+ """Configuration file (.mdbrc) support for mdb.
2
+
3
+ Handles JSONC parsing, config loading/validation, and template generation
4
+ for the .mdbrc project-level configuration file.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass
14
+ class MdbConfig:
15
+ """Parsed .mdbrc configuration."""
16
+ include: str | None = None
17
+ compaction: str = "full"
18
+ verbose: bool = False
19
+
20
+
21
+ MDBRC_TEMPLATE = """\
22
+ {
23
+ // \u2699\ufe0f uncomment below to change defaults
24
+
25
+ // "include": "**/*.md", // (string|null)
26
+ // "compaction": "full", // ("full"|"fit")
27
+ // "verbose": false, // (bool, default false)
28
+ }
29
+ """
30
+
31
+ _RECOGNIZED_KEYS = {"include", "compaction", "verbose"}
32
+
33
+
34
+ def strip_jsonc_comments(text: str) -> str:
35
+ """Remove // line comments from JSONC text, preserving // inside strings.
36
+
37
+ Also strips trailing commas before } and ] to handle JSONC trailing comma
38
+ syntax that json.loads() rejects.
39
+
40
+ Uses a character-by-character state machine to correctly handle string
41
+ literals containing // sequences.
42
+ """
43
+ result = []
44
+ i = 0
45
+ in_string = False
46
+ length = len(text)
47
+
48
+ while i < length:
49
+ ch = text[i]
50
+
51
+ if in_string:
52
+ result.append(ch)
53
+ if ch == '\\' and i + 1 < length:
54
+ # Escaped character inside string -- consume next char too
55
+ i += 1
56
+ result.append(text[i])
57
+ elif ch == '"':
58
+ in_string = False
59
+ i += 1
60
+ continue
61
+
62
+ # Outside string
63
+ if ch == '"':
64
+ in_string = True
65
+ result.append(ch)
66
+ i += 1
67
+ continue
68
+
69
+ if ch == '/' and i + 1 < length and text[i + 1] == '/':
70
+ # Line comment -- skip to end of line
71
+ while i < length and text[i] != '\n':
72
+ i += 1
73
+ continue
74
+
75
+ result.append(ch)
76
+ i += 1
77
+
78
+ stripped = ''.join(result)
79
+
80
+ # Remove trailing commas before } and ]
81
+ # Match: comma, optional whitespace/newlines, then } or ]
82
+ import re
83
+ stripped = re.sub(r',(\s*[}\]])', r'\1', stripped)
84
+
85
+ return stripped
86
+
87
+
88
+ def load_config(cwd: str | None = None) -> 'MdbConfig | None':
89
+ """Load and validate .mdbrc from the given directory.
90
+
91
+ Args:
92
+ cwd: Directory to search for .mdbrc. Defaults to os.getcwd().
93
+
94
+ Returns:
95
+ MdbConfig with parsed values, or None if no .mdbrc exists.
96
+ Calls sys.exit(1) on hard errors (invalid JSON, bad values, etc.).
97
+ """
98
+ config_dir = cwd or os.getcwd()
99
+ config_path = os.path.join(config_dir, ".mdbrc")
100
+
101
+ if not os.path.exists(config_path):
102
+ return None
103
+
104
+ # FR-016: Must be a regular file
105
+ if not os.path.isfile(config_path):
106
+ print("Error: .mdbrc is not a regular file", file=sys.stderr)
107
+ sys.exit(1)
108
+
109
+ # FR-017: Must be valid UTF-8
110
+ try:
111
+ with open(config_path, "r", encoding="utf-8") as f:
112
+ raw = f.read()
113
+ except UnicodeDecodeError:
114
+ print("Error: .mdbrc contains non-UTF-8 content", file=sys.stderr)
115
+ sys.exit(1)
116
+ except OSError as e:
117
+ print(f"Error: Cannot read .mdbrc: {e}", file=sys.stderr)
118
+ sys.exit(1)
119
+
120
+ # FR-007: Announce config discovery
121
+ print("Using .mdbrc", file=sys.stderr)
122
+
123
+ # Strip JSONC comments
124
+ stripped = strip_jsonc_comments(raw)
125
+
126
+ # FR-015: Empty/comments-only treated as empty object
127
+ if not stripped.strip():
128
+ return MdbConfig()
129
+
130
+ # FR-011: Parse JSON
131
+ try:
132
+ data = json.loads(stripped)
133
+ except json.JSONDecodeError as e:
134
+ print(f"Error: .mdbrc contains invalid JSON: {e}", file=sys.stderr)
135
+ sys.exit(1)
136
+
137
+ # CHK023: Root must be an object (dict)
138
+ if not isinstance(data, dict):
139
+ print("Error: .mdbrc root must be a JSON object", file=sys.stderr)
140
+ sys.exit(1)
141
+
142
+ # FR-012: Warn on unrecognized keys
143
+ for key in data:
144
+ if key not in _RECOGNIZED_KEYS:
145
+ print(f"Warning: .mdbrc contains unrecognized key: {key}", file=sys.stderr)
146
+
147
+ # FR-013: Validate recognized keys
148
+ config = MdbConfig()
149
+
150
+ if "include" in data:
151
+ val = data["include"]
152
+ if val is not None and not isinstance(val, str):
153
+ print("Error: .mdbrc 'include' must be a string or null", file=sys.stderr)
154
+ sys.exit(1)
155
+ config.include = val
156
+
157
+ if "compaction" in data:
158
+ val = data["compaction"]
159
+ if val not in ("full", "fit"):
160
+ print(f"Error: .mdbrc 'compaction' must be \"full\" or \"fit\", got: {val!r}", file=sys.stderr)
161
+ sys.exit(1)
162
+ config.compaction = val
163
+
164
+ if "verbose" in data:
165
+ val = data["verbose"]
166
+ if not isinstance(val, bool):
167
+ print(f"Error: .mdbrc 'verbose' must be a boolean, got: {val!r}", file=sys.stderr)
168
+ sys.exit(1)
169
+ config.verbose = val
170
+
171
+ return config
172
+
173
+
174
+ def generate_mdbrc(cwd: str | None = None) -> bool:
175
+ """Generate .mdbrc template file in the given directory.
176
+
177
+ Args:
178
+ cwd: Directory to write .mdbrc into. Defaults to os.getcwd().
179
+
180
+ Returns:
181
+ True on success (created or preserved), False on error.
182
+ """
183
+ config_dir = cwd or os.getcwd()
184
+ config_path = os.path.join(config_dir, ".mdbrc")
185
+
186
+ # FR-003: Never overwrite existing
187
+ if os.path.exists(config_path):
188
+ print(".mdbrc already exists (preserved)")
189
+ return True
190
+
191
+ try:
192
+ with open(config_path, "w", encoding="utf-8") as f:
193
+ f.write(MDBRC_TEMPLATE)
194
+ print("Created .mdbrc")
195
+ return True
196
+ except OSError as e:
197
+ print(f"Error: Cannot write .mdbrc: {e}", file=sys.stderr)
198
+ return False
@@ -51,10 +51,23 @@ mdb pull -i "docs/" "SELECT * FROM t" # restrict to directo
51
51
 
52
52
  Pull rules: only SELECT permitted; DML rejected.
53
53
 
54
+ ### `mdb status`
55
+
56
+ Read-only marker inventory. Reports structural metadata and detects problems
57
+
58
+ ```bash
59
+ mdb status # scan all markdown files
60
+ mdb status -i "docs/" # restrict to directory
61
+ ```
62
+
54
63
  ### Common flags
55
64
 
56
65
  `-i "path1/ path2/"` — restrict markdown file discovery to specific directories (default: recurse from CWD).
57
66
 
67
+ ### `.mdbrc` configuration
68
+
69
+ A `.mdbrc` file in the project root provides default values for CLI flags, generated by `mdb init`. Supported keys: `include`, `compaction`, `verbose`. CLI flags always override `.mdbrc` values.
70
+
58
71
  ## Key Behaviors
59
72
 
60
73
  - 🌀 markers across all files execute first (pull phase), then 💎 markers (push phase)
@@ -29,7 +29,7 @@ def resolve_paths(patterns: list[str]) -> tuple[list[str], list[str]]:
29
29
  """Resolve a list of glob patterns into deduplicated, sorted markdown file paths.
30
30
 
31
31
  Args:
32
- patterns: List of glob pattern strings (e.g., ["**"], ["docs/*"]).
32
+ patterns: List of glob pattern strings (e.g., ["**/*.md"], ["docs/*"]).
33
33
 
34
34
  Returns:
35
35
  Tuple of (files, warnings):
@@ -1,5 +1,6 @@
1
1
  """Advisory file locking for concurrency safety."""
2
2
 
3
+ import os
3
4
  import sys
4
5
  import time
5
6
  from contextlib import contextmanager
@@ -53,24 +54,38 @@ else:
53
54
  def batch_lock(filepaths):
54
55
  """Context manager: acquire exclusive locks on all filepaths in sorted order.
55
56
 
57
+ Uses separate .lock sidecar files so that atomic_write (which replaces the
58
+ data file via os.replace) does not invalidate the lock fd's inode — avoids
59
+ EIO on close() in overlayfs / container environments.
60
+
56
61
  On timeout or error, releases all previously acquired locks.
57
62
  """
58
63
  sorted_paths = sorted(filepaths)
59
- held = [] # list of open file objects
64
+ held = [] # list of (lock_path, open file object)
60
65
  try:
61
66
  for filepath in sorted_paths:
62
- fd = open(filepath, "r")
67
+ if not os.path.exists(filepath):
68
+ raise FileNotFoundError(f"No such file: '{filepath}'")
69
+ lock_path = filepath + ".lock"
70
+ fd = open(lock_path, "a")
63
71
  try:
64
72
  _lock(fd)
65
73
  except BaseException:
66
74
  fd.close()
67
75
  raise
68
- held.append(fd)
76
+ held.append((lock_path, fd))
69
77
  yield
70
78
  finally:
71
- for fd in reversed(held):
79
+ for lock_path, fd in reversed(held):
72
80
  try:
73
81
  _unlock(fd)
74
82
  except OSError:
75
83
  pass
76
- fd.close()
84
+ try:
85
+ fd.close()
86
+ except OSError:
87
+ pass
88
+ try:
89
+ os.unlink(lock_path)
90
+ except OSError:
91
+ pass