-
Notifications
You must be signed in to change notification settings - Fork 1
/
fnc.py
executable file
·181 lines (143 loc) · 8.74 KB
/
fnc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import datetime as DT
import urllib.request, urllib.parse, urllib.error
import json
import io
import tkinter as tk
from PIL import Image, ImageTk
import concurrent.futures
def btn_pressed(mainwindow, input_value):
# disable the submit button till the function completes or errors in other function
# error probably will only occur in httpreq()
# so there is a line of code to enable the button again in case of error
# tk.after(0, some_func, args) schedules some_func(args) to be run in tkinter's main thread after 0 ms
# writing it this way, because someguy said I should only do stuff to tkinter from its own thread
mainwindow.after(0, mainwindow.input_frame.get_btn.configure, {'state':tk.DISABLED})
current_weather = get_current_weather(mainwindow, input_value) # fetches current weather, displays it in the next line
mainwindow.after(0, write_current_output, mainwindow.current_weather_frame, current_weather)
#mainwindow.after(0, mainwindow.space_label.pack)
# city coordinates, this is required because the next api request does not support calling by city name
coords = get_city_coords(current_weather)
forecast_weather = get_forecast_weather(mainwindow, coords) # fetches forecast weather, displays it in the next line
mainwindow.after(0, write_forecast_daily_output, mainwindow.forecast_daily_frame, forecast_weather['daily'], forecast_weather['timezone_offset'])
# we shall now download icons and display them
process_icons(mainwindow, current_weather, forecast_weather['daily'])
# since we are done fetching and displaying everything, we can now enable the button so that it can be pressed again
mainwindow.after(0, mainwindow.input_frame.get_btn.configure, {'state':tk.NORMAL})
mainwindow.status_label.configure(text=f"Updated at {DT.datetime.now().strftime('%I:%M:%S %p')}")
mainwindow.after(0, update_history(mainwindow, input_value))
def update_history(mainwindow, input_value):
if input_value in mainwindow.history:
mainwindow.input_frame.history.remove(input_value)
mainwindow.input_frame.history.insert(0, input_value)
mainwindow.input_frame.input_combobox.configure(values = mainwindow.input_frame.history)
mainwindow.input_frame.input_combobox.current(0)
def get_current_weather(mainwindow, input_value):
params = {
'q':input_value,
'units': mainwindow.UNIT,
'appid': mainwindow.API_KEY
}
return httpreq(mainwindow, mainwindow.WEATHER_CURRENT_URL, params)
def get_city_coords(current_weather):
return {
"lon" : current_weather["coord"]["lon"],
"lat" : current_weather["coord"]["lat"]
}
def get_forecast_weather(mainwindow, coords):
params = {
'lon' : coords['lon'],
'lat' : coords['lat'],
'exclude' : 'current,minutely,hourly',
'units': mainwindow.UNIT,
'appid' : mainwindow.API_KEY
}
return httpreq(mainwindow, mainwindow.WEATHER_FORECAST_URL, params)
def process_icons(mainwindow, current_weather, daily_weather):
frames_list = [mainwindow.current_weather_frame] + mainwindow.forecast_daily_frame.forecast_day_list
icon_list = get_icon_codes(current_weather, daily_weather)
draw_all_icons(mainwindow, icon_list, frames_list)
def get_icon_codes(current_weather, daily_weather):
# initializing the icon list with the icon code of the current weather
# "@2x" is added because that's how openweathermap names bigger icons
icon_list = [current_weather['weather'][0]['icon'] + '@2x']
# range(1.8) because the api returns weather for 8 days
# the first day being the current day
# and we don't want the current day
for i in range(1,8):
icon_code = daily_weather[i]['weather'][0]['icon']
if icon_code not in icon_list:
icon_list.append(icon_code)
return icon_list
def download_icon(mainwindow, icon_code):
# mainwindow is the main window
# if the icon does not exist in the cache, then it is downloaded
# this function returns the icon code itself, because we will run this function in a threadpool
# and we want to know for which icon the function has completed
# then display the icon in the frames which contains this icon code
if icon_code in mainwindow.icon_cache:
return icon_code
else:
url = f"http://openweathermap.org/img/wn/{icon_code}.png"
photoimg = ImageTk.PhotoImage(Image.open(io.BytesIO(httpreq(mainwindow, url)))) # copied from SO, I think
mainwindow.icon_cache[icon_code] = photoimg
return icon_code
def draw_all_icons(mainwindow, icon_list, frame_list):
with concurrent.futures.ThreadPoolExecutor() as executor:
future_list = [executor.submit(download_icon, mainwindow, icon_code) for icon_code in icon_list]
# each "future" is a future object
# the result attribute is the return value of the function in that future
# the function being run is returning the icon codes
# if the icon code matches with any of the frames' icon code, we shall display it there
for future in concurrent.futures.as_completed(future_list):
for frame in frame_list:
if frame.icon_code == future.result():
frame.after(0, frame.icon_label.configure, {'image' : mainwindow.icon_cache[frame.icon_code]})
def httpreq(mainwindow, url, params=None):
if params is not None:
url = url + urllib.parse.urlencode(params)
try:
with urllib.request.urlopen(url) as req:
if req.info().get_content_subtype() == "json":
return json.loads(req.read().decode())
elif req.info().get_content_subtype() == "png":
return req.read()
except urllib.error.HTTPError as err:
mainwindow.status_label.configure(text=err)
# setting the submit button to normal state because it was set to disabled when it was pressed
mainwindow.after(0, mainwindow.input_frame.get_btn.configure, {'state':tk.NORMAL})
print(err) # printing this if anyone wants to know where the error occured
print(url.split("&appid")[0]) # we don't want the api key to be printed in the console
# raising the exception again
# because we don't want to run any code that follows this function call because of the error
# probably should have done it in a cleaner way
# TODO: may "fix" it later
raise
def write_current_output(current_weather_frame, weather):
#current_weather_frame.pack()
current_weather_frame['bg'] = '#CCCCCC'
current_weather_frame.city_lab.configure(text=f"City: {weather['name']}, {weather['sys']['country']}")
current_weather_frame.temp_lab.configure(text=f"Temp: {weather['main']['temp']}°C")
current_weather_frame.feels_lab.configure(text=f"Feels like: {weather['main']['feels_like']}°C")
current_weather_frame.wind_speed_lab.configure(text=f"Wind speed: {weather['wind']['speed']} m/s")
current_weather_frame.desc_lab.configure(text=weather['weather'][0]['description'].capitalize())
current_weather_frame.humid_lab.configure(text=f"Humidty: {weather['main']['humidity']}%")
current_weather_frame.clouds_lab.configure(text=f"Cloudiness: {weather['clouds']['all']}%")
current_weather_frame.sunrise_lab.configure(text=f"Sunrise: {DT.datetime.utcfromtimestamp(weather['sys']['sunrise']+weather['timezone']).strftime('%I:%M %p')}")
current_weather_frame.sunset_lab.configure(text=f"Sunset: {DT.datetime.utcfromtimestamp(weather['sys']['sunset']+weather['timezone']).strftime('%I:%M %p')}")
current_weather_frame.icon_code = weather['weather'][0]['icon'] + "@2x"
# the "@2x" is appended to the icon code because bigger icons from openweathermap has it in the filename
for widget in current_weather_frame.children.values():
widget['bg'] = "#CCCCCC"
def write_forecast_daily_output(forecast_daily_frame, daily_weather, timezone_offset):
#forecast_daily_frame.pack()
# starting from 1 because the 0th day is the current day
forecast_daily_frame['bg'] = '#CCCCCC'
i = 1
for frame in forecast_daily_frame.forecast_day_list:
frame.day_label.configure(text = DT.datetime.utcfromtimestamp(daily_weather[i]['dt'] + timezone_offset).strftime('%a'))
frame.icon_code = daily_weather[i]['weather'][0]['icon']
frame.weather_desc_label.configure(text = daily_weather[i]['weather'][0]['description'].capitalize())
frame.temp_max_label.configure(text = f"{daily_weather[i]['temp']['max']}°C")
frame.temp_min_label.configure(text = f"{daily_weather[i]['temp']['min']}°C")
frame.change_bg()
i += 1