refactor: update documentation server implementation and improve semester class logic

This commit is contained in:
2026-02-10 14:59:34 +01:00
parent 2e5cda6689
commit 29824e8c04
5 changed files with 79 additions and 59 deletions

View File

@@ -1,23 +1,28 @@
from PySide6.QtCore import QThread, Slot
from src.utils.documentation import website, QuietHandler
from wsgiref.simple_server import make_server
from src.utils.documentation import start_documentation_server
class DocumentationThread(QThread):
def __init__(self):
super().__init__()
self._server = None # store server so we can shut it down
self._process = None # store subprocess so we can shut it down
def run(self):
# launch_documentation()
self._server = make_server(
"localhost", 8000, website(), handler_class=QuietHandler
)
while not self.isInterruptionRequested():
self._server.handle_request()
# Start the zensical documentation server
self._process = start_documentation_server()
# Keep thread alive until interruption is requested
if self._process:
while not self.isInterruptionRequested():
self.msleep(100) # Check every 100ms
@Slot() # slot you can connect to aboutToQuit
def stop(self):
self.requestInterruption() # ask the loop above to exit
if self._server:
self._server.shutdown() # unblock handle_request()
if self._process:
self._process.terminate() # terminate the subprocess
try:
self._process.wait(timeout=5) # wait up to 5 seconds
except:
self._process.kill() # force kill if it doesn't stop

View File

@@ -29,9 +29,9 @@ class Semester:
# Classlevel defaults will be *copied* to each instance and then
# potentially overwritten in ``__init__``.
# ------------------------------------------------------------------
_year: int | None = int(str(datetime.datetime.now().year)[2:]) # 24 → 24
_year: int | None = None # Will be set in __post_init__
_semester: str | None = None # "WiSe" or "SoSe" set later
_month: int | None = datetime.datetime.now().month
_month: int | None = None # Will be set in __post_init__
value: str | None = None # Humanreadable label, e.g. "WiSe 23/24"
# ------------------------------------------------------------------
@@ -54,11 +54,23 @@ class Semester:
self.__post_init__()
def __post_init__(self) -> None: # noqa: D401 keep original name
if self._year is None:
self._year = int(str(datetime.datetime.now().year)[2:])
def __post_init__(self) -> None:
now = datetime.datetime.now()
if self._month is None:
self._month = datetime.datetime.now().month
self._month = now.month
if self._year is None:
# Extract last 2 digits of current year
current_year = int(str(now.year)[2:])
# For winter semester in Jan-Mar, we need to use the previous year
# because WiSe started in October of the previous calendar year
if self._month <= 3:
self._year = (current_year - 1) % 100
else:
self._year = current_year
if self._semester is None:
self._generate_semester_from_month()
self._compute_value()
@@ -66,7 +78,7 @@ class Semester:
# ------------------------------------------------------------------
# Dunder helpers
# ------------------------------------------------------------------
def __str__(self) -> str: # noqa: D401 keep original name
def __str__(self) -> str:
return self.value or "<invalid Semester>"
def __repr__(self) -> str: # Helpful for debugging lists
@@ -84,6 +96,7 @@ class Semester:
year = self._year
if self._semester == "WiSe":
next_year = (year + 1) % 100 # wrap 99 → 0
self.value = f"WiSe {year}/{next_year}"
else: # SoSe
self.value = f"SoSe {year}"
@@ -91,7 +104,7 @@ class Semester:
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def offset(self, value: int) -> "Semester":
def offset(self, value: int) -> Semester:
"""Return a new :class:`Semester` *value* steps away.
The algorithm maps every semester to a monotonically increasing
@@ -116,7 +129,7 @@ class Semester:
# ------------------------------------------------------------------
# Comparison helpers
# ------------------------------------------------------------------
def isPastSemester(self, current: "Semester") -> bool:
def is_past_semester(self, current: Semester) -> bool:
log.debug(f"Comparing {self} < {current}")
if self.year < current.year:
return True
@@ -126,7 +139,7 @@ class Semester:
) # WiSe before next SoSe
return False
def isFutureSemester(self, current: "Semester") -> bool:
def is_future_semester(self, current: Semester) -> bool:
if self.year > current.year:
return True
if self.year == current.year:
@@ -135,18 +148,18 @@ class Semester:
) # SoSe after WiSe of same year
return False
def isMatch(self, other: "Semester") -> bool:
def is_match(self, other: Semester) -> bool:
return self.year == other.year and self.semester == other.semester
# ------------------------------------------------------------------
# Convenience properties
# ------------------------------------------------------------------
@property
def next(self) -> "Semester":
def next(self) -> Semester:
return self.offset(1)
@property
def previous(self) -> "Semester":
def previous(self) -> Semester:
return self.offset(-1)
@property
@@ -161,11 +174,11 @@ class Semester:
# Static helpers
# ------------------------------------------------------------------
@staticmethod
def generate_missing(start: "Semester", end: "Semester") -> list[str]:
def generate_missing(start: Semester, end: Semester) -> list[str]:
"""Return all consecutive semesters from *start* to *end* (inclusive)."""
if not isinstance(start, Semester) or not isinstance(end, Semester):
raise TypeError("start and end must be Semester instances")
if start.isFutureSemester(end) and not start.isMatch(end):
if start.is_future_semester(end) and not start.isMatch(end):
raise ValueError("'start' must not be after 'end'")
chain: list[Semester] = [start.value]
@@ -181,7 +194,7 @@ class Semester:
# Parsing helper
# ------------------------------------------------------------------
@classmethod
def from_string(cls, s: str) -> "Semester":
def from_string(cls, s: str) -> Semester:
"""Parse a humanreadable semester label and return a :class:`Semester`.
Accepted formats (caseinsensitive)::
@@ -199,7 +212,7 @@ class Semester:
m = re.fullmatch(pattern, s, flags=re.IGNORECASE)
if not m:
raise ValueError(
"invalid semester string format expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)"
"invalid semester string format expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)",
)
term_raw, y1_str, y2_str = m.groups()
@@ -209,7 +222,7 @@ class Semester:
if term == "SoSe":
if y2_str is not None:
raise ValueError(
"SoSe string should not contain '/' followed by a second year"
"SoSe string should not contain '/' followed by a second year",
)
return cls(year, "SoSe")

View File

@@ -1,4 +1,3 @@
from src.shared.logging import log
from PySide6 import QtWidgets

View File

@@ -255,7 +255,6 @@ class Settings(QtWidgets.QDialog, _settings):
def launch_settings():
app = QtWidgets.QApplication(sys.argv)
window = Settings()
window.show()

View File

@@ -1,8 +1,7 @@
import logging
import os
from wsgiref.simple_server import WSGIRequestHandler
from flask import Flask, send_from_directory
import subprocess
import sys
from src import LOG_DIR
@@ -20,29 +19,34 @@ logger = logging.getLogger(__name__) # inherits the same file handler
docport = 8000
class QuietHandler(WSGIRequestHandler):
# suppress “GET /…” access log
def log_request(self, code="-", size="-"):
logger.info("Request: {} {}".format(self.requestline, code))
pass
# suppress all other messages (errors, etc.)
def log_message(self, fmt, *args):
logger.error("Error: {}, Args: {}".format(fmt, args))
pass
def website() -> object:
app = Flask(__name__, static_folder=os.path.join(os.getcwd(), "docs", "public"))
# Serve the main index.html at root
@app.route("/")
def index():
return send_from_directory(app.static_folder, "index.html")
# Serve all other static files
@app.route("/<path:path>")
def serve_static(path):
return send_from_directory(app.static_folder, path)
return app
def start_documentation_server():
"""
Start the Zensical documentation server as a subprocess.
Returns:
subprocess.Popen: The subprocess object, or None if startup failed.
"""
try:
# Prepare subprocess arguments
creationflags = 0
if sys.platform == "win32":
# Hide console window on Windows
creationflags = subprocess.CREATE_NO_WINDOW
# Start subprocess with all output suppressed
process = subprocess.Popen(
["uv", "run", "zensical", "serve"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
creationflags=creationflags,
cwd=os.getcwd(),
)
logger.info(f"Documentation server started with PID {process.pid}")
return process
except Exception as e:
logger.error(f"Failed to start documentation server: {e}")
return None