diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a565ccc --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# ---- Python ---- +__pycache__/ +*.pyc +*.pyo +*.pyd +*.pdb +*.log + +# ---- Virtual Environment ---- +venv/ +.venv/ +ENV/ +env/ + +# ---- VSCode ---- +.vscode/ + +# ---- macOS / Linux ---- +.DS_Store +Thumbs.db + +# ---- Project-specific ---- +data/*.log +data/*_cache.json +data/*.csv + +# Don't ignore config.ini (important) +!config/config.ini + +# ---- Build / Packaging ---- +build/ +dist/ +*.egg-info/ + +# ---- Jupyter ---- +.ipynb_checkpoints/ diff --git a/__pycache__/alerts.cpython-314.pyc b/__pycache__/alerts.cpython-314.pyc index 10f3510..b0d383a 100644 Binary files a/__pycache__/alerts.cpython-314.pyc and b/__pycache__/alerts.cpython-314.pyc differ diff --git a/__pycache__/cache.cpython-314.pyc b/__pycache__/cache.cpython-314.pyc index e8049f1..be34b07 100644 Binary files a/__pycache__/cache.cpython-314.pyc and b/__pycache__/cache.cpython-314.pyc differ diff --git a/__pycache__/cli.cpython-314.pyc b/__pycache__/cli.cpython-314.pyc index 7b31809..3be3ac3 100644 Binary files a/__pycache__/cli.cpython-314.pyc and b/__pycache__/cli.cpython-314.pyc differ diff --git a/__pycache__/config.cpython-314.pyc b/__pycache__/config.cpython-314.pyc index d6bc3b0..fcd4839 100644 Binary files a/__pycache__/config.cpython-314.pyc and b/__pycache__/config.cpython-314.pyc differ diff --git a/__pycache__/fetcher.cpython-314.pyc b/__pycache__/fetcher.cpython-314.pyc index 9d70ca5..ad28374 100644 Binary files a/__pycache__/fetcher.cpython-314.pyc and b/__pycache__/fetcher.cpython-314.pyc differ diff --git a/__pycache__/logging_system.cpython-314.pyc b/__pycache__/logging_system.cpython-314.pyc index d7e74ff..4e30b40 100644 Binary files a/__pycache__/logging_system.cpython-314.pyc and b/__pycache__/logging_system.cpython-314.pyc differ diff --git a/alerts.py b/alerts.py index 39c1784..467ee31 100644 --- a/alerts.py +++ b/alerts.py @@ -2,6 +2,7 @@ import platform from colorama import Fore, Style, init + init(autoreset=True) def check_alert(weather: dict, alert_temp: float): @@ -37,3 +38,11 @@ def play_alert_sound(): except Exception as e: logging.error(f"Failed to play sound alert: {e}") + +def detect_trend(prev_temp, new_temp): + if new_temp > prev_temp: + return "rising" + elif new_temp < prev_temp: + return "dropping" + else: + return "stable" diff --git a/cli.py b/cli.py index 161aa36..95fd3a2 100644 --- a/cli.py +++ b/cli.py @@ -1,64 +1,122 @@ -# cli.py -import argparse +# # cli.py +# import argparse -def get_args(): - parser = argparse.ArgumentParser(description="Weather Harvester CLI") +# def get_args(): +# parser = argparse.ArgumentParser(description="Weather Harvester CLI") - # Multiple cities: -c Delhi -c Pune - parser.add_argument( - "-c", "--city", - action="append", - help="City name (can be given multiple times: -c Delhi -c Pune)" - ) +# # Multiple cities: -c Delhi -c Pune +# parser.add_argument( +# "-c", "--city", +# action="append", +# help="City name (can be given multiple times: -c Delhi -c Pune)" +# ) - # Alternative: comma-separated cities - parser.add_argument( - "-C", "--cities", - type=str, - help="Comma-separated list of cities: --cities Delhi,Mumbai" - ) +# # Alternative: comma-separated cities +# parser.add_argument( +# "-C", "--cities", +# type=str, +# help="Comma-separated list of cities: --cities Delhi,Mumbai" +# ) - # Coordinate override (used as fallback if city not in known list) - parser.add_argument( - "--lat", - type=float, - help="Latitude value (used if city has no predefined coordinates)" - ) +# # Coordinate override (used as fallback if city not in known list) +# parser.add_argument( +# "--lat", +# type=float, +# help="Latitude value (used if city has no predefined coordinates)" +# ) - parser.add_argument( - "--lon", - type=float, - help="Longitude value (used if city has no predefined coordinates)" - ) +# parser.add_argument( +# "--lon", +# type=float, +# help="Longitude value (used if city has no predefined coordinates)" +# ) - # Cache toggle - parser.add_argument( - "--use-cache", - action="store_true", - help="Use cached data if available" - ) +# # Cache toggle +# parser.add_argument( +# "--use-cache", +# action="store_true", +# help="Use cached data if available" +# ) - # Logging level - parser.add_argument( - "--log-level", - type=str, - choices=["DEBUG", "INFO", "WARNING", "ERROR"], - help="Logging level" - ) +# # Logging level +# parser.add_argument( +# "--log-level", +# type=str, +# choices=["DEBUG", "INFO", "WARNING", "ERROR"], +# help="Logging level" +# ) + +# # Alert threshold +# parser.add_argument( +# "--alert-temp", +# type=float, +# help="Temperature threshold to trigger alert (°C)" +# ) - # Alert threshold +# # Config path +# parser.add_argument( +# "--config", +# type=str, +# default="config/config.ini", +# help="Path to configuration file" +# ) + +# return parser.parse_args() + + +# import argparse + +# def get_args(arg_list=None): +# parser = argparse.ArgumentParser(description="Weather Harvester CLI") + +# parser.add_argument("-c", "--city", dest="cities", action="append", +# help="City name", default=[]) + +# parser.add_argument("--alert-temp", type=float, default=None, +# help="Temperature threshold for alerts") + +# parser.add_argument("--log-level", type=str, default="INFO", +# help="Logging level") + +# parser.add_argument("--config", type=str, default="config/config.ini", +# help="Path to config file") + +# parser.add_argument("--lat", type=float, default=None) +# parser.add_argument("--lon", type=float, default=None) +# parser.add_argument("--use-cache", action="store_true") + +# if arg_list is not None: +# return parser.parse_args(arg_list) + +# return parser.parse_args() +import argparse + +def get_args(arg_list=None): + parser = argparse.ArgumentParser(description="Weather Harvester CLI") + + # POSitional argument (optional) parser.add_argument( - "--alert-temp", - type=float, - help="Temperature threshold to trigger alert (°C)" + "positional_city", + nargs="*", + help="City name(s) (optional if using -c)" ) - # Config path + # Optional flag -c parser.add_argument( - "--config", - type=str, - default="config/config.ini", - help="Path to configuration file" + "-c", "--city", dest="cities", action="append", + help="City name" ) + parser.add_argument("--alert-temp", type=float, default=None) + parser.add_argument("--log-level", type=str, default="INFO") + parser.add_argument("--config", type=str, default="config/config.ini") + parser.add_argument("--lat", type=float, default=None) + parser.add_argument("--lon", type=float, default=None) + parser.add_argument("--use-cache", action="store_true") + + # For tests + if arg_list is not None: + return parser.parse_args(arg_list) + return parser.parse_args() + diff --git a/data/app.log b/data/app.log index 06e465b..ec2666f 100644 --- a/data/app.log +++ b/data/app.log @@ -11,3 +11,113 @@ 2025-11-26 18:52:07,771 [INFO] No alert. Temperature 24.7C is below threshold 35.0C 2025-11-26 18:52:08,443 [INFO] No alert. Temperature 18.2C is below threshold 35.0C 2025-11-26 18:52:08,452 [INFO] All cities processed. +2025-11-28 00:31:56,419 [INFO] Logging initialized with level: INFO +2025-11-28 00:31:56,420 [INFO] Cities to process: ['Delhi'] +2025-11-28 00:31:56,420 [INFO] Caching enabled: True +2025-11-28 00:31:56,421 [INFO] Alert temperature threshold: 35.0C +2025-11-28 00:31:56,422 [INFO] Processing city: Delhi +2025-11-28 00:31:56,422 [INFO] Coordinates for Delhi: 28.6, 77.2 +2025-11-28 00:31:56,433 [INFO] Fetching new weather data for Delhi +2025-11-28 00:32:20,488 [ERROR] Network error: +2025-11-28 00:32:20,488 [ERROR] Failed to fetch weather for Delhi +2025-11-28 00:33:00,910 [INFO] Logging initialized with level: INFO +2025-11-28 00:33:00,911 [INFO] Cities to process: ['Delhi'] +2025-11-28 00:33:00,911 [INFO] Caching enabled: True +2025-11-28 00:33:00,912 [INFO] Alert temperature threshold: 35.0C +2025-11-28 00:33:00,924 [INFO] Processing city: Delhi +2025-11-28 00:33:00,925 [INFO] Coordinates for Delhi: 28.6, 77.2 +2025-11-28 00:33:00,926 [INFO] Fetching new weather data for Delhi +2025-11-28 00:33:20,993 [ERROR] Network error: +2025-11-28 00:33:20,994 [ERROR] Failed to fetch weather for Delhi +2025-11-28 00:33:20,995 [INFO] All cities processed. +2025-11-28 00:34:02,062 [INFO] Logging initialized with level: INFO +2025-11-28 00:34:02,063 [INFO] Cities to process: ['Pune', 'Ranchi'] +2025-11-28 00:34:02,063 [INFO] Caching enabled: True +2025-11-28 00:34:02,064 [INFO] Alert temperature threshold: 35.0C +2025-11-28 00:34:02,066 [INFO] Processing city: Pune +2025-11-28 00:34:02,066 [INFO] Coordinates for Pune: 18.52, 73.86 +2025-11-28 00:34:02,067 [INFO] Processing city: Ranchi +2025-11-28 00:34:02,067 [WARNING] No predefined coordinates for city 'Ranchi', using default lat/lon +2025-11-28 00:34:02,067 [INFO] Coordinates for Ranchi: 28.6, 77.2 +2025-11-28 00:34:02,068 [INFO] Fetching new weather data for Ranchi +2025-11-28 00:34:02,095 [INFO] Fetching new weather data for Pune +2025-11-28 00:34:22,110 [ERROR] Network error: +2025-11-28 00:34:22,111 [ERROR] Network error: +2025-11-28 00:34:22,111 [ERROR] Failed to fetch weather for Ranchi +2025-11-28 00:34:22,112 [ERROR] Failed to fetch weather for Pune +2025-11-28 00:34:22,112 [INFO] All cities processed. +2025-11-28 00:37:45,143 [INFO] Logging initialized with level: INFO +2025-11-28 00:37:45,144 [INFO] Cities to process: ['Pune', 'Ranchi'] +2025-11-28 00:37:45,144 [INFO] Caching enabled: True +2025-11-28 00:37:45,144 [INFO] Alert temperature threshold: 35.0C +2025-11-28 00:37:45,147 [INFO] Processing city: Pune +2025-11-28 00:37:45,148 [INFO] Processing city: Ranchi +2025-11-28 00:37:45,148 [INFO] Coordinates for Pune: 18.52, 73.86 +2025-11-28 00:37:45,148 [WARNING] No predefined coordinates for city 'Ranchi', using default lat/lon +2025-11-28 00:37:45,149 [INFO] Coordinates for Ranchi: 28.6, 77.2 +2025-11-28 00:37:45,149 [INFO] Fetching new weather data for Ranchi +2025-11-28 00:37:45,150 [INFO] Fetching new weather data for Pune +2025-11-28 00:37:46,525 [INFO] No alert. Temperature 21.2C is below threshold 35.0C +2025-11-28 00:37:46,525 [INFO] No alert. Temperature 15.7C is below threshold 35.0C +2025-11-28 00:37:46,526 [INFO] All cities processed. +2025-11-28 00:39:50,029 [INFO] Logging initialized with level: INFO +2025-11-28 00:39:50,029 [INFO] Cities to process: ['Pune', 'Ranchi'] +2025-11-28 00:39:50,029 [INFO] Caching enabled: True +2025-11-28 00:39:50,029 [INFO] Alert temperature threshold: 35.0C +2025-11-28 00:39:50,031 [INFO] Processing city: Pune +2025-11-28 00:39:50,032 [INFO] Processing city: Ranchi +2025-11-28 00:39:50,032 [INFO] Coordinates for Pune: 18.52, 73.86 +2025-11-28 00:39:50,032 [WARNING] No predefined coordinates for city 'Ranchi', using default lat/lon +2025-11-28 00:39:50,033 [INFO] Coordinates for Ranchi: 28.6, 77.2 +2025-11-28 00:39:50,052 [INFO] No alert. Temperature 21.2C is below threshold 35.0C +2025-11-28 00:39:50,052 [INFO] No alert. Temperature 15.7C is below threshold 35.0C +2025-11-28 00:39:50,053 [INFO] All cities processed. +2025-12-03 23:15:11,599 [INFO] Logging initialized with level: INFO +2025-12-03 23:15:42,457 [INFO] Logging initialized with level: INFO +2025-12-03 23:16:03,772 [INFO] Logging initialized with level: INFO +2025-12-03 23:17:13,518 [INFO] Logging initialized with level: INFO +2025-12-03 23:17:32,998 [INFO] Logging initialized with level: INFO +2025-12-03 23:27:19,707 [INFO] Logging initialized with level: INFO +2025-12-03 23:27:19,708 [INFO] Cities to process: ['Mumbai'] +2025-12-03 23:27:19,708 [INFO] Caching enabled: True +2025-12-03 23:27:19,717 [INFO] Alert temperature threshold: 35.0C +2025-12-03 23:27:19,720 [INFO] Processing city: Mumbai +2025-12-03 23:27:19,720 [INFO] Coordinates for Mumbai: 19.07, 72.88 +2025-12-03 23:27:19,720 [INFO] Fetching new weather data for Mumbai +2025-12-03 23:27:20,815 [INFO] No alert. Temperature 26.6C is below threshold 35.0C +2025-12-03 23:27:20,817 [INFO] All cities processed. +2025-12-03 23:27:59,961 [INFO] Logging initialized with level: INFO +2025-12-03 23:27:59,962 [INFO] Cities to process: ['Pune'] +2025-12-03 23:27:59,962 [INFO] Caching enabled: True +2025-12-03 23:27:59,963 [INFO] Alert temperature threshold: 35.0C +2025-12-03 23:27:59,964 [INFO] Processing city: Pune +2025-12-03 23:27:59,964 [INFO] Coordinates for Pune: 18.52, 73.86 +2025-12-03 23:27:59,982 [INFO] Fetching new weather data for Pune +2025-12-03 23:28:00,935 [INFO] No alert. Temperature 17.8C is below threshold 35.0C +2025-12-03 23:28:00,936 [INFO] All cities processed. +2025-12-04 02:35:46,275 [INFO] Logging initialized with level: INFO +2025-12-04 02:35:46,276 [INFO] Cities to process: ['Delhi'] +2025-12-04 02:35:46,276 [INFO] Caching enabled: True +2025-12-04 02:35:46,277 [INFO] Alert temperature threshold: 35.0C +2025-12-04 02:35:46,280 [INFO] Processing city: Delhi +2025-12-04 02:35:46,280 [INFO] Coordinates for Delhi: 28.6, 77.2 +2025-12-04 02:35:46,301 [INFO] Fetching new weather data for Delhi +2025-12-04 02:35:47,287 [INFO] No alert. Temperature 10.1C is below threshold 35.0C +2025-12-04 02:35:47,288 [INFO] All cities processed. +2025-12-04 02:47:33,463 [INFO] Logging initialized with level: INFO +2025-12-04 02:47:33,464 [INFO] Cities to process: ['Delhi'] +2025-12-04 02:47:33,464 [INFO] Caching enabled: True +2025-12-04 02:47:33,465 [INFO] Alert temperature threshold: 35.0C +2025-12-04 02:47:33,466 [INFO] Processing city: Delhi +2025-12-04 02:47:33,467 [INFO] Coordinates for Delhi: 28.6, 77.2 +2025-12-04 02:47:33,483 [INFO] No alert. Temperature 10.1C is below threshold 35.0C +2025-12-04 02:47:33,484 [INFO] All cities processed. +2025-12-04 02:48:16,630 [INFO] Logging initialized with level: INFO +2025-12-04 02:48:16,631 [INFO] Cities to process: ['Delhi'] +2025-12-04 02:48:16,632 [INFO] Caching enabled: True +2025-12-04 02:48:16,632 [INFO] Alert temperature threshold: 35.0C +2025-12-04 02:48:16,634 [INFO] Processing city: Delhi +2025-12-04 02:48:16,634 [INFO] Coordinates for Delhi: 28.6, 77.2 +2025-12-04 02:48:16,635 [INFO] No alert. Temperature 10.1C is below threshold 35.0C +2025-12-04 02:48:16,636 [INFO] All cities processed. +2025-12-04 02:48:28,343 [INFO] Logging initialized with level: INFO diff --git a/data/delhi_cache.json b/data/delhi_cache.json index d6093a8..a3870a3 100644 --- a/data/delhi_cache.json +++ b/data/delhi_cache.json @@ -1,13 +1,13 @@ { - "timestamp": "2025-11-26T18:52:08.442330", + "timestamp": "2025-12-04T02:35:47.286649", "payload": { - "time": "2025-11-26T13:15", + "time": "2025-12-03T21:00", "interval": 900, - "temperature": 18.2, - "windspeed": 3.6, - "winddirection": 354, + "temperature": 10.1, + "windspeed": 4.1, + "winddirection": 285, "is_day": 0, - "weathercode": 0 + "weathercode": 1 }, - "previous": 18.2 + "previous": 10.1 } \ No newline at end of file diff --git a/data/pune_cache.json b/data/pune_cache.json index 67980f0..3675bdc 100644 --- a/data/pune_cache.json +++ b/data/pune_cache.json @@ -1,13 +1,13 @@ { - "timestamp": "2025-11-26T18:52:07.770751", + "timestamp": "2025-12-03T23:28:00.933597", "payload": { - "time": "2025-11-26T13:15", + "time": "2025-12-03T17:45", "interval": 900, - "temperature": 24.7, - "windspeed": 4.1, - "winddirection": 105, + "temperature": 17.8, + "windspeed": 3.3, + "winddirection": 84, "is_day": 0, "weathercode": 0 }, - "previous": 24.7 + "previous": 17.8 } \ No newline at end of file diff --git a/fetcher.py b/fetcher.py index 097bf13..ef7b0cf 100644 --- a/fetcher.py +++ b/fetcher.py @@ -27,31 +27,55 @@ def get_coords_for_city(city: str, default_lat: float, default_lon: float) -> tu return default_lat, default_lon -def fetch_weather(lat: float, lon: float) -> dict | None: - """Fetch current weather for given coordinates using Open-Meteo.""" - url = ( - "https://api.open-meteo.com/v1/forecast" - f"?latitude={lat}&longitude={lon}¤t_weather=true" - ) +# def fetch_weather(lat: float, lon: float) -> dict | None: +# """Fetch current weather for given coordinates using Open-Meteo.""" +# url = ( +# "https://api.open-meteo.com/v1/forecast" +# f"?latitude={lat}&longitude={lon}¤t_weather=true" +# ) +# try: +# logging.debug(f"Requesting URL: {url}") +# with urllib.request.urlopen(url, timeout=10) as resp: +# if resp.status != 200: +# logging.error(f"HTTP Error: {resp.status}") +# return None +# data = json.load(resp) +# return data.get("current_weather") +# except urllib.error.HTTPError as e: +# logging.error(f"HTTP error: {e}") +# except urllib.error.URLError as e: +# logging.error(f"Network error: {e}") +# except json.JSONDecodeError as e: +# logging.error(f"JSON decode error: {e}") +# except Exception as e: +# logging.exception(f"Unexpected error: {e}") +# return None + + +# if __name__ == "__main__": +# # quick manual test +# print(fetch_weather(28.6, 77.2)) + + +import urllib.request +import json +import logging + +def fetch_weather(lat, lon): + url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true" + try: - logging.debug(f"Requesting URL: {url}") with urllib.request.urlopen(url, timeout=10) as resp: - if resp.status != 200: - logging.error(f"HTTP Error: {resp.status}") - return None - data = json.load(resp) + raw = resp.read() + data = json.loads(raw) return data.get("current_weather") - except urllib.error.HTTPError as e: - logging.error(f"HTTP error: {e}") - except urllib.error.URLError as e: - logging.error(f"Network error: {e}") - except json.JSONDecodeError as e: - logging.error(f"JSON decode error: {e}") - except Exception as e: - logging.exception(f"Unexpected error: {e}") - return None - + except Exception as e: + logging.error("Unexpected error: %s", e) + return None + + + if __name__ == "__main__": # quick manual test - print(fetch_weather(28.6, 77.2)) \ No newline at end of file + print(fetch_weather(28.6, 77.2)) diff --git a/main.py b/main.py index da60be5..a4eae29 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,132 @@ +# # main.py +# from cli import get_args +# from config import load_config +# from fetcher import fetch_weather, get_coords_for_city +# from cache import load_cache, save_cache +# from logging_system import setup_logging +# from alerts import check_alert + +# from concurrent.futures import ThreadPoolExecutor, as_completed +# from colorama import Fore, Style +# import logging + + +# def build_city_list(args, cfg) -> list[str]: +# cities: list[str] = [] + +# if args.city: +# cities.extend(args.city) + +# if args.cities: +# cities.extend( +# [c.strip() for c in args.cities.split(",") if c.strip()] +# ) + +# if not cities: +# cities = [cfg["city"]] + +# return cities + + +# def process_city(city, base_lat, base_lon, use_cache, alert_temp): +# print(f"\n{Fore.BLUE}====== {city.upper()} ======{Style.RESET_ALL}") +# logging.info(f"Processing city: {city}") + +# # coordinates +# lat, lon = get_coords_for_city(city, base_lat, base_lon) +# logging.info(f"Coordinates for {city}: {lat}, {lon}") + +# weather = None +# previous_temp = None + +# # load cache + previous temp for trend +# if use_cache: +# cached, prev = load_cache(city) +# if cached: +# weather = cached +# previous_temp = prev +# print(f"{Fore.GREEN}Using cached weather for {city}:{Style.RESET_ALL}") +# print(weather) + +# # fetch if needed +# if weather is None: +# logging.info(f"Fetching new weather data for {city}") +# weather = fetch_weather(lat, lon) +# if weather: +# print(f"{Fore.GREEN}Weather for {city}:{Style.RESET_ALL}") +# print(weather) +# save_cache(city, weather) +# else: +# logging.error(f"Failed to fetch weather for {city}") +# return + +# # trend evaluation +# if previous_temp is not None: +# curr = weather["temperature"] +# diff = curr - previous_temp + +# if diff > 0: +# print(f"{Fore.YELLOW}📈 Temperature Rising (+{diff:.1f}°C){Style.RESET_ALL}") +# elif diff < 0: +# print(f"{Fore.CYAN}📉 Temperature Dropping ({diff:.1f}°C){Style.RESET_ALL}") +# else: +# print(f"{Fore.WHITE}➖ Temperature Stable{Style.RESET_ALL}") +# else: +# print(f"{Fore.WHITE}No previous temperature to compare.{Style.RESET_ALL}") + +# # alert system +# alert_triggered = check_alert(weather, alert_temp) + +# if alert_triggered: +# print(f"{Fore.RED}⚠ Alert Active for {city}!{Style.RESET_ALL}") +# else: +# print(f"{Fore.GREEN}✅ No alert for {city}.{Style.RESET_ALL}") + + +# def main(): +# args = get_args() +# cfg = load_config(args.config) + +# log_level = args.log_level or cfg["log_level"] +# setup_logging(log_level) + +# use_cache = args.use_cache or cfg["use_cache"] +# alert_temp = args.alert_temp or cfg["alert_temp"] + +# base_lat = args.lat or cfg["lat"] +# base_lon = args.lon or cfg["lon"] + +# cities = build_city_list(args, cfg) + +# logging.info(f"Cities to process: {cities}") +# logging.info(f"Caching enabled: {use_cache}") +# logging.info(f"Alert temperature threshold: {alert_temp}°C") + +# # concurrency +# with ThreadPoolExecutor(max_workers=len(cities)) as executor: +# futures = [ +# executor.submit( +# process_city, +# city, +# base_lat, +# base_lon, +# use_cache, +# alert_temp +# ) +# for city in cities +# ] + +# for f in as_completed(futures): +# pass + +# logging.info("All cities processed.") +# print(f"{Fore.GREEN}✅ All cities processed.{Style.RESET_ALL}") + + + +# if __name__ == "__main__": +# main() + # main.py from cli import get_args from config import load_config @@ -9,23 +138,42 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from colorama import Fore, Style import logging +import re + +def validate_city_name(city: str) -> bool: + """ + Valid city names contain letters and optional spaces/hyphens. + Reject numeric or special-character-only inputs. + """ + pattern = r"^[A-Za-z\s\-]+$" + return bool(re.match(pattern, city)) def build_city_list(args, cfg) -> list[str]: cities: list[str] = [] - if args.city: - cities.extend(args.city) + # args.city DOES NOT EXIST — so removed + # args.cities is a list because of action="append" if args.cities: - cities.extend( - [c.strip() for c in args.cities.split(",") if c.strip()] - ) + cities.extend(args.cities) + # If nothing provided, use config.ini default if not cities: cities = [cfg["city"]] - return cities + valid = [] + for c in cities: + if validate_city_name(c): + valid.append(c) + else: + print(f"\n❌ Invalid city name: {c}") + print("City names must contain only letters, spaces, or hyphens.") + print("Example: Delhi, New York, Pune-East") + exit(1) + + return valid + def process_city(city, base_lat, base_lon, use_cache, alert_temp): @@ -120,10 +268,8 @@ def main(): pass logging.info("All cities processed.") -print(f"{Fore.GREEN}✅ All cities processed.{Style.RESET_ALL}") - + print(f"{Fore.GREEN}✅ All cities processed.{Style.RESET_ALL}") if __name__ == "__main__": main() - diff --git a/tests/test_alerts.py b/tests/test_alerts.py new file mode 100644 index 0000000..6d0fdbb --- /dev/null +++ b/tests/test_alerts.py @@ -0,0 +1,19 @@ +import unittest +from alerts import check_alert + +class TestAlerts(unittest.TestCase): + + def test_alert_triggered(self): + weather = {"temperature": 40} + self.assertTrue(check_alert(weather, 35)) + + def test_no_alert(self): + weather = {"temperature": 20} + self.assertFalse(check_alert(weather, 35)) + + def test_alert_exact_threshold(self): + weather = {"temperature": 35} + self.assertTrue(check_alert(weather, 35)) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..eb4a15e --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,23 @@ +import unittest +from cli import get_args + +class TestCLI(unittest.TestCase): + + def test_single_city(self): + args = get_args(["-c", "Delhi"]) + self.assertEqual(args.cities, ["Delhi"]) + + def test_multiple_cities(self): + args = get_args(["-c", "Delhi", "-c", "Pune"]) + self.assertEqual(args.cities, ["Delhi", "Pune"]) + + def test_alert_temp(self): + args = get_args(["--alert-temp", "30"]) + self.assertEqual(args.alert_temp, 30) + + def test_log_level(self): + args = get_args(["--log-level", "DEBUG"]) + self.assertEqual(args.log_level, "DEBUG") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_fetcher.py b/tests/test_fetcher.py new file mode 100644 index 0000000..09ef3f0 --- /dev/null +++ b/tests/test_fetcher.py @@ -0,0 +1,34 @@ +import unittest +from unittest.mock import patch, MagicMock +from fetcher import fetch_weather + +class TestFetcher(unittest.TestCase): + + @patch("urllib.request.urlopen") + def test_fetch_weather_success(self, mock_urlopen): + fake_response = MagicMock() + fake_response.read.return_value = b''' + { + "current_weather": { + "temperature": 25.5, + "windspeed": 6.4, + "winddirection": 120, + "weathercode": 1 + } + } + ''' + mock_urlopen.return_value.__enter__.return_value = fake_response + + data = fetch_weather(28.6, 77.2) + self.assertIn("temperature", data) + self.assertEqual(data["temperature"], 25.5) + + @patch("urllib.request.urlopen") + def test_fetch_weather_network_error(self, mock_urlopen): + mock_urlopen.side_effect = Exception("Network error") + + data = fetch_weather(28.6, 77.2) + self.assertIsNone(data) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_trend.py b/tests/test_trend.py new file mode 100644 index 0000000..68d7bfd --- /dev/null +++ b/tests/test_trend.py @@ -0,0 +1,16 @@ +import unittest +from alerts import detect_trend # or from trend import detect_trend if separate + +class TestTrend(unittest.TestCase): + + def test_rising(self): + self.assertEqual(detect_trend(20, 25), "rising") + + def test_dropping(self): + self.assertEqual(detect_trend(30, 20), "dropping") + + def test_stable(self): + self.assertEqual(detect_trend(25, 25), "stable") + +if __name__ == "__main__": + unittest.main()