omdev 0.0.0.dev500__py3-none-any.whl → 0.0.0.dev509__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.

Potentially problematic release.


This version of omdev might be problematic. Click here for more details.

omdev/tools/sqlrepl.py CHANGED
@@ -1,3 +1,10 @@
1
+ """
2
+ TODO:
3
+ - sqlite
4
+ - unify-ish with omlish.sql
5
+ """
6
+ import abc
7
+ import configparser
1
8
  import dataclasses as dc
2
9
  import os.path
3
10
  import shutil
@@ -76,104 +83,197 @@ def spec_from_cfg(cfg: ta.Mapping[str, ta.Any], prefix: str) -> ServerSpec:
76
83
  )
77
84
 
78
85
 
79
- @lang.cached_function
80
- def _maybe_warn_pgcli_keyring() -> None:
81
- import pgcli.config
86
+ ##
87
+
88
+
89
+ @dc.dataclass(frozen=True)
90
+ class ReplArgs:
91
+ spec: ServerSpec
92
+ extra_args: ta.Sequence[str] | None = None # noqa
93
+
94
+ _: dc.KW_ONLY
95
+
96
+ exe: ta.Sequence[str] | None = None
97
+
98
+ no_dbcli: bool = False
99
+ no_import: bool = False
100
+ no_uv: bool = False
101
+ dbcli_version: str | None = None
102
+
103
+
104
+ class ReplRunner(lang.Abstract):
105
+ def __init__(self, args: ReplArgs) -> None:
106
+ super().__init__()
107
+
108
+ self._args = args
109
+
110
+ exe_name: ta.ClassVar[str]
111
+ dbcli_name: ta.ClassVar[str | None] = None
112
+
113
+ #
114
+
115
+ class Exe(ta.NamedTuple):
116
+ args: ta.Sequence[str]
117
+ is_dbcli: bool
118
+
119
+ @lang.cached_function
120
+ def exe(self) -> Exe:
121
+ if self._args.exe is not None:
122
+ if isinstance(self._args.exe, str):
123
+ return ReplRunner.Exe([self._args.exe], False)
124
+ else:
125
+ return ReplRunner.Exe(list(self._args.exe), False)
126
+
127
+ def default():
128
+ return ReplRunner.Exe([check.not_none(shutil.which(self.exe_name))], False)
129
+
130
+ if self._args.no_dbcli or self.dbcli_name is None:
131
+ return default()
132
+
133
+ if not self._args.no_import:
134
+ main_mod = self.dbcli_name + '.main'
135
+
136
+ try:
137
+ __import__(main_mod)
138
+ except ImportError:
139
+ pass
140
+ else:
141
+ return ReplRunner.Exe([sys.executable, '-m', main_mod], True)
142
+
143
+ if not self._args.no_uv and (uv_exe := shutil.which('uv')) is not None:
144
+ uv_arg = self.dbcli_name
145
+ if self._args.dbcli_version is not None:
146
+ uv_arg += f'@{self._args.dbcli_version}'
147
+
148
+ return ReplRunner.Exe([uv_exe, 'tool', 'run', uv_arg], True)
149
+
150
+ return default()
151
+
152
+ #
153
+
154
+ @abc.abstractmethod
155
+ def build_args(self) -> ta.Sequence[str]:
156
+ raise NotImplementedError
157
+
158
+ def pre_exec(self) -> None:
159
+ pass
160
+
161
+ #
162
+
163
+ def run(self) -> ta.NoReturn:
164
+ lst: list[str] = [
165
+ *self.exe().args,
166
+ *self.build_args(),
167
+ ]
168
+
169
+ self.pre_exec()
170
+
171
+ os.execvp(lst[0], lst)
172
+
173
+
174
+ class MysqlReplRunner(ReplRunner):
175
+ dbcli_name = 'mycli'
176
+ exe_name = 'mysql'
177
+
178
+ def build_args(self) -> ta.Sequence[str]:
179
+ lst: list[str] = []
180
+
181
+ if self._args.spec.username:
182
+ lst.extend(['--user', self._args.spec.username])
183
+
184
+ lst.extend(['--host', self._args.spec.host])
185
+ if not self.exe().is_dbcli:
186
+ lst.append('--protocol=TCP')
187
+ if self._args.spec.port:
188
+ lst.extend(['--port', str(self._args.spec.port)])
189
+
190
+ if self._args.spec.db:
191
+ lst.append(self._args.spec.db)
192
+
193
+ lst.extend(self._args.extra_args or [])
194
+
195
+ return lst
196
+
197
+ def pre_exec(self) -> None:
198
+ super().pre_exec()
199
+
200
+ if self._args.spec.password:
201
+ os.environ['MYSQL_PWD'] = self._args.spec.password
202
+
203
+
204
+ class PostgresReplRunner(ReplRunner):
205
+ dbcli_name = 'pgcli'
206
+ exe_name = 'psql'
207
+
208
+ def build_args(self) -> ta.Sequence[str]:
209
+ lst: list[str] = []
210
+
211
+ if self._args.spec.username:
212
+ lst.extend(['--username', self._args.spec.username])
213
+
214
+ if self._args.spec.host:
215
+ lst.extend(['--host', self._args.spec.host])
216
+ if self._args.spec.port:
217
+ lst.extend(['--port', str(self._args.spec.port)])
218
+
219
+ if self._args.spec.db:
220
+ lst.append(self._args.spec.db)
221
+
222
+ lst.extend(self._args.extra_args or [])
223
+
224
+ return lst
225
+
226
+ def _maybe_warn_keyring(self) -> None:
227
+ if 'XDG_CONFIG_HOME' in os.environ:
228
+ cfg_dir = f'{os.path.expanduser(os.environ["XDG_CONFIG_HOME"])}/pgcli/'
229
+ else:
230
+ cfg_dir = os.path.expanduser('~/.config/pgcli/')
231
+ cfg_path = os.path.join(cfg_dir, 'config')
232
+
233
+ if os.path.exists(cfg_path):
234
+ cfg = configparser.ConfigParser()
235
+ cfg.read(cfg_path)
236
+
237
+ if cfg.has_section('main') and not cfg.getboolean('main', 'keyring', fallback=True):
238
+ return
82
239
 
83
- c = pgcli.config.get_config()
84
- if c['main'].as_bool('keyring'):
85
240
  warnings.warn(
86
241
  'pgcli keyring is not disabled, it will try to store credentials. '
87
- 'set `keyring = False` in ~/.config/pgcli/config',
242
+ 'set `[main] keyring = False` in ~/.config/pgcli/config',
88
243
  )
89
244
 
245
+ def pre_exec(self) -> None:
246
+ super().pre_exec()
90
247
 
91
- def _dbcli_or_fallback_exe(dbcli_mod: str | None, default_exe: str) -> tuple[ta.Sequence[str], bool]:
92
- if dbcli_mod is not None:
93
- main_mod = dbcli_mod + '.main'
94
- try:
95
- __import__(main_mod)
96
- except ImportError:
97
- pass
98
- else:
99
- if dbcli_mod == 'pgcli':
100
- _maybe_warn_pgcli_keyring()
101
- return [sys.executable, '-m', main_mod], True
102
- return [check.not_none(shutil.which(default_exe))], False
103
-
104
-
105
- def exec_mysql_cli(
106
- spec: ServerSpec,
107
- *extra_args: str,
108
- exe: ta.Iterable[str] | None = None,
109
- no_dbcli: bool = False,
110
- ) -> ta.NoReturn:
111
- if exe is not None:
112
- args, is_dbcli = list(exe), False
113
- else:
114
- argsx, is_dbcli = _dbcli_or_fallback_exe(
115
- 'mycli' if not no_dbcli else None,
116
- 'mysql',
117
- )
118
- args = list(argsx)
119
- if spec.username:
120
- args.extend(['--user', spec.username])
121
- if spec.password:
122
- os.environ['MYSQL_PWD'] = spec.password
123
- args.extend(['--host', spec.host])
124
- if not is_dbcli:
125
- args.append('--protocol=TCP')
126
- if spec.port:
127
- args.extend(['--port', str(spec.port)])
128
- if spec.db:
129
- args.append(spec.db)
130
- args.extend(extra_args)
131
- os.execvp(args[0], args)
132
-
133
-
134
- def exec_postgres_cli(
135
- spec: ServerSpec,
136
- *extra_args: str,
137
- exe: ta.Iterable[str] | None = None,
138
- no_dbcli: bool = False,
139
- ) -> ta.NoReturn:
140
- if exe is not None:
141
- args, is_dbcli = list(exe), False
142
- else:
143
- argsx, is_dbcli = _dbcli_or_fallback_exe(
144
- 'pgcli' if not no_dbcli else None,
145
- 'psql',
146
- )
147
- args = list(argsx)
148
- if spec.username:
149
- args.extend(['--username', spec.username])
150
- if spec.password:
151
- os.environ['PGPASSWORD'] = spec.password
152
- if spec.host:
153
- args.extend(['--host', spec.host])
154
- if spec.port:
155
- args.extend(['--port', str(spec.port)])
156
- if spec.db:
157
- args.append(spec.db)
158
- args.extend(extra_args)
159
- os.execvp(args[0], args)
248
+ self._maybe_warn_keyring()
249
+
250
+ if self._args.spec.password:
251
+ os.environ['PGPASSWORD'] = self._args.spec.password
252
+
253
+
254
+ ##
160
255
 
161
256
 
162
257
  class Cli(ap.Cli):
163
258
  @ap.cmd(
164
- ap.arg('--no-dbcli', action='store_true'),
165
259
  ap.arg('dialect'),
166
260
  ap.arg('target'),
167
261
  ap.arg('args', nargs='*'),
262
+ ap.arg('--no-dbcli', action='store_true'),
263
+ ap.arg('--no-import', action='store_true'),
264
+ ap.arg('--no-uv', action='store_true'),
168
265
  )
169
266
  def repl(self) -> None:
170
267
  l, _, r = (target := self.args.target).partition(':')
171
268
  _, lf = os.path.dirname(l), os.path.basename(l)
172
269
  if not lf.endswith('.yml'):
173
270
  raise Exception(f'unhandled target: {target=}')
271
+
174
272
  with open(l) as f:
175
273
  cfg = yaml.safe_load(f.read())
274
+
176
275
  dialect = self.args.dialect
276
+
177
277
  if lf == 'compose.yml':
178
278
  svc = cfg['services'][r]
179
279
  if dialect == 'mysql':
@@ -185,13 +285,24 @@ class Cli(ap.Cli):
185
285
  else:
186
286
  spec = spec_from_cfg(cfg, r)
187
287
 
288
+ repl_args = ReplArgs(
289
+ spec,
290
+ self.args.args,
291
+ no_dbcli=self.args.no_dbcli,
292
+ no_import=self.args.no_import,
293
+ no_uv=self.args.no_uv,
294
+ )
295
+
296
+ repl_run: ReplRunner
188
297
  if dialect == 'mysql':
189
- exec_mysql_cli(spec, *self.args.args, no_dbcli=self.args.no_dbcli)
298
+ repl_run = MysqlReplRunner(repl_args)
190
299
  elif dialect == 'postgres':
191
- exec_postgres_cli(spec, *self.args.args, no_dbcli=self.args.no_dbcli)
300
+ repl_run = PostgresReplRunner(repl_args)
192
301
  else:
193
302
  raise Exception(f'unhandled dialect: {dialect=}')
194
303
 
304
+ repl_run.run()
305
+
195
306
 
196
307
  # @omlish-manifest
197
308
  _CLI_MODULE = CliModule('sqlrepl', __name__)
@@ -130,6 +130,8 @@ class NaiveMarkdownLiveStream(MarkdownLiveStream):
130
130
 
131
131
 
132
132
  class IncrementalMarkdownLiveStream(MarkdownLiveStream):
133
+ # @omlish-llm-author "gemini-3-pro"
134
+
133
135
  def __init__(
134
136
  self,
135
137
  *,
@@ -147,40 +149,239 @@ class IncrementalMarkdownLiveStream(MarkdownLiveStream):
147
149
  ip_out = self._inc_parser.feed2(s)
148
150
 
149
151
  if ip_out.new_stable:
150
- # try:
151
- # srs = getattr(self, '_srs')
152
- # except AttributeError:
153
- # setattr(self, '_srs', srs := [])
154
- # from ...markdown.tokens import token_repr, flatten_tokens
155
- # srs.extend(map(token_repr, flatten_tokens(ip_out.new_stable)))
156
-
152
+ # Render the stable lines
157
153
  stable_lines = self._console_render(markdown_from_tokens(ip_out.new_stable))
158
- stable_lines.append(' ') # FIXME: lame hack
159
- self._live.console.print(rich.text.Text.from_ansi('\n'.join(stable_lines), no_wrap=True))
154
+
155
+ # Overlap Detection
156
+ already_printed_count = min(self._lines_printed_to_scrollback, len(stable_lines))
157
+
158
+ if already_printed_count < len(stable_lines):
159
+ new_stable_lines = stable_lines[already_printed_count:]
160
+ # Ensure no_wrap=True is used here to match strict line counting
161
+ self._live.console.print(rich.text.Text.from_ansi('\n'.join(new_stable_lines), no_wrap=True))
162
+
163
+ # Adjust counter
160
164
  self._lines_printed_to_scrollback = max(0, self._lines_printed_to_scrollback - len(stable_lines))
161
165
 
162
166
  unstable_lines = self._console_render(markdown_from_tokens(ip_out.unstable))
163
167
 
164
- # Calculate how many lines fit in the live window
165
168
  available_height = self._console.height - 2
166
-
167
- # Determine which lines overflow and need to be printed to scrollback
168
169
  total_lines = len(unstable_lines)
170
+
169
171
  if total_lines > available_height:
170
- # Lines that should be in scrollback
171
- lines_for_scrollback = total_lines - available_height
172
+ # We calculate lines allowed in scrollback, but we subtract 1. This ensures the very bottom line (often a
173
+ # transient border) stays in the Live window and is not committed to history until more content pushes it
174
+ # up.
175
+ lines_for_scrollback = max(0, total_lines - available_height - 1)
172
176
 
173
- # Print any new lines that weren't already printed
174
177
  if lines_for_scrollback > self._lines_printed_to_scrollback:
175
178
  new_lines_to_print = unstable_lines[self._lines_printed_to_scrollback:lines_for_scrollback]
176
- self._live.console.print(rich.text.Text.from_ansi('\n'.join(new_lines_to_print)))
179
+
180
+ # Ensure no_wrap=True is used here to prevent auto-wrap from creating "phantom" lines that desync our
181
+ # line counts.
182
+ self._live.console.print(rich.text.Text.from_ansi('\n'.join(new_lines_to_print), no_wrap=True))
183
+
177
184
  self._lines_printed_to_scrollback = lines_for_scrollback
178
185
 
179
- # Show only the bottom portion in the live window
180
186
  visible_lines = unstable_lines[-available_height:]
181
187
 
182
188
  else:
183
189
  visible_lines = unstable_lines
184
190
 
185
- # Update the live display
186
191
  self._live.update(rich.text.Text.from_ansi('\n'.join(visible_lines)))
192
+
193
+
194
+ class SteppedIncrementalMarkdownLiveStream(MarkdownLiveStream):
195
+ # @omlish-llm-author "gemini-3-pro"
196
+
197
+ def __init__(
198
+ self,
199
+ *,
200
+ parser: ta.Optional['md.MarkdownIt'] = None,
201
+ console: ta.Optional['rich.console.Console'] = None,
202
+ scroll_step: int | None = None,
203
+ ) -> None:
204
+ super().__init__(
205
+ parser=parser,
206
+ console=console,
207
+ )
208
+
209
+ self._inc_parser = IncrementalMarkdownParser(parser=self._parser)
210
+ self._scroll_step = scroll_step
211
+
212
+ def feed(self, s: str) -> None:
213
+ ip_out = self._inc_parser.feed2(s)
214
+
215
+ ##
216
+ # Handle stable content
217
+
218
+ if ip_out.new_stable:
219
+ stable_lines = self._console_render(markdown_from_tokens(ip_out.new_stable))
220
+
221
+ # Overlap Detection: Determine how many lines of this now-stable block were already pushed to scrollback
222
+ # while they were in the "unstable" phase.
223
+ already_printed_count = min(self._lines_printed_to_scrollback, len(stable_lines))
224
+
225
+ if already_printed_count < len(stable_lines):
226
+ new_stable_lines = stable_lines[already_printed_count:]
227
+ # Force no_wrap=True to ensure 1:1 line counting
228
+ self._live.console.print(rich.text.Text.from_ansi('\n'.join(new_stable_lines), no_wrap=True))
229
+
230
+ # Adjust the global scrollback counter. We effectively shift the "start" of the unstable window down by the
231
+ # size of the stable block.
232
+ self._lines_printed_to_scrollback = max(0, self._lines_printed_to_scrollback - len(stable_lines))
233
+
234
+ ##
235
+ # Handle unstable content
236
+
237
+ unstable_lines = self._console_render(markdown_from_tokens(ip_out.unstable))
238
+ total_lines = len(unstable_lines)
239
+ available_height = self._console.height - 2
240
+
241
+ # Calculate the absolute minimum lines that MUST be in scrollback to fit the current content. We subtract 1 to
242
+ # hold back the bottom-most line (e.g., table borders) from history until it is pushed up by further content.
243
+ min_needed_scrollback = max(0, total_lines - available_height - 1)
244
+
245
+ if min_needed_scrollback > self._lines_printed_to_scrollback:
246
+ # We need to scroll. Calculate how much.
247
+ diff = min_needed_scrollback - self._lines_printed_to_scrollback
248
+
249
+ # Use the step size if configured, otherwise just satisfy the immediate difference. If the difference is
250
+ # larger than the step (e.g., big paste), we jump the full difference.
251
+ step = self._scroll_step if self._scroll_step is not None else 1
252
+ jump_size = max(diff, step)
253
+
254
+ # Calculate the new target scrollback index. We must clamp this to 'total_lines - 1' to ensure we never push
255
+ # the strictly held-back last line into history.
256
+ max_pushable_index = max(0, total_lines - 1)
257
+ target_scrollback = min(self._lines_printed_to_scrollback + jump_size, max_pushable_index)
258
+
259
+ if target_scrollback > self._lines_printed_to_scrollback:
260
+ new_lines_to_print = unstable_lines[self._lines_printed_to_scrollback:target_scrollback]
261
+ self._live.console.print(rich.text.Text.from_ansi('\n'.join(new_lines_to_print), no_wrap=True))
262
+ self._lines_printed_to_scrollback = target_scrollback
263
+
264
+ # Update the live display. We slice from '_lines_printed_to_scrollback' to the end. If we just performed a large
265
+ # 'jump', this will naturally result in fewer lines than 'available_height', creating the desired visual "void"
266
+ # at the bottom.
267
+ visible_lines = unstable_lines[self._lines_printed_to_scrollback:]
268
+ self._live.update(rich.text.Text.from_ansi('\n'.join(visible_lines)))
269
+
270
+
271
+ ##
272
+
273
+
274
+ class ClaudeIncrementalMarkdownLiveStream(MarkdownLiveStream):
275
+ # @omlish-llm-author "claude-opus-4-5"
276
+
277
+ def __init__(
278
+ self,
279
+ *,
280
+ parser: ta.Optional['md.MarkdownIt'] = None,
281
+ console: ta.Optional['rich.console.Console'] = None,
282
+ ) -> None:
283
+ super().__init__(
284
+ parser=parser,
285
+ console=console,
286
+ )
287
+
288
+ from ...markdown.incparse import ClaudeIncrementalMarkdownParser # noqa
289
+ self._inc_parser = ClaudeIncrementalMarkdownParser(parser=self._parser)
290
+ self._last_unstable_line_count = 0
291
+
292
+ def _enter_contexts(self) -> None:
293
+ super()._enter_contexts()
294
+
295
+ # Override to configure Live with explicit vertical overflow handling
296
+ self._live = self._enter_context(rich.live.Live(
297
+ rich.text.Text(''),
298
+ console=self._console,
299
+ refresh_per_second=10,
300
+ vertical_overflow='crop',
301
+ ))
302
+
303
+ def feed(self, s: str) -> None:
304
+ ip_out = self._inc_parser.feed2(s)
305
+
306
+ if ip_out.new_stable:
307
+ # Stop live display to commit stable content cleanly
308
+ self._live.stop()
309
+
310
+ # Render and print stable content to true scrollback
311
+ stable_lines = self._console_render(markdown_from_tokens(ip_out.new_stable))
312
+ for line in stable_lines:
313
+ self._console.print(rich.text.Text.from_ansi(line), highlight=False)
314
+
315
+ # Reset tracking state since we're starting fresh with new unstable content
316
+ self._last_unstable_line_count = 0
317
+ self._lines_printed_to_scrollback = 0
318
+
319
+ # Restart live display
320
+ self._live.start()
321
+
322
+ # Render current unstable content
323
+ unstable_lines = self._console_render(markdown_from_tokens(ip_out.unstable))
324
+ current_line_count = len(unstable_lines)
325
+
326
+ # Calculate available display height
327
+ available_height = self._console.height - 2
328
+
329
+ # Handle content that exceeds available height. Key insight: never commit unstable content to scrollback since
330
+ # it may change.
331
+ if current_line_count > available_height:
332
+ # Only show the bottom portion that fits
333
+ visible_lines = unstable_lines[-available_height:]
334
+ else:
335
+ visible_lines = unstable_lines
336
+
337
+ # Handle shrinking content: if we had more lines before and now have fewer, we need to ensure the live region is
338
+ # properly cleared.
339
+ if current_line_count < self._last_unstable_line_count:
340
+ # Pad with empty lines to prevent artifacts from previous longer content. This ensures the live region fully
341
+ # overwrites its previous state.
342
+ lines_to_clear = min(
343
+ self._last_unstable_line_count - current_line_count,
344
+ available_height - len(visible_lines),
345
+ )
346
+ if lines_to_clear > 0:
347
+ visible_lines = visible_lines + [''] * lines_to_clear
348
+
349
+ self._last_unstable_line_count = current_line_count
350
+
351
+ # Update the live display
352
+ display_text = '\n'.join(visible_lines)
353
+ self._live.update(rich.text.Text.from_ansi(display_text))
354
+
355
+
356
+ class GptIncrementalMarkdownLiveStream(MarkdownLiveStream):
357
+ # @omlish-llm-author "gpt-5.2"
358
+
359
+ def __init__(
360
+ self,
361
+ *,
362
+ parser: ta.Optional['md.MarkdownIt'] = None,
363
+ console: ta.Optional['rich.console.Console'] = None,
364
+ ) -> None:
365
+ super().__init__(
366
+ parser=parser,
367
+ console=console,
368
+ )
369
+
370
+ from ...markdown.incparse import GptIncrementalMarkdownParser # noqa
371
+ self._inc_parser = GptIncrementalMarkdownParser(parser=self._parser)
372
+
373
+ def feed(self, s: str) -> None:
374
+ ip_out = self._inc_parser.feed2(s)
375
+
376
+ # Permanently commit only content the parser marked as stable. This avoids *all* "scrollback delta accounting"
377
+ # and the associated duplication/gap bugs when the rendered tail shrinks / reflows / reinterprets (streaming
378
+ # markdown is non-monotonic).
379
+ if ip_out.new_stable:
380
+ # Print stable renderables directly through Rich (avoid ANSI round-tripping / splitlines). Use end="" so we
381
+ # don't inject extra blank lines beyond what the markdown renderable produces.
382
+ self._live.console.print(markdown_from_tokens(ip_out.new_stable), end='')
383
+
384
+ # Show the remaining (unstable) tail in the live region. We intentionally do *not* try to "spill overflow of
385
+ # unstable to scrollback", since those lines are not provably stable and may retroactively change; printing them
386
+ # would violate correctness.
387
+ self._live.update(markdown_from_tokens(ip_out.unstable))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omdev
3
- Version: 0.0.0.dev500
3
+ Version: 0.0.0.dev509
4
4
  Summary: omdev
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause
@@ -14,9 +14,9 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Python: >=3.13
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: omlish==0.0.0.dev500
17
+ Requires-Dist: omlish==0.0.0.dev509
18
18
  Provides-Extra: all
19
- Requires-Dist: black~=25.12; extra == "all"
19
+ Requires-Dist: black~=26.1; extra == "all"
20
20
  Requires-Dist: pycparser~=2.23; extra == "all"
21
21
  Requires-Dist: pcpp~=1.30; extra == "all"
22
22
  Requires-Dist: docutils~=0.22; extra == "all"
@@ -27,11 +27,11 @@ Requires-Dist: mypy~=1.19; extra == "all"
27
27
  Requires-Dist: gprof2dot~=2025.4; extra == "all"
28
28
  Requires-Dist: segno~=1.6; extra == "all"
29
29
  Requires-Dist: rich~=14.2; extra == "all"
30
- Requires-Dist: textual~=7.0; extra == "all"
30
+ Requires-Dist: textual~=7.3; extra == "all"
31
31
  Requires-Dist: textual-dev~=1.8; extra == "all"
32
32
  Requires-Dist: textual-speedups~=0.2; extra == "all"
33
33
  Provides-Extra: black
34
- Requires-Dist: black~=25.12; extra == "black"
34
+ Requires-Dist: black~=26.1; extra == "black"
35
35
  Provides-Extra: c
36
36
  Requires-Dist: pycparser~=2.23; extra == "c"
37
37
  Requires-Dist: pcpp~=1.30; extra == "c"
@@ -48,7 +48,7 @@ Provides-Extra: qr
48
48
  Requires-Dist: segno~=1.6; extra == "qr"
49
49
  Provides-Extra: tui
50
50
  Requires-Dist: rich~=14.2; extra == "tui"
51
- Requires-Dist: textual~=7.0; extra == "tui"
51
+ Requires-Dist: textual~=7.3; extra == "tui"
52
52
  Requires-Dist: textual-dev~=1.8; extra == "tui"
53
53
  Requires-Dist: textual-speedups~=0.2; extra == "tui"
54
54
  Dynamic: license-file