1313# See the License for the specific language governing permissions and
1414# limitations under the License.
1515# ===============================================================================
16- from collections import defaultdict
16+ import os
17+ from collections import Counter , defaultdict
1718from enum import Enum
1819from pathlib import Path
19- from textwrap import wrap
20+ from textwrap import shorten , wrap
2021
2122import typer
2223from dotenv import load_dotenv
@@ -34,8 +35,56 @@ class OutputFormat(str, Enum):
3435 json = "json"
3536
3637
38+ class ThemeMode (str , Enum ):
39+ auto = "auto"
40+ light = "light"
41+ dark = "dark"
42+
43+
44+ def _resolve_theme (theme : ThemeMode ) -> ThemeMode :
45+ if theme != ThemeMode .auto :
46+ return theme
47+
48+ env_theme = os .environ .get ("OCO_THEME" , "" ).strip ().lower ()
49+ if env_theme in (ThemeMode .light .value , ThemeMode .dark .value ):
50+ return ThemeMode (env_theme )
51+
52+ colorfgbg = os .environ .get ("COLORFGBG" , "" )
53+ if colorfgbg :
54+ try :
55+ bg = int (colorfgbg .split (";" )[- 1 ])
56+ return ThemeMode .light if bg >= 8 else ThemeMode .dark
57+ except (TypeError , ValueError ):
58+ pass
59+
60+ return ThemeMode .dark
61+
62+
63+ def _palette (theme : ThemeMode ) -> dict [str , str ]:
64+ mode = _resolve_theme (theme )
65+ if mode == ThemeMode .light :
66+ return {
67+ "ok" : typer .colors .GREEN ,
68+ "issue" : typer .colors .RED ,
69+ "accent" : typer .colors .BLUE ,
70+ "muted" : typer .colors .BLACK ,
71+ "field" : typer .colors .RED ,
72+ }
73+ return {
74+ "ok" : typer .colors .GREEN ,
75+ "issue" : typer .colors .MAGENTA ,
76+ "accent" : typer .colors .BRIGHT_BLUE ,
77+ "muted" : typer .colors .BRIGHT_BLACK ,
78+ "field" : typer .colors .BRIGHT_YELLOW ,
79+ }
80+
81+
3782@cli .command ("initialize-lexicon" )
38- def initialize_lexicon ():
83+ def initialize_lexicon (
84+ theme : ThemeMode = typer .Option (
85+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
86+ ),
87+ ):
3988 from core .initializers import init_lexicon
4089
4190 init_lexicon ()
@@ -49,7 +98,10 @@ def associate_assets_command(
4998 file_okay = False ,
5099 dir_okay = True ,
51100 readable = True ,
52- )
101+ ),
102+ theme : ThemeMode = typer .Option (
103+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
104+ ),
53105):
54106 from cli .service_adapter import associate_assets
55107
@@ -64,7 +116,10 @@ def well_inventory_csv(
64116 file_okay = True ,
65117 dir_okay = False ,
66118 readable = True ,
67- )
119+ ),
120+ theme : ThemeMode = typer .Option (
121+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
122+ ),
68123):
69124 """
70125 parse and upload a csv to database
@@ -77,39 +132,88 @@ def well_inventory_csv(
77132 summary = payload .get ("summary" , {})
78133 validation_errors = payload .get ("validation_errors" , [])
79134 detail = payload .get ("detail" )
135+ colors = _palette (theme )
80136
81137 if result .exit_code == 0 :
82- typer .secho ("[WELL INVENTORY IMPORT] SUCCESS" , fg = typer . colors . GREEN , bold = True )
138+ typer .secho ("[WELL INVENTORY IMPORT] SUCCESS" , fg = colors [ "ok" ] , bold = True )
83139 else :
84140 typer .secho (
85141 "[WELL INVENTORY IMPORT] COMPLETED WITH ISSUES" ,
86- fg = typer . colors . BRIGHT_YELLOW ,
142+ fg = colors [ "issue" ] ,
87143 bold = True ,
88144 )
89- typer .secho ("=" * 72 , fg = typer . colors . BRIGHT_BLUE )
145+ typer .secho ("=" * 72 , fg = colors [ "accent" ] )
90146
91147 if summary :
92148 processed = summary .get ("total_rows_processed" , 0 )
93149 imported = summary .get ("total_rows_imported" , 0 )
94150 rows_with_issues = summary .get ("validation_errors_or_warnings" , 0 )
95- typer .secho ("SUMMARY" , fg = typer .colors .BRIGHT_BLUE , bold = True )
96- typer .echo (
97- f"Summary: processed={ processed } imported={ imported } rows_with_issues={ rows_with_issues } "
151+ typer .secho ("SUMMARY" , fg = colors ["accent" ], bold = True )
152+ label_width = 16
153+ value_width = 8
154+ typer .secho (" " + "-" * (label_width + 3 + value_width ), fg = colors ["muted" ])
155+ typer .secho (
156+ f" { 'processed' :<{label_width }} | { processed :>{value_width }} " ,
157+ fg = colors ["accent" ],
98158 )
99- typer .secho (f" processed : { processed } " , fg = typer .colors .CYAN )
100- typer .secho (f" imported : { imported } " , fg = typer .colors .GREEN )
101- issue_color = (
102- typer .colors .BRIGHT_YELLOW if rows_with_issues else typer .colors .GREEN
159+ typer .secho (
160+ f" { 'imported' :<{label_width }} | { imported :>{value_width }} " ,
161+ fg = colors ["ok" ],
162+ )
163+ issue_color = colors ["issue" ] if rows_with_issues else colors ["ok" ]
164+ typer .secho (
165+ f" { 'rows_with_issues' :<{label_width }} | { rows_with_issues :>{value_width }} " ,
166+ fg = issue_color ,
103167 )
104- typer .secho ( f" rows_with_issues : { rows_with_issues } " , fg = issue_color )
168+ typer .echo ( )
105169
106170 if validation_errors :
107- typer .secho ("VALIDATION" , fg = typer . colors . BRIGHT_BLUE , bold = True )
171+ typer .secho ("VALIDATION" , fg = colors [ "accent" ] , bold = True )
108172 typer .secho (
109173 f"Validation errors: { len (validation_errors )} " ,
110- fg = typer . colors . BRIGHT_YELLOW ,
174+ fg = colors [ "issue" ] ,
111175 bold = True ,
112176 )
177+ common_errors = Counter ()
178+ for err in validation_errors :
179+ field = err .get ("field" , "unknown" )
180+ message = err .get ("error" ) or err .get ("msg" ) or "validation error"
181+ common_errors [(field , message )] += 1
182+
183+ if common_errors :
184+ typer .secho (
185+ "Most common validation errors:" , fg = colors ["accent" ], bold = True
186+ )
187+ field_width = 28
188+ count_width = 5
189+ error_width = 100
190+ typer .secho (
191+ f" { '#' :>2} | { 'field' :<{field_width }} | { 'count' :>{count_width }} | error" ,
192+ fg = colors ["muted" ],
193+ bold = True ,
194+ )
195+ typer .secho (
196+ " " + "-" * (2 + 3 + field_width + 3 + count_width + 3 + error_width ),
197+ fg = colors ["muted" ],
198+ )
199+ for idx , ((field , message ), count ) in enumerate (
200+ common_errors .most_common (5 ), start = 1
201+ ):
202+ error_one_line = shorten (
203+ str (message ).replace ("\n " , " " ),
204+ width = error_width ,
205+ placeholder = "..." ,
206+ )
207+ field_text = shorten (str (field ), width = field_width , placeholder = "..." )
208+ field_part = typer .style (
209+ f"{ field_text :<{field_width }} " , fg = colors ["field" ], bold = True
210+ )
211+ count_part = f"{ int (count ):>{count_width }} "
212+ idx_part = typer .style (f"{ idx :>2} " , fg = colors ["issue" ])
213+ error_part = typer .style (error_one_line , fg = colors ["issue" ])
214+ typer .echo (f" { idx_part } | { field_part } | { count_part } | { error_part } " )
215+ typer .echo ()
216+
113217 grouped_errors = defaultdict (list )
114218 for err in validation_errors :
115219 row = err .get ("row" , "?" )
@@ -130,14 +234,11 @@ def _row_sort_key(row_value):
130234
131235 row_errors = grouped_errors [row ]
132236 if not first_group :
133- typer .secho (
134- " " + "-" * 56 ,
135- fg = typer .colors .BRIGHT_BLACK ,
136- )
237+ typer .secho (" " + "-" * 56 , fg = colors ["muted" ])
137238 first_group = False
138239 typer .secho (
139240 f" Row { row } ({ len (row_errors )} issue{ 's' if len (row_errors ) != 1 else '' } )" ,
140- fg = typer . colors . CYAN ,
241+ fg = colors [ "accent" ] ,
141242 bold = True ,
142243 )
143244
@@ -153,42 +254,35 @@ def _row_sort_key(row_value):
153254 str (message ),
154255 width = max (20 , 200 - len (prefix_raw ) - len (field_raw ) - 1 ),
155256 ) or ["" ]
156- prefix = typer .style (prefix_raw , fg = typer .colors .BRIGHT_YELLOW )
157- field_part = f"\033 [1;38;5;208m{ field_raw } \033 [0m"
158- first_msg_part = typer .style (
159- msg_chunks [0 ], fg = typer .colors .BRIGHT_YELLOW
160- )
257+ prefix = typer .style (prefix_raw , fg = colors ["issue" ])
258+ field_part = typer .style (field_raw , fg = colors ["field" ], bold = True )
259+ first_msg_part = typer .style (msg_chunks [0 ], fg = colors ["issue" ])
161260 typer .echo (f"{ prefix } { field_part } { first_msg_part } " )
162261 msg_indent = " " * (len (prefix_raw ) + len (field_raw ) + 1 )
163262 for chunk in msg_chunks [1 :]:
164- typer .secho (f"{ msg_indent } { chunk } " , fg = typer . colors . BRIGHT_YELLOW )
263+ typer .secho (f"{ msg_indent } { chunk } " , fg = colors [ "issue" ] )
165264 if input_value is not None :
166- input_prefix = " input= "
265+ input_prefix = " input: "
167266 input_chunks = wrap (
168267 str (input_value ), width = max (20 , 200 - len (input_prefix ))
169268 ) or ["" ]
170- typer .secho (
171- f"{ input_prefix } { input_chunks [0 ]} " , fg = typer .colors .BRIGHT_WHITE
172- )
269+ typer .echo (f"{ input_prefix } { input_chunks [0 ]} " )
173270 input_indent = " " * len (input_prefix )
174271 for chunk in input_chunks [1 :]:
175- typer .secho (
176- f"{ input_indent } { chunk } " , fg = typer .colors .BRIGHT_WHITE
177- )
272+ typer .echo (f"{ input_indent } { chunk } " )
178273 shown += 1
179274 typer .echo ()
180275
181276 if len (validation_errors ) > shown :
182277 typer .secho (
183278 f"... and { len (validation_errors ) - shown } more validation errors" ,
184- fg = typer . colors . YELLOW ,
279+ fg = colors [ "issue" ] ,
185280 )
186-
187281 if detail :
188- typer .secho ("ERRORS" , fg = typer . colors . BRIGHT_BLUE , bold = True )
189- typer .secho (f"Error: { detail } " , fg = typer . colors . BRIGHT_YELLOW , bold = True )
282+ typer .secho ("ERRORS" , fg = colors [ "accent" ] , bold = True )
283+ typer .secho (f"Error: { detail } " , fg = colors [ "issue" ] , bold = True )
190284
191- typer .secho ("=" * 72 , fg = typer . colors . BRIGHT_BLUE )
285+ typer .secho ("=" * 72 , fg = colors [ "accent" ] )
192286
193287 raise typer .Exit (result .exit_code )
194288
@@ -209,6 +303,9 @@ def water_levels_bulk_upload(
209303 "--output" ,
210304 help = "Optional output format" ,
211305 ),
306+ theme : ThemeMode = typer .Option (
307+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
308+ ),
212309):
213310 """
214311 parse and upload a csv
@@ -221,7 +318,11 @@ def water_levels_bulk_upload(
221318
222319
223320@data_migrations .command ("list" )
224- def data_migrations_list ():
321+ def data_migrations_list (
322+ theme : ThemeMode = typer .Option (
323+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
324+ ),
325+ ):
225326 from data_migrations .registry import list_migrations
226327
227328 migrations = list_migrations ()
@@ -234,7 +335,11 @@ def data_migrations_list():
234335
235336
236337@data_migrations .command ("status" )
237- def data_migrations_status ():
338+ def data_migrations_status (
339+ theme : ThemeMode = typer .Option (
340+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
341+ ),
342+ ):
238343 from db .engine import session_ctx
239344 from data_migrations .runner import get_status
240345
@@ -258,6 +363,9 @@ def data_migrations_run(
258363 force : bool = typer .Option (
259364 False , "--force" , help = "Re-run even if already applied."
260365 ),
366+ theme : ThemeMode = typer .Option (
367+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
368+ ),
261369):
262370 from db .engine import session_ctx
263371 from data_migrations .runner import run_migration_by_id
@@ -277,6 +385,9 @@ def data_migrations_run_all(
277385 force : bool = typer .Option (
278386 False , "--force" , help = "Re-run non-repeatable migrations."
279387 ),
388+ theme : ThemeMode = typer .Option (
389+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
390+ ),
280391):
281392 from db .engine import session_ctx
282393 from data_migrations .runner import run_all
@@ -297,6 +408,9 @@ def alembic_upgrade_and_data(
297408 force : bool = typer .Option (
298409 False , "--force" , help = "Re-run non-repeatable migrations."
299410 ),
411+ theme : ThemeMode = typer .Option (
412+ ThemeMode .auto , "--theme" , help = "Color theme: auto, light, dark."
413+ ),
300414):
301415 from alembic import command
302416 from alembic .config import Config
0 commit comments