From b89ccc458a9d9dc2cdc5b2d0fa226c26ad72109a Mon Sep 17 00:00:00 2001 From: Mouli24 Date: Sun, 30 Nov 2025 21:44:47 +0530 Subject: [PATCH 1/3] add gitignore --- .gitignore | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .gitignore 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/ From 0b45ee254eebb169ed3908fb5fae02677523fe0b Mon Sep 17 00:00:00 2001 From: Mouli24 Date: Wed, 3 Dec 2025 12:05:33 +0530 Subject: [PATCH 2/3] error resolved --- __pycache__/alerts.cpython-314.pyc | Bin 2193 -> 2399 bytes __pycache__/cli.cpython-314.pyc | Bin 1705 -> 1365 bytes __pycache__/fetcher.cpython-314.pyc | Bin 4016 -> 2828 bytes alerts.py | 9 ++ cli.py | 144 +++++++++++++++++----------- data/app.log | 61 ++++++++++++ data/pune_cache.json | 14 +-- fetcher.py | 68 ++++++++----- tests/test_alerts.py | 19 ++++ tests/test_cli.py | 23 +++++ tests/test_fetcher.py | 34 +++++++ tests/test_trend.py | 16 ++++ 12 files changed, 301 insertions(+), 87 deletions(-) create mode 100644 tests/test_alerts.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_fetcher.py create mode 100644 tests/test_trend.py diff --git a/__pycache__/alerts.cpython-314.pyc b/__pycache__/alerts.cpython-314.pyc index 10f351039a9efd5f1e3da0729d9ca5640dc15424..2b7b92d0ee37209686a30dfbf2f317adb23fee0d 100644 GIT binary patch delta 347 zcmbOzcwdN5n~#@^0SJEI)X$v6I+0I;F=V2;A2)|MLkUL^qcVdg=ftW%j2xTG8O@o2 z)OwbqOpH>KOW7CJCo_TcK>>)(48)(+fJ6gBxPZkJ28I-%hzvszgEELO1m^=y4PpYC z%zTTjD6=>-FZ~urN>P460f@y`T#}fSle&^Ylj#;`K~ZX1d`W6=mf+Bh25TtN1qy&gfo1}YdQOmr*GLcV$(Ql%$8%fNC2p_h!05I;;_lhPbtkwwJXvFav6cRSQ$usU}j`wyv@LO in}Pp6gW5#~wTBEW9VJ&8M82}{Gm3su;bXJ{8v+2h86s%_ diff --git a/__pycache__/cli.cpython-314.pyc b/__pycache__/cli.cpython-314.pyc index 7b31809a1335b3188212290c2f08ffd4625ed3d2..af4dce5ccba128e73055c3223cfb09d63686fd86 100644 GIT binary patch literal 1365 zcmb7EOK;Oa5Z+CkN8>taDQPNbi+Kp)f{Q>^BLM=TLTRL`RLk=aQ7mVZ7~I%eZ-DyL zpTUs>2atN;*dM?TNCBzQibEw%+$t4uX4Y}5AW;D;?d7QWGLSyZOO(lTP_1IlQ8!_0=kZ>5{t(2 ze$0IU){}@pefe;xWIii-8zjzE* zv-??zM)LpQQRwp+BE$dSapbpCjg)1LU>tgjqyNb-l2KAB$2Eqih#o;`T-HeKW$Kp? z(M(+P(}yr-VmfqrEG#~v7H?9fFIema4LE3XixmS4wMGM*IX-v1R+upDFm0N)!`DO_ zq(cLdwtSzqYT>{fpnA(%rG_FD@Zw;yrJGQi)b&NCM(bAF<>8=dS}tYW&nQc%4`UqN3SuN~ZRm@DSc6{bPta7M^Jm`M!c&#nZl>c@ou5GUC_4*`kS%O zg{e+!y0@z3gHN15D%dm{rcL2vV*MFn|}nEdgvQ4k_~ZPlx_U3#4zgRT5- Xf*}yQNsRH{d;;O2pUNmc5qbOqoER)i literal 1705 zcmbVMO>f&q5M4>sM=ViN+&FS#!Pz1~5h9_B0!@P^Mu9|5Y_+Z}ESlybG+2`>YEz0N zc9&|+Q;z*Bz4bTr+(Xh9ut4~hQ*Q#CbI~E`ILtwC&;__VvorJN?aY9;n)(ugW&ZmK ze^^22b)vjo#Z%$r5(qEQ4~P`{NFh?}AXOlR>&Phmlep|zr;h1Sg}nsd$sdIzYItTF zyu~}aLFi*d0QD|-CM!VSqd_^7^E)Y$646MRRPJio{k27&RZ<(mJF-PCkfn1Y>ciTZ ze*^pz?yweS&~yIH5Abi1Hn|AQ&%<4Q$9n^jwfzG5h+I0C{Ys9_uxCnyEb2ay45!yNwyB~CD>|jzr3WsHAk6h0(`8fUM*6Pr zINWn^sv0GcjCWkM#y+>0%pTk@P3khCOvNTacpDtP1A##SLjj!_3ns_DYh&eOCAi~2 zpHqK)^Y`C+Mp}K}8*wMPXrQb%;e-?BsX?NOxIu0E8CcW1 zxFnT`%OlsPDqczh5}SnatfZQd$$tjNU-a$`p`Jnxx7g1x!`u($HWx0a@<-(d!=JJ# zHv{26XOqEZ@vHG@w|e{^&_qG zO6$yOjbFyIR%fQ~%&Vxp`UYv`%DnlW{rR!pp6Oc)?5$&cd8U7zvo>D~E-lv)|6wR7N?)71ADgfAzw+t+8hkqd94#`00U`>NjktG@nH zTR(v^A)_eTaHMtn6^Sc}+<`5wZBBeUblF#84b-GPQip9`DijK@QR|JiS-6(E{|74{ Bm3#mI diff --git a/__pycache__/fetcher.cpython-314.pyc b/__pycache__/fetcher.cpython-314.pyc index 9d70ca55e629b315ee12d724e9aa3f585f03a86c..844c06e4f9dc39aba9ce8e9128c3ab6f5214806b 100644 GIT binary patch delta 918 zcmY*X%}*0S6rb(x?6%wP7HMfw`3!>D6ah*32qGU<4qi;1YK;ldtnI3+rCn#Ifp~$7 z2~iVL_kb4@kDiPs9ykIQIT9g}3>f1-U{FX*u=P*u4rcKZ~9s2YhVgd`M3UbcLw2FWOb{3zP#CE^5_1dq5z zfC5o~lqkqo5or=2?ixotm0ci0WrqYJH42D#!iR$8Hy63u51)5<=P3+Dz#id%TN0!} zfSpZqOnaPn@Z6asbh|q#Y0spLc&9;ZJTIM$xL*7SvV?K(anzX;!~M-5EyP(JS1g!@ zN#)S~U!#8+CBEcgcW0lYO0gJh0hjoCU~)}hTs=M^f?Nv#b-)tGtB$c`wX+OsV&Y9P z?m5I7K|Mgw5{E^^SPm_dGmrQ*$+_c8&uSc%h&g2xON0iqC5(*%$xItMd0^n{Pr|nf z#*AfTiIHnEFfQVOrdC^%saV4Kym^;O*mztrY(gal6fL7b1#IX!Dm=7{1q$;;J!j)O z)(-WJ&14OWn8gBaaD7;z)7mA<=XJsq%jX%1OJG}+$NDrCayro|XV~tv+uE9(Z}Mr) z9rFlV_5xFLU@I`Vlv)~FKD(A$8(Y6|Z(Y7WcV$=b!mu;sub-`sENW%ViOpZx2}KtN zUJksYlOHkc&;U6@Ud^S7p}x)X0G)56OwP06PUd!@vfGG{zMMn&W|lagl%HB37fK6rS~Z?X|t@b?n%ViNTwOrZIsyKTZV!!cXF;LM6Bx6R9F3HTD!-60ezE z7ZeWWQmM3+(y*1t(TBEjrJ~ZF+GBx4Jyn}FlG9a=y;Qw8f(ohh(3!QJ00N0;&zo=F zyuW!fv-4TtZp7W==A8i3SO0b8?%0>yZO*UpC=G4^sb#6JC*hSC$tT&QR>>~;B^Eb_ z6p)-!P;yCaF;=ym2Q%O@6#)_#1QH(va~{<$xiJ<@%$|2ho~0l9snGX-Zqfk?J132h z9XA@cho`hzHdvCVX(BaAvOI<9Y0I|Ux}R7zC`NaJD1+R1XwY+M6*D=Hym&YfwV+la zwq|n~HAu6ckIqNa{`61 zYGykhJZP-K`fvfS1y_xl0vwPmQxxX3_Lp^UyaPzqDGLV%Eb+$DBVgK!O-)MLnrNp_ zIy8X^QjGQn6X`LxXJ?L}nO_cG1t1#(APja2mE<(R78swRlP@rNHheq4+lha1Qh!W% z`|O=ynpD!Hq(_q({}A0wxL}>KCVkD>onB&_8b!Kjsr%G^VtOXAx5M;qkfX$>m&)x} zHc^sn({p44S-c}*P+*=}T#WY5J5tQEE7^@RFEGa#l7AiLsHckiqKM=yJV!5&_^$Jn zBJNaF|Y~!WynjiF*}KxyG(U`Guq7P|aKtGev}8UKOvxw0a35 z@qHPI7jqxNyqJ+?l+ESSDpbUxlFMHd&lF((z-g#Lxp$%OlByPz;aDtP$o0wvsJ&-P zlomV^yi$aUO0>^PZx4%6%CM`sc_Dk&xqlF_g@VF-=TvyIS=d03xc5GJ5{ zRYsS^roeY<^2~&og&8>uw}sxbd3db=Gb+rAu(fU)6%e`A29t#siWfD*)?TtHYFaHS z24gxitREAU%*Y=3MA?x8h4 z{)h#wZMP5II<%b9j(t3~;S1hAxO7ktANbwZ``CqbuwVSgnpKA;6T$rs8D4^5A;;o{W`mU z%V`=oFt55IYpw{hJ=|NlQpxDM z2CD3!&JO HJ3Iad+pfCh 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..5a943f8 100644 --- a/cli.py +++ b/cli.py @@ -1,64 +1,92 @@ -# cli.py +# # cli.py +# import argparse + +# 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)" +# ) + +# # 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)" +# ) + +# 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" +# ) + +# # 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)" +# ) + +# # 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(): +def get_args(arg_list=None): 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)" - ) - - # 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)" - ) - - 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" - ) - - # 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)" - ) - - # Config path - parser.add_argument( - "--config", - type=str, - default="config/config.ini", - help="Path to configuration file" - ) + 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() + diff --git a/data/app.log b/data/app.log index 06e465b..badcf56 100644 --- a/data/app.log +++ b/data/app.log @@ -11,3 +11,64 @@ 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. diff --git a/data/pune_cache.json b/data/pune_cache.json index 67980f0..561a490 100644 --- a/data/pune_cache.json +++ b/data/pune_cache.json @@ -1,13 +1,13 @@ { - "timestamp": "2025-11-26T18:52:07.770751", + "timestamp": "2025-11-28T00:37:46.522353", "payload": { - "time": "2025-11-26T13:15", + "time": "2025-11-27T19:00", "interval": 900, - "temperature": 24.7, - "windspeed": 4.1, - "winddirection": 105, + "temperature": 21.2, + "windspeed": 1.6, + "winddirection": 117, "is_day": 0, - "weathercode": 0 + "weathercode": 1 }, - "previous": 24.7 + "previous": 21.2 } \ 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/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() From 92256dd1bf0cd570cb6a4c6cffc58c650b793928 Mon Sep 17 00:00:00 2001 From: Mouli24 Date: Thu, 4 Dec 2025 02:49:47 +0530 Subject: [PATCH 3/3] error handling --- __pycache__/alerts.cpython-314.pyc | Bin 2399 -> 2399 bytes __pycache__/cache.cpython-314.pyc | Bin 2649 -> 2649 bytes __pycache__/cli.cpython-314.pyc | Bin 1365 -> 1381 bytes __pycache__/config.cpython-314.pyc | Bin 1557 -> 1557 bytes __pycache__/fetcher.cpython-314.pyc | Bin 2828 -> 2828 bytes __pycache__/logging_system.cpython-314.pyc | Bin 1215 -> 1215 bytes cli.py | 52 +++++-- data/app.log | 49 ++++++ data/delhi_cache.json | 14 +- data/pune_cache.json | 14 +- main.py | 164 +++++++++++++++++++-- 11 files changed, 259 insertions(+), 34 deletions(-) diff --git a/__pycache__/alerts.cpython-314.pyc b/__pycache__/alerts.cpython-314.pyc index 2b7b92d0ee37209686a30dfbf2f317adb23fee0d..b0d383a7c110f0694eaf015878b41b2a11237234 100644 GIT binary patch delta 20 acmcaFbYF;Dn~#@^0SGn<7;NMY;RFCOjRZ&l delta 20 acmcaFbYF;Dn~#@^0SJEI)ZfS*!U+I5L-BG;5)0EjA0iB+80-uosnRMU97A7la;)vLw@FVUt~UH)3

fY zVnb|D^x^Lv@431H**d72x?6A!_mYd<;)*62tF)<0pDfpO$yCSg0s_ylWo^n=lX5Sg zD`sZ@_p!IYfx^!|gAY&mOcW-5>W6(Vc-8XLZ4RytmsbU_`(BUMlDb_Bqv_rDx9maU z_dB-Js@GMn^}V>0aMYzi#Maf?W5`jPCMnmwmUP52%&_D*UO?m2Nog-y6E>7?N0b-Z zbjuqA>9FWHLD+Eu`kDqDHP;?JV<+m8t_j*pSpzPE5^KYc_=Yd0#Kb_3AT~XQAa--Mi4Ww3IbzsA zN{(qbRjFBR?laAuYd10j#W8llc})NhMGT$Xi7q&s1^i>gMF9>*l1E zrRLmX@$_@^uj2K|PfyRxOIOHY0f|mlXO>eJ4oED?P$3= zEYV~t;szN9GIO#wvoa&cj3afR2j!R-MH z*L4>0i!9do;CqAG{ zi$LyP$xtK#Vv7NZTO2mI`6;D2sdhzDKrSN?7YhK156p~=jGx_@7#Ibk;GD=HumJ0D_GI1{=8k;GD=HumJ0D`%}`Wv|!xdA6A1BU 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() -