@@ -115,6 +115,91 @@ def format_app_detail(app: dict) -> dict[str, str]:
115115 return info
116116
117117
118+ def format_summary (
119+ app : dict ,
120+ env : dict ,
121+ jobs : list [dict ],
122+ stages : list [dict ],
123+ executors : list [dict ],
124+ sqls : list [dict ],
125+ ) -> dict [str , dict [str , str ]]:
126+ """Build a multi-section summary from several API responses.
127+
128+ Returns an ordered dict of {section_title: {key: value}} pairs.
129+ """
130+ from collections import Counter
131+
132+ attempts = app .get ("attempts" , [])
133+ latest = attempts [0 ] if attempts else {}
134+ status = "RUNNING" if not latest .get ("completed" , True ) else "COMPLETED"
135+ runtime = env .get ("runtime" , {})
136+ sp = dict (env .get ("sparkProperties" , []))
137+
138+ # ── Application ──
139+ application = {
140+ "App ID" : app .get ("id" , "" ),
141+ "Name" : app .get ("name" , "" ),
142+ "Status" : f"{ _status_icon (status )} { status } " ,
143+ "Duration" : _duration (latest .get ("duration" )),
144+ "Spark Version" : (
145+ f"{ latest .get ('appSparkVersion' , 'N/A' )} "
146+ f"(Scala { runtime .get ('scalaVersion' , 'N/A' ).replace ('version ' , '' )} , "
147+ f"Java { runtime .get ('javaVersion' , 'N/A' )} )"
148+ ),
149+ "Master" : sp .get ("spark.master" , "N/A" ),
150+ "User" : latest .get ("sparkUser" , "" ),
151+ "Started" : _ts (latest .get ("startTimeEpoch" )),
152+ "Ended" : _ts (latest .get ("endTimeEpoch" )),
153+ }
154+
155+ # ── Resources ──
156+ driver_mem = sp .get ("spark.driver.memory" , "N/A" )
157+ driver_cores = sp .get ("spark.driver.cores" , "N/A" )
158+ exec_mem = sp .get ("spark.executor.memory" , "N/A" )
159+ exec_cores = sp .get ("spark.executor.cores" , "N/A" )
160+ exec_instances = sp .get ("spark.executor.instances" , "N/A" )
161+ active_execs = sum (1 for e in executors if e .get ("isActive" ))
162+ total_execs = len (executors )
163+ dyn_alloc = sp .get ("spark.dynamicAllocation.enabled" , "false" )
164+
165+ resources = {
166+ "Driver" : f"{ driver_mem } / { driver_cores } cores" ,
167+ "Executors" : f"{ exec_instances } × { exec_mem } / { exec_cores } cores ({ total_execs } total, { active_execs } active)" ,
168+ "Dynamic Allocation" : dyn_alloc ,
169+ "Shuffle Partitions" : sp .get ("spark.sql.shuffle.partitions" , "200" ),
170+ "Serializer" : sp .get ("spark.serializer" , "JavaSerializer" ).rsplit ("." , 1 )[- 1 ],
171+ }
172+
173+ # ── Workload ──
174+ job_statuses = Counter (j .get ("status" , "UNKNOWN" ) for j in jobs )
175+ stage_statuses = Counter (s .get ("status" , "UNKNOWN" ) for s in stages )
176+ sql_statuses = Counter (s .get ("status" , "UNKNOWN" ) for s in sqls )
177+
178+ total_tasks = sum (j .get ("numTasks" , 0 ) for j in jobs )
179+ completed_tasks = sum (j .get ("numCompletedTasks" , 0 ) for j in jobs )
180+
181+ def _status_summary (counts : Counter ) -> str :
182+ total = sum (counts .values ())
183+ parts = []
184+ for s in ["SUCCEEDED" , "COMPLETED" , "COMPLETE" , "RUNNING" , "FAILED" , "SKIPPED" , "KILLED" , "PENDING" , "UNKNOWN" ]:
185+ if counts .get (s ):
186+ parts .append (f"{ counts [s ]} { s .lower ()} " )
187+ return f"{ total } ({ ', ' .join (parts )} )" if parts else str (total )
188+
189+ workload = {
190+ "Jobs" : _status_summary (job_statuses ),
191+ "Stages" : _status_summary (stage_statuses ),
192+ "Tasks" : f"{ completed_tasks :,} /{ total_tasks :,} completed" ,
193+ "SQL Executions" : _status_summary (sql_statuses ),
194+ }
195+
196+ return {
197+ "Application" : application ,
198+ "Resources" : resources ,
199+ "Workload" : workload ,
200+ }
201+
202+
118203# ── Job Formatters ────────────────────────────────────────────────────
119204
120205
0 commit comments