quickshell-mcp 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,4 @@
1
+ from .server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,96 @@
1
+ """Caching: a 30-minute in-memory layer for every fetch, plus a disk layer
2
+ that keeps the expensive bulk doc indexes (guide/type/qt) warm across server
3
+ restarts. Raw HTML is never persisted; disk holds only the built indexes."""
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from .config import _CACHE_TTL
13
+
14
+ _log = logging.getLogger(__name__)
15
+
16
+ _DEFAULT_DISK_TTL_HOURS = 720 # 30 days; published versions are effectively frozen
17
+
18
+
19
+ @dataclass
20
+ class _CacheEntry:
21
+ value: object
22
+ ts: float = field(default_factory=time.time)
23
+
24
+
25
+ _cache: dict[str, _CacheEntry] = {}
26
+
27
+
28
+ def _cache_get(key: str):
29
+ entry = _cache.get(key)
30
+ if entry and (time.time() - entry.ts) < _CACHE_TTL:
31
+ return entry.value
32
+ return None
33
+
34
+
35
+ def _cache_set(key: str, value: object):
36
+ _cache[key] = _CacheEntry(value)
37
+
38
+
39
+ def _disk_dir() -> Path | None:
40
+ """Cache root, or None when disabled via QUICKSHELL_DOCS_MCP_DISK_CACHE.
41
+ Read per call so tests can redirect it without re-importing."""
42
+ override = os.environ.get("QUICKSHELL_DOCS_MCP_DISK_CACHE", "").strip()
43
+ if override.lower() in ("off", "0", "false"):
44
+ return None
45
+ if override:
46
+ return Path(override)
47
+ xdg = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
48
+ return Path(xdg) / "quickshell-mcp"
49
+
50
+
51
+ def _disk_ttl_seconds() -> float:
52
+ raw = os.environ.get("QUICKSHELL_DOCS_MCP_DISK_TTL_HOURS", "").strip()
53
+ try:
54
+ hours = float(raw) if raw else _DEFAULT_DISK_TTL_HOURS
55
+ except ValueError:
56
+ log_warning = logging.getLogger("quickshell-mcp")
57
+ log_warning.warning("invalid QUICKSHELL_DOCS_MCP_DISK_TTL_HOURS=%r; using default", raw)
58
+ hours = _DEFAULT_DISK_TTL_HOURS
59
+ return max(hours, 0.0) * 3600
60
+
61
+
62
+ def _disk_path(key: str) -> Path | None:
63
+ directory = _disk_dir()
64
+ if directory is None:
65
+ return None
66
+ # ':' would be legal on Linux but smells like a Windows portability trap.
67
+ return directory / (key.replace(":", "_") + ".json")
68
+
69
+
70
+ def _disk_get(key: str) -> object | None:
71
+ path = _disk_path(key)
72
+ if path is None:
73
+ return None
74
+ try:
75
+ envelope = json.loads(path.read_text(encoding="utf-8"))
76
+ except (OSError, ValueError):
77
+ return None
78
+ saved_at = envelope.get("saved_at", 0)
79
+ if not isinstance(saved_at, (int, float)):
80
+ return None
81
+ if time.time() - saved_at > _disk_ttl_seconds():
82
+ return None
83
+ return envelope.get("value")
84
+
85
+
86
+ def _disk_set(key: str, value: object) -> None:
87
+ path = _disk_path(key)
88
+ if path is None:
89
+ return
90
+ try:
91
+ path.parent.mkdir(parents=True, exist_ok=True)
92
+ envelope = {"saved_at": time.time(), "value": value}
93
+ path.write_text(json.dumps(envelope), encoding="utf-8")
94
+ except OSError as error:
95
+ # Disk cache is an optimization only; never fail a request over it.
96
+ _log.warning("disk cache write failed for %s: %s", key, error)
@@ -0,0 +1,40 @@
1
+ """Upstream locations, identities, and tuning constants for every source."""
2
+
3
+ BASE = "https://quickshell.org"
4
+ QT_DOCS_BASE = "https://doc.qt.io/qt-6"
5
+ EXAMPLES_REPO_WEB = "https://git.outfoxxed.me/quickshell/quickshell-examples"
6
+ EXAMPLES_REPO_API = "https://git.outfoxxed.me/api/v1/repos/quickshell/quickshell-examples"
7
+
8
+ USER_AGENT = "quickshell-mcp/0.1 (+https://github.com/; contact: local use)"
9
+
10
+ _CACHE_TTL = 60 * 30 # 30 minutes; long enough to be polite, short enough to track site updates
11
+
12
+ _RETRY_ATTEMPTS = 3
13
+
14
+ # Version numbers are vX.Y.Z today but tolerate prerelease suffixes (v0.4.0-rc1)
15
+ # so "latest" doesn't silently lag behind when the site publishes one.
16
+ _VERSION_NUM = r"\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)*"
17
+
18
+ # doc.qt.io hosts its page body inside <article class="b-sidebar__content...">,
19
+ # so the generic [class*=sidebar] strip rule would delete the actual content.
20
+ _STRIP_SELECTORS = [
21
+ "nav",
22
+ "header",
23
+ "footer",
24
+ "aside",
25
+ "script",
26
+ "style",
27
+ "[class*=sidebar]",
28
+ "[class*=toc]",
29
+ "[class*=search]",
30
+ "[class*=version-switch]",
31
+ "[class*=breadcrumb]",
32
+ ]
33
+ _QT_STRIP_SELECTORS = ["script", "style"]
34
+
35
+ IMPLEMENTATION_REPOS: dict[str, dict[str, str]] = {
36
+ "caelestia": {"owner": "caelestia-dots", "repo": "shell"},
37
+ # The Quickshell shell lives on the legacy-v4 branch; noctalia's main
38
+ # branch moved to a C++ compositor project, so the ref is pinned here.
39
+ "noctalia": {"owner": "noctalia-dev", "repo": "noctalia", "branch": "legacy-v4"},
40
+ }
@@ -0,0 +1,31 @@
1
+ """HTML -> Markdown extraction, with per-source strip rules."""
2
+
3
+ import re
4
+
5
+ from bs4 import BeautifulSoup
6
+ from markdownify import markdownify as html_to_md
7
+
8
+ from . import utils
9
+ from .config import _QT_STRIP_SELECTORS, _STRIP_SELECTORS
10
+
11
+
12
+ def _extract_main_content(html: str, strip: list[str] | None = None) -> str:
13
+ soup = BeautifulSoup(html, "html.parser")
14
+
15
+ for selector in strip if strip is not None else _STRIP_SELECTORS:
16
+ for tag in soup.select(selector):
17
+ tag.decompose()
18
+
19
+ main = soup.find("main") or soup.find("article") or soup.body or soup
20
+ markdown = html_to_md(str(main), heading_style="ATX", bullets="-")
21
+ # Collapse excessive blank lines left over from stripped nav junk.
22
+ markdown = re.sub(r"\n{3,}", "\n\n", markdown).strip()
23
+ return markdown
24
+
25
+
26
+ def _fetch_page_markdown(url: str) -> str:
27
+ return _extract_main_content(utils._fetch_raw(url))
28
+
29
+
30
+ def _fetch_qt_page_markdown(url: str) -> str:
31
+ return _extract_main_content(utils._fetch_raw(url), strip=_QT_STRIP_SELECTORS)