Mostly works! Image loading and caching seems slow
This commit is contained in:
parent
de565fce4f
commit
b2ed4cc4e5
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,3 +8,5 @@ wheels/
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
example/
|
||||
|
4
config.toml
Normal file
4
config.toml
Normal file
@ -0,0 +1,4 @@
|
||||
[paths]
|
||||
content_dir = "/home/dubey/projects/foldsite/example/content"
|
||||
templates_dir = "/home/dubey/projects/foldsite/example/templates"
|
||||
styles_dir = "/home/dubey/projects/foldsite/example/styles"
|
26
main.py
26
main.py
@ -1,13 +1,31 @@
|
||||
from flask import Flask
|
||||
from src.server.server import Server
|
||||
from src.routes.routes import default_route
|
||||
from src.routes.routes import RouteManager
|
||||
from src.config.args import create_parser
|
||||
from src.config.config import Configuration
|
||||
from src.rendering.helpers import TemplateHelpers
|
||||
|
||||
|
||||
def main():
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
c = Configuration(args.config)
|
||||
c.load_config()
|
||||
|
||||
r = RouteManager(c)
|
||||
t = TemplateHelpers(c)
|
||||
|
||||
server = Server()
|
||||
|
||||
server.register_route("/", default_route, defaults={"path": ""})
|
||||
server.register_route("/<path:path>", default_route)
|
||||
server.register_template_function("get_sibling_content_files", t.get_sibling_content_files)
|
||||
server.register_template_function("get_text_document_preview", t.get_text_document_preview)
|
||||
server.register_template_function("get_sibling_content_folders", t.get_sibling_content_folders)
|
||||
server.register_template_function("get_folder_contents", t.get_folder_contents)
|
||||
|
||||
server.register_route("/styles/<path:path>", r.get_style)
|
||||
server.register_route("/download/<path:path>", r.get_static)
|
||||
server.register_route("/", r.default_route, defaults={"path": ""})
|
||||
server.register_route("/<path:path>", r.default_route)
|
||||
|
||||
server.run()
|
||||
|
||||
|
@ -6,6 +6,10 @@ readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"flask>=3.1.0",
|
||||
"mistune>=3.1.1",
|
||||
"pillow>=10.4.0",
|
||||
"python-frontmatter>=1.1.0",
|
||||
"rich>=13.9.4",
|
||||
"thumbhash-python>=1.0.1",
|
||||
"waitress>=3.0.2",
|
||||
]
|
||||
|
@ -20,6 +20,10 @@ mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
pygments==2.19.1
|
||||
# via rich
|
||||
python-frontmatter==1.1.0
|
||||
# via foldsite (pyproject.toml)
|
||||
pyyaml==6.0.2
|
||||
# via python-frontmatter
|
||||
rich==13.9.4
|
||||
# via foldsite (pyproject.toml)
|
||||
waitress==3.0.2
|
||||
|
10
src/config/args.py
Normal file
10
src/config/args.py
Normal file
@ -0,0 +1,10 @@
|
||||
import argparse
|
||||
|
||||
|
||||
def create_parser():
|
||||
parser = argparse.ArgumentParser(description="foldsite is a dynamic site generator")
|
||||
parser.add_argument(
|
||||
"--config", type=str, default="config.toml", help="config file path"
|
||||
)
|
||||
|
||||
return parser
|
50
src/config/config.py
Normal file
50
src/config/config.py
Normal file
@ -0,0 +1,50 @@
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
|
||||
CONTENT_DIR = None
|
||||
TEMPLATES_DIR = None
|
||||
STYLES_DIR = None
|
||||
|
||||
class Configuration:
|
||||
|
||||
def __init__(self, config_path):
|
||||
self.config_path = config_path
|
||||
self.content_dir: Path = None
|
||||
self.templates_dir: Path = None
|
||||
self.styles_dir: Path = None
|
||||
|
||||
def load_config(self):
|
||||
try:
|
||||
with open(self.config_path, "rb") as f:
|
||||
self.config_data = tomllib.load(f)
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Config file not found at {self.config_path}")
|
||||
except tomllib.TOMLDecodeError:
|
||||
raise tomllib.TOMLDecodeError(f"Config file at {self.config_path} is not valid TOML")
|
||||
|
||||
paths = self.config_data.get("paths", {})
|
||||
if not paths:
|
||||
raise ValueError("Config file does not contain paths section")
|
||||
|
||||
self.content_dir = paths.get("content_dir")
|
||||
if not self.content_dir:
|
||||
raise ValueError("Config file does not contain content_dir path")
|
||||
self.content_dir = Path(self.content_dir)
|
||||
|
||||
self.templates_dir = paths.get("templates_dir")
|
||||
if not self.templates_dir:
|
||||
raise ValueError("Config file does not contain templates_dir path")
|
||||
self.templates_dir = Path(self.templates_dir)
|
||||
|
||||
self.styles_dir = paths.get("styles_dir")
|
||||
if not self.styles_dir:
|
||||
raise ValueError("Config file does not contain styles_dir path")
|
||||
self.styles_dir = Path(self.styles_dir)
|
||||
|
||||
def set_globals(self):
|
||||
global CONTENT_DIR, TEMPLATES_DIR, STYLES_DIR
|
||||
CONTENT_DIR = self.content_dir
|
||||
TEMPLATES_DIR = self.templates_dir
|
||||
STYLES_DIR = self.styles_dir
|
||||
|
||||
|
@ -0,0 +1,10 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
GENERIC_FILE_MAPPING = OrderedDict()
|
||||
GENERIC_FILE_MAPPING["document"] = ["md", "txt", "html"]
|
||||
GENERIC_FILE_MAPPING["image"] = ["png", "jpg", "jpeg", "gif", "svg"]
|
||||
GENERIC_FILE_MAPPING["directory"] = [None]
|
||||
GENERIC_FILE_MAPPING["other"] = [None]
|
||||
|
||||
# Combinations of Groups
|
||||
GENERIC_FILE_MAPPING["multimedia"] = GENERIC_FILE_MAPPING["image"]
|
116
src/rendering/helpers.py
Normal file
116
src/rendering/helpers.py
Normal file
@ -0,0 +1,116 @@
|
||||
from dataclasses import dataclass
|
||||
from src.config.config import Configuration
|
||||
from src.rendering import GENERIC_FILE_MAPPING
|
||||
from enum import Enum
|
||||
|
||||
from thumbhash import image_to_thumbhash
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageMetadata:
|
||||
path: str
|
||||
width: int
|
||||
height: int
|
||||
alt: str
|
||||
thumbhash: str
|
||||
# exif attributes
|
||||
exif: dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileMetadata:
|
||||
path: str
|
||||
first_hundred_chars: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateFile:
|
||||
name: str
|
||||
extension: str
|
||||
categories: list[str]
|
||||
date_modified: str
|
||||
date_created: str
|
||||
size_kb: int
|
||||
metadata: ImageMetadata | FileMetadata | None
|
||||
dir_item_count: int
|
||||
is_dir: bool
|
||||
|
||||
|
||||
class TemplateHelpers:
|
||||
def __init__(self, config: Configuration):
|
||||
self.config: Configuration = config
|
||||
|
||||
def get_folder_contents(self, path: str = ""):
|
||||
"""Returns the contents of a folder as a list of TemplateFile objects
|
||||
|
||||
The metadata field is populated with the appropriate metadata object
|
||||
"""
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
ret = []
|
||||
for f in files:
|
||||
t = TemplateFile(
|
||||
name=f.name,
|
||||
extension=f.suffix.lower(),
|
||||
categories=[],
|
||||
date_modified=f.stat().st_mtime,
|
||||
date_created=f.stat().st_ctime,
|
||||
size_kb=f.stat().st_size / 1024,
|
||||
metadata=None,
|
||||
dir_item_count=len(list(f.glob("*"))) if f.is_dir() else 0,
|
||||
is_dir=f.is_dir(),
|
||||
)
|
||||
if f.is_file():
|
||||
# Build metadata depending on the mapping in GENERIC_FILE_MAPPING
|
||||
for k, v in GENERIC_FILE_MAPPING.items():
|
||||
if f.suffix[1:].lower() in v:
|
||||
t.categories.append(k)
|
||||
if k == "image":
|
||||
img = Image.open(f)
|
||||
exif = img._getexif()
|
||||
orientation = exif.get(274, 1) if exif else 1
|
||||
width, height = img.width, img.height
|
||||
if orientation in [5, 6, 7, 8]:
|
||||
width, height = height, width
|
||||
t.metadata = ImageMetadata(
|
||||
path=str(f.relative_to(self.config.content_dir)),
|
||||
width=width,
|
||||
height=height,
|
||||
alt=f.name,
|
||||
thumbhash=image_to_thumbhash(img),
|
||||
exif=img.info,
|
||||
)
|
||||
elif k == "document":
|
||||
with open(f, "r") as fdoc:
|
||||
t.metadata = FileMetadata(
|
||||
path=str(f.relative_to(self.config.content_dir)),
|
||||
first_hundred_chars=fdoc.read(100),
|
||||
)
|
||||
ret.append(t)
|
||||
return ret
|
||||
|
||||
|
||||
def get_sibling_content_files(self, path: str = ""):
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
return [
|
||||
(file.name, str(file.relative_to(self.config.content_dir)))
|
||||
for file in files
|
||||
if file.is_file()
|
||||
]
|
||||
|
||||
def get_text_document_preview(self, path: str):
|
||||
file_path = self.config.content_dir / path
|
||||
with open(file_path, "r") as f:
|
||||
content = f.read(100)
|
||||
return content
|
||||
|
||||
def get_sibling_content_folders(self, path: str = ""):
|
||||
search_contnet_path = self.config.content_dir / path
|
||||
files = search_contnet_path.glob("*")
|
||||
return [
|
||||
(file.name, str(file.relative_to(self.config.content_dir)))
|
||||
for file in files
|
||||
if file.is_dir()
|
||||
]
|
46
src/rendering/image.py
Normal file
46
src/rendering/image.py
Normal file
@ -0,0 +1,46 @@
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def generate_thumbnail(image_path, resize_percent, min_width):
|
||||
# Generate a unique key based on the image path, resize percentage, and minimum width
|
||||
key = f"{image_path}_{resize_percent}_{min_width}"
|
||||
|
||||
# Open the image file
|
||||
with Image.open(image_path) as img:
|
||||
# Calculate the new size based on the resize percentage
|
||||
width, height = img.size
|
||||
new_width = int(width * resize_percent / 100)
|
||||
new_height = int(height * resize_percent / 100)
|
||||
|
||||
# Ensure the minimum width is maintained
|
||||
if new_width < min_width:
|
||||
scale_factor = min_width / new_width
|
||||
new_width = min_width
|
||||
new_height = int(new_height * scale_factor)
|
||||
|
||||
# Resize the image while maintaining the aspect ratio
|
||||
img.thumbnail((new_width, new_height))
|
||||
|
||||
# Rotate the image based on the EXIF orientation tag
|
||||
try:
|
||||
exif = img._getexif()
|
||||
orientation = exif.get(0x0112, 1) # 0x0112 is the EXIF orientation tag
|
||||
if orientation == 3:
|
||||
img = img.rotate(180, expand=True)
|
||||
elif orientation == 6:
|
||||
img = img.rotate(270, expand=True)
|
||||
elif orientation == 8:
|
||||
img = img.rotate(90, expand=True)
|
||||
except (AttributeError, KeyError, IndexError):
|
||||
# cases: image don't have getexif
|
||||
pass
|
||||
|
||||
# Save the thumbnail to a BytesIO object
|
||||
thumbnail_io = BytesIO()
|
||||
img_format = img.format if img.format in ["JPEG", "JPG", "PNG"] else "JPEG"
|
||||
img.save(thumbnail_io, format=img_format)
|
||||
thumbnail_io.seek(0)
|
||||
|
||||
return (thumbnail_io, img_format)
|
16
src/rendering/markdown.py
Normal file
16
src/rendering/markdown.py
Normal file
@ -0,0 +1,16 @@
|
||||
import frontmatter
|
||||
import mistune
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
mistune_render = mistune.create_markdown(
|
||||
renderer=mistune.HTMLRenderer(escape=False),
|
||||
plugins=["strikethrough", "footnotes", "table", "task_lists", "abbr", "math"],
|
||||
)
|
||||
|
||||
|
||||
def render_markdown(path: Path) -> str:
|
||||
with open(path, "r") as f:
|
||||
obj = frontmatter.load(f)
|
||||
res = mistune_render(obj.content)
|
||||
return res
|
188
src/rendering/renderer.py
Normal file
188
src/rendering/renderer.py
Normal file
@ -0,0 +1,188 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from pprint import pprint as pp
|
||||
from flask import render_template_string, send_file
|
||||
|
||||
from src.rendering import GENERIC_FILE_MAPPING
|
||||
from src.rendering.markdown import render_markdown
|
||||
|
||||
|
||||
def count_file_extensions(path):
|
||||
files = path.glob("*")
|
||||
files_map = {}
|
||||
for file in files:
|
||||
if file.is_file():
|
||||
extension = file.suffix.lower()
|
||||
if extension in files_map:
|
||||
files_map[extension] += 1
|
||||
else:
|
||||
files_map[extension] = 1
|
||||
return files_map
|
||||
|
||||
|
||||
def determine_type(path: Path) -> tuple[str, str, Optional[str]]:
|
||||
if path.is_file():
|
||||
# Find extension in GENERIC_FILE_MAPPING, it may match multiple
|
||||
generic_mapping = None
|
||||
for file_type, extensions in GENERIC_FILE_MAPPING.items():
|
||||
if path.suffix[1:] in extensions:
|
||||
if generic_mapping is None:
|
||||
generic_mapping = [file_type]
|
||||
else:
|
||||
generic_mapping.append(file_type)
|
||||
generic_mapping.reverse()
|
||||
if generic_mapping is None:
|
||||
generic_mapping = ["other"]
|
||||
extension = path.suffix[1:]
|
||||
return "file", generic_mapping, extension
|
||||
# Directory
|
||||
files_map = count_file_extensions(path)
|
||||
if files_map:
|
||||
most_seen_extension = max(files_map, key=files_map.get)
|
||||
generic_mapping = None
|
||||
for file_type, extensions in GENERIC_FILE_MAPPING.items():
|
||||
if most_seen_extension[1:] in extensions:
|
||||
if generic_mapping is None:
|
||||
generic_mapping = [file_type]
|
||||
else:
|
||||
generic_mapping.append(file_type)
|
||||
generic_mapping.reverse()
|
||||
if generic_mapping is None:
|
||||
generic_mapping = ["other"]
|
||||
return "folder", generic_mapping, most_seen_extension[1:]
|
||||
else:
|
||||
return "folder", [], None
|
||||
|
||||
|
||||
DEFAULT_ERROR_TEMPLATE = """
|
||||
<div class="content">
|
||||
<h1>Error: {{ code }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
<p>{{ description }}</p>
|
||||
<p>PS: If you are a theme developer, or the owner of this site, you can style this page with a __error.html file</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def render_error_page(
|
||||
error_code: int,
|
||||
error_message: str,
|
||||
error_description: str,
|
||||
template_path: Path = Path("./"),
|
||||
):
|
||||
inp = DEFAULT_ERROR_TEMPLATE
|
||||
if (template_path / "__error.html").exists():
|
||||
inp = (template_path / "__error.html").read_text()
|
||||
content = render_template_string(
|
||||
inp,
|
||||
error_code=error_code,
|
||||
error_message=error_message,
|
||||
error_description=error_description,
|
||||
)
|
||||
return (
|
||||
render_template_string(
|
||||
(template_path / "base.html").read_text(),
|
||||
content=content,
|
||||
styles=["/base.css", "/__error.css"],
|
||||
),
|
||||
error_code,
|
||||
)
|
||||
|
||||
|
||||
def render_page(
|
||||
path: Path,
|
||||
base_path: Path = Path("./"),
|
||||
template_path: Path = Path("./"),
|
||||
style_path: Path = Path("./"),
|
||||
):
|
||||
if not path.exists():
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
template_path,
|
||||
)
|
||||
target_path = path
|
||||
target_file = path
|
||||
if path.is_file():
|
||||
target_path = path.parent
|
||||
type, category, extension = determine_type(target_file)
|
||||
|
||||
relative_path = target_file.relative_to(base_path)
|
||||
relative_dir = target_path.relative_to(base_path)
|
||||
|
||||
# Generate the possible paths for style
|
||||
styles = []
|
||||
styles.append("/" + str(relative_path) + ".css")
|
||||
|
||||
search_path = style_path / relative_dir
|
||||
while search_path >= style_path:
|
||||
if (search_path / f"__{type}.{extension}.css").exists():
|
||||
styles.append(
|
||||
"/"
|
||||
+ str(search_path.relative_to(style_path))
|
||||
+ f"/__{type}.{extension}.css"
|
||||
)
|
||||
for c in reversed(category):
|
||||
if (search_path / f"__{type}.{c}.css").exists():
|
||||
styles.append(
|
||||
"/"
|
||||
+ str(search_path.relative_to(style_path))
|
||||
+ f"/__{type}.{c}.css"
|
||||
)
|
||||
search_path = search_path.parent
|
||||
|
||||
styles.append("/base.css")
|
||||
|
||||
styles = [t for t in styles if (style_path / t[1:]).exists()]
|
||||
|
||||
templates = []
|
||||
if type == "folder":
|
||||
if (template_path / relative_dir / "__folder.html").exists():
|
||||
templates.append(relative_dir / "__folder.html")
|
||||
else:
|
||||
if (template_path / (str(relative_path) + ".html")).exists():
|
||||
templates.append(template_path / (str(relative_path) + ".html"))
|
||||
|
||||
if len(templates) == 0:
|
||||
search_path = template_path / relative_dir
|
||||
while search_path >= template_path:
|
||||
if (search_path / f"__{type}.{extension}.html").exists():
|
||||
templates.append(search_path / f"__{type}.{extension}.html")
|
||||
break
|
||||
for c in reversed(category):
|
||||
if (search_path / f"__{type}.{c}.html").exists():
|
||||
templates.append(search_path / f"__{type}.{c}.html")
|
||||
break
|
||||
search_path = search_path.parent
|
||||
|
||||
if len(templates) == 0:
|
||||
if type == "file":
|
||||
return send_file(target_file)
|
||||
else:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
template_path,
|
||||
)
|
||||
|
||||
content = ""
|
||||
if "document" in category and type == "file":
|
||||
content = render_markdown(target_file)
|
||||
|
||||
if not (template_path / "base.html").exists():
|
||||
raise Exception("Base template not found")
|
||||
|
||||
templates.append(template_path / "base.html")
|
||||
|
||||
# Filter templates to only those that exist
|
||||
for template in templates:
|
||||
content = render_template_string(
|
||||
template.read_text(),
|
||||
content=content,
|
||||
styles=styles,
|
||||
currentPath=str(relative_path),
|
||||
)
|
||||
|
||||
return content
|
@ -1,3 +1,43 @@
|
||||
from pathlib import Path
|
||||
from src.config.config import Configuration
|
||||
from src.rendering.renderer import render_page, render_error_page
|
||||
from flask import send_file
|
||||
from src.rendering.image import generate_thumbnail
|
||||
from functools import lru_cache
|
||||
|
||||
def default_route(path: str):
|
||||
return f"Default route for {path}"
|
||||
|
||||
class RouteManager:
|
||||
def __init__(self, config: Configuration):
|
||||
self.config = config
|
||||
|
||||
def default_route(self, path: str):
|
||||
file_path: Path = self.config.content_dir / (path if path else "index.md")
|
||||
return render_page(
|
||||
file_path,
|
||||
base_path=self.config.content_dir,
|
||||
template_path=self.config.templates_dir,
|
||||
style_path=self.config.styles_dir,
|
||||
)
|
||||
|
||||
def get_style(self, path: str):
|
||||
file_path: Path = self.config.styles_dir / path
|
||||
if file_path.exists():
|
||||
return send_file(file_path)
|
||||
else:
|
||||
return render_error_page(404, "Not Found", "The requested resource was not found on this server.", self.config.templates_dir)
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_static(self, path: str):
|
||||
file_path: Path = self.config.content_dir / path
|
||||
if file_path.exists():
|
||||
# Check to see if the file is an image, if it is, render a thumbnail
|
||||
if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]:
|
||||
thumbnail_bytes, img_format = generate_thumbnail(str(file_path), 10, 300)
|
||||
return (
|
||||
thumbnail_bytes.getvalue(),
|
||||
200,
|
||||
{"Content-Type": f"image/{img_format.lower()}"},
|
||||
)
|
||||
return send_file(file_path)
|
||||
else:
|
||||
return render_error_page(404, "Not Found", "The requested resource was not found on this server.", self.config.templates_dir)
|
||||
|
111
uv.lock
generated
111
uv.lock
generated
@ -54,14 +54,22 @@ version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "mistune" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-frontmatter" },
|
||||
{ name = "rich" },
|
||||
{ name = "thumbhash-python" },
|
||||
{ name = "waitress" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "flask", specifier = ">=3.1.0" },
|
||||
{ name = "mistune", specifier = ">=3.1.1" },
|
||||
{ name = "pillow", specifier = ">=10.4.0" },
|
||||
{ name = "python-frontmatter", specifier = ">=1.1.0" },
|
||||
{ name = "rich", specifier = ">=13.9.4" },
|
||||
{ name = "thumbhash-python", specifier = ">=1.0.1" },
|
||||
{ name = "waitress", specifier = ">=3.0.2" },
|
||||
]
|
||||
|
||||
@ -135,6 +143,34 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "3.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/1d/6b2b634e43bacc3239006e61800676aa6c41ac1836b2c57497ed27a7310b/mistune-3.1.1.tar.gz", hash = "sha256:e0740d635f515119f7d1feb6f9b192ee60f0cc649f80a8f944f905706a21654c", size = 94645 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/02/c66bdfdadbb021adb642ca4e8a5ed32ada0b4a3e4b39c5d076d19543452f/mistune-3.1.1-py3-none-any.whl", hash = "sha256:02106ac2aa4f66e769debbfa028509a275069dcffce0dfa578edd7b991ee700a", size = 53696 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
@ -144,6 +180,35 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-frontmatter"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/de/910fa208120314a12f9a88ea63e03707261692af782c99283f1a2c8a5e6f/python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d", size = 16256 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
@ -157,6 +222,52 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thumbhash-python"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pillow" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/29/bcb4a01ce7faab0888b00f2eb73e1a6a18b241549ebda61e8e5b31424a37/thumbhash_python-1.0.1.tar.gz", hash = "sha256:9212871c5ddf10487148c767ebd8785c8010f79e378709805bc8729d4293e03d", size = 54381 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/0e/b77ae42436765e3e6323a02df99d6999b82edea7123530448979bbe97045/thumbhash_python-1.0.1-py3-none-any.whl", hash = "sha256:b1bc4ac08ff4354dba70a0caf85a9b3f3522181fbd3e05eb4f03aa786a36578b", size = 9705 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waitress"
|
||||
version = "3.0.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user