ucast/ucast/service/util.py
2022-07-05 20:34:58 +02:00

201 lines
5.7 KiB
Python

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)