import datetime import io import json import os import re from pathlib import Path from typing import Any, Optional, Tuple, Union from urllib import parse import requests import slugify from django.utils import timezone from PIL import Image EMOJI_PATTERN = re.compile( "[" "\U0001F1E0-\U0001F1FF" # flags (iOS) "\U0001F300-\U0001F5FF" # symbols & pictographs "\U0001F600-\U0001F64F" # emoticons "\U0001F680-\U0001F6FF" # transport & map symbols "\U0001F700-\U0001F77F" # alchemical symbols "\U0001F780-\U0001F7FF" # Geometric Shapes Extended "\U0001F800-\U0001F8FF" # Supplemental Arrows-C "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs "\U0001FA00-\U0001FA6F" # Chess Symbols "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A "\U00002702-\U000027B0" # Dingbats "\U000024C2-\U0001F251" "]+" ) def download_file(url: str, download_path: Path): r = requests.get(url, allow_redirects=True) r.raise_for_status() open(download_path, "wb").write(r.content) def resize_image(img: Image, resize: Tuple[int, int]): if img.size == resize: return img w_ratio = resize[0] / img.width h_ratio = resize[1] / img.height box = None # Too tall if h_ratio < w_ratio: crop_height = int(img.width / resize[0] * resize[1]) border = int((img.height - crop_height) / 2) box = (0, border, img.width, img.height - border) # Too wide elif w_ratio < h_ratio: crop_width = int(img.height / resize[1] * resize[0]) border = int((img.width - crop_width) / 2) box = (border, 0, img.width - border, img.height) return img.resize(resize, Image.Resampling.LANCZOS, box) def download_image_file( url: str, download_path: Path, resize: Optional[Tuple[int, int]] = None ): """ Download an image and convert it to the type given by the path. :param url: Image URL :param download_path: Download path :param resize: target image size (set to None for no resizing) """ r = requests.get(url, allow_redirects=True) r.raise_for_status() img = Image.open(io.BytesIO(r.content)) img_ext = img.format.lower() if img_ext == "jpeg": img_ext = "jpg" if resize: img = resize_image(img, resize) if "." + img_ext == download_path.suffix: open(download_path, "wb").write(r.content) else: img.save(download_path) def get_slug(text: str) -> str: return slugify.slugify(text, lowercase=False, separator="_") def to_localtime(time: datetime.datetime): """Converts naive datetime to localtime based on settings""" utc_time = time.replace(tzinfo=datetime.timezone.utc) to_zone = timezone.get_default_timezone() return utc_time.astimezone(to_zone) def _get_np_attrs(o) -> dict: """ Return all non-protected attributes of the given object. :param o: Object :return: Dict of attributes """ return {k: v for k, v in o.__dict__.items() if not k.startswith("_")} def serializer(o: Any) -> Union[str, dict, int, float, bool]: """ Serialize object to json-storable format :param o: Object to serialize :return: Serialized output data """ if hasattr(o, "serialize"): return o.serialize() if isinstance(o, (datetime.datetime, datetime.date)): return o.isoformat() if isinstance(o, (bool, float, int)): return o if hasattr(o, "__dict__"): return _get_np_attrs(o) return str(o) def to_json(o, pretty=False) -> str: """ Convert object to json. Uses the ``serialize()`` method of the target object if available. :param o: Object to serialize :param pretty: Prettify with indents :return: JSON string """ return json.dumps( o, default=serializer, indent=2 if pretty else None, ensure_ascii=False ) def _urlencode(query, safe="", encoding=None, errors=None, quote_via=parse.quote_plus): """ Same as the urllib.parse.urlencode function, but does not add an equals sign to no-value flags. """ if hasattr(query, "items"): query = query.items() else: # It's a bother at times that strings and string-like objects are # sequences. try: # non-sequence items should not work with len() # non-empty strings will fail this if len(query) and not isinstance(query[0], tuple): raise TypeError # Zero-length sequences of all types will get here and succeed, # but that's a minor nit. Since the original implementation # allowed empty dicts that type of behavior probably should be # preserved for consistency except TypeError: raise TypeError("not a valid non-string sequence " "or mapping object") lst = [] for k, v in query: if isinstance(k, bytes): k = quote_via(k, safe) else: k = quote_via(str(k), safe, encoding, errors) if isinstance(v, bytes): v = quote_via(v, safe) else: v = quote_via(str(v), safe, encoding, errors) if v: lst.append(k + "=" + v) else: lst.append(k) return "&".join(lst) def add_key_to_url(url: str, key: str): if not key: return url url_parts = list(parse.urlparse(url)) query = dict(parse.parse_qsl(url_parts[4], keep_blank_values=True)) query["key"] = key url_parts[4] = _urlencode(query) return parse.urlunparse(url_parts) def remove_if_exists(file: Path): if os.path.isfile(file): os.remove(file) def strip_emoji(str_in: str) -> str: stripped = EMOJI_PATTERN.sub("", str_in) return re.sub(" +", " ", stripped)