about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNguyễn Gia Phong <cnx@loang.net>2025-06-07 17:41:29 +0900
committerNguyễn Gia Phong <cnx@loang.net>2025-06-07 17:41:29 +0900
commitac8091aadcd8933707709473c4d7c40299e56e6b (patch)
treecb1b3a671b591ad282a67ee8bfa592404d55f9af
parentc540eea9405223860a8854b0b8adc16dcdfa707d (diff)
downloadscadere-ac8091aadcd8933707709473c4d7c40299e56e6b.tar.gz
Cache XML and file I/O calls
-rw-r--r--src/scadere/listen.py52
1 files changed, 34 insertions, 18 deletions
diff --git a/src/scadere/listen.py b/src/scadere/listen.py
index add4b05..7c683e5 100644
--- a/src/scadere/listen.py
+++ b/src/scadere/listen.py
@@ -20,7 +20,7 @@ from argparse import ArgumentParser
 from asyncio import run, start_server
 from base64 import urlsafe_b64decode as from_base64
 from datetime import datetime, timezone
-from functools import partial
+from functools import lru_cache, partial
 from http import HTTPStatus
 from operator import call
 from pathlib import Path
@@ -29,7 +29,7 @@ from typing import assert_never
 from urllib.parse import parse_qs, urljoin, urlsplit
 from xml.etree.ElementTree import (Element as xml_element,
                                    SubElement as xml_subelement,
-                                   indent, tostringlist as strings_from_xml)
+                                   indent, tostring as string_from_xml)
 from sys import argv
 
 from . import __version__, GNUHelpFormatter, NetLoc
@@ -38,6 +38,12 @@ from .check import base64_from_str
 __all__ = ['main']
 
 
+@lru_cache
+def read_text(file, time):
+    """Read the given text file if it has been updated."""
+    return file.read_text()
+
+
 def datetime_from_str(string, unavailable_ok=False):
     """Parse datetime from string in ISO 8601 format."""
     if string == 'N/A' and unavailable_ok:
@@ -151,13 +157,13 @@ def is_subdomain(subject, objects):
                for obj_parts in map(split_domain, objects))
 
 
-def feed(base_url, filename, certificates, domains):
+def feed(base_url, name, mtime, certificates, domains):
     """Construct an Atom feed based on the given information."""
     return ('feed', {'xmlns': 'http://www.w3.org/2005/Atom'},
             ('id', base_url),
             ('link', {'rel': 'self', 'href': base_url}),
-            ('title', filename),
-            ('updated', datetime.now(timezone.utc).isoformat()),
+            ('title', name),
+            ('updated', mtime),
             ('generator',
              {'uri': 'https://trong.loang.net/scadere/about',
               'version': __version__},
@@ -208,13 +214,24 @@ def xml(tree, parent=None):
     return elem
 
 
+@lru_cache
+def unparsed_feed(*args):
+    """Cache Atom feed."""
+    return string_from_xml(xml(feed(*args)), 'unicode',
+                           xml_declaration=True, default_namespace=None)
+
+
+@lru_cache
+def unparsed_page(*args):
+    """Cache XHTML page."""
+    return string_from_xml(xml(page(*args)), 'unicode',
+                           xml_declaration=True, default_namespace=None)
+
+
 async def write_xml(writer, http_version, application, func, *args):
     """Write given document as XML."""
     try:
-        content = tuple(map(str.encode,
-                            strings_from_xml(xml(func(*args)), 'unicode',
-                                             xml_declaration=True,
-                                             default_namespace=None)))
+        content = func(*args).encode()
     except Exception:  # pragma: no cover
         await describe_status(writer, HTTPStatus.INTERNAL_SERVER_ERROR,
                               http_version)
@@ -222,11 +239,9 @@ async def write_xml(writer, http_version, application, func, *args):
     else:
         await write_status(writer, http_version, HTTPStatus.OK)
         await write_content_type(writer, f'application/{application}+xml')
-        length = sum(map(len, content))
-        writer.write(f'Content-Length: {length}\r\n\r\n'.encode())
-        for part in content:
-            writer.write(part)
-            await writer.drain()
+        writer.write(f'Content-Length: {len(content)}\r\n\r\n'.encode())
+        writer.write(content)
+        await writer.drain()
 
 
 async def handle(certs, base_url, reader, writer):
@@ -254,8 +269,9 @@ async def handle(certs, base_url, reader, writer):
             return
 
         try:
+            mtime = datetime.fromtimestamp(certs.stat().st_mtime, timezone.utc)
             summaries = tuple(map(parse_summary,
-                                  certs.read_text().splitlines()))
+                                  read_text(certs, mtime).splitlines()))
             paths = tuple(urlsplit(urljoin(base_url, path(*s[-4:]))).path
                           for s in summaries)
             lookup = dict(map(tuple, zip(paths, summaries)))
@@ -267,10 +283,10 @@ async def handle(certs, base_url, reader, writer):
             raise
 
         if url_parts.path == urlsplit(base_url).path:  # Atom feed
-            await write_xml(writer, http_version, 'atom', feed,
-                            base_url, certs.name, lookup.values(), domains)
+            await write_xml(writer, http_version, 'atom', unparsed_feed,
+                            base_url, certs.name, mtime, summaries, domains)
         elif url_parts.path in lookup:  # accessible Atom entry's link/ID
-            await write_xml(writer, http_version, 'xhtml', page,
+            await write_xml(writer, http_version, 'xhtml', unparsed_page,
                             lookup.get(url_parts.path))
         else:
             await describe_status(writer, HTTPStatus.NOT_FOUND, http_version)