11#!/usr/bin/env python3
22
33import contextlib
4+ import csv
45import datetime
56import os
67import shlex
@@ -111,6 +112,7 @@ def start_kube_config_watcher() -> None:
111112
112113SNAPSHOT_EXPORT_DIR = Path ("/var/tmp/kmp" )
113114SNAPSHOT_SAVE_COMMANDS = {"s" , ":s" , "save" , ":save" , ":export" }
115+ CSV_SAVE_COMMANDS = {"csv" , ":csv" }
114116
115117WINDOWS_INPUT_BUFFER : List [str ] = []
116118POSIX_INPUT_BUFFER : List [str ] = []
@@ -368,6 +370,24 @@ def _save_markdown_snapshot(markdown: str) -> Path:
368370 return file_path
369371
370372
373+ def _save_csv_snapshot (headers : Sequence [str ], rows : Sequence [Sequence [str ]]) -> Path :
374+ """CSV 파일을 저장하고 경로 반환."""
375+ try :
376+ SNAPSHOT_EXPORT_DIR .mkdir (parents = True , exist_ok = True )
377+ except OSError as exc :
378+ raise SnapshotSaveError (SNAPSHOT_EXPORT_DIR , exc ) from exc
379+ timestamp = datetime .datetime .now ().strftime ("%Y-%m-%d-%H-%M-%S" )
380+ file_path = SNAPSHOT_EXPORT_DIR / f"{ timestamp } .csv"
381+ try :
382+ with file_path .open ("w" , newline = "" , encoding = "utf-8" ) as csvfile :
383+ writer = csv .writer (csvfile )
384+ writer .writerow (headers )
385+ writer .writerows (rows )
386+ except OSError as exc :
387+ raise SnapshotSaveError (file_path , exc ) from exc
388+ return file_path
389+
390+
371391def _read_nonblocking_command () -> Optional [str ]:
372392 """사용자 입력(line 단위)을 블로킹 없이 읽어 Slack 저장 요청 감지."""
373393 if not sys .stdin .isatty ():
@@ -456,48 +476,83 @@ def _read_nonblocking_command() -> Optional[str]:
456476 return None
457477
458478
459- def _handle_snapshot_command (
460- live : Live , markdown : Optional [str ], command : Optional [str ]
461- ) -> None :
462- """저장 요청이 있는 경우 Markdown을 파일로 기록."""
463- if command is None :
464- return
465- display_command = command .strip () or "<empty>"
466- normalized = command .lower ()
467- if normalized not in SNAPSHOT_SAVE_COMMANDS :
479+ def _handle_csv_save (live : Live , tracker : "LiveFrameTracker" , command : str ) -> None :
480+ """CSV 저장 요청을 처리."""
481+ if not tracker .latest_structured_data :
468482 live .console .print (
469- f"\n 입력 '{ display_command } ' 은(는) 지원하지 않는 명령입니다. "
470- "사용 가능한 입력: s, :s, save, :save, :export" ,
471- style = "bold yellow" ,
472- )
473- return
474- if not markdown :
475- live .console .print (
476- f"\n 입력 '{ display_command } ' 처리 실패: 저장할 데이터가 없습니다." ,
483+ f"\n 입력 '{ command } ' 처리 실패: CSV로 저장할 수 있는 테이블 데이터가 없습니다." ,
477484 style = "bold yellow" ,
478485 )
479486 return
487+
480488 try :
481- path = _save_markdown_snapshot (markdown )
482- except SnapshotSaveError as exc :
489+ headers = tracker .latest_structured_data ["headers" ]
490+ rows = tracker .latest_structured_data ["rows" ]
491+ path = _save_csv_snapshot (headers , rows )
492+ except (SnapshotSaveError , KeyError ) as exc :
483493 live .console .print (
484- f"\n 입력 '{ display_command } ' 처리 실패: { exc } " ,
494+ f"\n 입력 '{ command } ' 처리 실패: { exc } " ,
485495 style = "bold red" ,
486496 )
487497 _clear_input_display ()
488498 return
499+
489500 live .console .print (
490- f"\n 입력 '{ display_command } ' 처리 성공: Slack Markdown 스냅샷 저장 완료 → { path } " ,
501+ f"\n 입력 '{ command } ' 처리 성공: CSV 스냅샷 저장 완료 → { path } " ,
491502 style = "bold green" ,
492503 )
493504 _clear_input_display ()
494505
495506
496- def _tick_iteration (live : Live , markdown : Optional [str ]) -> None :
507+ def _handle_snapshot_command (
508+ live : Live , tracker : "LiveFrameTracker" , command : Optional [str ]
509+ ) -> None :
510+ """저장 요청이 있는 경우 Markdown 또는 CSV를 파일로 기록."""
511+ if command is None :
512+ return
513+
514+ display_command = command .strip () or "<empty>"
515+ normalized = command .lower ()
516+
517+ if normalized in CSV_SAVE_COMMANDS :
518+ _handle_csv_save (live , tracker , display_command )
519+ return
520+
521+ if normalized in SNAPSHOT_SAVE_COMMANDS :
522+ if not tracker .latest_snapshot :
523+ live .console .print (
524+ f"\n 입력 '{ display_command } ' 처리 실패: 저장할 데이터가 없습니다." ,
525+ style = "bold yellow" ,
526+ )
527+ return
528+ try :
529+ path = _save_markdown_snapshot (tracker .latest_snapshot )
530+ except SnapshotSaveError as exc :
531+ live .console .print (
532+ f"\n 입력 '{ display_command } ' 처리 실패: { exc } " ,
533+ style = "bold red" ,
534+ )
535+ _clear_input_display ()
536+ return
537+ live .console .print (
538+ f"\n 입력 '{ display_command } ' 처리 성공: Slack Markdown 스냅샷 저장 완료 → { path } " ,
539+ style = "bold green" ,
540+ )
541+ _clear_input_display ()
542+ return
543+
544+ live .console .print (
545+ f"\n 입력 '{ display_command } ' 은(는) 지원하지 않는 명령입니다. "
546+ "사용 가능한 입력: s, save, csv, ..." ,
547+ style = "bold yellow" ,
548+ )
549+
550+
551+ def _tick_iteration (live : Live , tracker : "LiveFrameTracker" ) -> None :
497552 """루프 종료 전 저장 요청을 처리."""
498553 user_command = _read_nonblocking_command ()
499554 if user_command :
500- _handle_snapshot_command (live , markdown , user_command )
555+ _handle_snapshot_command (live , tracker , user_command )
501556
502557
503558class LiveFrameTracker :
@@ -522,6 +577,7 @@ def __init__(self, live: Live) -> None:
522577 "footer" : None ,
523578 }
524579 self .latest_snapshot : Optional [str ] = None
580+ self .latest_structured_data : Optional [Dict [str , Any ]] = None
525581 self .last_input_state : str = ""
526582 self .live .update (self .layout )
527583
@@ -549,6 +605,7 @@ def update(
549605 frame_key : FrameKey ,
550606 renderable : RenderableType ,
551607 snapshot_markdown : Optional [str ],
608+ structured_data : Optional [Dict [str , Any ]] = None ,
552609 input_state : Optional [str ] = None ,
553610 ) -> None :
554611 current_input_state = (
@@ -591,6 +648,10 @@ def update(
591648 self .live .refresh ()
592649 if snapshot_markdown is not None :
593650 self .latest_snapshot = snapshot_markdown
651+ if structured_data is not None :
652+ self .latest_structured_data = structured_data
653+ else :
654+ self .latest_structured_data = None
594655
595656 def tick (self , interval : float = LIVE_REFRESH_INTERVAL ) -> None :
596657 global TERMINAL_RESIZED
@@ -600,7 +661,7 @@ def tick(self, interval: float = LIVE_REFRESH_INTERVAL) -> None:
600661 TERMINAL_RESIZED = False
601662 self .live .refresh ()
602663
603- _tick_iteration (self .live , self . latest_snapshot )
664+ _tick_iteration (self .live , self )
604665 if self ._sync_input_panel (CURRENT_INPUT_DISPLAY ):
605666 self .live .refresh ()
606667 remaining = deadline - time .monotonic ()
@@ -1345,10 +1406,22 @@ def watch_event_monitoring() -> None:
13451406 command = command_descriptor ,
13461407 status = "success" ,
13471408 )
1409+ structured_data = {
1410+ "headers" : [
1411+ "Namespace" ,
1412+ "LastSeen" ,
1413+ "Type" ,
1414+ "Reason" ,
1415+ "Object" ,
1416+ "Message" ,
1417+ ],
1418+ "rows" : markdown_rows ,
1419+ }
13481420 tracker .update (
13491421 frame_key ,
13501422 _compose_group (command_descriptor , table ),
13511423 snapshot ,
1424+ structured_data = structured_data ,
13521425 input_state = CURRENT_INPUT_DISPLAY ,
13531426 )
13541427 tracker .tick ()
@@ -1705,10 +1778,12 @@ def watch_pod_monitoring_by_creation() -> None:
17051778 command = command_descriptor ,
17061779 status = "success" ,
17071780 )
1781+ structured_data = {"headers" : headers , "rows" : markdown_rows }
17081782 tracker .update (
17091783 frame_key ,
17101784 _compose_group (command_descriptor , table ),
17111785 snapshot ,
1786+ structured_data = structured_data ,
17121787 input_state = CURRENT_INPUT_DISPLAY ,
17131788 )
17141789 tracker .tick ()
@@ -1983,10 +2058,12 @@ def _is_non_running(pod: V1Pod) -> bool:
19832058 command = command_descriptor ,
19842059 status = "success" ,
19852060 )
2061+ structured_data = {"headers" : headers , "rows" : markdown_rows }
19862062 tracker .update (
19872063 frame_key ,
19882064 _compose_group (command_descriptor , table ),
19892065 snapshot ,
2066+ structured_data = structured_data ,
19902067 input_state = CURRENT_INPUT_DISPLAY ,
19912068 )
19922069 tracker .tick ()
@@ -2301,10 +2378,23 @@ def watch_node_monitoring_by_creation() -> None:
23012378 command = command_descriptor ,
23022379 status = "success" ,
23032380 )
2381+ structured_data = {
2382+ "headers" : [
2383+ "Name" ,
2384+ "Status" ,
2385+ "Roles" ,
2386+ "NodeGroup" ,
2387+ "Zone" ,
2388+ "Version" ,
2389+ "CreatedAt" ,
2390+ ],
2391+ "rows" : markdown_rows ,
2392+ }
23042393 tracker .update (
23052394 frame_key ,
23062395 _compose_group (command_descriptor , table ),
23072396 snapshot ,
2397+ structured_data = structured_data ,
23082398 input_state = CURRENT_INPUT_DISPLAY ,
23092399 )
23102400 tracker .tick ()
@@ -2546,10 +2636,23 @@ def watch_unhealthy_nodes() -> None:
25462636 command = command_descriptor ,
25472637 status = "warning" ,
25482638 )
2639+ structured_data = {
2640+ "headers" : [
2641+ "Name" ,
2642+ "Status" ,
2643+ "Reason" ,
2644+ "NodeGroup" ,
2645+ "Zone" ,
2646+ "Version" ,
2647+ "CreatedAt" ,
2648+ ],
2649+ "rows" : markdown_rows ,
2650+ }
25492651 tracker .update (
25502652 frame_key ,
25512653 _compose_group (command_descriptor , table ),
25522654 snapshot ,
2655+ structured_data = structured_data ,
25532656 input_state = CURRENT_INPUT_DISPLAY ,
25542657 )
25552658 tracker .tick ()
@@ -2914,10 +3017,21 @@ def watch_pod_resources() -> None:
29143017 command = kubectl_cmd ,
29153018 status = "success" ,
29163019 )
3020+ structured_data = {
3021+ "headers" : [
3022+ "Namespace" ,
3023+ "Pod" ,
3024+ "CPU(cores)" ,
3025+ "Memory(bytes)" ,
3026+ "Node" ,
3027+ ],
3028+ "rows" : markdown_rows ,
3029+ }
29173030 tracker .update (
29183031 frame_key ,
29193032 _compose_group (kubectl_cmd , header , table ),
29203033 snapshot ,
3034+ structured_data = structured_data ,
29213035 input_state = CURRENT_INPUT_DISPLAY ,
29223036 )
29233037 tracker .tick ()
0 commit comments