devnomads-cli 0.5.2__py3-none-any.whl → 0.5.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devnomads-cli
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Manage your DevNomads services from the command line
5
5
  Author-email: DevNomads <support@devnomads.nl>
6
6
  License: MIT
@@ -0,0 +1,7 @@
1
+ dncli.py,sha256=3WdV9xsEWXaJnaqyzCviKYV7hdADMZa2v69afkMPvOw,124105
2
+ devnomads_cli-0.5.4.dist-info/licenses/LICENSE,sha256=WPXQ_lmpr3tVzTD9Z6VTiBgFnFey-cEwEYoG4HlmbxY,1066
3
+ devnomads_cli-0.5.4.dist-info/METADATA,sha256=r1_WaqFGTmdROvJL3RdfCZX8xI8S751POgZ_Pygtn4U,5268
4
+ devnomads_cli-0.5.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ devnomads_cli-0.5.4.dist-info/entry_points.txt,sha256=irLaKiGtIzEKi87exmXAg0srUmoDK31fL0SlqJasAEU,69
6
+ devnomads_cli-0.5.4.dist-info/top_level.txt,sha256=rFWIJDQgL3jA2q6b2GAqZgCknDED4WLxNcHTktuWS1I,6
7
+ devnomads_cli-0.5.4.dist-info/RECORD,,
dncli.py CHANGED
@@ -44,6 +44,7 @@ from devnomads.api import Client as ApiClient
44
44
  from devnomads.api import DevNomadsError
45
45
  from devnomads.api.client import _unwrap as _lib_unwrap
46
46
  from devnomads.dns import Dns, challenge_name
47
+ from rich import box
47
48
  from rich.console import Console
48
49
  from rich.markup import escape
49
50
  from rich.table import Table
@@ -233,7 +234,35 @@ def _summarize_item(item: dict[str, Any]) -> str:
233
234
  return " ".join(parts)
234
235
 
235
236
 
236
- def _cell(value: Any) -> str:
237
+ def _summarize_list(items: list[dict[str, Any]]) -> str:
238
+ """Compact list-view summary of an object array: the count, plus a grouped
239
+ state breakdown when the items carry a state-like field (e.g. instances).
240
+ Full per-item detail is available via the matching ``show`` command."""
241
+
242
+ n = len(items)
243
+ key = next(
244
+ (
245
+ k
246
+ for k in ("state_last_known", "state", "status", "phase")
247
+ if any(k in it for it in items)
248
+ ),
249
+ None,
250
+ )
251
+ if key is None:
252
+ return str(n)
253
+ counts: dict[str, int] = {}
254
+ for item in items:
255
+ value = str(item.get(key, ""))
256
+ counts[value] = counts.get(value, 0) + 1
257
+ if len(counts) == 1:
258
+ return f"{n} {next(iter(counts))}".strip()
259
+ return ", ".join(f"{count} {value}" for value, count in counts.items())
260
+
261
+
262
+ def _cell(value: Any, *, compact: bool = False) -> str:
263
+ """Render a value for a table cell. ``compact`` (list views) collapses an
264
+ object array to a count summary; otherwise each item gets its own line."""
265
+
237
266
  if value is None:
238
267
  return ""
239
268
  if isinstance(value, bool):
@@ -244,8 +273,10 @@ def _cell(value: Any) -> str:
244
273
  if not value:
245
274
  return ""
246
275
  if all(isinstance(item, dict) for item in value):
247
- # a list of objects (instances, mailboxes, ips, ...) -> one
248
- # readable line per item instead of an unreadable JSON blob.
276
+ # a list of objects (instances, mailboxes, ips, ...): a count
277
+ # summary in lists, one line per item in detail views.
278
+ if compact:
279
+ return _summarize_list(value)
249
280
  return "\n".join(_summarize_item(item) for item in value)
250
281
  if not any(isinstance(item, (dict, list)) for item in value):
251
282
  return ", ".join(str(item) for item in value)
@@ -272,15 +303,80 @@ def _flatten_row(row: dict[str, Any]) -> dict[str, Any]:
272
303
  return flat
273
304
 
274
305
 
306
+ def _is_object_list(value: Any) -> bool:
307
+ return (
308
+ isinstance(value, list)
309
+ and len(value) > 0
310
+ and all(isinstance(item, dict) for item in value)
311
+ )
312
+
313
+
314
+ def _object_table(items: list[dict[str, Any]]) -> Table:
315
+ """A compact sub-table for an array of objects, used inside the detail
316
+ view so nested data (instances, mailboxes, ips, ...) reads as a table
317
+ rather than one long line."""
318
+
319
+ cols = _auto_columns(items)
320
+ if not cols: # all columns dropped (e.g. identical rows): show everything
321
+ cols = list(dict.fromkeys(key for item in items for key in item))
322
+ table = Table(box=box.SIMPLE_HEAD, show_header=True, pad_edge=False)
323
+ for col in cols:
324
+ table.add_column(col)
325
+ for item in items:
326
+ table.add_row(*(_cell(item.get(col), compact=True) for col in cols))
327
+ return table
328
+
329
+
275
330
  def _render_kv(data: dict[str, Any], title: str | None) -> None:
276
331
  table = Table(title=title, show_header=False)
277
332
  table.add_column(style="bold")
278
- table.add_column()
333
+ table.add_column(overflow="fold")
279
334
  for key, value in _flatten_row(data).items():
280
- table.add_row(key, _cell(value))
335
+ if value is None or value == "" or value == [] or value == {}:
336
+ continue # drop empty fields so the detail view stays readable
337
+ cell: Any = _object_table(value) if _is_object_list(value) else _cell(value)
338
+ table.add_row(key, cell)
281
339
  out_console.print(table)
282
340
 
283
341
 
342
+ def _auto_columns(rows: list[dict[str, Any]]) -> list[str]:
343
+ """Choose the columns worth showing in a list view, from the data itself.
344
+
345
+ Two things get dropped, because a list is an overview and the full record
346
+ is always available via the matching ``show`` command (or ``-o json``):
347
+
348
+ * columns empty in every row (no information at all);
349
+ * columns holding a list of objects (instances, mailboxes, ips, ...) -
350
+ these are detail, belonging in the ``show`` view, not the overview.
351
+
352
+ Every other field is kept, even if its value is the same on every row.
353
+ """
354
+
355
+ cols: list[str] = []
356
+ for row in rows:
357
+ if isinstance(row, dict):
358
+ for key in row:
359
+ if key not in cols:
360
+ cols.append(key)
361
+
362
+ def empty(value: Any) -> bool:
363
+ return value is None or value == "" or value == [] or value == {}
364
+
365
+ def is_object_list(col: str) -> bool:
366
+ return any(
367
+ isinstance(row.get(col), list)
368
+ and row.get(col)
369
+ and all(isinstance(item, dict) for item in row[col])
370
+ for row in rows
371
+ )
372
+
373
+ return [
374
+ c
375
+ for c in cols
376
+ if any(not empty(row.get(c)) for row in rows) and not is_object_list(c)
377
+ ]
378
+
379
+
284
380
  def _render_rows(
285
381
  rows: list[dict[str, Any]], columns: list[str] | None, title: str | None
286
382
  ) -> None:
@@ -288,12 +384,12 @@ def _render_rows(
288
384
  err_console.print("[dim]no results[/]")
289
385
  return
290
386
  rows = [_flatten_row(row) if isinstance(row, dict) else row for row in rows]
291
- cols = columns or list(rows[0].keys())
387
+ cols = columns if columns is not None else _auto_columns(rows)
292
388
  table = Table(title=title)
293
389
  for col in cols:
294
390
  table.add_column(col)
295
391
  for row in rows:
296
- table.add_row(*(_cell(row.get(col)) for col in cols))
392
+ table.add_row(*(_cell(row.get(col), compact=True) for col in cols))
297
393
  out_console.print(table)
298
394
 
299
395
 
@@ -1,7 +0,0 @@
1
- dncli.py,sha256=h6cfEBaxc5RrRRmVnD3OiOCD7GMJxAfez0iY7gHNEoQ,120626
2
- devnomads_cli-0.5.2.dist-info/licenses/LICENSE,sha256=WPXQ_lmpr3tVzTD9Z6VTiBgFnFey-cEwEYoG4HlmbxY,1066
3
- devnomads_cli-0.5.2.dist-info/METADATA,sha256=OzLplEX2KBKUnnUuzPDrzyyTV71kZaLF10G1U0YjLzc,5268
4
- devnomads_cli-0.5.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
- devnomads_cli-0.5.2.dist-info/entry_points.txt,sha256=irLaKiGtIzEKi87exmXAg0srUmoDK31fL0SlqJasAEU,69
6
- devnomads_cli-0.5.2.dist-info/top_level.txt,sha256=rFWIJDQgL3jA2q6b2GAqZgCknDED4WLxNcHTktuWS1I,6
7
- devnomads_cli-0.5.2.dist-info/RECORD,,