refactor: update documentation server implementation and improve semester class logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -29,9 +29,9 @@ class Semester:
|
||||
# Class‑level 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 # Human‑readable 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 human‑readable semester label and return a :class:`Semester`.
|
||||
|
||||
Accepted formats (case‑insensitive)::
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from src.shared.logging import log
|
||||
from PySide6 import QtWidgets
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@ class Settings(QtWidgets.QDialog, _settings):
|
||||
|
||||
|
||||
def launch_settings():
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
window = Settings()
|
||||
window.show()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user