isobar-cli 1.3.2__tar.gz → 1.3.3__tar.gz
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.
- {isobar_cli-1.3.2/src/isobar_cli.egg-info → isobar_cli-1.3.3}/PKG-INFO +1 -1
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/pyproject.toml +1 -1
- isobar_cli-1.3.3/src/isobar_cli/__init__.py +1 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/logic.py +124 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/ui.py +123 -240
- {isobar_cli-1.3.2 → isobar_cli-1.3.3/src/isobar_cli.egg-info}/PKG-INFO +1 -1
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_isobar_extra.py +4 -4
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_ui.py +1 -1
- isobar_cli-1.3.2/src/isobar_cli/__init__.py +0 -1
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/LICENSE +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/README.md +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/setup.cfg +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/api.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/config.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/location.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/main.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/models.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/SOURCES.txt +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/dependency_links.txt +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/entry_points.txt +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/requires.txt +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/top_level.txt +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_api.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_location.py +0 -0
- {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_main.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "isobar-cli"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.3"
|
|
8
8
|
description = "A terminal weather tool with industrial aesthetic, focusing on Real Feel and Windchill."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Beau Bremer / KnowOneActual" },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.3"
|
|
@@ -308,3 +308,127 @@ def get_temporal_context(
|
|
|
308
308
|
return f"{direction} {abs_diff:.1f}°C {'warmer' if diff > 0 else 'cooler'} than yesterday"
|
|
309
309
|
else:
|
|
310
310
|
return f"{direction} {abs_diff:.1f}°F {'warmer' if diff > 0 else 'cooler'} than yesterday"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_temperature_comfort(temp: float, unit: str = "°F") -> tuple[str, str]:
|
|
314
|
+
"""Get temperature comfort category and color.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
temp: Temperature value
|
|
318
|
+
unit: Temperature unit (°F or °C)
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Tuple of (category, color) where category is a comfort level
|
|
322
|
+
"""
|
|
323
|
+
if unit == "°F":
|
|
324
|
+
if temp < 32:
|
|
325
|
+
return "FREEZING", "bright_cyan"
|
|
326
|
+
elif temp < 50:
|
|
327
|
+
return "COLD", "cyan"
|
|
328
|
+
elif temp < 75:
|
|
329
|
+
return "COMFORTABLE", "bright_green"
|
|
330
|
+
elif temp < 90:
|
|
331
|
+
return "WARM", "bright_yellow"
|
|
332
|
+
else:
|
|
333
|
+
return "HOT", "bright_red"
|
|
334
|
+
else: # °C
|
|
335
|
+
if temp < 0:
|
|
336
|
+
return "FREEZING", "bright_cyan"
|
|
337
|
+
elif temp < 10:
|
|
338
|
+
return "COLD", "cyan"
|
|
339
|
+
elif temp < 24:
|
|
340
|
+
return "COMFORTABLE", "bright_green"
|
|
341
|
+
elif temp < 32:
|
|
342
|
+
return "WARM", "bright_yellow"
|
|
343
|
+
else:
|
|
344
|
+
return "HOT", "bright_red"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def get_humidity_category(humidity: int) -> tuple[str, str]:
|
|
348
|
+
"""Get humidity category and color.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
humidity: Humidity percentage (0-100)
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Tuple of (category, color)
|
|
355
|
+
"""
|
|
356
|
+
if humidity < 30:
|
|
357
|
+
return "DRY", "bright_yellow"
|
|
358
|
+
elif humidity < 60:
|
|
359
|
+
return "IDEAL", "bright_green"
|
|
360
|
+
elif humidity < 80:
|
|
361
|
+
return "HUMID", "bright_yellow"
|
|
362
|
+
else:
|
|
363
|
+
return "MUGGY", "bright_red"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def get_wind_category_label(speed: float, unit: str) -> tuple[str, str]:
|
|
367
|
+
"""Get wind category label and color.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
speed: Wind speed
|
|
371
|
+
unit: Wind unit (mph or km/h)
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Tuple of (category, color)
|
|
375
|
+
"""
|
|
376
|
+
if unit == "mph":
|
|
377
|
+
if speed < 1:
|
|
378
|
+
return "CALM", "bright_cyan"
|
|
379
|
+
elif speed < 7:
|
|
380
|
+
return "LIGHT", "cyan"
|
|
381
|
+
elif speed < 12:
|
|
382
|
+
return "GENTLE", "bright_green"
|
|
383
|
+
elif speed < 18:
|
|
384
|
+
return "MODERATE", "bright_yellow"
|
|
385
|
+
elif speed < 24:
|
|
386
|
+
return "FRESH", "yellow"
|
|
387
|
+
elif speed < 31:
|
|
388
|
+
return "STRONG", "bright_red"
|
|
389
|
+
elif speed < 38:
|
|
390
|
+
return "GALE", "red"
|
|
391
|
+
elif speed < 46:
|
|
392
|
+
return "SEVERE", "bright_red"
|
|
393
|
+
else:
|
|
394
|
+
return "STORM", "bright_red"
|
|
395
|
+
else: # km/h
|
|
396
|
+
if speed < 2:
|
|
397
|
+
return "CALM", "bright_cyan"
|
|
398
|
+
elif speed < 12:
|
|
399
|
+
return "LIGHT", "cyan"
|
|
400
|
+
elif speed < 20:
|
|
401
|
+
return "GENTLE", "bright_green"
|
|
402
|
+
elif speed < 29:
|
|
403
|
+
return "MODERATE", "bright_yellow"
|
|
404
|
+
elif speed < 39:
|
|
405
|
+
return "FRESH", "yellow"
|
|
406
|
+
elif speed < 50:
|
|
407
|
+
return "STRONG", "bright_red"
|
|
408
|
+
elif speed < 62:
|
|
409
|
+
return "GALE", "red"
|
|
410
|
+
elif speed < 75:
|
|
411
|
+
return "SEVERE", "bright_red"
|
|
412
|
+
else:
|
|
413
|
+
return "STORM", "bright_red"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def get_uv_category(uv_index: float) -> tuple[str, str]:
|
|
417
|
+
"""Get UV index category and color.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
uv_index: UV index value
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Tuple of (category, color)
|
|
424
|
+
"""
|
|
425
|
+
if uv_index <= 2:
|
|
426
|
+
return "LOW", "bright_green"
|
|
427
|
+
elif uv_index <= 5:
|
|
428
|
+
return "MODERATE", "bright_yellow"
|
|
429
|
+
elif uv_index <= 7:
|
|
430
|
+
return "HIGH", "bright_red"
|
|
431
|
+
elif uv_index <= 10:
|
|
432
|
+
return "VERY HIGH", "red"
|
|
433
|
+
else:
|
|
434
|
+
return "EXTREME", "bright_red"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
2
|
|
|
3
|
-
from rich import box
|
|
4
3
|
from rich.columns import Columns
|
|
5
4
|
from rich.console import Console
|
|
6
5
|
from rich.table import Table
|
|
@@ -9,11 +8,13 @@ from .logic import (
|
|
|
9
8
|
WMO_CODES,
|
|
10
9
|
get_aqi_label,
|
|
11
10
|
get_feels_like_label,
|
|
12
|
-
|
|
11
|
+
get_humidity_category,
|
|
13
12
|
get_preparation_guidance,
|
|
14
13
|
get_temp_color,
|
|
14
|
+
get_temperature_comfort,
|
|
15
15
|
get_temporal_context,
|
|
16
|
-
|
|
16
|
+
get_uv_category,
|
|
17
|
+
get_wind_category_label,
|
|
17
18
|
get_wind_gust_alert,
|
|
18
19
|
)
|
|
19
20
|
from .models import WeatherData
|
|
@@ -34,15 +35,6 @@ COLORS = {
|
|
|
34
35
|
"concrete": "white",
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
# Weather severity indicators
|
|
38
|
-
SEVERITY_ICONS = {
|
|
39
|
-
"extreme": "⚡",
|
|
40
|
-
"high": "▲",
|
|
41
|
-
"medium": "●",
|
|
42
|
-
"low": "○",
|
|
43
|
-
"normal": "◇",
|
|
44
|
-
}
|
|
45
|
-
|
|
46
38
|
|
|
47
39
|
def get_weather_icon(code: int) -> tuple[str, str]:
|
|
48
40
|
"""Return (emoji, description) for a WMO weather code."""
|
|
@@ -50,7 +42,7 @@ def get_weather_icon(code: int) -> tuple[str, str]:
|
|
|
50
42
|
|
|
51
43
|
|
|
52
44
|
def build_weather_table(weather: WeatherData) -> Table:
|
|
53
|
-
"""Constructs the current conditions weather table with industrial aesthetic."""
|
|
45
|
+
"""Constructs the current conditions weather table with borderless industrial aesthetic."""
|
|
54
46
|
icon, desc = get_weather_icon(weather.weather_code)
|
|
55
47
|
temp_color = get_temp_color(weather.temp, weather.units.temp)
|
|
56
48
|
feels_color = get_temp_color(weather.feels_like, weather.units.temp)
|
|
@@ -58,47 +50,50 @@ def build_weather_table(weather: WeatherData) -> Table:
|
|
|
58
50
|
weather.temp, weather.feels_like, weather.units.temp
|
|
59
51
|
)
|
|
60
52
|
|
|
61
|
-
#
|
|
53
|
+
# Get meaningful labels for weather data
|
|
54
|
+
temp_comfort, temp_comfort_color = get_temperature_comfort(
|
|
55
|
+
weather.temp, weather.units.temp
|
|
56
|
+
)
|
|
57
|
+
humidity_category, humidity_color = get_humidity_category(weather.humidity)
|
|
58
|
+
wind_category, wind_color = get_wind_category_label(
|
|
59
|
+
weather.wind_speed, weather.units.wind
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Create borderless industrial-style table
|
|
62
63
|
table = Table(
|
|
63
|
-
title=f"[{COLORS['primary']} bold]
|
|
64
|
-
f"[{COLORS['steel']}]{weather.city.upper()}[/{COLORS['steel']}]",
|
|
64
|
+
title=f"[{COLORS['primary']} bold]WEATHER OBSERVATORY · {weather.city.upper()}[/{COLORS['primary']} bold]",
|
|
65
65
|
show_header=False,
|
|
66
|
-
box=
|
|
67
|
-
|
|
68
|
-
title_style=f"{COLORS['primary']} bold",
|
|
69
|
-
padding=(0, 2),
|
|
66
|
+
box=None, # No borders
|
|
67
|
+
padding=(0, 1),
|
|
70
68
|
width=60,
|
|
69
|
+
title_style=f"{COLORS['primary']} bold",
|
|
71
70
|
)
|
|
72
71
|
|
|
73
|
-
# Add columns
|
|
74
|
-
table.add_column("", justify="center", width=
|
|
75
|
-
table.add_column(
|
|
76
|
-
|
|
77
|
-
table.add_column("STATUS", justify="right", width=15, style=COLORS["secondary"])
|
|
78
|
-
|
|
79
|
-
# Weather condition row with severity indicator
|
|
80
|
-
severity = (
|
|
81
|
-
"medium" if weather.weather_code in [95, 96, 99, 65, 75, 86] else "normal"
|
|
72
|
+
# Add columns: icon, metric, value, category
|
|
73
|
+
table.add_column("", justify="center", width=3, style=COLORS["accent"])
|
|
74
|
+
table.add_column(
|
|
75
|
+
"METRIC", justify="left", width=18, style=f"{COLORS['muted']} bold"
|
|
82
76
|
)
|
|
77
|
+
table.add_column("VALUE", justify="right", width=12, style=COLORS["primary"])
|
|
78
|
+
table.add_column("CATEGORY", justify="right", width=20, style=COLORS["secondary"])
|
|
79
|
+
|
|
80
|
+
# Weather condition
|
|
83
81
|
table.add_row(
|
|
84
82
|
f"[{COLORS['accent']}]{icon}[/{COLORS['accent']}]",
|
|
85
|
-
"
|
|
86
|
-
f"[{COLORS['primary']}
|
|
87
|
-
|
|
83
|
+
"CONDITIONS",
|
|
84
|
+
f"[{COLORS['primary']}]{desc.upper()}[/{COLORS['primary']}]",
|
|
85
|
+
"",
|
|
88
86
|
)
|
|
89
87
|
|
|
90
|
-
# Temperature with
|
|
91
|
-
temp_gauge = create_gauge(
|
|
92
|
-
weather.temp, weather.units.temp, 100 if weather.units.temp == "°F" else 40
|
|
93
|
-
)
|
|
88
|
+
# Temperature with comfort category
|
|
94
89
|
table.add_row(
|
|
95
90
|
"🌡️",
|
|
96
|
-
"
|
|
91
|
+
"TEMPERATURE",
|
|
97
92
|
f"[{temp_color}]{weather.temp}{weather.units.temp}[/{temp_color}]",
|
|
98
|
-
|
|
93
|
+
f"[{temp_comfort_color}]{temp_comfort}[/{temp_comfort_color}]",
|
|
99
94
|
)
|
|
100
95
|
|
|
101
|
-
# Feels like with difference
|
|
96
|
+
# Feels like with difference
|
|
102
97
|
temp_diff = weather.feels_like - weather.temp
|
|
103
98
|
diff_symbol = "▲" if temp_diff > 0 else "▼" if temp_diff < 0 else "▬"
|
|
104
99
|
diff_color = (
|
|
@@ -108,195 +103,115 @@ def build_weather_table(weather: WeatherData) -> Table:
|
|
|
108
103
|
if temp_diff < -5
|
|
109
104
|
else COLORS["muted"]
|
|
110
105
|
)
|
|
106
|
+
feels_comfort, feels_comfort_color = get_temperature_comfort(
|
|
107
|
+
weather.feels_like, weather.units.temp
|
|
108
|
+
)
|
|
111
109
|
table.add_row(
|
|
112
110
|
"🤔",
|
|
113
|
-
f"
|
|
111
|
+
f"{feels_label.upper()}",
|
|
114
112
|
f"[{feels_color}]{weather.feels_like}{weather.units.temp}[/{feels_color}]",
|
|
115
|
-
f"[{diff_color}]{diff_symbol}
|
|
113
|
+
f"[{feels_comfort_color}]{feels_comfort} [{diff_color}]{diff_symbol}{abs(temp_diff):.1f}{weather.units.temp}[/{diff_color}][/{feels_comfort_color}]",
|
|
116
114
|
)
|
|
117
115
|
|
|
118
|
-
# Wind with
|
|
119
|
-
wind_category = get_wind_category(weather.wind_speed, weather.units.wind)
|
|
116
|
+
# Wind with category
|
|
120
117
|
table.add_row(
|
|
121
118
|
"💨",
|
|
122
|
-
"
|
|
119
|
+
"WIND SPEED",
|
|
123
120
|
f"{weather.wind_speed} {weather.units.wind}",
|
|
124
|
-
f"[{
|
|
121
|
+
f"[{wind_color}]{wind_category}[/{wind_color}]",
|
|
125
122
|
)
|
|
126
123
|
|
|
127
|
-
# Humidity with
|
|
128
|
-
humidity_indicator = "▓" * (weather.humidity // 20) + "░" * (
|
|
129
|
-
5 - weather.humidity // 20
|
|
130
|
-
)
|
|
124
|
+
# Humidity with category
|
|
131
125
|
table.add_row(
|
|
132
126
|
"💧",
|
|
133
|
-
"
|
|
127
|
+
"HUMIDITY",
|
|
134
128
|
f"{weather.humidity}%",
|
|
135
|
-
f"[{
|
|
129
|
+
f"[{humidity_color}]{humidity_category}[/{humidity_color}]",
|
|
136
130
|
)
|
|
137
131
|
|
|
138
|
-
# Air Quality
|
|
132
|
+
# Air Quality
|
|
139
133
|
if weather.aqi is not None:
|
|
140
134
|
label, color = get_aqi_label(weather.aqi)
|
|
141
|
-
aqi_severity = (
|
|
142
|
-
"high" if weather.aqi > 100 else "medium" if weather.aqi > 50 else "low"
|
|
143
|
-
)
|
|
144
135
|
table.add_row(
|
|
145
136
|
"😷",
|
|
146
|
-
"
|
|
137
|
+
"AIR QUALITY",
|
|
147
138
|
f"[{color}]{weather.aqi}[/{color}]",
|
|
148
|
-
f"[{color}]{label.upper()}
|
|
139
|
+
f"[{color}]{label.upper()}[/{color}]",
|
|
149
140
|
)
|
|
150
141
|
|
|
151
|
-
# Precipitation
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
weather.precip_prob
|
|
142
|
+
# Precipitation
|
|
143
|
+
precip_category = (
|
|
144
|
+
"DRY"
|
|
145
|
+
if weather.precip_prob < 20
|
|
146
|
+
else "LIGHT"
|
|
147
|
+
if weather.precip_prob < 50
|
|
148
|
+
else "MODERATE"
|
|
149
|
+
if weather.precip_prob < 80
|
|
150
|
+
else "HEAVY"
|
|
151
|
+
)
|
|
152
|
+
precip_color = (
|
|
153
|
+
COLORS["success"]
|
|
154
|
+
if weather.precip_prob < 20
|
|
155
|
+
else COLORS["warning"]
|
|
156
|
+
if weather.precip_prob < 50
|
|
157
|
+
else COLORS["danger"]
|
|
155
158
|
)
|
|
156
159
|
table.add_row(
|
|
157
160
|
"☔",
|
|
158
|
-
"
|
|
161
|
+
"PRECIPITATION",
|
|
159
162
|
f"{weather.precip_prob}% (6h)",
|
|
160
|
-
f"[{
|
|
163
|
+
f"[{precip_color}]{precip_category}[/{precip_color}]",
|
|
161
164
|
)
|
|
162
165
|
|
|
163
|
-
|
|
164
|
-
table.add_row(
|
|
165
|
-
"",
|
|
166
|
-
"[dim]FORECAST[/dim]",
|
|
167
|
-
f"[{COLORS['warning']} italic]{headline.upper()}[/{COLORS['warning']} italic]",
|
|
168
|
-
"",
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Sunrise/Sunset with day progress
|
|
166
|
+
# Sunrise/Sunset
|
|
172
167
|
table.add_row(
|
|
173
168
|
"🌅",
|
|
174
|
-
"
|
|
169
|
+
"SUNRISE",
|
|
175
170
|
f"[{COLORS['warning']}]{weather.sunrise}[/{COLORS['warning']}]",
|
|
176
|
-
"
|
|
171
|
+
"",
|
|
177
172
|
)
|
|
178
173
|
|
|
179
174
|
table.add_row(
|
|
180
175
|
"🌇",
|
|
181
|
-
"
|
|
176
|
+
"SUNSET",
|
|
182
177
|
f"[{COLORS['danger']}]{weather.sunset}[/{COLORS['danger']}]",
|
|
183
|
-
"
|
|
178
|
+
"",
|
|
184
179
|
)
|
|
185
180
|
|
|
186
|
-
# UV Index
|
|
181
|
+
# UV Index
|
|
187
182
|
if weather.uv_index is not None:
|
|
188
|
-
|
|
189
|
-
uv_level = min(int(weather.uv_index / 2), 5)
|
|
190
|
-
uv_indicator = "☀️" * uv_level
|
|
183
|
+
uv_category, uv_color = get_uv_category(weather.uv_index)
|
|
191
184
|
table.add_row(
|
|
192
185
|
"☀️",
|
|
193
|
-
"
|
|
186
|
+
"UV INDEX",
|
|
194
187
|
f"[{uv_color}]{weather.uv_index:.1f}[/{uv_color}]",
|
|
195
|
-
f"[{uv_color}]{
|
|
188
|
+
f"[{uv_color}]{uv_category}[/{uv_color}]",
|
|
196
189
|
)
|
|
197
190
|
|
|
198
|
-
# Wind Gust
|
|
191
|
+
# Wind Gust
|
|
199
192
|
if weather.wind_gust is not None:
|
|
200
193
|
gust_alert = get_wind_gust_alert(weather.wind_speed, weather.wind_gust)
|
|
194
|
+
gust_category, gust_color = get_wind_category_label(
|
|
195
|
+
weather.wind_gust, weather.units.wind
|
|
196
|
+
)
|
|
201
197
|
if gust_alert:
|
|
202
198
|
table.add_row(
|
|
203
199
|
"⚡",
|
|
204
200
|
"[bold yellow]GUST ALERT[/bold yellow]",
|
|
205
201
|
f"[bold yellow]{weather.wind_gust} {weather.units.wind}[/bold yellow]",
|
|
206
|
-
"[bold yellow]⚠️
|
|
202
|
+
f"[bold yellow]{gust_category} ⚠️[/bold yellow]",
|
|
207
203
|
)
|
|
208
204
|
else:
|
|
209
|
-
gust_ratio = (
|
|
210
|
-
weather.wind_gust / weather.wind_speed if weather.wind_speed > 0 else 1
|
|
211
|
-
)
|
|
212
|
-
gust_indicator = "!" * min(int(gust_ratio * 2), 3)
|
|
213
205
|
table.add_row(
|
|
214
206
|
"💨",
|
|
215
|
-
"
|
|
207
|
+
"WIND GUST",
|
|
216
208
|
f"{weather.wind_gust} {weather.units.wind}",
|
|
217
|
-
f"[{
|
|
209
|
+
f"[{gust_color}]{gust_category}[/{gust_color}]",
|
|
218
210
|
)
|
|
219
211
|
|
|
220
212
|
return table
|
|
221
213
|
|
|
222
214
|
|
|
223
|
-
def create_gauge(value: float, unit: str, max_value: float) -> str:
|
|
224
|
-
"""Create a text-based gauge visualization."""
|
|
225
|
-
if unit == "°F":
|
|
226
|
-
ranges = [
|
|
227
|
-
(32, "bold cyan"),
|
|
228
|
-
(60, "bold blue"),
|
|
229
|
-
(80, "bold green"),
|
|
230
|
-
(95, "bold yellow"),
|
|
231
|
-
(max_value, "bold red"),
|
|
232
|
-
]
|
|
233
|
-
else:
|
|
234
|
-
ranges = [
|
|
235
|
-
(0, "bold cyan"),
|
|
236
|
-
(15, "bold blue"),
|
|
237
|
-
(26, "bold green"),
|
|
238
|
-
(35, "bold yellow"),
|
|
239
|
-
(max_value, "bold red"),
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
# Find appropriate color
|
|
243
|
-
gauge_color = "bold white"
|
|
244
|
-
for threshold, color in ranges:
|
|
245
|
-
if value <= threshold:
|
|
246
|
-
gauge_color = color
|
|
247
|
-
break
|
|
248
|
-
|
|
249
|
-
# Create gauge visualization
|
|
250
|
-
gauge_width = 10
|
|
251
|
-
filled = min(int((value / max_value) * gauge_width), gauge_width)
|
|
252
|
-
gauge = "[" + gauge_color + "]" + "█" * filled + "[/" + gauge_color + "]"
|
|
253
|
-
gauge += "[dim]" + "░" * (gauge_width - filled) + "[/dim]"
|
|
254
|
-
|
|
255
|
-
return gauge
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def get_wind_category(speed: float, unit: str) -> str:
|
|
259
|
-
"""Categorize wind speed."""
|
|
260
|
-
if unit == "mph":
|
|
261
|
-
if speed < 1:
|
|
262
|
-
return "CALM"
|
|
263
|
-
elif speed < 7:
|
|
264
|
-
return "LIGHT"
|
|
265
|
-
elif speed < 12:
|
|
266
|
-
return "GENTLE"
|
|
267
|
-
elif speed < 18:
|
|
268
|
-
return "MODERATE"
|
|
269
|
-
elif speed < 24:
|
|
270
|
-
return "FRESH"
|
|
271
|
-
elif speed < 31:
|
|
272
|
-
return "STRONG"
|
|
273
|
-
elif speed < 38:
|
|
274
|
-
return "GALE"
|
|
275
|
-
elif speed < 46:
|
|
276
|
-
return "SEVERE"
|
|
277
|
-
else:
|
|
278
|
-
return "STORM"
|
|
279
|
-
else: # km/h
|
|
280
|
-
if speed < 2:
|
|
281
|
-
return "CALM"
|
|
282
|
-
elif speed < 12:
|
|
283
|
-
return "LIGHT"
|
|
284
|
-
elif speed < 20:
|
|
285
|
-
return "GENTLE"
|
|
286
|
-
elif speed < 29:
|
|
287
|
-
return "MODERATE"
|
|
288
|
-
elif speed < 39:
|
|
289
|
-
return "FRESH"
|
|
290
|
-
elif speed < 50:
|
|
291
|
-
return "STRONG"
|
|
292
|
-
elif speed < 62:
|
|
293
|
-
return "GALE"
|
|
294
|
-
elif speed < 75:
|
|
295
|
-
return "SEVERE"
|
|
296
|
-
else:
|
|
297
|
-
return "STORM"
|
|
298
|
-
|
|
299
|
-
|
|
300
215
|
def display_weather(weather: WeatherData):
|
|
301
216
|
"""Renders the current conditions weather card for a single city."""
|
|
302
217
|
if not weather:
|
|
@@ -339,19 +254,28 @@ def display_preparation_guidance(weather: WeatherData):
|
|
|
339
254
|
|
|
340
255
|
if temporal_context:
|
|
341
256
|
console.print(
|
|
342
|
-
f"[{COLORS['
|
|
257
|
+
f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
|
|
258
|
+
)
|
|
259
|
+
console.print(
|
|
260
|
+
f"[{COLORS['primary']} bold]TREND ANALYSIS[/{COLORS['primary']} bold]"
|
|
343
261
|
)
|
|
344
262
|
console.print(
|
|
345
263
|
f"[{COLORS['muted']}]{temporal_context.upper()}[/{COLORS['muted']}]"
|
|
346
264
|
)
|
|
347
265
|
console.print(
|
|
348
|
-
f"[{COLORS['
|
|
266
|
+
f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
|
|
349
267
|
)
|
|
350
268
|
print()
|
|
351
269
|
|
|
352
|
-
# Create
|
|
270
|
+
# Create borderless guidance panel
|
|
353
271
|
console.print(
|
|
354
|
-
f"[{COLORS['
|
|
272
|
+
f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
|
|
273
|
+
)
|
|
274
|
+
console.print(
|
|
275
|
+
f"[{COLORS['primary']} bold]PREPARATION PROTOCOL[/{COLORS['primary']} bold]"
|
|
276
|
+
)
|
|
277
|
+
console.print(
|
|
278
|
+
f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
|
|
355
279
|
)
|
|
356
280
|
|
|
357
281
|
# Categorize suggestions by priority
|
|
@@ -376,33 +300,31 @@ def display_preparation_guidance(weather: WeatherData):
|
|
|
376
300
|
# Display high priority items with danger styling
|
|
377
301
|
if high_priority:
|
|
378
302
|
console.print(
|
|
379
|
-
f"[{COLORS['danger']} bold]
|
|
303
|
+
f"[{COLORS['danger']} bold]⚠️ HIGH PRIORITY[/{COLORS['danger']} bold]"
|
|
380
304
|
)
|
|
381
305
|
for suggestion in high_priority:
|
|
382
|
-
console.print(
|
|
383
|
-
f" [{COLORS['danger']}]▶[/{COLORS['danger']}] {suggestion}"
|
|
384
|
-
)
|
|
306
|
+
console.print(f" [{COLORS['danger']}]▶[/{COLORS['danger']}] {suggestion}")
|
|
385
307
|
print()
|
|
386
308
|
|
|
387
309
|
# Display medium priority items with warning styling
|
|
388
310
|
if medium_priority:
|
|
389
311
|
console.print(
|
|
390
|
-
f"[{COLORS['warning']} bold]
|
|
312
|
+
f"[{COLORS['warning']} bold]▲ RECOMMENDED[/{COLORS['warning']} bold]"
|
|
391
313
|
)
|
|
392
314
|
for suggestion in medium_priority:
|
|
393
315
|
console.print(
|
|
394
|
-
f"
|
|
316
|
+
f" [{COLORS['warning']}]▶[/{COLORS['warning']}] {suggestion}"
|
|
395
317
|
)
|
|
396
318
|
print()
|
|
397
319
|
|
|
398
320
|
# Display low priority items with muted styling
|
|
399
321
|
if low_priority:
|
|
400
|
-
console.print(f"[{COLORS['muted']} bold]
|
|
322
|
+
console.print(f"[{COLORS['muted']} bold]○ ADVISORY[/{COLORS['muted']} bold]")
|
|
401
323
|
for suggestion in low_priority:
|
|
402
|
-
console.print(f"
|
|
324
|
+
console.print(f" [{COLORS['muted']}]▶[/{COLORS['muted']}] {suggestion}")
|
|
403
325
|
|
|
404
326
|
console.print(
|
|
405
|
-
f"[{COLORS['
|
|
327
|
+
f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
|
|
406
328
|
)
|
|
407
329
|
|
|
408
330
|
|
|
@@ -425,39 +347,34 @@ def display_multi_weather(weather_list: list[WeatherData]):
|
|
|
425
347
|
|
|
426
348
|
|
|
427
349
|
def display_forecast(weather: WeatherData):
|
|
428
|
-
"""Renders a 7-day forecast table with industrial aesthetic."""
|
|
350
|
+
"""Renders a 7-day forecast table with borderless industrial aesthetic."""
|
|
429
351
|
if not weather.forecast:
|
|
430
352
|
console.print(
|
|
431
353
|
f"[{COLORS['warning']}]No forecast data available.[/{COLORS['warning']}]"
|
|
432
354
|
)
|
|
433
355
|
return
|
|
434
356
|
|
|
435
|
-
# Create
|
|
357
|
+
# Create borderless forecast table
|
|
436
358
|
table = Table(
|
|
437
|
-
title=f"[{COLORS['primary']} bold]
|
|
438
|
-
f"[{COLORS['steel']}]{weather.city.upper()}[/{COLORS['steel']}]",
|
|
359
|
+
title=f"[{COLORS['primary']} bold]FORECAST PANEL · {weather.city.upper()}[/{COLORS['primary']} bold]",
|
|
439
360
|
show_header=True,
|
|
361
|
+
box=None, # No borders
|
|
440
362
|
header_style=f"{COLORS['muted']} bold",
|
|
441
|
-
box=box.HEAVY,
|
|
442
|
-
border_style=COLORS["secondary"],
|
|
443
363
|
padding=(0, 1),
|
|
444
364
|
width=70,
|
|
445
365
|
)
|
|
446
366
|
|
|
447
|
-
#
|
|
448
|
-
table.add_column("DAY", justify="left", width=
|
|
367
|
+
# Borderless column headers
|
|
368
|
+
table.add_column("DAY", justify="left", width=10, style=f"{COLORS['accent']} bold")
|
|
449
369
|
table.add_column("", justify="center", width=2, style=COLORS["accent"])
|
|
450
|
-
table.add_column("CONDITIONS", justify="left", width=
|
|
370
|
+
table.add_column("CONDITIONS", justify="left", width=18, style=COLORS["muted"])
|
|
451
371
|
table.add_column(
|
|
452
|
-
"HIGH", justify="right", width=
|
|
453
|
-
)
|
|
454
|
-
table.add_column(
|
|
455
|
-
"LOW", justify="right", width=10, style=f"{COLORS['primary']} bold"
|
|
372
|
+
"HIGH", justify="right", width=8, style=f"{COLORS['primary']} bold"
|
|
456
373
|
)
|
|
374
|
+
table.add_column("LOW", justify="right", width=8, style=f"{COLORS['primary']} bold")
|
|
457
375
|
table.add_column(
|
|
458
376
|
"RAIN%", justify="right", width=8, style=f"{COLORS['accent']} bold"
|
|
459
377
|
)
|
|
460
|
-
table.add_column("STATUS", justify="center", width=8, style=COLORS["secondary"])
|
|
461
378
|
|
|
462
379
|
for i, day in enumerate(weather.forecast):
|
|
463
380
|
day_label = "TODAY" if i == 0 else day.dt.strftime("%a").upper()
|
|
@@ -467,7 +384,7 @@ def display_forecast(weather: WeatherData):
|
|
|
467
384
|
high_color = get_temp_color(day.high, weather.units.temp)
|
|
468
385
|
low_color = get_temp_color(day.low, weather.units.temp)
|
|
469
386
|
|
|
470
|
-
# Precipitation
|
|
387
|
+
# Precipitation color based on probability
|
|
471
388
|
if day.precip_prob >= 70:
|
|
472
389
|
rain_color = COLORS["danger"]
|
|
473
390
|
elif day.precip_prob >= 40:
|
|
@@ -477,17 +394,6 @@ def display_forecast(weather: WeatherData):
|
|
|
477
394
|
else:
|
|
478
395
|
rain_color = COLORS["muted"]
|
|
479
396
|
|
|
480
|
-
# Day severity based on weather conditions
|
|
481
|
-
if day.weather_code in [95, 96, 99, 65, 75, 86]:
|
|
482
|
-
day_severity = "▲"
|
|
483
|
-
severity_color = COLORS["warning"]
|
|
484
|
-
elif day.precip_prob > 60:
|
|
485
|
-
day_severity = "●"
|
|
486
|
-
severity_color = COLORS["accent"]
|
|
487
|
-
else:
|
|
488
|
-
day_severity = "○"
|
|
489
|
-
severity_color = COLORS["success"]
|
|
490
|
-
|
|
491
397
|
table.add_row(
|
|
492
398
|
f"[{COLORS['primary']} bold]{day_label}[/{COLORS['primary']} bold]"
|
|
493
399
|
if i == 0
|
|
@@ -497,7 +403,6 @@ def display_forecast(weather: WeatherData):
|
|
|
497
403
|
f"[{high_color}]{day.high}{weather.units.temp}[/{high_color}]",
|
|
498
404
|
f"[{low_color}]{day.low}{weather.units.temp}[/{low_color}]",
|
|
499
405
|
f"[{rain_color}]{day.precip_prob}%[/{rain_color}]",
|
|
500
|
-
f"[{severity_color}]{day_severity}[/{severity_color}]",
|
|
501
406
|
)
|
|
502
407
|
|
|
503
408
|
console.print(table)
|
|
@@ -505,49 +410,40 @@ def display_forecast(weather: WeatherData):
|
|
|
505
410
|
|
|
506
411
|
|
|
507
412
|
def display_hourly(weather: WeatherData):
|
|
508
|
-
"""Renders an hourly forecast table with industrial aesthetic."""
|
|
413
|
+
"""Renders an hourly forecast table with borderless industrial aesthetic."""
|
|
509
414
|
if not weather.hourly:
|
|
510
415
|
console.print(
|
|
511
416
|
f"[{COLORS['warning']}]No hourly data available.[/{COLORS['warning']}]"
|
|
512
417
|
)
|
|
513
418
|
return
|
|
514
419
|
|
|
515
|
-
# Create
|
|
420
|
+
# Create borderless hourly table
|
|
516
421
|
table = Table(
|
|
517
|
-
title=f"[{COLORS['primary']} bold]
|
|
518
|
-
f"[{COLORS['steel']}]{weather.city.upper()}[/{COLORS['steel']}]",
|
|
422
|
+
title=f"[{COLORS['primary']} bold]HOURLY FORECAST · {weather.city.upper()}[/{COLORS['primary']} bold]",
|
|
519
423
|
show_header=True,
|
|
424
|
+
box=None, # No borders
|
|
520
425
|
header_style=f"{COLORS['muted']} bold",
|
|
521
|
-
box=box.HEAVY,
|
|
522
|
-
border_style=COLORS["secondary"],
|
|
523
426
|
padding=(0, 1),
|
|
524
|
-
width=
|
|
427
|
+
width=60,
|
|
525
428
|
)
|
|
526
429
|
|
|
527
|
-
#
|
|
528
|
-
table.add_column("TIME", justify="left", width=
|
|
430
|
+
# Simplified column headers
|
|
431
|
+
table.add_column("TIME", justify="left", width=6, style=f"{COLORS['accent']} bold")
|
|
529
432
|
table.add_column("", justify="center", width=2, style=COLORS["accent"])
|
|
530
433
|
table.add_column(
|
|
531
|
-
"TEMP", justify="right", width=
|
|
434
|
+
"TEMP", justify="right", width=8, style=f"{COLORS['primary']} bold"
|
|
532
435
|
)
|
|
533
|
-
table.add_column("GAUGE", justify="left", width=12, style=COLORS["muted"])
|
|
534
436
|
table.add_column(
|
|
535
|
-
"RAIN%", justify="right", width=
|
|
437
|
+
"RAIN%", justify="right", width=6, style=f"{COLORS['accent']} bold"
|
|
536
438
|
)
|
|
537
|
-
table.add_column("CONDITIONS", justify="left", width=
|
|
538
|
-
table.add_column("", justify="center", width=2, style=COLORS["secondary"])
|
|
439
|
+
table.add_column("CONDITIONS", justify="left", width=18, style=COLORS["concrete"])
|
|
539
440
|
|
|
540
441
|
for hour in weather.hourly[:12]:
|
|
541
442
|
time_label = hour.dt.strftime("%-I%p").upper()
|
|
542
443
|
icon, desc = get_weather_icon(hour.weather_code)
|
|
543
444
|
temp_color = get_temp_color(hour.temp, weather.units.temp)
|
|
544
445
|
|
|
545
|
-
#
|
|
546
|
-
temp_gauge = create_gauge(
|
|
547
|
-
hour.temp, weather.units.temp, 100 if weather.units.temp == "°F" else 40
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
# Precipitation indicator
|
|
446
|
+
# Precipitation color
|
|
551
447
|
if hour.precip_prob >= 70:
|
|
552
448
|
rain_color = COLORS["danger"]
|
|
553
449
|
elif hour.precip_prob >= 40:
|
|
@@ -557,25 +453,12 @@ def display_hourly(weather: WeatherData):
|
|
|
557
453
|
else:
|
|
558
454
|
rain_color = COLORS["muted"]
|
|
559
455
|
|
|
560
|
-
# Hour severity indicator
|
|
561
|
-
if hour.weather_code in [95, 96, 99]:
|
|
562
|
-
severity_icon = "⚡"
|
|
563
|
-
severity_color = COLORS["warning"]
|
|
564
|
-
elif hour.precip_prob > 60:
|
|
565
|
-
severity_icon = "☔"
|
|
566
|
-
severity_color = COLORS["accent"]
|
|
567
|
-
else:
|
|
568
|
-
severity_icon = "○"
|
|
569
|
-
severity_color = COLORS["success"]
|
|
570
|
-
|
|
571
456
|
table.add_row(
|
|
572
457
|
f"[{COLORS['muted']}]{time_label}[/{COLORS['muted']}]",
|
|
573
458
|
f"[{COLORS['accent']}]{icon}[/{COLORS['accent']}]",
|
|
574
459
|
f"[{temp_color}]{hour.temp}{weather.units.temp}[/{temp_color}]",
|
|
575
|
-
temp_gauge,
|
|
576
460
|
f"[{rain_color}]{hour.precip_prob}%[/{rain_color}]",
|
|
577
461
|
f"[{COLORS['concrete']}]{desc.upper()}[/{COLORS['concrete']}]",
|
|
578
|
-
f"[{severity_color}]{severity_icon}[/{severity_color}]",
|
|
579
462
|
)
|
|
580
463
|
|
|
581
464
|
console.print(table)
|
|
@@ -380,8 +380,8 @@ def test_main_with_flags(monkeypatch):
|
|
|
380
380
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
381
381
|
)
|
|
382
382
|
assert result.exit_code == 0
|
|
383
|
-
# In industrial aesthetic, hourly display has "HOURLY
|
|
384
|
-
assert "HOURLY
|
|
383
|
+
# In borderless industrial aesthetic, hourly display has "HOURLY FORECAST" header
|
|
384
|
+
assert "HOURLY FORECAST" in result.output.upper()
|
|
385
385
|
assert "CITYH" in result.output.upper()
|
|
386
386
|
|
|
387
387
|
# Forecast
|
|
@@ -572,8 +572,8 @@ def test_display_multi_weather(mock_cache_dir):
|
|
|
572
572
|
|
|
573
573
|
|
|
574
574
|
def test_get_precip_headline_extra():
|
|
575
|
-
assert
|
|
576
|
-
assert
|
|
575
|
+
assert logic.get_precip_headline(80, 0.5, 0, "in") == "Moderate rain likely"
|
|
576
|
+
assert logic.get_precip_headline(80, 0.1, 0, "in") == "Light rain likely"
|
|
577
577
|
|
|
578
578
|
|
|
579
579
|
def test_build_weather_table_extra():
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.3.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|