python-docs-mcp-server 0.1.2__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.
- mcp_server_python_docs/__init__.py +17 -0
- mcp_server_python_docs/__main__.py +574 -0
- mcp_server_python_docs/app_context.py +33 -0
- mcp_server_python_docs/data/synonyms.yaml +194 -0
- mcp_server_python_docs/detection.py +82 -0
- mcp_server_python_docs/diagnostics.py +74 -0
- mcp_server_python_docs/errors.py +33 -0
- mcp_server_python_docs/ingestion/__init__.py +1 -0
- mcp_server_python_docs/ingestion/cpython_versions.py +32 -0
- mcp_server_python_docs/ingestion/inventory.py +178 -0
- mcp_server_python_docs/ingestion/publish.py +504 -0
- mcp_server_python_docs/ingestion/sphinx_json.py +617 -0
- mcp_server_python_docs/models.py +216 -0
- mcp_server_python_docs/py.typed +0 -0
- mcp_server_python_docs/retrieval/__init__.py +31 -0
- mcp_server_python_docs/retrieval/budget.py +63 -0
- mcp_server_python_docs/retrieval/query.py +183 -0
- mcp_server_python_docs/retrieval/ranker.py +369 -0
- mcp_server_python_docs/server.py +329 -0
- mcp_server_python_docs/services/__init__.py +7 -0
- mcp_server_python_docs/services/cache.py +111 -0
- mcp_server_python_docs/services/content.py +155 -0
- mcp_server_python_docs/services/observability.py +130 -0
- mcp_server_python_docs/services/package_docs.py +161 -0
- mcp_server_python_docs/services/persistent_cache.py +147 -0
- mcp_server_python_docs/services/search.py +111 -0
- mcp_server_python_docs/services/version.py +47 -0
- mcp_server_python_docs/services/version_resolution.py +79 -0
- mcp_server_python_docs/storage/__init__.py +1 -0
- mcp_server_python_docs/storage/db.py +146 -0
- mcp_server_python_docs/storage/schema.sql +136 -0
- python_docs_mcp_server-0.1.2.dist-info/METADATA +528 -0
- python_docs_mcp_server-0.1.2.dist-info/RECORD +36 -0
- python_docs_mcp_server-0.1.2.dist-info/WHEEL +4 -0
- python_docs_mcp_server-0.1.2.dist-info/entry_points.txt +3 -0
- python_docs_mcp_server-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Python stdlib concept synonym/expansion table
|
|
2
|
+
# Format: concept: [expansion terms]
|
|
3
|
+
# Used for query expansion before FTS5 search
|
|
4
|
+
# ~150 curated entries covering major Python stdlib concepts
|
|
5
|
+
|
|
6
|
+
# Concurrency & Parallelism
|
|
7
|
+
parallel: [concurrent, multiprocessing, threading, asyncio, concurrent.futures, parallel execution]
|
|
8
|
+
async: [asyncio, await, coroutine, async for, async with, event loop, asynchronous]
|
|
9
|
+
threading: [thread, threading, Thread, Lock, RLock, Semaphore, Barrier, Event, Condition]
|
|
10
|
+
multiprocessing: [multiprocessing, Process, Pool, Queue, Pipe, shared memory]
|
|
11
|
+
concurrent: [concurrent.futures, ThreadPoolExecutor, ProcessPoolExecutor, Future, Executor]
|
|
12
|
+
subprocess: [subprocess, subprocess.run, subprocess.Popen, shlex, shell command]
|
|
13
|
+
event loop: [asyncio, asyncio.run, asyncio.get_event_loop, AbstractEventLoop]
|
|
14
|
+
coroutine: [asyncio, async def, await, coroutine, Task, gather]
|
|
15
|
+
task group: [asyncio.TaskGroup, asyncio.gather, asyncio.wait, task management]
|
|
16
|
+
synchronization: [Lock, RLock, Semaphore, Event, Condition, Barrier, threading]
|
|
17
|
+
future: [concurrent.futures, Future, asyncio.Future, result, exception]
|
|
18
|
+
|
|
19
|
+
# Data Serialization
|
|
20
|
+
json: [json, json.loads, json.dumps, json.load, json.dump, JSON parsing]
|
|
21
|
+
parse json: [json, json.loads, json.load, JSON decoding]
|
|
22
|
+
serialize json: [json, json.dumps, json.dump, JSON encoding]
|
|
23
|
+
csv: [csv, csv.reader, csv.writer, csv.DictReader, csv.DictWriter, delimiter]
|
|
24
|
+
xml: [xml, xml.etree.ElementTree, xml.dom, xml.sax, XML parsing]
|
|
25
|
+
html parsing: [html.parser, HTMLParser, html, html.entities]
|
|
26
|
+
serialization: [json, marshal, object serialization, data format]
|
|
27
|
+
struct: [struct, struct.pack, struct.unpack, binary data, byte packing]
|
|
28
|
+
base64: [base64, base64.b64encode, base64.b64decode, encoding]
|
|
29
|
+
configparser: [configparser, ConfigParser, ini file, configuration]
|
|
30
|
+
toml: [tomllib, tomllib.load, TOML parsing]
|
|
31
|
+
|
|
32
|
+
# File System & I/O
|
|
33
|
+
file io: [open, pathlib, os.path, io, shutil, fileinput, file reading, file writing]
|
|
34
|
+
path: [pathlib, pathlib.Path, os.path, path manipulation, file path]
|
|
35
|
+
pathlib: [pathlib, Path, PurePath, PosixPath, WindowsPath]
|
|
36
|
+
temp files: [tempfile, TemporaryFile, NamedTemporaryFile, mkdtemp, temporary]
|
|
37
|
+
glob: [glob, glob.glob, fnmatch, pattern matching, file search]
|
|
38
|
+
shutil: [shutil, shutil.copy, shutil.move, shutil.rmtree, file operations]
|
|
39
|
+
directory: [os, os.listdir, os.scandir, os.walk, pathlib, directory listing]
|
|
40
|
+
file copy: [shutil.copy, shutil.copy2, shutil.copytree, file duplication]
|
|
41
|
+
file delete: [os.remove, os.unlink, shutil.rmtree, pathlib.Path.unlink]
|
|
42
|
+
read file: [open, read, readline, readlines, pathlib.Path.read_text]
|
|
43
|
+
write file: [open, write, writelines, pathlib.Path.write_text]
|
|
44
|
+
binary io: [io, io.BytesIO, io.BufferedReader, io.BufferedWriter, binary]
|
|
45
|
+
text io: [io, io.StringIO, io.TextIOWrapper, text stream]
|
|
46
|
+
|
|
47
|
+
# Networking & Web
|
|
48
|
+
http: [http, http.client, http.server, urllib, urllib.request, HTTP]
|
|
49
|
+
http requests: [urllib, http.client, urllib.request, urllib.parse, URL fetch]
|
|
50
|
+
socket: [socket, socket.socket, TCP, UDP, network programming]
|
|
51
|
+
ssl: [ssl, ssl.create_default_context, TLS, certificate, HTTPS]
|
|
52
|
+
email: [email, email.message, smtplib, imaplib, email parsing]
|
|
53
|
+
url: [urllib, urllib.parse, urlparse, urlencode, URL manipulation]
|
|
54
|
+
ftp: [ftplib, FTP, file transfer]
|
|
55
|
+
http server: [http.server, HTTPServer, SimpleHTTPRequestHandler]
|
|
56
|
+
url parsing: [urllib.parse, urlparse, urljoin, quote, unquote]
|
|
57
|
+
ip address: [ipaddress, IPv4Address, IPv6Address, ip_network]
|
|
58
|
+
|
|
59
|
+
# Text Processing
|
|
60
|
+
string: [str, string, string formatting, string methods, text]
|
|
61
|
+
string formatting: [str.format, f-string, format, printf, string interpolation]
|
|
62
|
+
regex: [re, regular expression, pattern, re.match, re.search, re.findall, re.sub]
|
|
63
|
+
text processing: [string, textwrap, difflib, unicodedata, text manipulation]
|
|
64
|
+
unicode: [unicodedata, codecs, encoding, utf-8, character, codepoint]
|
|
65
|
+
encoding: [codecs, encode, decode, utf-8, ascii, latin-1, chardet]
|
|
66
|
+
template: [string.Template, string formatting, placeholder]
|
|
67
|
+
textwrap: [textwrap, textwrap.wrap, textwrap.dedent, text wrapping]
|
|
68
|
+
difflib: [difflib, SequenceMatcher, unified_diff, context_diff]
|
|
69
|
+
translate: [str.translate, str.maketrans, character mapping]
|
|
70
|
+
|
|
71
|
+
# Data Structures
|
|
72
|
+
list: [list, append, extend, insert, sort, sorted, sequence]
|
|
73
|
+
dict: [dict, dictionary, mapping, keys, values, items, defaultdict]
|
|
74
|
+
set: [set, frozenset, intersection, union, difference, symmetric_difference]
|
|
75
|
+
tuple: [tuple, namedtuple, immutable sequence]
|
|
76
|
+
collections: [collections, deque, defaultdict, OrderedDict, Counter, ChainMap, namedtuple]
|
|
77
|
+
deque: [collections.deque, double-ended queue, appendleft, popleft]
|
|
78
|
+
counter: [collections.Counter, counting, most_common, frequency]
|
|
79
|
+
defaultdict: [collections.defaultdict, default factory, missing key]
|
|
80
|
+
ordered dict: [collections.OrderedDict, ordered dictionary, insertion order]
|
|
81
|
+
named tuple: [collections.namedtuple, typing.NamedTuple, named fields]
|
|
82
|
+
array: [array, array.array, typed array, numeric array]
|
|
83
|
+
queue: [queue, Queue, LifoQueue, PriorityQueue, SimpleQueue]
|
|
84
|
+
heap: [heapq, heappush, heappop, priority queue, heap]
|
|
85
|
+
bisect: [bisect, bisect_left, bisect_right, insort, sorted insertion]
|
|
86
|
+
chain map: [collections.ChainMap, nested scopes, mapping chain]
|
|
87
|
+
|
|
88
|
+
# Date & Time
|
|
89
|
+
date time: [datetime, time, calendar, zoneinfo, timedelta, timezone]
|
|
90
|
+
datetime: [datetime, datetime.datetime, datetime.date, datetime.time, datetime.timedelta]
|
|
91
|
+
time: [time, time.time, time.sleep, time.monotonic, time.perf_counter]
|
|
92
|
+
calendar: [calendar, calendar.month, weekday, leap year]
|
|
93
|
+
timezone: [zoneinfo, ZoneInfo, timezone, datetime.timezone, UTC]
|
|
94
|
+
timedelta: [datetime.timedelta, time difference, duration]
|
|
95
|
+
|
|
96
|
+
# Type System
|
|
97
|
+
type hint: [typing, annotation, generic, TypeVar, Protocol, Union, Optional]
|
|
98
|
+
typing: [typing, List, Dict, Set, Tuple, Optional, Union, Any, Generic]
|
|
99
|
+
protocol: [typing.Protocol, structural subtyping, duck typing, ABC]
|
|
100
|
+
generic: [typing.Generic, TypeVar, ParamSpec, type variable]
|
|
101
|
+
dataclass: [dataclasses, dataclass, field, asdict, astuple]
|
|
102
|
+
enum: [enum, Enum, IntEnum, Flag, auto, enumeration]
|
|
103
|
+
abstract: [abc, ABC, abstractmethod, abstract base class]
|
|
104
|
+
type checking: [typing, TYPE_CHECKING, runtime type, mypy, pyright]
|
|
105
|
+
annotated: [typing.Annotated, type annotation, metadata]
|
|
106
|
+
type alias: [typing.TypeAlias, type, NewType]
|
|
107
|
+
|
|
108
|
+
# Testing
|
|
109
|
+
testing: [unittest, pytest, mock, doctest, unittest.mock, test]
|
|
110
|
+
unittest: [unittest, TestCase, setUp, tearDown, assert, test runner]
|
|
111
|
+
mock: [unittest.mock, Mock, MagicMock, patch, side_effect]
|
|
112
|
+
doctest: [doctest, doctest.testmod, interactive example, docstring test]
|
|
113
|
+
assert: [assert, assertEqual, assertTrue, assertRaises, assertion]
|
|
114
|
+
|
|
115
|
+
# Math & Numbers
|
|
116
|
+
math: [math, math.sqrt, math.ceil, math.floor, math.log, mathematics]
|
|
117
|
+
decimal: [decimal, Decimal, precision, rounding, financial]
|
|
118
|
+
fractions: [fractions, Fraction, rational number, numerator, denominator]
|
|
119
|
+
random: [random, random.choice, random.randint, random.shuffle, random.sample]
|
|
120
|
+
statistics: [statistics, mean, median, stdev, variance, statistical]
|
|
121
|
+
complex: [complex, cmath, complex number, imaginary]
|
|
122
|
+
number: [int, float, complex, decimal, fractions, numeric]
|
|
123
|
+
infinity: [math.inf, float inf, math.isinf, infinite]
|
|
124
|
+
|
|
125
|
+
# Functional Programming
|
|
126
|
+
itertools: [itertools, chain, product, permutations, combinations, cycle, repeat]
|
|
127
|
+
functools: [functools, lru_cache, partial, reduce, wraps, total_ordering]
|
|
128
|
+
operator: [operator, itemgetter, attrgetter, methodcaller, operator overloading]
|
|
129
|
+
contextlib: [contextlib, contextmanager, suppress, redirect_stdout, ExitStack]
|
|
130
|
+
context manager: [contextlib, with statement, __enter__, __exit__, contextmanager]
|
|
131
|
+
decorator: [functools.wraps, decorator pattern, wrapper function]
|
|
132
|
+
lambda: [lambda, anonymous function, inline function]
|
|
133
|
+
map filter: [map, filter, reduce, list comprehension, generator expression]
|
|
134
|
+
partial: [functools.partial, partial application, currying]
|
|
135
|
+
cache: [functools.lru_cache, functools.cache, memoization, caching]
|
|
136
|
+
|
|
137
|
+
# OS & System
|
|
138
|
+
os: [os, os.path, os.environ, os.getcwd, operating system]
|
|
139
|
+
platform: [platform, sys.platform, os.name, system information]
|
|
140
|
+
signal: [signal, signal.signal, SIGINT, SIGTERM, signal handler]
|
|
141
|
+
environment: [os.environ, os.getenv, environment variable, env]
|
|
142
|
+
process: [os, os.getpid, os.fork, multiprocessing, process management]
|
|
143
|
+
permissions: [os.chmod, os.chown, stat, file permissions, access]
|
|
144
|
+
system info: [platform, sys, os.uname, machine, processor]
|
|
145
|
+
working directory: [os.getcwd, os.chdir, pathlib.Path.cwd]
|
|
146
|
+
symlink: [os.symlink, os.readlink, symbolic link, pathlib.Path.symlink_to]
|
|
147
|
+
|
|
148
|
+
# Debugging & Inspection
|
|
149
|
+
logging: [logging, logger, getLogger, basicConfig, log level, handler]
|
|
150
|
+
warnings: [warnings, warn, DeprecationWarning, filter, simplefilter]
|
|
151
|
+
traceback: [traceback, traceback.format_exc, traceback.print_exc, stack trace]
|
|
152
|
+
pdb: [pdb, breakpoint, debugger, pdb.set_trace, step, continue]
|
|
153
|
+
inspect: [inspect, inspect.signature, inspect.getmembers, introspection]
|
|
154
|
+
ast: [ast, abstract syntax tree, ast.parse, ast.dump, code analysis]
|
|
155
|
+
dis: [dis, disassemble, bytecode, opcode, instruction]
|
|
156
|
+
gc: [gc, garbage collector, gc.collect, gc.get_objects, memory]
|
|
157
|
+
tracemalloc: [tracemalloc, memory tracking, memory leak, allocation]
|
|
158
|
+
profiling: [cProfile, profile, pstats, timeit, performance measurement]
|
|
159
|
+
weakref: [weakref, WeakRef, weak reference, reference counting]
|
|
160
|
+
|
|
161
|
+
# Security & Cryptography
|
|
162
|
+
hashlib: [hashlib, md5, sha256, sha512, hash, digest, checksum]
|
|
163
|
+
secrets: [secrets, token_hex, token_urlsafe, secure random, password]
|
|
164
|
+
hmac: [hmac, message authentication, HMAC, digest, signature]
|
|
165
|
+
password: [hashlib, secrets, password hashing, salt]
|
|
166
|
+
|
|
167
|
+
# CLI & Arguments
|
|
168
|
+
argparse: [argparse, ArgumentParser, add_argument, parse_args, command line]
|
|
169
|
+
command line: [argparse, sys.argv, click, CLI, command line interface]
|
|
170
|
+
getopt: [getopt, option parsing, command line options]
|
|
171
|
+
shlex: [shlex, shlex.split, shell parsing, command splitting]
|
|
172
|
+
|
|
173
|
+
# Compression & Archives
|
|
174
|
+
zip: [zipfile, ZipFile, compression, archive, extract]
|
|
175
|
+
tar: [tarfile, TarFile, tar archive, gzip, bzip2]
|
|
176
|
+
gzip: [gzip, GzipFile, compression, decompress]
|
|
177
|
+
compression: [zlib, gzip, bz2, lzma, compress, decompress]
|
|
178
|
+
|
|
179
|
+
# Copy & Comparison
|
|
180
|
+
copy: [copy, copy.copy, copy.deepcopy, shallow copy, deep copy]
|
|
181
|
+
comparison: [operator, functools.total_ordering, __eq__, __lt__, rich comparison]
|
|
182
|
+
pprint: [pprint, pprint.pprint, pretty print, formatted output]
|
|
183
|
+
repr: [repr, __repr__, __str__, string representation]
|
|
184
|
+
|
|
185
|
+
# Internationalization
|
|
186
|
+
locale: [locale, localization, number format, currency format]
|
|
187
|
+
gettext: [gettext, translation, internationalization, i18n]
|
|
188
|
+
|
|
189
|
+
# Networking Protocols
|
|
190
|
+
xmlrpc: [xmlrpc, xmlrpc.client, xmlrpc.server, RPC, remote procedure call]
|
|
191
|
+
|
|
192
|
+
# Database
|
|
193
|
+
sqlite: [sqlite3, sqlite3.connect, SQL, database, cursor, execute]
|
|
194
|
+
dbm: [dbm, dbm.open, key-value store, persistent dictionary]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Python version detection from the user's environment.
|
|
2
|
+
|
|
3
|
+
Probes multiple sources to determine which Python version the user
|
|
4
|
+
is working with, so the server can default to the right documentation.
|
|
5
|
+
|
|
6
|
+
Detection order:
|
|
7
|
+
1. .python-version file in cwd (pyenv, mise, rtx convention)
|
|
8
|
+
2. ``python3 --version`` in PATH (user's active interpreter)
|
|
9
|
+
3. ``sys.version_info`` (server's own runtime — last resort)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_VERSION_RE = re.compile(r"(\d+\.\d+)")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_major_minor(raw: str) -> str | None:
|
|
25
|
+
"""Extract 'X.Y' from a version string like '3.13.2', 'Python 3.13.2', 'cpython-3.13'."""
|
|
26
|
+
m = _VERSION_RE.search(raw)
|
|
27
|
+
return m.group(1) if m else None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def detect_python_version() -> tuple[str, str]:
|
|
31
|
+
"""Detect the user's Python version.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Tuple of (major_minor, source) where major_minor is like '3.13'
|
|
35
|
+
and source describes how it was detected.
|
|
36
|
+
"""
|
|
37
|
+
# 1. .python-version file (pyenv / mise / rtx)
|
|
38
|
+
pv_file = Path.cwd() / ".python-version"
|
|
39
|
+
if pv_file.is_file():
|
|
40
|
+
try:
|
|
41
|
+
first_line = pv_file.read_text().strip().splitlines()[0].strip()
|
|
42
|
+
version = _parse_major_minor(first_line)
|
|
43
|
+
if version:
|
|
44
|
+
logger.info("Detected Python %s from .python-version", version)
|
|
45
|
+
return version, ".python-version file"
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# 2. python3 --version in PATH
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["python3", "--version"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=5,
|
|
56
|
+
)
|
|
57
|
+
if result.returncode == 0:
|
|
58
|
+
version = _parse_major_minor(result.stdout.strip())
|
|
59
|
+
if version:
|
|
60
|
+
logger.info("Detected Python %s from python3 in PATH", version)
|
|
61
|
+
return version, "python3 in PATH"
|
|
62
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# 3. Server's own runtime
|
|
66
|
+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
67
|
+
logger.info("Using server runtime Python %s as fallback", version)
|
|
68
|
+
return version, "server runtime"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def match_to_indexed(
|
|
72
|
+
detected: str, indexed_versions: list[str]
|
|
73
|
+
) -> str | None:
|
|
74
|
+
"""Match a detected version to the closest indexed version.
|
|
75
|
+
|
|
76
|
+
Returns the detected version if it's in the index, otherwise None.
|
|
77
|
+
We don't guess -- if a detected version is not indexed, return None and
|
|
78
|
+
let the normal default resolution handle it.
|
|
79
|
+
"""
|
|
80
|
+
if detected in indexed_versions:
|
|
81
|
+
return detected
|
|
82
|
+
return None
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Environment diagnostics shared by CLI health checks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class DiagnosticResult:
|
|
11
|
+
"""Result for a single environment diagnostic probe."""
|
|
12
|
+
|
|
13
|
+
passed: bool
|
|
14
|
+
detail: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _combined_output_excerpt(stdout: str, stderr: str, limit: int = 500) -> str:
|
|
18
|
+
combined = "\n".join(part.strip() for part in (stderr, stdout) if part.strip())
|
|
19
|
+
if len(combined) <= limit:
|
|
20
|
+
return combined
|
|
21
|
+
return combined[-limit:]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_build_venv_support(
|
|
25
|
+
python_executable: str | None = None,
|
|
26
|
+
timeout: float = 10.0,
|
|
27
|
+
) -> DiagnosticResult:
|
|
28
|
+
"""Check that build-index can create pip-enabled Sphinx environments.
|
|
29
|
+
|
|
30
|
+
``build-index`` creates a disposable Sphinx virtual environment with pip.
|
|
31
|
+
Debian/Ubuntu hosts without the matching ``pythonX.Y-venv`` package can run
|
|
32
|
+
the server but fail when that environment needs ``ensurepip``.
|
|
33
|
+
"""
|
|
34
|
+
executable = python_executable or sys.executable
|
|
35
|
+
package_name = f"python{sys.version_info.major}.{sys.version_info.minor}-venv"
|
|
36
|
+
command = [executable, "-c", "import ensurepip; import venv"]
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
command,
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
timeout=timeout,
|
|
44
|
+
)
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
return DiagnosticResult(
|
|
47
|
+
passed=False,
|
|
48
|
+
detail=f"{executable} not found; build-index cannot create Sphinx venvs",
|
|
49
|
+
)
|
|
50
|
+
except subprocess.TimeoutExpired:
|
|
51
|
+
return DiagnosticResult(
|
|
52
|
+
passed=False,
|
|
53
|
+
detail=(
|
|
54
|
+
f"{executable} timed out while checking venv/ensurepip support "
|
|
55
|
+
"for build-index"
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if result.returncode == 0:
|
|
60
|
+
return DiagnosticResult(
|
|
61
|
+
passed=True,
|
|
62
|
+
detail=f"{executable} has venv and ensurepip available for build-index",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
detail = (
|
|
66
|
+
f"{executable} cannot import venv/ensurepip; build-index needs them to "
|
|
67
|
+
"create Sphinx venvs. On Debian/Ubuntu, install "
|
|
68
|
+
f"{package_name} for this interpreter."
|
|
69
|
+
)
|
|
70
|
+
output = _combined_output_excerpt(result.stdout, result.stderr)
|
|
71
|
+
if output:
|
|
72
|
+
detail = f"{detail} Output: {output}"
|
|
73
|
+
|
|
74
|
+
return DiagnosticResult(passed=False, detail=detail)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Error taxonomy for python-docs-mcp-server.
|
|
2
|
+
|
|
3
|
+
Named exceptions for predictable failure modes, mapped to MCP error responses.
|
|
4
|
+
See build guide section 10.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DocsServerError(Exception):
|
|
9
|
+
"""Base error for all docs server errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VersionNotFoundError(DocsServerError):
|
|
13
|
+
"""Requested Python version not found in index."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SymbolNotFoundError(DocsServerError):
|
|
17
|
+
"""Requested symbol not found in index."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PageNotFoundError(DocsServerError):
|
|
21
|
+
"""Requested page/slug not found in index."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IndexNotBuiltError(DocsServerError):
|
|
25
|
+
"""No index.db found at expected cache path."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IngestionError(DocsServerError):
|
|
29
|
+
"""Error during index building/ingestion."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FTS5UnavailableError(DocsServerError):
|
|
33
|
+
"""SQLite FTS5 extension not available in this build."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ingestion layer: objects.inv parsing and content ingestion."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Pinned CPython documentation build targets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Final, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CPythonDocsBuildConfig(TypedDict):
|
|
8
|
+
"""Build settings for one CPython documentation release."""
|
|
9
|
+
|
|
10
|
+
tag: str
|
|
11
|
+
sphinx_pin: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SUPPORTED_DOC_VERSIONS: Final[tuple[str, ...]] = (
|
|
15
|
+
"3.10",
|
|
16
|
+
"3.11",
|
|
17
|
+
"3.12",
|
|
18
|
+
"3.13",
|
|
19
|
+
"3.14",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
SUPPORTED_DOC_VERSIONS_CSV: Final[str] = ",".join(SUPPORTED_DOC_VERSIONS)
|
|
23
|
+
|
|
24
|
+
# CPython git tags are pinned so content builds are reproducible and do not
|
|
25
|
+
# drift when a maintenance branch receives new commits.
|
|
26
|
+
CPYTHON_DOCS_BUILD_CONFIG: Final[dict[str, CPythonDocsBuildConfig]] = {
|
|
27
|
+
"3.10": {"tag": "v3.10.20", "sphinx_pin": "sphinx==3.4.3"},
|
|
28
|
+
"3.11": {"tag": "v3.11.15", "sphinx_pin": "sphinx~=7.2.0"},
|
|
29
|
+
"3.12": {"tag": "v3.12.13", "sphinx_pin": "sphinx~=8.2.0"},
|
|
30
|
+
"3.13": {"tag": "v3.13.13", "sphinx_pin": "sphinx<9.0.0"},
|
|
31
|
+
"3.14": {"tag": "v3.14.4", "sphinx_pin": "sphinx<9.0.0"},
|
|
32
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Ingest symbols from Python docs objects.inv via sphobjinv."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import sqlite3
|
|
6
|
+
|
|
7
|
+
import sphobjinv as soi
|
|
8
|
+
|
|
9
|
+
from mcp_server_python_docs.storage.db import bootstrap_schema
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Priority ordering for duplicate symbol resolution (INGR-I-05)
|
|
14
|
+
# Lower number = higher priority
|
|
15
|
+
ROLE_PRIORITY: dict[str, int] = {
|
|
16
|
+
"class": 0,
|
|
17
|
+
"exception": 1,
|
|
18
|
+
"function": 2,
|
|
19
|
+
"method": 3,
|
|
20
|
+
"attribute": 4,
|
|
21
|
+
"data": 5,
|
|
22
|
+
"module": 6,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _expand_uri(obj: soi.DataObjStr) -> str:
|
|
27
|
+
"""Expand $ shorthand in URI to full qualified name (INGR-I-03).
|
|
28
|
+
|
|
29
|
+
objects.inv uses $ as shorthand for the object name.
|
|
30
|
+
E.g., uri="library/asyncio-task.html#$" with name="asyncio.TaskGroup"
|
|
31
|
+
becomes "library/asyncio-task.html#asyncio.TaskGroup"
|
|
32
|
+
"""
|
|
33
|
+
uri: str = obj.uri # type: ignore[assignment] # sphobjinv lacks type stubs
|
|
34
|
+
if "$" in uri:
|
|
35
|
+
uri = uri.replace("$", obj.name) # type: ignore[arg-type]
|
|
36
|
+
return uri
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_display_name(obj: soi.DataObjStr) -> str:
|
|
40
|
+
"""Get display name, falling back to obj.name when dispname is '-' (INGR-I-04)."""
|
|
41
|
+
if obj.dispname == "-": # type: ignore[comparison-overlap] # sphobjinv lacks stubs
|
|
42
|
+
return obj.name # type: ignore[return-value]
|
|
43
|
+
return obj.dispname # type: ignore[return-value]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _extract_module(qualified_name: str) -> str | None:
|
|
47
|
+
"""Extract module from qualified name.
|
|
48
|
+
|
|
49
|
+
'asyncio.TaskGroup' -> 'asyncio'
|
|
50
|
+
'json.dumps' -> 'json'
|
|
51
|
+
'os.path.join' -> 'os.path'
|
|
52
|
+
"""
|
|
53
|
+
parts = qualified_name.rsplit(".", 1)
|
|
54
|
+
if len(parts) > 1:
|
|
55
|
+
return parts[0]
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_name(name: str) -> str:
|
|
60
|
+
"""Normalize a symbol name for search.
|
|
61
|
+
|
|
62
|
+
Lowercases and strips leading/trailing whitespace.
|
|
63
|
+
Preserves dots and underscores for identifier matching.
|
|
64
|
+
"""
|
|
65
|
+
return name.strip().lower()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ingest_inventory(
|
|
69
|
+
conn: sqlite3.Connection, version: str, *, is_default: bool = False
|
|
70
|
+
) -> int:
|
|
71
|
+
"""Download objects.inv for a Python version and populate symbols.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
conn: Read-write SQLite connection
|
|
75
|
+
version: Python version string (e.g., "3.13")
|
|
76
|
+
is_default: Whether this version is the default for queries (MVER-02)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Number of symbols inserted
|
|
80
|
+
|
|
81
|
+
Steps:
|
|
82
|
+
1. Bootstrap schema (idempotent)
|
|
83
|
+
2. Insert/update doc_set for this version
|
|
84
|
+
3. Download objects.inv via sphobjinv
|
|
85
|
+
4. Parse each DataObjStr into a symbol row
|
|
86
|
+
5. Handle duplicates via priority ordering (INGR-I-05)
|
|
87
|
+
6. Populate symbols_fts in same transaction (INGR-I-06)
|
|
88
|
+
"""
|
|
89
|
+
# Validate version format first, before any DB writes (WR-02)
|
|
90
|
+
import re
|
|
91
|
+
|
|
92
|
+
if not re.match(r"^\d+\.\d+$", version):
|
|
93
|
+
from mcp_server_python_docs.errors import IngestionError
|
|
94
|
+
|
|
95
|
+
raise IngestionError(f"Invalid version format: {version!r}")
|
|
96
|
+
|
|
97
|
+
bootstrap_schema(conn)
|
|
98
|
+
|
|
99
|
+
# Upsert doc_set for this version
|
|
100
|
+
conn.execute(
|
|
101
|
+
"INSERT OR REPLACE INTO doc_sets "
|
|
102
|
+
"(source, version, language, label, is_default, base_url) "
|
|
103
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
104
|
+
(
|
|
105
|
+
"python-docs",
|
|
106
|
+
version,
|
|
107
|
+
"en",
|
|
108
|
+
f"Python {version}",
|
|
109
|
+
1 if is_default else 0,
|
|
110
|
+
f"https://docs.python.org/{version}/",
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
doc_set_id = conn.execute(
|
|
114
|
+
"SELECT id FROM doc_sets "
|
|
115
|
+
"WHERE source = 'python-docs' AND version = ? AND language = 'en'",
|
|
116
|
+
(version,),
|
|
117
|
+
).fetchone()[0]
|
|
118
|
+
|
|
119
|
+
# Clear existing symbols for this doc_set (re-ingestion support)
|
|
120
|
+
conn.execute("DELETE FROM symbols WHERE doc_set_id = ?", (doc_set_id,))
|
|
121
|
+
|
|
122
|
+
# Download and parse objects.inv (INGR-I-01)
|
|
123
|
+
url = f"https://docs.python.org/{version}/objects.inv"
|
|
124
|
+
logger.info("Downloading %s...", url)
|
|
125
|
+
inv = soi.Inventory(url=url) # type: ignore[call-arg] # sphobjinv lacks type stubs
|
|
126
|
+
logger.info("Downloaded %d inventory objects", len(inv.objects))
|
|
127
|
+
|
|
128
|
+
# Filter to Python domain objects and collect by qualified_name
|
|
129
|
+
# for duplicate resolution (INGR-I-05)
|
|
130
|
+
best_symbols: dict[str, tuple[soi.DataObjStr, int]] = {}
|
|
131
|
+
|
|
132
|
+
for obj in inv.objects:
|
|
133
|
+
if obj.domain != "py":
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
name = obj.name
|
|
137
|
+
role = obj.role
|
|
138
|
+
priority = ROLE_PRIORITY.get(role, 99)
|
|
139
|
+
|
|
140
|
+
key = name # Group by qualified_name for dedup
|
|
141
|
+
if key not in best_symbols or priority < best_symbols[key][1]:
|
|
142
|
+
best_symbols[key] = (obj, priority)
|
|
143
|
+
|
|
144
|
+
# Insert symbols (INGR-I-02)
|
|
145
|
+
count = 0
|
|
146
|
+
for name, (obj, _priority) in best_symbols.items():
|
|
147
|
+
uri = _expand_uri(obj)
|
|
148
|
+
module = _extract_module(name)
|
|
149
|
+
anchor_part = uri.split("#", 1)[1] if "#" in uri else None
|
|
150
|
+
|
|
151
|
+
conn.execute(
|
|
152
|
+
"INSERT OR IGNORE INTO symbols "
|
|
153
|
+
"(doc_set_id, qualified_name, normalized_name, module, "
|
|
154
|
+
"symbol_type, uri, anchor) "
|
|
155
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
156
|
+
(
|
|
157
|
+
doc_set_id,
|
|
158
|
+
name,
|
|
159
|
+
_normalize_name(name),
|
|
160
|
+
module,
|
|
161
|
+
obj.role,
|
|
162
|
+
uri,
|
|
163
|
+
anchor_part,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
count += 1
|
|
167
|
+
|
|
168
|
+
conn.commit()
|
|
169
|
+
|
|
170
|
+
# Rebuild symbols_fts from symbols table (INGR-I-06)
|
|
171
|
+
# For external-content FTS5 tables, use the 'rebuild' command instead of
|
|
172
|
+
# DELETE + INSERT. The rebuild command re-reads all content from the
|
|
173
|
+
# content table and repopulates the FTS index atomically.
|
|
174
|
+
conn.execute("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild')")
|
|
175
|
+
conn.commit()
|
|
176
|
+
|
|
177
|
+
logger.info("Ingested %d symbols for Python %s", count, version)
|
|
178
|
+
return count
|