Module app.models
View Source
import hashlib
from pathlib import Path
from typing import Dict, List, Optional
from urllib.parse import urlparse
import aiofiles
import aiohttp
from aiohttp.client_exceptions import ClientConnectionError, InvalidURL
from datafiles import datafile, field
from sanic import Sanic
from sanic.log import logger
from spongemock import spongemock
from . import settings, utils
from .types import Dimensions, Point
@datafile
class Text:
color: str = "white"
style: str = "upper"
anchor_x: float = 0.0
anchor_y: float = 0.0
angle: float = 0
scale_x: float = 1.0
scale_y: float = 0.2
def get_anchor(self, image_size: Dimensions) -> Point:
image_width, image_height = image_size
return int(image_width * self.anchor_x), int(image_height * self.anchor_y)
def get_size(self, image_size: Dimensions) -> Dimensions:
image_width, image_height = image_size
return int(image_width * self.scale_x), int(image_height * self.scale_y)
def stylize(self, text: str) -> str:
if self.style == "none":
return text
if self.style == "default":
return text.capitalize() if text.islower() else text
if self.style == "mock":
return spongemock.mock(text, diversity_bias=0.75, random_seed=0)
method = getattr(text, self.style or self.__class__.style, None)
if method:
return method()
logger.warning(f"Unsupported text style: {self.style}")
return text
@datafile("../templates/{self.key}/config.yml", defaults=True)
class Template:
key: str
name: str = ""
source: Optional[str] = None
text: List[Text] = field(
default_factory=lambda: [Text(), Text(anchor_x=0.0, anchor_y=0.8)]
)
styles: List[str] = field(default_factory=lambda: [settings.DEFAULT_STYLE])
sample: List[str] = field(default_factory=lambda: ["YOUR TEXT", "GOES HERE"])
def __str__(self):
return str(self.directory)
@property
def valid(self) -> bool:
if not settings.DEPLOYED:
styles = []
for path in self.directory.iterdir():
if path.stem not in {"config", settings.DEFAULT_STYLE}:
styles.append(path.stem)
styles.sort()
if styles != self.styles:
self.styles = styles
return not self.key.startswith("_") and self.image.suffix != ".img"
@property
def directory(self) -> Path:
return self.datafile.path.parent
@property
def image(self) -> Path:
return self.get_image()
def get_image(self, style: str = "") -> Path:
style = style or settings.DEFAULT_STYLE
self.directory.mkdir(exist_ok=True)
for path in self.directory.iterdir():
if path.stem == style:
return path
if style == settings.DEFAULT_STYLE:
logger.debug(f"No default background image for template: {self.key}")
return self.directory / f"{settings.DEFAULT_STYLE}.img"
else:
logger.warning(f"Style {style!r} not available for {self.key}")
return self.get_image()
def jsonify(self, app: Sanic) -> Dict:
return {
"name": self.name,
"key": self.key,
"styles": self.styles,
"blank": app.url_for(
f"images.blank_{settings.DEFAULT_EXT}",
template_key=self.key,
_external=True,
_scheme=settings.SCHEME,
),
"sample": self.build_sample_url(app),
"source": self.source,
"_self": self.build_self_url(app),
}
def build_self_url(self, app: Sanic) -> str:
return app.url_for(
"templates.detail",
key=self.key,
_external=True,
_scheme=settings.SCHEME,
)
def build_sample_url(
self,
app: Sanic,
view_name: str = f"images.text_{settings.DEFAULT_EXT}",
*,
external: bool = True,
) -> str:
kwargs = {
"template_key": self.key,
"text_paths": utils.text.encode(self.sample),
"_external": external,
}
if external:
kwargs["_scheme"] = settings.SCHEME
return app.url_for(view_name, **kwargs)
def build_custom_url(
self,
app: Sanic,
text_lines: List[str],
*,
extension: str = "",
background: str = "",
external: bool = False,
):
if extension in {"jpg", "png"}:
view_name = f"images.text_{extension}"
else:
view_name = f"images.text_{settings.DEFAULT_EXT}"
url = app.url_for(
view_name,
template_key="custom" if self.key == "_custom" else self.key,
text_paths=utils.text.encode(text_lines),
_external=True,
_scheme=settings.SCHEME,
)
if background:
url += "?background=" + background
return url
@classmethod
async def create(cls, url: str) -> "Template":
parts = urlparse(url)
if "memegen.link" in parts.netloc:
logger.debug(f"Handling builtin template: {url}")
key = parts.path.split(".")[0].split("/")[2]
return cls.objects.get(key)
key = "_custom-" + hashlib.sha1(url.encode()).hexdigest()
template = cls.objects.get_or_create(key, url)
if template.image.exists() and not settings.DEBUG:
logger.info(f"Found background {url} at {template.image}")
else:
logger.info(f"Saving background {url} to {template.image}")
async with aiohttp.ClientSession() as session:
try:
async with session.get(url) as response:
if response.status == 200:
template.directory.mkdir(exist_ok=True)
f = await aiofiles.open(template.image, mode="wb") # type: ignore
await f.write(await response.read())
await f.close()
else:
logger.error(f"{response.status} response from {url}")
except (InvalidURL, ClientConnectionError):
logger.error(f"Invalid response from {url}")
if template.image.exists():
try:
utils.images.load(template.image)
except OSError as e:
logger.error(e)
template.image.unlink()
return template
Classes
Template
class Template(
*args,
**kwargs
)
Template(key: str, name: str = '', source: Union[str, NoneType] = None, text: List[app.models.Text] =
View Source
class Template:
key: str
name: str = ""
source: Optional[str] = None
text: List[Text] = field(
default_factory=lambda: [Text(), Text(anchor_x=0.0, anchor_y=0.8)]
)
styles: List[str] = field(default_factory=lambda: [settings.DEFAULT_STYLE])
sample: List[str] = field(default_factory=lambda: ["YOUR TEXT", "GOES HERE"])
def __str__(self):
return str(self.directory)
@property
def valid(self) -> bool:
if not settings.DEPLOYED:
styles = []
for path in self.directory.iterdir():
if path.stem not in {"config", settings.DEFAULT_STYLE}:
styles.append(path.stem)
styles.sort()
if styles != self.styles:
self.styles = styles
return not self.key.startswith("_") and self.image.suffix != ".img"
@property
def directory(self) -> Path:
return self.datafile.path.parent
@property
def image(self) -> Path:
return self.get_image()
def get_image(self, style: str = "") -> Path:
style = style or settings.DEFAULT_STYLE
self.directory.mkdir(exist_ok=True)
for path in self.directory.iterdir():
if path.stem == style:
return path
if style == settings.DEFAULT_STYLE:
logger.debug(f"No default background image for template: {self.key}")
return self.directory / f"{settings.DEFAULT_STYLE}.img"
else:
logger.warning(f"Style {style!r} not available for {self.key}")
return self.get_image()
def jsonify(self, app: Sanic) -> Dict:
return {
"name": self.name,
"key": self.key,
"styles": self.styles,
"blank": app.url_for(
f"images.blank_{settings.DEFAULT_EXT}",
template_key=self.key,
_external=True,
_scheme=settings.SCHEME,
),
"sample": self.build_sample_url(app),
"source": self.source,
"_self": self.build_self_url(app),
}
def build_self_url(self, app: Sanic) -> str:
return app.url_for(
"templates.detail",
key=self.key,
_external=True,
_scheme=settings.SCHEME,
)
def build_sample_url(
self,
app: Sanic,
view_name: str = f"images.text_{settings.DEFAULT_EXT}",
*,
external: bool = True,
) -> str:
kwargs = {
"template_key": self.key,
"text_paths": utils.text.encode(self.sample),
"_external": external,
}
if external:
kwargs["_scheme"] = settings.SCHEME
return app.url_for(view_name, **kwargs)
def build_custom_url(
self,
app: Sanic,
text_lines: List[str],
*,
extension: str = "",
background: str = "",
external: bool = False,
):
if extension in {"jpg", "png"}:
view_name = f"images.text_{extension}"
else:
view_name = f"images.text_{settings.DEFAULT_EXT}"
url = app.url_for(
view_name,
template_key="custom" if self.key == "_custom" else self.key,
text_paths=utils.text.encode(text_lines),
_external=True,
_scheme=settings.SCHEME,
)
if background:
url += "?background=" + background
return url
@classmethod
async def create(cls, url: str) -> "Template":
parts = urlparse(url)
if "memegen.link" in parts.netloc:
logger.debug(f"Handling builtin template: {url}")
key = parts.path.split(".")[0].split("/")[2]
return cls.objects.get(key)
key = "_custom-" + hashlib.sha1(url.encode()).hexdigest()
template = cls.objects.get_or_create(key, url)
if template.image.exists() and not settings.DEBUG:
logger.info(f"Found background {url} at {template.image}")
else:
logger.info(f"Saving background {url} to {template.image}")
async with aiohttp.ClientSession() as session:
try:
async with session.get(url) as response:
if response.status == 200:
template.directory.mkdir(exist_ok=True)
f = await aiofiles.open(template.image, mode="wb") # type: ignore
await f.write(await response.read())
await f.close()
else:
logger.error(f"{response.status} response from {url}")
except (InvalidURL, ClientConnectionError):
logger.error(f"Invalid response from {url}")
if template.image.exists():
try:
utils.images.load(template.image)
except OSError as e:
logger.error(e)
template.image.unlink()
return template
Class variables
Meta
name
objects
source
Static methods
create
def create(
url: str
) -> 'Template'
View Source
@classmethod
async def create(cls, url: str) -> "Template":
parts = urlparse(url)
if "memegen.link" in parts.netloc:
logger.debug(f"Handling builtin template: {url}")
key = parts.path.split(".")[0].split("/")[2]
return cls.objects.get(key)
key = "_custom-" + hashlib.sha1(url.encode()).hexdigest()
template = cls.objects.get_or_create(key, url)
if template.image.exists() and not settings.DEBUG:
logger.info(f"Found background {url} at {template.image}")
else:
logger.info(f"Saving background {url} to {template.image}")
async with aiohttp.ClientSession() as session:
try:
async with session.get(url) as response:
if response.status == 200:
template.directory.mkdir(exist_ok=True)
f = await aiofiles.open(template.image, mode="wb") # type: ignore
await f.write(await response.read())
await f.close()
else:
logger.error(f"{response.status} response from {url}")
except (InvalidURL, ClientConnectionError):
logger.error(f"Invalid response from {url}")
if template.image.exists():
try:
utils.images.load(template.image)
except OSError as e:
logger.error(e)
template.image.unlink()
return template
Instance variables
directory
image
valid
Methods
build_custom_url
def build_custom_url(
self,
app: sanic.app.Sanic,
text_lines: List[str],
*,
extension: str = '',
background: str = '',
external: bool = False
)
View Source
def build_custom_url(
self,
app: Sanic,
text_lines: List[str],
*,
extension: str = "",
background: str = "",
external: bool = False,
):
if extension in {"jpg", "png"}:
view_name = f"images.text_{extension}"
else:
view_name = f"images.text_{settings.DEFAULT_EXT}"
url = app.url_for(
view_name,
template_key="custom" if self.key == "_custom" else self.key,
text_paths=utils.text.encode(text_lines),
_external=True,
_scheme=settings.SCHEME,
)
if background:
url += "?background=" + background
return url
build_sample_url
def build_sample_url(
self,
app: sanic.app.Sanic,
view_name: str = 'images.text_png',
*,
external: bool = True
) -> str
View Source
def build_sample_url(
self,
app: Sanic,
view_name: str = f"images.text_{settings.DEFAULT_EXT}",
*,
external: bool = True,
) -> str:
kwargs = {
"template_key": self.key,
"text_paths": utils.text.encode(self.sample),
"_external": external,
}
if external:
kwargs["_scheme"] = settings.SCHEME
return app.url_for(view_name, **kwargs)
build_self_url
def build_self_url(
self,
app: sanic.app.Sanic
) -> str
View Source
def build_self_url(self, app: Sanic) -> str:
return app.url_for(
"templates.detail",
key=self.key,
_external=True,
_scheme=settings.SCHEME,
)
get_image
def get_image(
self,
style: str = ''
) -> pathlib.Path
View Source
def get_image(self, style: str = "") -> Path:
style = style or settings.DEFAULT_STYLE
self.directory.mkdir(exist_ok=True)
for path in self.directory.iterdir():
if path.stem == style:
return path
if style == settings.DEFAULT_STYLE:
logger.debug(f"No default background image for template: {self.key}")
return self.directory / f"{settings.DEFAULT_STYLE}.img"
else:
logger.warning(f"Style {style!r} not available for {self.key}")
return self.get_image()
jsonify
def jsonify(
self,
app: sanic.app.Sanic
) -> Dict
View Source
def jsonify(self, app: Sanic) -> Dict:
return {
"name": self.name,
"key": self.key,
"styles": self.styles,
"blank": app.url_for(
f"images.blank_{settings.DEFAULT_EXT}",
template_key=self.key,
_external=True,
_scheme=settings.SCHEME,
),
"sample": self.build_sample_url(app),
"source": self.source,
"_self": self.build_self_url(app),
}
Text
class Text(
color: str = 'white',
style: str = 'upper',
anchor_x: float = 0.0,
anchor_y: float = 0.0,
angle: float = 0,
scale_x: float = 1.0,
scale_y: float = 0.2
)
Text(color: str = 'white', style: str = 'upper', anchor_x: float = 0.0, anchor_y: float = 0.0, angle: float = 0, scale_x: float = 1.0, scale_y: float = 0.2)
View Source
class Text:
color: str = "white"
style: str = "upper"
anchor_x: float = 0.0
anchor_y: float = 0.0
angle: float = 0
scale_x: float = 1.0
scale_y: float = 0.2
def get_anchor(self, image_size: Dimensions) -> Point:
image_width, image_height = image_size
return int(image_width * self.anchor_x), int(image_height * self.anchor_y)
def get_size(self, image_size: Dimensions) -> Dimensions:
image_width, image_height = image_size
return int(image_width * self.scale_x), int(image_height * self.scale_y)
def stylize(self, text: str) -> str:
if self.style == "none":
return text
if self.style == "default":
return text.capitalize() if text.islower() else text
if self.style == "mock":
return spongemock.mock(text, diversity_bias=0.75, random_seed=0)
method = getattr(text, self.style or self.__class__.style, None)
if method:
return method()
logger.warning(f"Unsupported text style: {self.style}")
return text
Class variables
anchor_x
anchor_y
angle
color
scale_x
scale_y
style
Methods
get_anchor
def get_anchor(
self,
image_size: Tuple[int, int]
) -> Tuple[int, int]
View Source
def get_anchor(self, image_size: Dimensions) -> Point:
image_width, image_height = image_size
return int(image_width * self.anchor_x), int(image_height * self.anchor_y)
get_size
def get_size(
self,
image_size: Tuple[int, int]
) -> Tuple[int, int]
View Source
def get_size(self, image_size: Dimensions) -> Dimensions:
image_width, image_height = image_size
return int(image_width * self.scale_x), int(image_height * self.scale_y)
stylize
def stylize(
self,
text: str
) -> str
View Source
def stylize(self, text: str) -> str:
if self.style == "none":
return text
if self.style == "default":
return text.capitalize() if text.islower() else text
if self.style == "mock":
return spongemock.mock(text, diversity_bias=0.75, random_seed=0)
method = getattr(text, self.style or self.__class__.style, None)
if method:
return method()
logger.warning(f"Unsupported text style: {self.style}")
return text