Merge pull request #278 from aceinnolab/feature/#275
python 3.11 & code quality improvements
This commit is contained in:
		| @@ -1,2 +1,3 @@ | |||||||
| from .functions import * | from .functions import * | ||||||
| from .inkycal_exceptions import * | from .inkycal_exceptions import * | ||||||
|  | from .openweathermap_wrapper import OpenWeatherMap | ||||||
| @@ -6,8 +6,10 @@ Inkycal custom-functions for ease-of-use | |||||||
| Copyright by aceinnolab | Copyright by aceinnolab | ||||||
| """ | """ | ||||||
| import logging | import logging | ||||||
|  | import traceback | ||||||
|  |  | ||||||
| from PIL import Image, ImageDraw, ImageFont, ImageColor | from PIL import Image, ImageDraw, ImageFont, ImageColor | ||||||
| from urllib.request import urlopen | import requests | ||||||
| import os | import os | ||||||
| import time | import time | ||||||
|  |  | ||||||
| @@ -98,11 +100,13 @@ def auto_fontsize(font, max_height): | |||||||
|       Returns: |       Returns: | ||||||
|           A PIL font object with modified height. |           A PIL font object with modified height. | ||||||
|       """ |       """ | ||||||
|  |     text_bbox = font.getbbox("hg") | ||||||
|     fontsize = font.getsize('hg')[1] |     text_height = text_bbox[3] - text_bbox[1] | ||||||
|     while font.getsize('hg')[1] <= (max_height * 0.80): |     fontsize = text_height | ||||||
|  |     while text_height <= (max_height * 0.80): | ||||||
|         fontsize += 1 |         fontsize += 1 | ||||||
|         font = ImageFont.truetype(font.path, fontsize) |         font = ImageFont.truetype(font.path, fontsize) | ||||||
|  |         text_height = text_bbox[3] - text_bbox[1] | ||||||
|     return font |     return font | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -154,21 +158,34 @@ def write(image, xy, box_size, text, font=None, **kwargs): | |||||||
|     if autofit or (fill_width != 1.0) or (fill_height != 0.8): |     if autofit or (fill_width != 1.0) or (fill_height != 0.8): | ||||||
|         size = 8 |         size = 8 | ||||||
|         font = ImageFont.truetype(font.path, size) |         font = ImageFont.truetype(font.path, size) | ||||||
|         text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] |         text_bbox = font.getbbox(text) | ||||||
|  |         text_width = text_bbox[2] - text_bbox[0] | ||||||
|  |         text_bbox_height = font.getbbox("hg") | ||||||
|  |         text_height = text_bbox_height[3] - text_bbox_height[1] | ||||||
|  |  | ||||||
|         while (text_width < int(box_width * fill_width) and |         while (text_width < int(box_width * fill_width) and | ||||||
|                text_height < int(box_height * fill_height)): |                text_height < int(box_height * fill_height)): | ||||||
|             size += 1 |             size += 1 | ||||||
|             font = ImageFont.truetype(font.path, size) |             font = ImageFont.truetype(font.path, size) | ||||||
|             text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] |             text_bbox = font.getbbox(text) | ||||||
|  |             text_width = text_bbox[2] - text_bbox[0] | ||||||
|  |             text_bbox_height = font.getbbox("hg") | ||||||
|  |             text_height = text_bbox_height[3] - text_bbox_height[1] | ||||||
|  |  | ||||||
|     text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] |     text_bbox = font.getbbox(text) | ||||||
|  |     text_width = text_bbox[2] - text_bbox[0] | ||||||
|  |     text_bbox_height = font.getbbox("hg") | ||||||
|  |     text_height = text_bbox_height[3] - text_bbox_height[1] | ||||||
|  |  | ||||||
|     # Truncate text if text is too long so it can fit inside the box |     # Truncate text if text is too long so it can fit inside the box | ||||||
|     if (text_width, text_height) > (box_width, box_height): |     if (text_width, text_height) > (box_width, box_height): | ||||||
|         logs.debug(('truncating {}'.format(text))) |         logs.debug(('truncating {}'.format(text))) | ||||||
|         while (text_width, text_height) > (box_width, box_height): |         while (text_width, text_height) > (box_width, box_height): | ||||||
|             text = text[0:-1] |             text = text[0:-1] | ||||||
|             text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] |             text_bbox = font.getbbox(text) | ||||||
|  |             text_width = text_bbox[2] - text_bbox[0] | ||||||
|  |             text_bbox_height = font.getbbox("hg") | ||||||
|  |             text_height = text_bbox_height[3] - text_bbox_height[1] | ||||||
|         logs.debug(text) |         logs.debug(text) | ||||||
|  |  | ||||||
|     # Align text to desired position |     # Align text to desired position | ||||||
| @@ -215,14 +232,17 @@ def text_wrap(text, font=None, max_width=None): | |||||||
|       A list containing chunked strings of the full text. |       A list containing chunked strings of the full text. | ||||||
|     """ |     """ | ||||||
|     lines = [] |     lines = [] | ||||||
|     if font.getsize(text)[0] < max_width: |  | ||||||
|  |     text_width = font.getlength(text) | ||||||
|  |  | ||||||
|  |     if text_width < max_width: | ||||||
|         lines.append(text) |         lines.append(text) | ||||||
|     else: |     else: | ||||||
|         words = text.split(' ') |         words = text.split(' ') | ||||||
|         i = 0 |         i = 0 | ||||||
|         while i < len(words): |         while i < len(words): | ||||||
|             line = '' |             line = '' | ||||||
|             while i < len(words) and font.getsize(line + words[i])[0] <= max_width: |             while i < len(words) and font.getlength(line + words[i]) <= max_width: | ||||||
|                 line = line + words[i] + " " |                 line = line + words[i] + " " | ||||||
|                 i += 1 |                 i += 1 | ||||||
|             if not line: |             if not line: | ||||||
| @@ -249,9 +269,10 @@ def internet_available(): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         urlopen('https://google.com', timeout=5) |         requests.get('https://google.com', timeout=5) | ||||||
|         return True |         return True | ||||||
|     except: |     except: | ||||||
|  |         print(f"Network could not be reached: {traceback.print_exc()}") | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								inkycal/custom/openweathermap_wrapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								inkycal/custom/openweathermap_wrapper.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | import logging | ||||||
|  | from enum import Enum | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | class WEATHER_OPTIONS(Enum): | ||||||
|  |     CURRENT_WEATHER = "weather" | ||||||
|  |  | ||||||
|  | class FORECAST_INTERVAL(Enum): | ||||||
|  |     THREE_HOURS = "3h" | ||||||
|  |     FIVE_DAYS = "5d" | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OpenWeatherMap: | ||||||
|  |     def __init__(self, api_key:str, city_id:int, units:str) -> None: | ||||||
|  |         self.api_key = api_key | ||||||
|  |         self.city_id = city_id | ||||||
|  |         assert (units  in ["metric", "imperial"] ) | ||||||
|  |         self.units = units | ||||||
|  |         self._api_version = "2.5" | ||||||
|  |         self._base_url = f"https://api.openweathermap.org/data/{self._api_version}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def get_current_weather(self) -> dict: | ||||||
|  |         current_weather_url = f"{self._base_url}/weather?id={self.city_id}&appid={self.api_key}&units={self.units}" | ||||||
|  |         response = requests.get(current_weather_url) | ||||||
|  |         if not response.ok: | ||||||
|  |             raise AssertionError(f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}") | ||||||
|  |         data = json.loads(response.text) | ||||||
|  |         return data | ||||||
|  |  | ||||||
|  |     def get_weather_forecast(self) -> dict: | ||||||
|  |         forecast_url = f"{self._base_url}/forecast?id={self.city_id}&appid={self.api_key}&units={self.units}" | ||||||
|  |         response = requests.get(forecast_url) | ||||||
|  |         if not response.ok: | ||||||
|  |             raise AssertionError(f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}") | ||||||
|  |         data = json.loads(response.text)["list"] | ||||||
|  |         return data | ||||||
|  |  | ||||||
| @@ -98,7 +98,9 @@ class Agenda(inkycal_module): | |||||||
|  |  | ||||||
|         # Calculate the max number of lines that can fit on the image |         # Calculate the max number of lines that can fit on the image | ||||||
|         line_spacing = 1 |         line_spacing = 1 | ||||||
|         line_height = int(self.font.getsize('hg')[1]) + line_spacing |  | ||||||
|  |         text_bbox_height = self.font.getbbox("hg") | ||||||
|  |         line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|         line_width = im_width |         line_width = im_width | ||||||
|         max_lines = im_height // line_height |         max_lines = im_height // line_height | ||||||
|         logger.debug(f'max lines: {max_lines}') |         logger.debug(f'max lines: {max_lines}') | ||||||
| @@ -133,8 +135,8 @@ class Agenda(inkycal_module): | |||||||
|         # parser.show_events() |         # parser.show_events() | ||||||
|  |  | ||||||
|         # Set the width for date, time and event titles |         # Set the width for date, time and event titles | ||||||
|         date_width = int(max([self.font.getsize( |         date_width = int(max([self.font.getlength( | ||||||
|             dates['begin'].format(self.date_format, locale=self.language))[0] |             dates['begin'].format(self.date_format, locale=self.language)) | ||||||
|                               for dates in agenda_events]) * 1.2) |                               for dates in agenda_events]) * 1.2) | ||||||
|         logger.debug(f'date_width: {date_width}') |         logger.debug(f'date_width: {date_width}') | ||||||
|  |  | ||||||
| @@ -147,8 +149,9 @@ class Agenda(inkycal_module): | |||||||
|             logger.info('Managed to parse events from urls') |             logger.info('Managed to parse events from urls') | ||||||
|  |  | ||||||
|             # Find out how much space the event times take |             # Find out how much space the event times take | ||||||
|             time_width = int(max([self.font.getsize( |  | ||||||
|                 events['begin'].format(self.time_format, locale=self.language))[0] |             time_width = int(max([self.font.getlength( | ||||||
|  |                 events['begin'].format(self.time_format, locale=self.language)) | ||||||
|                                   for events in upcoming_events]) * 1.2) |                                   for events in upcoming_events]) * 1.2) | ||||||
|             logger.debug(f'time_width: {time_width}') |             logger.debug(f'time_width: {time_width}') | ||||||
|  |  | ||||||
|   | |||||||
| @@ -110,7 +110,8 @@ class Calendar(inkycal_module): | |||||||
|  |  | ||||||
|         # Allocate space for month-names, weekdays etc. |         # Allocate space for month-names, weekdays etc. | ||||||
|         month_name_height = int(im_height * 0.10) |         month_name_height = int(im_height * 0.10) | ||||||
|         weekdays_height = int(self.font.getsize('hg')[1] * 1.25) |         text_bbox_height = self.font.getbbox("hg") | ||||||
|  |         weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25) | ||||||
|         logger.debug(f"month_name_height: {month_name_height}") |         logger.debug(f"month_name_height: {month_name_height}") | ||||||
|         logger.debug(f"weekdays_height: {weekdays_height}") |         logger.debug(f"weekdays_height: {weekdays_height}") | ||||||
|  |  | ||||||
| @@ -182,15 +183,15 @@ class Calendar(inkycal_module): | |||||||
|         ] |         ] | ||||||
|         logger.debug(f'weekday names: {weekday_names}') |         logger.debug(f'weekday names: {weekday_names}') | ||||||
|  |  | ||||||
|         for idx, weekday in enumerate(weekday_pos): |         for index, weekday in enumerate(weekday_pos): | ||||||
|             write( |             write( | ||||||
|                 im_black, |                 im_black, | ||||||
|                 weekday, |                 weekday, | ||||||
|                 (icon_width, weekdays_height), |                 (icon_width, weekdays_height), | ||||||
|                 weekday_names[idx], |                 weekday_names[index], | ||||||
|                 font=self.font, |                 font=self.font, | ||||||
|                 autofit=True, |                 autofit=True, | ||||||
|                 fill_height=1.0, |                 fill_height=0.9, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Create a calendar template and flatten (remove nestings) |         # Create a calendar template and flatten (remove nestings) | ||||||
| @@ -207,6 +208,10 @@ class Calendar(inkycal_module): | |||||||
|         # remove zeros from calendar since they are not required |         # remove zeros from calendar since they are not required | ||||||
|         calendar_flat = [num for num in calendar_flat if num != 0] |         calendar_flat = [num for num in calendar_flat if num != 0] | ||||||
|  |  | ||||||
|  |         # ensure all numbers have the same size | ||||||
|  |         fontsize_numbers = int(min(icon_width, icon_height) * 0.5) | ||||||
|  |         number_font = ImageFont.truetype(self.font.path, fontsize_numbers) | ||||||
|  |  | ||||||
|         # Add the numbers on the correct positions |         # Add the numbers on the correct positions | ||||||
|         for number in calendar_flat: |         for number in calendar_flat: | ||||||
|             if number != int(now.day): |             if number != int(now.day): | ||||||
| @@ -215,9 +220,7 @@ class Calendar(inkycal_module): | |||||||
|                     grid[number], |                     grid[number], | ||||||
|                     (icon_width, icon_height), |                     (icon_width, icon_height), | ||||||
|                     str(number), |                     str(number), | ||||||
|                     font=self.num_font, |                     font=number_font, | ||||||
|                     fill_height=0.5, |  | ||||||
|                     fill_width=0.5, |  | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|         # Draw a red/black circle with the current day of month in white |         # Draw a red/black circle with the current day of month in white | ||||||
| @@ -262,10 +265,10 @@ class Calendar(inkycal_module): | |||||||
|             from inkycal.modules.ical_parser import iCalendar |             from inkycal.modules.ical_parser import iCalendar | ||||||
|  |  | ||||||
|             # find out how many lines can fit at max in the event section |             # find out how many lines can fit at max in the event section | ||||||
|             line_spacing = 0 |             line_spacing = 2 | ||||||
|             max_event_lines = events_height // ( |             text_bbox_height = self.font.getbbox("hg") | ||||||
|                 self.font.getsize('hg')[1] + line_spacing |             line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|             ) |             max_event_lines = events_height // (line_height + line_spacing) | ||||||
|  |  | ||||||
|             # generate list of coordinates for each line |             # generate list of coordinates for each line | ||||||
|             events_offset = im_height - events_height |             events_offset = im_height - events_height | ||||||
| @@ -329,31 +332,18 @@ class Calendar(inkycal_module): | |||||||
|                 # Find out how much space (width) the date format requires |                 # Find out how much space (width) the date format requires | ||||||
|                 lang = self.language |                 lang = self.language | ||||||
|  |  | ||||||
|                 date_width = int( |                 date_width = int(max(( | ||||||
|                     max( |                     self.font.getlength(events['begin'].format(self.date_format, locale=lang)) | ||||||
|                         ( |                     for events in upcoming_events))* 1.1 | ||||||
|                             self.font.getsize( |  | ||||||
|                                 events['begin'].format(self.date_format, locale=lang) |  | ||||||
|                             )[0] |  | ||||||
|                             for events in upcoming_events |  | ||||||
|                         ) |  | ||||||
|                     ) |  | ||||||
|                     * 1.1 |  | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|                 time_width = int( |                 time_width = int(max(( | ||||||
|                     max( |                     self.font.getlength(events['begin'].format(self.time_format, locale=lang)) | ||||||
|                         ( |                     for events in upcoming_events))* 1.1 | ||||||
|                             self.font.getsize( |  | ||||||
|                                 events['begin'].format(self.time_format, locale=lang) |  | ||||||
|                             )[0] |  | ||||||
|                             for events in upcoming_events |  | ||||||
|                         ) |  | ||||||
|                     ) |  | ||||||
|                     * 1.1 |  | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|                 line_height = self.font.getsize('hg')[1] + line_spacing |                 text_bbox_height = self.font.getbbox("hg") | ||||||
|  |                 line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|  |  | ||||||
|                 event_width_s = im_width - date_width - time_width |                 event_width_s = im_width - date_width - time_width | ||||||
|                 event_width_l = im_width - date_width |                 event_width_l = im_width - date_width | ||||||
| @@ -411,12 +401,13 @@ class Calendar(inkycal_module): | |||||||
|                             cursor += 1 |                             cursor += 1 | ||||||
|             else: |             else: | ||||||
|                 symbol = '- ' |                 symbol = '- ' | ||||||
|                 while self.font.getsize(symbol)[0] < im_width * 0.9: |  | ||||||
|  |                 while self.font.getlength(symbol) < im_width * 0.9: | ||||||
|                     symbol += ' -' |                     symbol += ' -' | ||||||
|                 write( |                 write( | ||||||
|                     im_black, |                     im_black, | ||||||
|                     event_lines[0], |                     event_lines[0], | ||||||
|                     (im_width, self.font.getsize(symbol)[1]), |                     (im_width, line_height), | ||||||
|                     symbol, |                     symbol, | ||||||
|                     font=self.font, |                     font=self.font, | ||||||
|                 ) |                 ) | ||||||
|   | |||||||
| @@ -91,9 +91,11 @@ class Feeds(inkycal_module): | |||||||
|  |  | ||||||
|         # Set some parameters for formatting feeds |         # Set some parameters for formatting feeds | ||||||
|         line_spacing = 1 |         line_spacing = 1 | ||||||
|         line_height = self.font.getsize('hg')[1] + line_spacing |  | ||||||
|         line_width = im_width |         line_width = im_width | ||||||
|         max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing)) |         text_bbox_height = self.font.getbbox("hg") | ||||||
|  |         line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|  |         max_lines = (im_height // (line_height + line_spacing)) | ||||||
|  |  | ||||||
|         # Calculate padding from top so the lines look centralised |         # Calculate padding from top so the lines look centralised | ||||||
|         spacing_top = int(im_height % line_height / 2) |         spacing_top = int(im_height % line_height / 2) | ||||||
|   | |||||||
| @@ -54,10 +54,11 @@ class Jokes(inkycal_module): | |||||||
|             raise NetworkNotReachableError |             raise NetworkNotReachableError | ||||||
|  |  | ||||||
|         # Set some parameters for formatting feeds |         # Set some parameters for formatting feeds | ||||||
|         line_spacing = 1 |         line_spacing = 5 | ||||||
|         line_height = self.font.getsize('hg')[1] + line_spacing |         text_bbox = self.font.getbbox("hg") | ||||||
|  |         line_height = text_bbox[3] - text_bbox[1] + line_spacing | ||||||
|         line_width = im_width |         line_width = im_width | ||||||
|         max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing)) |         max_lines = (im_height // (line_height + line_spacing)) | ||||||
|  |  | ||||||
|         logger.debug(f"max_lines: {max_lines}") |         logger.debug(f"max_lines: {max_lines}") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -96,9 +96,10 @@ class Stocks(inkycal_module): | |||||||
|  |  | ||||||
|         # Set some parameters for formatting feeds |         # Set some parameters for formatting feeds | ||||||
|         line_spacing = 1 |         line_spacing = 1 | ||||||
|         line_height = self.font.getsize('hg')[1] + line_spacing |         text_bbox_height = self.font.getbbox("hg") | ||||||
|  |         line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|         line_width = im_width |         line_width = im_width | ||||||
|         max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing)) |         max_lines = (im_height // line_height) | ||||||
|  |  | ||||||
|         logger.debug(f"max_lines: {max_lines}") |         logger.debug(f"max_lines: {max_lines}") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,11 +7,11 @@ If the content is too long, it will be truncated from the back until it fits | |||||||
|  |  | ||||||
| Copyright by aceinnolab | Copyright by aceinnolab | ||||||
| """ | """ | ||||||
| from inkycal.modules.template import inkycal_module |  | ||||||
| from inkycal.custom import * |  | ||||||
|  |  | ||||||
| from urllib.request import urlopen | from urllib.request import urlopen | ||||||
|  |  | ||||||
|  | from inkycal.custom import * | ||||||
|  | from inkycal.modules.template import inkycal_module | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -44,7 +44,6 @@ class TextToDisplay(inkycal_module): | |||||||
|  |  | ||||||
|         self.make_request = True if self.filepath.startswith("https://") else False |         self.make_request = True if self.filepath.startswith("https://") else False | ||||||
|  |  | ||||||
|  |  | ||||||
|         # give an OK message |         # give an OK message | ||||||
|         print(f'{__name__} loaded') |         print(f'{__name__} loaded') | ||||||
|  |  | ||||||
| @@ -73,10 +72,11 @@ class TextToDisplay(inkycal_module): | |||||||
|             raise NetworkNotReachableError |             raise NetworkNotReachableError | ||||||
|  |  | ||||||
|         # Set some parameters for formatting feeds |         # Set some parameters for formatting feeds | ||||||
|         line_spacing = 1 |         line_spacing = 4 | ||||||
|         line_height = self.font.getsize('hg')[1] + line_spacing |         text_bbox_height = self.font.getbbox("hg") | ||||||
|  |         line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|         line_width = im_width |         line_width = im_width | ||||||
|         max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing)) |         max_lines = im_height // line_height | ||||||
|  |  | ||||||
|         # Calculate padding from top so the lines look centralised |         # Calculate padding from top so the lines look centralised | ||||||
|         spacing_top = int(im_height % line_height / 2) |         spacing_top = int(im_height % line_height / 2) | ||||||
|   | |||||||
| @@ -86,9 +86,10 @@ class Todoist(inkycal_module): | |||||||
|  |  | ||||||
|         # Set some parameters for formatting todos |         # Set some parameters for formatting todos | ||||||
|         line_spacing = 1 |         line_spacing = 1 | ||||||
|         line_height = self.font.getsize('hg')[1] + line_spacing |         text_bbox_height = self.font.getbbox("hg") | ||||||
|  |         line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing | ||||||
|         line_width = im_width |         line_width = im_width | ||||||
|         max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing)) |         max_lines = im_height // line_height | ||||||
|  |  | ||||||
|         # Calculate padding from top so the lines look centralised |         # Calculate padding from top so the lines look centralised | ||||||
|         spacing_top = int(im_height % line_height / 2) |         spacing_top = int(im_height % line_height / 2) | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import math | |||||||
| import decimal | import decimal | ||||||
| import arrow | import arrow | ||||||
|  |  | ||||||
| from pyowm.owm import OWM | from inkycal.custom import OpenWeatherMap | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -95,7 +95,7 @@ class Weather(inkycal_module): | |||||||
|         self.use_beaufort = config['use_beaufort'] |         self.use_beaufort = config['use_beaufort'] | ||||||
|  |  | ||||||
|         # additional configuration |         # additional configuration | ||||||
|         self.owm = OWM(self.api_key).weather_manager() |         self.owm =  OpenWeatherMap(api_key=self.api_key, city_id=self.location, units=config['units']) | ||||||
|         self.timezone = get_system_tz() |         self.timezone = get_system_tz() | ||||||
|         self.locale = config['language'] |         self.locale = config['language'] | ||||||
|         self.weatherfont = ImageFont.truetype( |         self.weatherfont = ImageFont.truetype( | ||||||
| @@ -104,6 +104,42 @@ class Weather(inkycal_module): | |||||||
|         # give an OK message |         # give an OK message | ||||||
|         print(f"{__name__} loaded") |         print(f"{__name__} loaded") | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def mps_to_beaufort(meters_per_second:float) -> int: | ||||||
|  |         """Map meters per second to the beaufort scale. | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             meters_per_second: | ||||||
|  |                 float representing meters per seconds | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             an integer of the beaufort scale mapping the input | ||||||
|  |         """ | ||||||
|  |         thresholds = [0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.7, 24.5, 28.4] | ||||||
|  |         return next((i for i, threshold in enumerate(thresholds) if meters_per_second < threshold), 11) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def mps_to_mph(meters_per_second:float) -> float: | ||||||
|  |         """Map meters per second to miles per hour, rounded to one decimal place. | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             meters_per_second: | ||||||
|  |                 float representing meters per seconds. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             float representing the input value in miles per hour. | ||||||
|  |         """ | ||||||
|  |         # 1 m/s is approximately equal to 2.23694 mph | ||||||
|  |         miles_per_hour = meters_per_second * 2.23694 | ||||||
|  |         return round(miles_per_hour, 1) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def celsius_to_fahrenheit(celsius:int or float): | ||||||
|  |         """Converts the given temperate from degrees Celsius to Fahrenheit.""" | ||||||
|  |         fahrenheit = (celsius * 9 / 5) + 32 | ||||||
|  |         return fahrenheit | ||||||
|  |  | ||||||
|     def generate_image(self): |     def generate_image(self): | ||||||
|         """Generate image for this module""" |         """Generate image for this module""" | ||||||
|  |  | ||||||
| @@ -124,7 +160,11 @@ class Weather(inkycal_module): | |||||||
|             raise NetworkNotReachableError |             raise NetworkNotReachableError | ||||||
|  |  | ||||||
|         def get_moon_phase(): |         def get_moon_phase(): | ||||||
|             """Calculate the current (approximate) moon phase""" |             """Calculate the current (approximate) moon phase | ||||||
|  |  | ||||||
|  |             Returns: | ||||||
|  |                 The corresponding moonphase-icon. | ||||||
|  |             """ | ||||||
|  |  | ||||||
|             dec = decimal.Decimal |             dec = decimal.Decimal | ||||||
|             diff = now - arrow.get(2001, 1, 1) |             diff = now - arrow.get(2001, 1, 1) | ||||||
| @@ -154,7 +194,7 @@ class Weather(inkycal_module): | |||||||
|             return answer |             return answer | ||||||
|  |  | ||||||
|         # Lookup-table for weather icons and weather codes |         # Lookup-table for weather icons and weather codes | ||||||
|         weathericons = { |         weather_icons = { | ||||||
|             '01d': '\uf00d', |             '01d': '\uf00d', | ||||||
|             '02d': '\uf002', |             '02d': '\uf002', | ||||||
|             '03d': '\uf013', |             '03d': '\uf013', | ||||||
| @@ -227,26 +267,26 @@ class Weather(inkycal_module): | |||||||
|             # Increase fontsize to fit specified height and width of text box |             # Increase fontsize to fit specified height and width of text box | ||||||
|             size = 8 |             size = 8 | ||||||
|             font = ImageFont.truetype(font.path, size) |             font = ImageFont.truetype(font.path, size) | ||||||
|             text_width, text_height = font.getsize(text) |             text_width, text_height = font.getbbox(text)[2:] | ||||||
|  |  | ||||||
|             while (text_width < int(box_width * 0.9) and |             while (text_width < int(box_width * 0.9) and | ||||||
|                    text_height < int(box_height * 0.9)): |                    text_height < int(box_height * 0.9)): | ||||||
|                 size += 1 |                 size += 1 | ||||||
|                 font = ImageFont.truetype(font.path, size) |                 font = ImageFont.truetype(font.path, size) | ||||||
|                 text_width, text_height = font.getsize(text) |                 text_width, text_height = font.getbbox(text)[2:] | ||||||
|  |  | ||||||
|             text_width, text_height = font.getsize(text) |             text_width, text_height = font.getbbox(text)[2:] | ||||||
|  |  | ||||||
|             # Align text to desired position |             # Align text to desired position | ||||||
|             x = int((box_width / 2) - (text_width / 2)) |             x = int((box_width / 2) - (text_width / 2)) | ||||||
|             y = int((box_height / 2) - (text_height / 2) - (icon_size_correction[icon] * size) / 2) |             y = int((box_height / 2) - (text_height / 2)) | ||||||
|  |  | ||||||
|             # Draw the text in the text-box |             # Draw the text in the text-box | ||||||
|             draw = ImageDraw.Draw(image) |             draw = ImageDraw.Draw(image) | ||||||
|             space = Image.new('RGBA', (box_width, box_height)) |             space = Image.new('RGBA', (box_width, box_height)) | ||||||
|             ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) |             ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) | ||||||
|  |  | ||||||
|             if rotation != None: |             if rotation: | ||||||
|                 space.rotate(rotation, expand=True) |                 space.rotate(rotation, expand=True) | ||||||
|  |  | ||||||
|             # Update only region with text (add text with transparent background) |             # Update only region with text (add text with transparent background) | ||||||
| @@ -350,14 +390,9 @@ class Weather(inkycal_module): | |||||||
|         temp_fc4 = (col7, row3) |         temp_fc4 = (col7, row3) | ||||||
|  |  | ||||||
|         # Create current-weather and weather-forecast objects |         # Create current-weather and weather-forecast objects | ||||||
|         if self.location.isdigit(): |         logging.debug('looking up location by ID') | ||||||
|             logging.debug('looking up location by ID') |         weather = self.owm.get_current_weather() | ||||||
|             weather = self.owm.weather_at_id(int(self.location)).weather |         forecast = self.owm.get_weather_forecast() | ||||||
|             forecast = self.owm.forecast_at_id(int(self.location), '3h') |  | ||||||
|         else: |  | ||||||
|             logging.debug('looking up location by string') |  | ||||||
|             weather = self.owm.weather_at_place(self.location).weather |  | ||||||
|             forecast = self.owm.forecast_at_place(self.location, '3h') |  | ||||||
|  |  | ||||||
|         # Set decimals |         # Set decimals | ||||||
|         dec_temp = None if self.round_temperature == True else 1 |         dec_temp = None if self.round_temperature == True else 1 | ||||||
| @@ -369,12 +404,14 @@ class Weather(inkycal_module): | |||||||
|         elif self.units == 'imperial': |         elif self.units == 'imperial': | ||||||
|             temp_unit = 'fahrenheit' |             temp_unit = 'fahrenheit' | ||||||
|  |  | ||||||
|         logging.debug(f'temperature unit: {temp_unit}') |         logging.debug(f'temperature unit: {self.units}') | ||||||
|         logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}') |         logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}') | ||||||
|  |  | ||||||
|         # Get current time |         # Get current time | ||||||
|         now = arrow.utcnow() |         now = arrow.utcnow() | ||||||
|  |  | ||||||
|  |         fc_data = {} | ||||||
|  |  | ||||||
|         if self.forecast_interval == 'hourly': |         if self.forecast_interval == 'hourly': | ||||||
|  |  | ||||||
|             logger.debug("getting hourly forecasts") |             logger.debug("getting hourly forecasts") | ||||||
| @@ -386,21 +423,22 @@ class Weather(inkycal_module): | |||||||
|             else: |             else: | ||||||
|                 hour_gap = 3 |                 hour_gap = 3 | ||||||
|  |  | ||||||
|             # Create timings for hourly forcasts |             # Create timings for hourly forecasts | ||||||
|             forecast_timings = [now.shift(hours=+ hour_gap + _).floor('hour') |             forecast_timings = [now.shift(hours=+ hour_gap + _).floor('hour') | ||||||
|                                 for _ in range(0, 12, 3)] |                                 for _ in range(0, 12, 3)] | ||||||
|  |  | ||||||
|             # Create forecast objects for given timings |             # Create forecast objects for given timings | ||||||
|             forecasts = [forecast.get_weather_at(forecast_time.datetime) for |             forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in forecast_timings] | ||||||
|                          forecast_time in forecast_timings] |  | ||||||
|  |  | ||||||
|             # Add forecast-data to fc_data dictionary |             # Add forecast-data to fc_data dictionary | ||||||
|             fc_data = {} |             fc_data = {} | ||||||
|             for forecast in forecasts: |             for forecast in forecasts: | ||||||
|                 temp = '{}°'.format(round( |                 if self.units == "metric": | ||||||
|                     forecast.temperature(unit=temp_unit)['temp'], ndigits=dec_temp)) |                     temp = f"{round(weather['main']['temp'], ndigits=dec_temp)}°C" | ||||||
|  |                 else: | ||||||
|  |                     temp = f"{round(self.celsius_to_fahrenheit(weather['weather']['main']['temp']), ndigits=dec_temp)}°F" | ||||||
|  |  | ||||||
|                 icon = forecast.weather_icon_name |                 icon = forecast["weather"][0]["icon"] | ||||||
|                 fc_data['fc' + str(forecasts.index(forecast) + 1)] = { |                 fc_data['fc' + str(forecasts.index(forecast) + 1)] = { | ||||||
|                     'temp': temp, |                     'temp': temp, | ||||||
|                     'icon': icon, |                     'icon': icon, | ||||||
| @@ -412,38 +450,35 @@ class Weather(inkycal_module): | |||||||
|  |  | ||||||
|             logger.debug("getting daily forecasts") |             logger.debug("getting daily forecasts") | ||||||
|  |  | ||||||
|             def calculate_forecast(days_from_today): |             def calculate_forecast(days_from_today) -> dict: | ||||||
|                 """Get temperature range and most frequent icon code for forecast |                 """Get temperature range and most frequent icon code for forecast | ||||||
|                 days_from_today should be int from 1-4: e.g. 2 -> 2 days from today |                 days_from_today should be int from 1-4: e.g. 2 -> 2 days from today | ||||||
|                 """ |                 """ | ||||||
|  |  | ||||||
|                 # Create a list containing time-objects for every 3rd hour of the day |                 # Create a list containing time-objects for every 3rd hour of the day | ||||||
|                 time_range = list(arrow.Arrow.range('hour', |                 time_range = list( | ||||||
|                                                     now.shift(days=days_from_today).floor('day'), |                     arrow.Arrow.range('hour', | ||||||
|                                                     now.shift(days=days_from_today).ceil('day') |                     now.shift(days=days_from_today).floor('day'),now.shift(days=days_from_today).ceil('day') | ||||||
|                                                     ))[::3] |                     ))[::3] | ||||||
|  |  | ||||||
|                 # Get forecasts for each time-object |                 # Get forecasts for each time-object | ||||||
|                 forecasts = [forecast.get_weather_at(_.datetime) for _ in time_range] |                 forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in time_range] | ||||||
|  |  | ||||||
|                 # Get all temperatures for this day |                 # Get all temperatures for this day | ||||||
|                 daily_temp = [round(_.temperature(unit=temp_unit)['temp'], |                 daily_temp = [round(_["main"]["temp"]) for _ in forecasts] | ||||||
|                                     ndigits=dec_temp) for _ in forecasts] |  | ||||||
|                 # Calculate min. and max. temp for this day |                 # Calculate min. and max. temp for this day | ||||||
|                 temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°' |                 temp_range = f'{min(daily_temp)}°/{max(daily_temp)}°' | ||||||
|  |  | ||||||
|                 # Get all weather icon codes for this day |                 # Get all weather icon codes for this day | ||||||
|                 daily_icons = [_.weather_icon_name for _ in forecasts] |                 daily_icons = [_["weather"][0]["icon"] for _ in forecasts] | ||||||
|                 # Find most common element from all weather icon codes |                 # Find most common element from all weather icon codes | ||||||
|                 status = max(set(daily_icons), key=daily_icons.count) |                 status = max(set(daily_icons), key=daily_icons.count) | ||||||
|  |  | ||||||
|                 weekday = now.shift(days=days_from_today).format('ddd', locale= |                 weekday = now.shift(days=days_from_today).format('ddd', locale=self.locale) | ||||||
|                 self.locale) |  | ||||||
|                 return {'temp': temp_range, 'icon': status, 'stamp': weekday} |                 return {'temp': temp_range, 'icon': status, 'stamp': weekday} | ||||||
|  |  | ||||||
|             forecasts = [calculate_forecast(days) for days in range(1, 5)] |             forecasts = [calculate_forecast(days) for days in range(1, 5)] | ||||||
|  |  | ||||||
|             fc_data = {} |  | ||||||
|             for forecast in forecasts: |             for forecast in forecasts: | ||||||
|                 fc_data['fc' + str(forecasts.index(forecast) + 1)] = { |                 fc_data['fc' + str(forecasts.index(forecast) + 1)] = { | ||||||
|                     'temp': forecast['temp'], |                     'temp': forecast['temp'], | ||||||
| @@ -455,13 +490,15 @@ class Weather(inkycal_module): | |||||||
|             logger.debug((key, val)) |             logger.debug((key, val)) | ||||||
|  |  | ||||||
|         # Get some current weather details |         # Get some current weather details | ||||||
|         temperature = '{}°'.format(round( |         if dec_temp != 0: | ||||||
|             weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp)) |             temperature = f"{round(weather['main']['temp'])}°" | ||||||
|  |         else: | ||||||
|  |             temperature = f"{round(weather['main']['temp'],ndigits=dec_temp)}°" | ||||||
|  |  | ||||||
|         weather_icon = weather.weather_icon_name |         weather_icon = weather["weather"][0]["icon"] | ||||||
|         humidity = str(weather.humidity) |         humidity = str(weather["main"]["humidity"]) | ||||||
|         sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone) |         sunrise_raw = arrow.get(weather["sys"]["sunrise"]).to(self.timezone) | ||||||
|         sunset_raw = arrow.get(weather.sunset_time()).to(self.timezone) |         sunset_raw = arrow.get(weather["sys"]["sunset"]).to(self.timezone) | ||||||
|  |  | ||||||
|         logger.debug(f'weather_icon: {weather_icon}') |         logger.debug(f'weather_icon: {weather_icon}') | ||||||
|  |  | ||||||
| @@ -469,33 +506,29 @@ class Weather(inkycal_module): | |||||||
|             logger.debug('using 12 hour format for sunrise/sunset') |             logger.debug('using 12 hour format for sunrise/sunset') | ||||||
|             sunrise = sunrise_raw.format('h:mm a') |             sunrise = sunrise_raw.format('h:mm a') | ||||||
|             sunset = sunset_raw.format('h:mm a') |             sunset = sunset_raw.format('h:mm a') | ||||||
|  |         else: | ||||||
|         elif self.hour_format == 24: |             # 24 hours format | ||||||
|             logger.debug('using 24 hour format for sunrise/sunset') |             logger.debug('using 24 hour format for sunrise/sunset') | ||||||
|             sunrise = sunrise_raw.format('H:mm') |             sunrise = sunrise_raw.format('H:mm') | ||||||
|             sunset = sunset_raw.format('H:mm') |             sunset = sunset_raw.format('H:mm') | ||||||
|  |  | ||||||
|         # Format the windspeed to user preference |         # Format the wind-speed to user preference | ||||||
|         if self.use_beaufort: |         if self.use_beaufort: | ||||||
|             logger.debug("using beaufort for wind") |             logger.debug("using beaufort for wind") | ||||||
|             wind = str(weather.wind(unit='beaufort')['speed']) |             wind = str(self.mps_to_beaufort(weather["wind"]["speed"])) | ||||||
|  |  | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|             if self.units == 'metric': |             if self.units == 'metric': | ||||||
|                 logging.debug('getting windspeed in metric unit') |                 logging.debug('getting wind speed in meters per second') | ||||||
|                 wind = str(weather.wind(unit='meters_sec')['speed']) + 'm/s' |                 wind = f"{weather['wind']['speed']} m/s" | ||||||
|  |             else: | ||||||
|  |                 logging.debug('getting wind speed in imperial unit') | ||||||
|  |                 wind = f"{self.mps_to_mph(weather['wind']['speed'])} miles/h" | ||||||
|  |  | ||||||
|             elif self.units == 'imperial': |         moon_phase = get_moon_phase() | ||||||
|                 logging.debug('getting windspeed in imperial unit') |  | ||||||
|                 wind = str(weather.wind(unit='miles_hour')['speed']) + 'miles/h' |  | ||||||
|  |  | ||||||
|         dec = decimal.Decimal |  | ||||||
|         moonphase = get_moon_phase() |  | ||||||
|  |  | ||||||
|         # Fill weather details in col 1 (current weather icon) |         # Fill weather details in col 1 (current weather icon) | ||||||
|         draw_icon(im_colour, weather_icon_pos, (col_width, im_height), |         draw_icon(im_colour, weather_icon_pos, (col_width, im_height), | ||||||
|                   weathericons[weather_icon]) |                   weather_icons[weather_icon]) | ||||||
|  |  | ||||||
|         # Fill weather details in col 2 (temp, humidity, wind) |         # Fill weather details in col 2 (temp, humidity, wind) | ||||||
|         draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height), |         draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height), | ||||||
| @@ -521,7 +554,7 @@ class Weather(inkycal_module): | |||||||
|               wind, font=self.font) |               wind, font=self.font) | ||||||
|  |  | ||||||
|         # Fill weather details in col 3 (moonphase, sunrise, sunset) |         # Fill weather details in col 3 (moonphase, sunrise, sunset) | ||||||
|         draw_icon(im_colour, moonphase_pos, (col_width, row_height), moonphase) |         draw_icon(im_colour, moonphase_pos, (col_width, row_height), moon_phase) | ||||||
|  |  | ||||||
|         draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051') |         draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051') | ||||||
|         write(im_black, sunrise_time_pos, (col_width - icon_small, row_height), |         write(im_black, sunrise_time_pos, (col_width - icon_small, row_height), | ||||||
| @@ -535,7 +568,7 @@ class Weather(inkycal_module): | |||||||
|         for pos in range(1, len(fc_data) + 1): |         for pos in range(1, len(fc_data) + 1): | ||||||
|             stamp = fc_data[f'fc{pos}']['stamp'] |             stamp = fc_data[f'fc{pos}']['stamp'] | ||||||
|  |  | ||||||
|             icon = weathericons[fc_data[f'fc{pos}']['icon']] |             icon = weather_icons[fc_data[f'fc{pos}']['icon']] | ||||||
|             temp = fc_data[f'fc{pos}']['temp'] |             temp = fc_data[f'fc{pos}']['temp'] | ||||||
|  |  | ||||||
|             write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height), |             write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height), | ||||||
| @@ -548,7 +581,7 @@ class Weather(inkycal_module): | |||||||
|         border_h = row3 + row_height |         border_h = row3 + row_height | ||||||
|         border_w = col_width - 3  # leave 3 pixels gap |         border_w = col_width - 3  # leave 3 pixels gap | ||||||
|  |  | ||||||
|         # Add borders around each sub-section |         # Add borders around each subsection | ||||||
|         draw_border(im_black, (col1, row1), (col_width * 3 - 3, border_h), |         draw_border(im_black, (col1, row1), (col_width * 3 - 3, border_h), | ||||||
|                     shrinkage=(0, 0)) |                     shrinkage=(0, 0)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| from config import Config | from .config import Config | ||||||
|   | |||||||
| @@ -57,8 +57,6 @@ class module_test(unittest.TestCase): | |||||||
|             print('OK') |             print('OK') | ||||||
|             if Config.USE_PREVIEW: |             if Config.USE_PREVIEW: | ||||||
|                 preview(merge(im_black, im_colour)) |                 preview(merge(im_black, im_colour)) | ||||||
|             im = merge(im_black, im_colour) |  | ||||||
|             im.show() |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
| @@ -112,8 +112,6 @@ class TestTextToDisplay(unittest.TestCase): | |||||||
|             print('OK') |             print('OK') | ||||||
|             if Config.USE_PREVIEW: |             if Config.USE_PREVIEW: | ||||||
|                 preview(merge(im_black, im_colour)) |                 preview(merge(im_black, im_colour)) | ||||||
|             im = merge(im_black, im_colour) |  | ||||||
|             im.show() |  | ||||||
|  |  | ||||||
|         if delete_file_after_parse: |         if delete_file_after_parse: | ||||||
|             print("cleaning up temp file") |             print("cleaning up temp file") | ||||||
|   | |||||||
| @@ -46,7 +46,6 @@ class module_test(unittest.TestCase): | |||||||
|                 print('OK') |                 print('OK') | ||||||
|                 if Config.USE_PREVIEW: |                 if Config.USE_PREVIEW: | ||||||
|                     preview(merge(im_black, im_colour)) |                     preview(merge(im_black, im_colour)) | ||||||
|                 merge(im_black, im_colour).show() |  | ||||||
|         else: |         else: | ||||||
|             print('No api key given, omitting test') |             print('No api key given, omitting test') | ||||||
| 
 | 
 | ||||||
| @@ -13,7 +13,7 @@ preview = Inkyimage.preview | |||||||
| merge = Inkyimage.merge | merge = Inkyimage.merge | ||||||
| 
 | 
 | ||||||
| owm_api_key = Config.OPENWEATHERMAP_API_KEY | owm_api_key = Config.OPENWEATHERMAP_API_KEY | ||||||
| location = 'Stuttgart, DE' | location = '2825297' | ||||||
| 
 | 
 | ||||||
| tests = [ | tests = [ | ||||||
|     { |     { | ||||||
| @@ -184,7 +184,8 @@ class module_test(unittest.TestCase): | |||||||
|             im_black, im_colour = module.generate_image() |             im_black, im_colour = module.generate_image() | ||||||
|             print('OK') |             print('OK') | ||||||
|             if Config.USE_PREVIEW: |             if Config.USE_PREVIEW: | ||||||
|                 preview(merge(im_black, im_colour)) |                 merged = merge(im_black, im_colour) | ||||||
|  |                 preview(merged) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -1,30 +1,25 @@ | |||||||
| arrow==1.2.3 | arrow==1.3.0 | ||||||
| certifi==2023.7.22 | certifi==2023.7.22 | ||||||
| cycler==0.11.0 | cycler==0.12.1 | ||||||
| feedparser==6.0.10 | feedparser==6.0.10 | ||||||
| fonttools==4.40.0 | fonttools==4.44.0 | ||||||
| geojson==2.3.0 | icalendar==5.0.11 | ||||||
| icalendar==5.0.7 | kiwisolver==1.4.5 | ||||||
| kiwisolver==1.4.4 | lxml==4.9.3 | ||||||
| lxml==4.9.2 | matplotlib==3.8.1 | ||||||
| matplotlib==3.7.1 | numpy==1.26.1 | ||||||
| multitasking==0.0.11 | packaging==23.2 | ||||||
| numpy==1.25.0 | Pillow==10.1.0 | ||||||
| packaging==23.1 | pyparsing==3.1.1 | ||||||
| pandas==2.0.2 |  | ||||||
| Pillow==9.5.0 |  | ||||||
| pyowm==3.3.0 |  | ||||||
| pyparsing==3.1.0 |  | ||||||
| PySocks==1.7.1 | PySocks==1.7.1 | ||||||
| python-dateutil==2.8.2 | python-dateutil==2.8.2 | ||||||
| pytz==2023.3 | pytz==2023.3.post1 | ||||||
| recurring-ical-events==2.0.2 | recurring-ical-events==2.1.0 | ||||||
| requests==2.31.0 | requests==2.31.0 | ||||||
| sgmllib3k==1.0.0 | sgmllib3k==1.0.0 | ||||||
| six==1.16.0 | six==1.16.0 | ||||||
| todoist-api-python==2.0.2 | todoist-api-python==2.1.3 | ||||||
| typing_extensions==4.6.3 | typing_extensions==4.8.0 | ||||||
| urllib3==2.0.7 | urllib3==2.0.7 | ||||||
| yfinance==0.2.21 |  | ||||||
| python-dotenv==1.0.0 | python-dotenv==1.0.0 | ||||||
| setuptools==68.0.0 | setuptools==68.2.2 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user