201 lines
5.7 KiB
Python
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)
|