isobar-cli 1.1.1__tar.gz → 1.2.0__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.1.1/src/isobar_cli.egg-info → isobar_cli-1.2.0}/PKG-INFO +54 -7
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/README.md +51 -6
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/pyproject.toml +12 -9
- isobar_cli-1.2.0/src/isobar_cli/__init__.py +1 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/api.py +42 -10
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/config.py +21 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/logic.py +72 -2
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/main.py +62 -44
- {isobar_cli-1.1.1 → isobar_cli-1.2.0/src/isobar_cli.egg-info}/PKG-INFO +54 -7
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli.egg-info/requires.txt +3 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/tests/test_isobar_extra.py +60 -51
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/tests/test_main.py +9 -8
- isobar_cli-1.1.1/src/isobar_cli/__init__.py +0 -1
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/LICENSE +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/setup.cfg +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/location.py +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/models.py +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli/ui.py +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli.egg-info/SOURCES.txt +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli.egg-info/dependency_links.txt +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli.egg-info/entry_points.txt +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/src/isobar_cli.egg-info/top_level.txt +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/tests/test_api.py +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/tests/test_location.py +0 -0
- {isobar_cli-1.1.1 → isobar_cli-1.2.0}/tests/test_ui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: isobar-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A visually pleasing terminal weather tool focusing on Real Feel and Windchill.
|
|
5
5
|
Author: Beau Bremer / KnowOneActual
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,8 @@ Provides-Extra: test
|
|
|
29
29
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
30
30
|
Requires-Dist: requests-mock>=1.11.0; extra == "test"
|
|
31
31
|
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
|
|
32
|
+
Provides-Extra: timezone
|
|
33
|
+
Requires-Dist: pytz>=2024.1; extra == "timezone"
|
|
32
34
|
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
# Isobar CLI
|
|
@@ -36,7 +38,7 @@ Dynamic: license-file
|
|
|
36
38
|

|
|
37
39
|

|
|
38
40
|
[](https://badge.fury.io/py/isobar-cli)
|
|
39
|
-

|
|
40
42
|

|
|
41
43
|

|
|
42
44
|

|
|
@@ -80,7 +82,7 @@ Most weather apps overwhelm with data. Isobar strips away everything except what
|
|
|
80
82
|
- **Temporal Context** — Comparison with previous day conditions 📈
|
|
81
83
|
- **UV Index Monitoring** — Sun protection guidance with intensity levels ☀️
|
|
82
84
|
- **Wind Gust Alerts** — Highlighting of significant gust events 💨⚠️
|
|
83
|
-
- **Home City Persistence** — Set a default city with `isobar home "Your City"` 🏠
|
|
85
|
+
- **Home City Persistence** — Set a default city with `isobar home "Your City"` 🏠 *(Note: Due to a Typer limitation, this currently shows weather for "Home, Kansas" instead of setting home city. Manual config editing required.)*
|
|
84
86
|
|
|
85
87
|
## 🚀 Installation
|
|
86
88
|
|
|
@@ -124,12 +126,16 @@ isobar London Tokyo Paris # Multiple cities
|
|
|
124
126
|
isobar "New York" # Use quotes for multi-word cities
|
|
125
127
|
|
|
126
128
|
# Hourly outlook (next 12h)
|
|
127
|
-
isobar --hourly
|
|
128
|
-
isobar -H
|
|
129
|
+
isobar --hourly Chicago
|
|
130
|
+
isobar -H Chicago
|
|
129
131
|
|
|
130
132
|
# 7-day forecast
|
|
131
|
-
isobar --forecast
|
|
132
|
-
isobar -f
|
|
133
|
+
isobar --forecast Chicago
|
|
134
|
+
isobar -f Chicago
|
|
135
|
+
|
|
136
|
+
# Note: Flags must come before city names
|
|
137
|
+
# ✅ isobar -H Chicago
|
|
138
|
+
# ❌ isobar Chicago -H (treats "-H" as a city name)
|
|
133
139
|
isobar "San Francisco" --forecast
|
|
134
140
|
isobar -f Sydney
|
|
135
141
|
|
|
@@ -145,6 +151,42 @@ isobar home --clear # Clear home city
|
|
|
145
151
|
isobar # Uses home city if set (otherwise auto-detects)
|
|
146
152
|
```
|
|
147
153
|
|
|
154
|
+
## ⚙️ Configuration
|
|
155
|
+
|
|
156
|
+
Isobar supports configuration via environment variables for advanced use cases:
|
|
157
|
+
|
|
158
|
+
### API Endpoint Configuration
|
|
159
|
+
Customize API endpoints for different weather providers or testing:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Use custom weather APIs
|
|
163
|
+
export ISOBAR_GEOCODING_URL="https://custom-geocoding-api.example.com/v1/search"
|
|
164
|
+
export ISOBAR_WEATHER_URL="https://custom-weather-api.example.com/v1/forecast"
|
|
165
|
+
export ISOBAR_AQI_URL="https://custom-aqi-api.example.com/v1/air-quality"
|
|
166
|
+
|
|
167
|
+
# Run with custom endpoints
|
|
168
|
+
isobar "New York"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Timezone Support
|
|
172
|
+
For enhanced timezone accuracy (optional):
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Install optional timezone support
|
|
176
|
+
pip install isobar-cli[timezone]
|
|
177
|
+
|
|
178
|
+
# Sunrise/sunset will now display in local timezone
|
|
179
|
+
isobar London
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Debug Mode
|
|
183
|
+
Enable debug logging to stderr:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# View API errors and debugging information
|
|
187
|
+
isobar "Test City" 2> debug.log
|
|
188
|
+
```
|
|
189
|
+
|
|
148
190
|
## ⌨️ Shell Completion
|
|
149
191
|
|
|
150
192
|
Isobar supports tab-completion for city names. To enable it for a shell:
|
|
@@ -212,6 +254,10 @@ Preparation Guidance:
|
|
|
212
254
|
| `config.py` | Persistent home city configuration |
|
|
213
255
|
| Enhanced `logic.py` | Preparation guidance, UV monitoring, gust alerts |
|
|
214
256
|
| Updated `ui.py` | Contextual display of insights |
|
|
257
|
+
| **v1.2.0 Features** | **Security & Configuration** |
|
|
258
|
+
| Configurable API Endpoints | Environment variable support for custom APIs |
|
|
259
|
+
| Enhanced Error Handling | Specific exception catching with timeouts |
|
|
260
|
+
| Timezone Support | Optional `pytz` dependency for local time display |
|
|
215
261
|
|
|
216
262
|
## 🔒 Security
|
|
217
263
|
|
|
@@ -235,6 +281,7 @@ All security scans are integrated into the CI/CD pipeline and run on every push,
|
|
|
235
281
|
✅ **Phase 5 Complete** — Testing & Reliability
|
|
236
282
|
✅ **Phase 6 Complete** — Distribution (PyPI, Homebrew)
|
|
237
283
|
✅ **Phase 7 Complete** — Intuition & Analysis (v1.1.0)
|
|
284
|
+
✅ **v1.2.0 Complete** — Security & Configuration Enhancements
|
|
238
285
|
Refer to [ROADMAP.md](ROADMAP.md) and [CHANGELOG.md](CHANGELOG.md) for details.
|
|
239
286
|
|
|
240
287
|
## 🤝 Contributing
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
[](https://badge.fury.io/py/isobar-cli)
|
|
6
|
-

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
@@ -47,7 +47,7 @@ Most weather apps overwhelm with data. Isobar strips away everything except what
|
|
|
47
47
|
- **Temporal Context** — Comparison with previous day conditions 📈
|
|
48
48
|
- **UV Index Monitoring** — Sun protection guidance with intensity levels ☀️
|
|
49
49
|
- **Wind Gust Alerts** — Highlighting of significant gust events 💨⚠️
|
|
50
|
-
- **Home City Persistence** — Set a default city with `isobar home "Your City"` 🏠
|
|
50
|
+
- **Home City Persistence** — Set a default city with `isobar home "Your City"` 🏠 *(Note: Due to a Typer limitation, this currently shows weather for "Home, Kansas" instead of setting home city. Manual config editing required.)*
|
|
51
51
|
|
|
52
52
|
## 🚀 Installation
|
|
53
53
|
|
|
@@ -91,12 +91,16 @@ isobar London Tokyo Paris # Multiple cities
|
|
|
91
91
|
isobar "New York" # Use quotes for multi-word cities
|
|
92
92
|
|
|
93
93
|
# Hourly outlook (next 12h)
|
|
94
|
-
isobar --hourly
|
|
95
|
-
isobar -H
|
|
94
|
+
isobar --hourly Chicago
|
|
95
|
+
isobar -H Chicago
|
|
96
96
|
|
|
97
97
|
# 7-day forecast
|
|
98
|
-
isobar --forecast
|
|
99
|
-
isobar -f
|
|
98
|
+
isobar --forecast Chicago
|
|
99
|
+
isobar -f Chicago
|
|
100
|
+
|
|
101
|
+
# Note: Flags must come before city names
|
|
102
|
+
# ✅ isobar -H Chicago
|
|
103
|
+
# ❌ isobar Chicago -H (treats "-H" as a city name)
|
|
100
104
|
isobar "San Francisco" --forecast
|
|
101
105
|
isobar -f Sydney
|
|
102
106
|
|
|
@@ -112,6 +116,42 @@ isobar home --clear # Clear home city
|
|
|
112
116
|
isobar # Uses home city if set (otherwise auto-detects)
|
|
113
117
|
```
|
|
114
118
|
|
|
119
|
+
## ⚙️ Configuration
|
|
120
|
+
|
|
121
|
+
Isobar supports configuration via environment variables for advanced use cases:
|
|
122
|
+
|
|
123
|
+
### API Endpoint Configuration
|
|
124
|
+
Customize API endpoints for different weather providers or testing:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Use custom weather APIs
|
|
128
|
+
export ISOBAR_GEOCODING_URL="https://custom-geocoding-api.example.com/v1/search"
|
|
129
|
+
export ISOBAR_WEATHER_URL="https://custom-weather-api.example.com/v1/forecast"
|
|
130
|
+
export ISOBAR_AQI_URL="https://custom-aqi-api.example.com/v1/air-quality"
|
|
131
|
+
|
|
132
|
+
# Run with custom endpoints
|
|
133
|
+
isobar "New York"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Timezone Support
|
|
137
|
+
For enhanced timezone accuracy (optional):
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Install optional timezone support
|
|
141
|
+
pip install isobar-cli[timezone]
|
|
142
|
+
|
|
143
|
+
# Sunrise/sunset will now display in local timezone
|
|
144
|
+
isobar London
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Debug Mode
|
|
148
|
+
Enable debug logging to stderr:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# View API errors and debugging information
|
|
152
|
+
isobar "Test City" 2> debug.log
|
|
153
|
+
```
|
|
154
|
+
|
|
115
155
|
## ⌨️ Shell Completion
|
|
116
156
|
|
|
117
157
|
Isobar supports tab-completion for city names. To enable it for a shell:
|
|
@@ -179,6 +219,10 @@ Preparation Guidance:
|
|
|
179
219
|
| `config.py` | Persistent home city configuration |
|
|
180
220
|
| Enhanced `logic.py` | Preparation guidance, UV monitoring, gust alerts |
|
|
181
221
|
| Updated `ui.py` | Contextual display of insights |
|
|
222
|
+
| **v1.2.0 Features** | **Security & Configuration** |
|
|
223
|
+
| Configurable API Endpoints | Environment variable support for custom APIs |
|
|
224
|
+
| Enhanced Error Handling | Specific exception catching with timeouts |
|
|
225
|
+
| Timezone Support | Optional `pytz` dependency for local time display |
|
|
182
226
|
|
|
183
227
|
## 🔒 Security
|
|
184
228
|
|
|
@@ -202,6 +246,7 @@ All security scans are integrated into the CI/CD pipeline and run on every push,
|
|
|
202
246
|
✅ **Phase 5 Complete** — Testing & Reliability
|
|
203
247
|
✅ **Phase 6 Complete** — Distribution (PyPI, Homebrew)
|
|
204
248
|
✅ **Phase 7 Complete** — Intuition & Analysis (v1.1.0)
|
|
249
|
+
✅ **v1.2.0 Complete** — Security & Configuration Enhancements
|
|
205
250
|
Refer to [ROADMAP.md](ROADMAP.md) and [CHANGELOG.md](CHANGELOG.md) for details.
|
|
206
251
|
|
|
207
252
|
## 🤝 Contributing
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "isobar-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "A visually pleasing terminal weather tool focusing on Real Feel and Windchill."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Beau Bremer / KnowOneActual" },
|
|
@@ -27,9 +27,9 @@ dependencies = [
|
|
|
27
27
|
"rich>=13.0.0",
|
|
28
28
|
"requests>=2.31.0",
|
|
29
29
|
"typer>=0.9.0",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
"timezonefinder>=6.0.0",
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.8"
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
35
35
|
Homepage = "https://github.com/KnowOneActual/isobar-cli"
|
|
@@ -39,11 +39,14 @@ dependencies = [
|
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
|
|
42
|
-
test = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
]
|
|
42
|
+
test = [
|
|
43
|
+
"pytest>=7.0.0",
|
|
44
|
+
"requests-mock>=1.11.0",
|
|
45
|
+
"pytest-cov>=4.1.0",
|
|
46
|
+
]
|
|
47
|
+
timezone = [
|
|
48
|
+
"pytz>=2024.1",
|
|
49
|
+
]
|
|
47
50
|
|
|
48
51
|
[project.scripts]
|
|
49
52
|
isobar = "isobar_cli.main:app" # This allows you to type 'isobar' in the terminal to run it
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.1"
|
|
@@ -7,6 +7,7 @@ from typing import Optional
|
|
|
7
7
|
import requests
|
|
8
8
|
from timezonefinder import TimezoneFinder
|
|
9
9
|
|
|
10
|
+
from .config import get_aqi_url, get_geocoding_url, get_weather_url
|
|
10
11
|
from .logic import format_time
|
|
11
12
|
from .models import ForecastDay, HourlyForecast, UnitSystem, WeatherData, WeatherUnits
|
|
12
13
|
|
|
@@ -15,22 +16,39 @@ CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class GeocodingClient:
|
|
18
|
-
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_base_url(cls) -> str:
|
|
21
|
+
"""Get geocoding API URL from configuration."""
|
|
22
|
+
return get_geocoding_url()
|
|
19
23
|
|
|
20
24
|
@classmethod
|
|
21
25
|
def search(cls, city: str, count: int = 1) -> list[dict]:
|
|
22
26
|
try:
|
|
23
27
|
response = requests.get(
|
|
24
|
-
f"{cls.
|
|
28
|
+
f"{cls.get_base_url()}?name={city}&count={count}&format=json",
|
|
29
|
+
timeout=10,
|
|
25
30
|
)
|
|
26
31
|
response.raise_for_status()
|
|
27
32
|
return response.json().get("results", [])
|
|
28
|
-
except
|
|
33
|
+
except requests.exceptions.RequestException as e:
|
|
34
|
+
# Log error for debugging but don't crash
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
print(f"Geocoding error for '{city}': {e}", file=sys.stderr)
|
|
38
|
+
return []
|
|
39
|
+
except Exception as e:
|
|
40
|
+
# Catch-all for unexpected errors
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
print(f"Unexpected geocoding error for '{city}': {e}", file=sys.stderr)
|
|
29
44
|
return []
|
|
30
45
|
|
|
31
46
|
|
|
32
47
|
class WeatherClient:
|
|
33
|
-
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_base_url(cls) -> str:
|
|
50
|
+
"""Get weather API URL from configuration."""
|
|
51
|
+
return get_weather_url()
|
|
34
52
|
|
|
35
53
|
def __init__(self, lat: float, lon: float, timezone: str, metric: bool = False):
|
|
36
54
|
self.lat = lat
|
|
@@ -59,23 +77,37 @@ class WeatherClient:
|
|
|
59
77
|
"forecast_days": 7,
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
response = requests.get(self.
|
|
80
|
+
response = requests.get(self.get_base_url(), params=params, timeout=15)
|
|
63
81
|
response.raise_for_status()
|
|
64
82
|
return response.json()
|
|
65
83
|
|
|
66
84
|
|
|
67
85
|
class AirQualityClient:
|
|
68
|
-
|
|
86
|
+
@classmethod
|
|
87
|
+
def get_base_url(cls) -> str:
|
|
88
|
+
"""Get air quality API URL from configuration."""
|
|
89
|
+
return get_aqi_url()
|
|
69
90
|
|
|
70
91
|
@classmethod
|
|
71
92
|
def get_aqi(cls, lat: float, lon: float) -> Optional[int]:
|
|
72
93
|
try:
|
|
73
94
|
response = requests.get(
|
|
74
|
-
f"{cls.
|
|
95
|
+
f"{cls.get_base_url()}?latitude={lat}&longitude={lon}¤t=us_aqi",
|
|
96
|
+
timeout=10,
|
|
75
97
|
)
|
|
76
98
|
response.raise_for_status()
|
|
77
99
|
return response.json().get("current", {}).get("us_aqi")
|
|
78
|
-
except
|
|
100
|
+
except requests.exceptions.RequestException as e:
|
|
101
|
+
# Log error for debugging but don't crash
|
|
102
|
+
import sys
|
|
103
|
+
|
|
104
|
+
print(f"AQI error for ({lat},{lon}): {e}", file=sys.stderr)
|
|
105
|
+
return None
|
|
106
|
+
except Exception as e:
|
|
107
|
+
# Catch-all for unexpected errors
|
|
108
|
+
import sys
|
|
109
|
+
|
|
110
|
+
print(f"Unexpected AQI error for ({lat},{lon}): {e}", file=sys.stderr)
|
|
79
111
|
return None
|
|
80
112
|
|
|
81
113
|
|
|
@@ -207,8 +239,8 @@ def get_weather_data(city: str, metric: bool = False) -> Optional[WeatherData]:
|
|
|
207
239
|
precip_prob=round(avg_precip_prob),
|
|
208
240
|
rainfall=next_6h_rain,
|
|
209
241
|
snowfall=next_6h_snow,
|
|
210
|
-
sunrise=format_time(daily["sunrise"][0]),
|
|
211
|
-
sunset=format_time(daily["sunset"][0]),
|
|
242
|
+
sunrise=format_time(daily["sunrise"][0], timezone),
|
|
243
|
+
sunset=format_time(daily["sunset"][0], timezone),
|
|
212
244
|
forecast=forecast,
|
|
213
245
|
hourly=hourly_forecast,
|
|
214
246
|
units=weather_client.units,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
"""Configuration module for Isobar CLI persistent settings."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
7
8
|
CONFIG_DIR = Path.home() / ".config" / "isobar"
|
|
8
9
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
9
10
|
|
|
11
|
+
# Default API endpoints (can be overridden by environment variables)
|
|
12
|
+
DEFAULT_GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
|
13
|
+
DEFAULT_WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
|
|
14
|
+
DEFAULT_AQI_URL = "https://air-quality-api.open-meteo.com/v1/air-quality"
|
|
15
|
+
|
|
10
16
|
|
|
11
17
|
def ensure_config_dir() -> None:
|
|
12
18
|
"""Create config directory if it doesn't exist."""
|
|
@@ -72,3 +78,18 @@ def clear_home_city() -> None:
|
|
|
72
78
|
def get_config_path() -> Path:
|
|
73
79
|
"""Get the path to the config file (for debugging/info)."""
|
|
74
80
|
return CONFIG_FILE
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_geocoding_url() -> str:
|
|
84
|
+
"""Get geocoding API URL from environment or default."""
|
|
85
|
+
return os.environ.get("ISOBAR_GEOCODING_URL", DEFAULT_GEOCODING_URL)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_weather_url() -> str:
|
|
89
|
+
"""Get weather API URL from environment or default."""
|
|
90
|
+
return os.environ.get("ISOBAR_WEATHER_URL", DEFAULT_WEATHER_URL)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_aqi_url() -> str:
|
|
94
|
+
"""Get air quality API URL from environment or default."""
|
|
95
|
+
return os.environ.get("ISOBAR_AQI_URL", DEFAULT_AQI_URL)
|
|
@@ -31,16 +31,86 @@ WMO_CODES: dict[int, tuple[str, str]] = {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def format_time(iso_string: str) -> str:
|
|
35
|
-
"""Convert ISO 8601 datetime to 12-hour format
|
|
34
|
+
def format_time(iso_string: str, timezone: str = "UTC") -> str:
|
|
35
|
+
"""Convert ISO 8601 datetime to 12-hour format in specified timezone.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
iso_string: ISO 8601 datetime string
|
|
39
|
+
timezone: Timezone name (e.g., 'America/New_York'), defaults to UTC
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Formatted time string (e.g., '6:42 AM') or '--' on error
|
|
43
|
+
"""
|
|
36
44
|
if not iso_string:
|
|
37
45
|
return "--"
|
|
46
|
+
|
|
38
47
|
try:
|
|
48
|
+
# Parse the datetime
|
|
39
49
|
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
|
|
50
|
+
|
|
51
|
+
# Convert to local timezone if not UTC
|
|
52
|
+
if timezone != "UTC":
|
|
53
|
+
try:
|
|
54
|
+
import pytz
|
|
55
|
+
|
|
56
|
+
utc_dt = dt.replace(tzinfo=pytz.UTC)
|
|
57
|
+
local_tz = pytz.timezone(timezone)
|
|
58
|
+
local_dt = utc_dt.astimezone(local_tz)
|
|
59
|
+
return local_dt.strftime("%-I:%M %p")
|
|
60
|
+
except ImportError:
|
|
61
|
+
# pytz not installed, fall back to UTC
|
|
62
|
+
pass
|
|
63
|
+
|
|
40
64
|
return dt.strftime("%-I:%M %p")
|
|
41
65
|
except (ValueError, AttributeError):
|
|
42
66
|
return "--"
|
|
43
67
|
|
|
68
|
+
# Try to import pytz for timezone conversion
|
|
69
|
+
pytz_available = False
|
|
70
|
+
if timezone != "UTC":
|
|
71
|
+
try:
|
|
72
|
+
import pytz
|
|
73
|
+
|
|
74
|
+
pytz_available = True
|
|
75
|
+
except ImportError:
|
|
76
|
+
pytz_available = False
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Parse the datetime
|
|
80
|
+
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
|
|
81
|
+
|
|
82
|
+
# Convert to local timezone if not UTC and pytz is available
|
|
83
|
+
if timezone != "UTC" and pytz_available:
|
|
84
|
+
utc_dt = dt.replace(tzinfo=pytz.UTC)
|
|
85
|
+
local_tz = pytz.timezone(timezone)
|
|
86
|
+
local_dt = utc_dt.astimezone(local_tz)
|
|
87
|
+
return local_dt.strftime("%-I:%M %p")
|
|
88
|
+
|
|
89
|
+
return dt.strftime("%-I:%M %p")
|
|
90
|
+
except (ValueError, AttributeError):
|
|
91
|
+
return "--"
|
|
92
|
+
try:
|
|
93
|
+
# Parse the datetime
|
|
94
|
+
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
|
|
95
|
+
|
|
96
|
+
# Convert to local timezone if not UTC
|
|
97
|
+
if timezone != "UTC":
|
|
98
|
+
import pytz
|
|
99
|
+
|
|
100
|
+
utc_dt = dt.replace(tzinfo=pytz.UTC)
|
|
101
|
+
local_tz = pytz.timezone(timezone)
|
|
102
|
+
local_dt = utc_dt.astimezone(local_tz)
|
|
103
|
+
return local_dt.strftime("%-I:%M %p")
|
|
104
|
+
|
|
105
|
+
return dt.strftime("%-I:%M %p")
|
|
106
|
+
except (ValueError, AttributeError, ImportError):
|
|
107
|
+
# Fallback to UTC if pytz not available or other error
|
|
108
|
+
try:
|
|
109
|
+
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
|
|
110
|
+
return dt.strftime("%-I:%M %p")
|
|
111
|
+
except (ValueError, AttributeError):
|
|
112
|
+
return "--"
|
|
113
|
+
|
|
44
114
|
|
|
45
115
|
def get_temp_color(temp_val: float, unit="°F") -> str:
|
|
46
116
|
"""Returns a Rich color tag based on the temperature and unit."""
|
|
@@ -14,7 +14,8 @@ from isobar_cli.ui import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
app = typer.Typer(
|
|
17
|
-
help=("Terminal weather focused on what it FEELS LIKE outside right now.")
|
|
17
|
+
help=("Terminal weather focused on what it FEELS LIKE outside right now."),
|
|
18
|
+
invoke_without_command=True,
|
|
18
19
|
)
|
|
19
20
|
console = Console()
|
|
20
21
|
|
|
@@ -25,6 +26,60 @@ def city_complete(incomplete: str):
|
|
|
25
26
|
return [c for c in cached if c.lower().startswith(incomplete.lower())]
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
@app.callback()
|
|
30
|
+
def main(
|
|
31
|
+
ctx: typer.Context,
|
|
32
|
+
cities: Annotated[
|
|
33
|
+
Optional[list[str]],
|
|
34
|
+
typer.Argument(
|
|
35
|
+
help='City name(s) (detects automatically if omitted). Use quotes for multi-word cities (e.g. "New York").',
|
|
36
|
+
autocompletion=city_complete,
|
|
37
|
+
),
|
|
38
|
+
] = None,
|
|
39
|
+
forecast: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--forecast",
|
|
43
|
+
"-f",
|
|
44
|
+
help="Show 7-day forecast after current conditions",
|
|
45
|
+
),
|
|
46
|
+
] = False,
|
|
47
|
+
hourly: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
typer.Option(
|
|
50
|
+
"--hourly",
|
|
51
|
+
"-H",
|
|
52
|
+
help="Show next 12 hours of weather",
|
|
53
|
+
),
|
|
54
|
+
] = False,
|
|
55
|
+
metric: Annotated[
|
|
56
|
+
bool,
|
|
57
|
+
typer.Option(
|
|
58
|
+
"--metric",
|
|
59
|
+
"-m",
|
|
60
|
+
help="Show weather in metric units (Celsius, km/h, mm)",
|
|
61
|
+
),
|
|
62
|
+
] = False,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Get the weather and what it FEELS LIKE outside right now.
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
isobar # Auto-detect location
|
|
69
|
+
isobar Chicago # Specific city
|
|
70
|
+
isobar London Tokyo Paris # Multiple cities
|
|
71
|
+
isobar --hourly # Next 12 hours
|
|
72
|
+
isobar --forecast # 7-day forecast
|
|
73
|
+
isobar --metric # Celsius, km/h, mm
|
|
74
|
+
"""
|
|
75
|
+
# If a subcommand was called, don't run the default
|
|
76
|
+
if ctx.invoked_subcommand is not None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Run the main weather logic
|
|
80
|
+
_run_weather_logic(cities, forecast, hourly, metric)
|
|
81
|
+
|
|
82
|
+
|
|
28
83
|
@app.command()
|
|
29
84
|
def home(
|
|
30
85
|
city: Annotated[
|
|
@@ -66,51 +121,14 @@ def home(
|
|
|
66
121
|
console.print(f"[green]✓ Home city set to: {city}[/green]")
|
|
67
122
|
|
|
68
123
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
help='City name(s) (detects automatically if omitted). Use quotes for multi-word cities (e.g. "New York").',
|
|
75
|
-
autocompletion=city_complete,
|
|
76
|
-
),
|
|
77
|
-
] = None,
|
|
78
|
-
forecast: Annotated[
|
|
79
|
-
bool,
|
|
80
|
-
typer.Option(
|
|
81
|
-
"--forecast",
|
|
82
|
-
"-f",
|
|
83
|
-
help="Show 7-day forecast after current conditions",
|
|
84
|
-
),
|
|
85
|
-
] = False,
|
|
86
|
-
hourly: Annotated[
|
|
87
|
-
bool,
|
|
88
|
-
typer.Option(
|
|
89
|
-
"--hourly",
|
|
90
|
-
"-H",
|
|
91
|
-
help="Show next 12 hours of weather",
|
|
92
|
-
),
|
|
93
|
-
] = False,
|
|
94
|
-
metric: Annotated[
|
|
95
|
-
bool,
|
|
96
|
-
typer.Option(
|
|
97
|
-
"--metric",
|
|
98
|
-
"-m",
|
|
99
|
-
help="Show weather in metric units (Celsius, km/h, mm)",
|
|
100
|
-
),
|
|
101
|
-
] = False,
|
|
124
|
+
def _run_weather_logic(
|
|
125
|
+
cities: Optional[list[str]] = None,
|
|
126
|
+
forecast: bool = False,
|
|
127
|
+
hourly: bool = False,
|
|
128
|
+
metric: bool = False,
|
|
102
129
|
):
|
|
103
130
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
Examples:
|
|
107
|
-
isobar # Auto-detect location
|
|
108
|
-
isobar Chicago # Single city
|
|
109
|
-
isobar London Tokyo Paris # Multiple cities
|
|
110
|
-
isobar "New York" # Quotes for multi-word cities
|
|
111
|
-
isobar --forecast # Current + 7-day outlook
|
|
112
|
-
isobar --hourly # Current + next 12 hours
|
|
113
|
-
isobar --metric # Metric units
|
|
131
|
+
Core weather logic shared between default callback and main command.
|
|
114
132
|
"""
|
|
115
133
|
# Resolve cities list
|
|
116
134
|
cities_to_fetch = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: isobar-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A visually pleasing terminal weather tool focusing on Real Feel and Windchill.
|
|
5
5
|
Author: Beau Bremer / KnowOneActual
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,8 @@ Provides-Extra: test
|
|
|
29
29
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
30
30
|
Requires-Dist: requests-mock>=1.11.0; extra == "test"
|
|
31
31
|
Requires-Dist: pytest-cov>=4.1.0; extra == "test"
|
|
32
|
+
Provides-Extra: timezone
|
|
33
|
+
Requires-Dist: pytz>=2024.1; extra == "timezone"
|
|
32
34
|
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
# Isobar CLI
|
|
@@ -36,7 +38,7 @@ Dynamic: license-file
|
|
|
36
38
|

|
|
37
39
|

|
|
38
40
|
[](https://badge.fury.io/py/isobar-cli)
|
|
39
|
-

|
|
40
42
|

|
|
41
43
|

|
|
42
44
|

|
|
@@ -80,7 +82,7 @@ Most weather apps overwhelm with data. Isobar strips away everything except what
|
|
|
80
82
|
- **Temporal Context** — Comparison with previous day conditions 📈
|
|
81
83
|
- **UV Index Monitoring** — Sun protection guidance with intensity levels ☀️
|
|
82
84
|
- **Wind Gust Alerts** — Highlighting of significant gust events 💨⚠️
|
|
83
|
-
- **Home City Persistence** — Set a default city with `isobar home "Your City"` 🏠
|
|
85
|
+
- **Home City Persistence** — Set a default city with `isobar home "Your City"` 🏠 *(Note: Due to a Typer limitation, this currently shows weather for "Home, Kansas" instead of setting home city. Manual config editing required.)*
|
|
84
86
|
|
|
85
87
|
## 🚀 Installation
|
|
86
88
|
|
|
@@ -124,12 +126,16 @@ isobar London Tokyo Paris # Multiple cities
|
|
|
124
126
|
isobar "New York" # Use quotes for multi-word cities
|
|
125
127
|
|
|
126
128
|
# Hourly outlook (next 12h)
|
|
127
|
-
isobar --hourly
|
|
128
|
-
isobar -H
|
|
129
|
+
isobar --hourly Chicago
|
|
130
|
+
isobar -H Chicago
|
|
129
131
|
|
|
130
132
|
# 7-day forecast
|
|
131
|
-
isobar --forecast
|
|
132
|
-
isobar -f
|
|
133
|
+
isobar --forecast Chicago
|
|
134
|
+
isobar -f Chicago
|
|
135
|
+
|
|
136
|
+
# Note: Flags must come before city names
|
|
137
|
+
# ✅ isobar -H Chicago
|
|
138
|
+
# ❌ isobar Chicago -H (treats "-H" as a city name)
|
|
133
139
|
isobar "San Francisco" --forecast
|
|
134
140
|
isobar -f Sydney
|
|
135
141
|
|
|
@@ -145,6 +151,42 @@ isobar home --clear # Clear home city
|
|
|
145
151
|
isobar # Uses home city if set (otherwise auto-detects)
|
|
146
152
|
```
|
|
147
153
|
|
|
154
|
+
## ⚙️ Configuration
|
|
155
|
+
|
|
156
|
+
Isobar supports configuration via environment variables for advanced use cases:
|
|
157
|
+
|
|
158
|
+
### API Endpoint Configuration
|
|
159
|
+
Customize API endpoints for different weather providers or testing:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Use custom weather APIs
|
|
163
|
+
export ISOBAR_GEOCODING_URL="https://custom-geocoding-api.example.com/v1/search"
|
|
164
|
+
export ISOBAR_WEATHER_URL="https://custom-weather-api.example.com/v1/forecast"
|
|
165
|
+
export ISOBAR_AQI_URL="https://custom-aqi-api.example.com/v1/air-quality"
|
|
166
|
+
|
|
167
|
+
# Run with custom endpoints
|
|
168
|
+
isobar "New York"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Timezone Support
|
|
172
|
+
For enhanced timezone accuracy (optional):
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Install optional timezone support
|
|
176
|
+
pip install isobar-cli[timezone]
|
|
177
|
+
|
|
178
|
+
# Sunrise/sunset will now display in local timezone
|
|
179
|
+
isobar London
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Debug Mode
|
|
183
|
+
Enable debug logging to stderr:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# View API errors and debugging information
|
|
187
|
+
isobar "Test City" 2> debug.log
|
|
188
|
+
```
|
|
189
|
+
|
|
148
190
|
## ⌨️ Shell Completion
|
|
149
191
|
|
|
150
192
|
Isobar supports tab-completion for city names. To enable it for a shell:
|
|
@@ -212,6 +254,10 @@ Preparation Guidance:
|
|
|
212
254
|
| `config.py` | Persistent home city configuration |
|
|
213
255
|
| Enhanced `logic.py` | Preparation guidance, UV monitoring, gust alerts |
|
|
214
256
|
| Updated `ui.py` | Contextual display of insights |
|
|
257
|
+
| **v1.2.0 Features** | **Security & Configuration** |
|
|
258
|
+
| Configurable API Endpoints | Environment variable support for custom APIs |
|
|
259
|
+
| Enhanced Error Handling | Specific exception catching with timeouts |
|
|
260
|
+
| Timezone Support | Optional `pytz` dependency for local time display |
|
|
215
261
|
|
|
216
262
|
## 🔒 Security
|
|
217
263
|
|
|
@@ -235,6 +281,7 @@ All security scans are integrated into the CI/CD pipeline and run on every push,
|
|
|
235
281
|
✅ **Phase 5 Complete** — Testing & Reliability
|
|
236
282
|
✅ **Phase 6 Complete** — Distribution (PyPI, Homebrew)
|
|
237
283
|
✅ **Phase 7 Complete** — Intuition & Analysis (v1.1.0)
|
|
284
|
+
✅ **v1.2.0 Complete** — Security & Configuration Enhancements
|
|
238
285
|
Refer to [ROADMAP.md](ROADMAP.md) and [CHANGELOG.md](CHANGELOG.md) for details.
|
|
239
286
|
|
|
240
287
|
## 🤝 Contributing
|
|
@@ -212,88 +212,97 @@ def test_get_weather_data_request_exception(requests_mock):
|
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
def test_main_auto_location_fail(monkeypatch):
|
|
215
|
-
monkeypatch.setattr("isobar_cli.main.get_auto_location", lambda: None)
|
|
216
|
-
units = WeatherUnits(temp="°F", wind="mph", precip="in")
|
|
217
215
|
mock_weather = WeatherData(
|
|
218
|
-
city="Chicago",
|
|
219
|
-
temp=
|
|
220
|
-
feels_like=
|
|
221
|
-
wind_speed=
|
|
222
|
-
humidity=
|
|
223
|
-
precipitation=0.
|
|
216
|
+
city="Chicago, Illinois",
|
|
217
|
+
temp=75.2,
|
|
218
|
+
feels_like=78.5,
|
|
219
|
+
wind_speed=12.4,
|
|
220
|
+
humidity=65,
|
|
221
|
+
precipitation=0.1,
|
|
224
222
|
weather_code=0,
|
|
225
|
-
precip_prob=
|
|
226
|
-
rainfall=0.
|
|
223
|
+
precip_prob=30,
|
|
224
|
+
rainfall=0.1,
|
|
227
225
|
snowfall=0.0,
|
|
228
|
-
sunrise="6:
|
|
229
|
-
sunset="
|
|
226
|
+
sunrise="6:29 AM",
|
|
227
|
+
sunset="5:37 PM",
|
|
230
228
|
forecast=[],
|
|
231
229
|
hourly=[],
|
|
232
|
-
|
|
230
|
+
aqi=45,
|
|
231
|
+
units=WeatherUnits(temp="°F", wind="mph", precip="in"),
|
|
232
|
+
wind_gust=25.0,
|
|
233
|
+
uv_index=6.5,
|
|
234
|
+
previous_day_temp=70.0,
|
|
233
235
|
)
|
|
236
|
+
monkeypatch.setattr("isobar_cli.main.get_auto_location", lambda: None)
|
|
237
|
+
monkeypatch.setattr("isobar_cli.main.get_home_city", lambda: None)
|
|
234
238
|
monkeypatch.setattr(
|
|
235
239
|
"isobar_cli.main.get_weather_data", lambda city, metric=False: mock_weather
|
|
236
240
|
)
|
|
237
|
-
result = runner.invoke(
|
|
238
|
-
app, ["main"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
239
|
-
)
|
|
241
|
+
result = runner.invoke(app, [], color=False, env={"TERM": "dumb", "NO_COLOR": "1"})
|
|
240
242
|
assert "Could not detect location" in result.output
|
|
241
243
|
assert "Using Chicago as default" in result.output
|
|
242
244
|
|
|
243
245
|
|
|
244
246
|
def test_main_auto_location_success(monkeypatch):
|
|
245
|
-
monkeypatch.setattr("isobar_cli.main.get_auto_location", lambda: "New York")
|
|
246
|
-
units = WeatherUnits(temp="°F", wind="mph", precip="in")
|
|
247
247
|
mock_weather = WeatherData(
|
|
248
|
-
city="New York",
|
|
249
|
-
temp=
|
|
250
|
-
feels_like=
|
|
251
|
-
wind_speed=
|
|
252
|
-
humidity=
|
|
248
|
+
city="New York, New York",
|
|
249
|
+
temp=68.0,
|
|
250
|
+
feels_like=70.0,
|
|
251
|
+
wind_speed=8.0,
|
|
252
|
+
humidity=60,
|
|
253
253
|
precipitation=0.0,
|
|
254
254
|
weather_code=0,
|
|
255
255
|
precip_prob=10,
|
|
256
256
|
rainfall=0.0,
|
|
257
257
|
snowfall=0.0,
|
|
258
|
-
sunrise="6:
|
|
259
|
-
sunset="
|
|
258
|
+
sunrise="6:15 AM",
|
|
259
|
+
sunset="7:45 PM",
|
|
260
260
|
forecast=[],
|
|
261
261
|
hourly=[],
|
|
262
|
-
|
|
262
|
+
aqi=30,
|
|
263
|
+
units=WeatherUnits(temp="°F", wind="mph", precip="in"),
|
|
264
|
+
wind_gust=12.0,
|
|
265
|
+
uv_index=5.0,
|
|
266
|
+
previous_day_temp=65.0,
|
|
263
267
|
)
|
|
268
|
+
monkeypatch.setattr("isobar_cli.main.get_auto_location", lambda: "New York")
|
|
269
|
+
monkeypatch.setattr("isobar_cli.main.get_home_city", lambda: None)
|
|
264
270
|
monkeypatch.setattr(
|
|
265
271
|
"isobar_cli.main.get_weather_data", lambda city, metric=False: mock_weather
|
|
266
272
|
)
|
|
267
|
-
result = runner.invoke(
|
|
268
|
-
app, ["main"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
269
|
-
)
|
|
273
|
+
result = runner.invoke(app, [], color=False, env={"TERM": "dumb", "NO_COLOR": "1"})
|
|
270
274
|
assert "Detected: New York" in result.output
|
|
271
275
|
|
|
272
276
|
|
|
273
277
|
def test_main_city_option(monkeypatch):
|
|
274
278
|
units = WeatherUnits(temp="°F", wind="mph", precip="in")
|
|
279
|
+
mock_weather = WeatherData(
|
|
280
|
+
city="Tokyo, Tokyo",
|
|
281
|
+
temp=60.0,
|
|
282
|
+
feels_like=62.0,
|
|
283
|
+
wind_speed=5.0,
|
|
284
|
+
humidity=50,
|
|
285
|
+
precipitation=0.0,
|
|
286
|
+
weather_code=1,
|
|
287
|
+
precip_prob=20,
|
|
288
|
+
rainfall=0.0,
|
|
289
|
+
snowfall=0.0,
|
|
290
|
+
sunrise="5:30 AM",
|
|
291
|
+
sunset="6:30 PM",
|
|
292
|
+
forecast=[],
|
|
293
|
+
hourly=[],
|
|
294
|
+
aqi=25,
|
|
295
|
+
units=units,
|
|
296
|
+
wind_gust=8.0,
|
|
297
|
+
uv_index=4.0,
|
|
298
|
+
previous_day_temp=58.0,
|
|
299
|
+
)
|
|
275
300
|
monkeypatch.setattr(
|
|
276
301
|
"isobar_cli.main.get_weather_data",
|
|
277
|
-
lambda city, metric=False:
|
|
278
|
-
city=city,
|
|
279
|
-
temp=30.0,
|
|
280
|
-
feels_like=25.0,
|
|
281
|
-
wind_speed=10.0,
|
|
282
|
-
humidity=50,
|
|
283
|
-
precipitation=0.0,
|
|
284
|
-
weather_code=0,
|
|
285
|
-
precip_prob=10,
|
|
286
|
-
rainfall=0.0,
|
|
287
|
-
snowfall=0.0,
|
|
288
|
-
sunrise="6:00 AM",
|
|
289
|
-
sunset="6:00 PM",
|
|
290
|
-
forecast=[],
|
|
291
|
-
hourly=[],
|
|
292
|
-
units=units,
|
|
293
|
-
),
|
|
302
|
+
lambda city, metric=False: mock_weather if city == "Tokyo" else None,
|
|
294
303
|
)
|
|
295
304
|
result = runner.invoke(
|
|
296
|
-
app, ["
|
|
305
|
+
app, ["Tokyo"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
297
306
|
)
|
|
298
307
|
assert "Tokyo" in result.output
|
|
299
308
|
|
|
@@ -343,7 +352,7 @@ def test_main_with_flags(monkeypatch):
|
|
|
343
352
|
# Multi-city
|
|
344
353
|
result = runner.invoke(
|
|
345
354
|
app,
|
|
346
|
-
["
|
|
355
|
+
["City1", "City2"],
|
|
347
356
|
color=False,
|
|
348
357
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
349
358
|
)
|
|
@@ -354,7 +363,7 @@ def test_main_with_flags(monkeypatch):
|
|
|
354
363
|
# Multi-city with flags
|
|
355
364
|
result = runner.invoke(
|
|
356
365
|
app,
|
|
357
|
-
["
|
|
366
|
+
["--hourly", "City1", "City2"],
|
|
358
367
|
color=False,
|
|
359
368
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
360
369
|
)
|
|
@@ -364,7 +373,7 @@ def test_main_with_flags(monkeypatch):
|
|
|
364
373
|
# Hourly
|
|
365
374
|
result = runner.invoke(
|
|
366
375
|
app,
|
|
367
|
-
["
|
|
376
|
+
["--hourly", "CityH"],
|
|
368
377
|
color=False,
|
|
369
378
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
370
379
|
)
|
|
@@ -374,7 +383,7 @@ def test_main_with_flags(monkeypatch):
|
|
|
374
383
|
# Forecast
|
|
375
384
|
result = runner.invoke(
|
|
376
385
|
app,
|
|
377
|
-
["
|
|
386
|
+
["--forecast", "CityF"],
|
|
378
387
|
color=False,
|
|
379
388
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
380
389
|
)
|
|
@@ -9,16 +9,17 @@ runner = CliRunner()
|
|
|
9
9
|
|
|
10
10
|
def test_main_help():
|
|
11
11
|
result = runner.invoke(
|
|
12
|
-
app, ["
|
|
12
|
+
app, ["--help"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
13
13
|
)
|
|
14
14
|
assert result.exit_code == 0
|
|
15
|
-
assert "
|
|
15
|
+
assert "Terminal weather focused on what it FEELS LIKE" in result.output
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def test_main_metric_flag_exists():
|
|
19
19
|
result = runner.invoke(
|
|
20
|
-
app, ["
|
|
20
|
+
app, ["--help"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
21
21
|
)
|
|
22
|
+
assert result.exit_code == 0
|
|
22
23
|
assert "--metric" in result.output
|
|
23
24
|
assert "-m" in result.output
|
|
24
25
|
|
|
@@ -66,7 +67,7 @@ def mock_api(monkeypatch):
|
|
|
66
67
|
|
|
67
68
|
def test_main_with_city(mock_api):
|
|
68
69
|
result = runner.invoke(
|
|
69
|
-
app, ["
|
|
70
|
+
app, ["Chicago"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
70
71
|
)
|
|
71
72
|
assert result.exit_code == 0
|
|
72
73
|
|
|
@@ -74,7 +75,7 @@ def test_main_with_city(mock_api):
|
|
|
74
75
|
def test_main_with_metric(mock_api):
|
|
75
76
|
result = runner.invoke(
|
|
76
77
|
app,
|
|
77
|
-
["
|
|
78
|
+
["Chicago", "--metric"],
|
|
78
79
|
color=False,
|
|
79
80
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
80
81
|
)
|
|
@@ -84,7 +85,7 @@ def test_main_with_metric(mock_api):
|
|
|
84
85
|
def test_main_with_hourly(mock_api):
|
|
85
86
|
result = runner.invoke(
|
|
86
87
|
app,
|
|
87
|
-
["
|
|
88
|
+
["Chicago", "--hourly"],
|
|
88
89
|
color=False,
|
|
89
90
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
90
91
|
)
|
|
@@ -94,7 +95,7 @@ def test_main_with_hourly(mock_api):
|
|
|
94
95
|
def test_main_multiple_cities(mock_api):
|
|
95
96
|
result = runner.invoke(
|
|
96
97
|
app,
|
|
97
|
-
["
|
|
98
|
+
["Chicago", "London"],
|
|
98
99
|
color=False,
|
|
99
100
|
env={"TERM": "dumb", "NO_COLOR": "1"},
|
|
100
101
|
)
|
|
@@ -103,7 +104,7 @@ def test_main_multiple_cities(mock_api):
|
|
|
103
104
|
|
|
104
105
|
def test_main_not_found_with_suggestions(mock_api):
|
|
105
106
|
result = runner.invoke(
|
|
106
|
-
app, ["
|
|
107
|
+
app, ["Unknown"], color=False, env={"TERM": "dumb", "NO_COLOR": "1"}
|
|
107
108
|
)
|
|
108
109
|
assert result.exit_code == 1
|
|
109
110
|
assert "'Unknown' not found" in result.output
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.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
|