Coverage for archdocs/features/app_servers/parser.py: 100%
37 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 00:00 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 00:00 +0000
1import dataclasses
2import re as py_re
3import types
4import typing
6from archdocs import prefilter, settings
7from archdocs.features.app_servers.const import ApplicationServerEnum, ApplicationServerFeatures
10@typing.final
11@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
12class _ServerSignature:
13 server_name: str
14 prefilter_literals: tuple[str, ...]
15 compiled_pattern: py_re.Pattern[str]
18# Servers not found by the module name alone: each has its own entry point, and werkzeug
19# is also reachable through a literal wider than its package name.
20_PATTERN_OF_SERVER: typing.Final = types.MappingProxyType(
21 {
22 ApplicationServerEnum.tornado_server: r"\btornado\.(?:httpserver|web|ioloop)\b",
23 ApplicationServerEnum.gevent_server: r"\bgevent\.pywsgi\b",
24 ApplicationServerEnum.eventlet_server: r"\beventlet\.wsgi\b",
25 ApplicationServerEnum.werkzeug_server: r"\bwerkzeug\.serving\b|\brun_simple\s*\(",
26 ApplicationServerEnum.wsgiref_server: r"\bwsgiref\.simple_server\b",
27 },
28)
29_LITERALS_OF_SERVER: typing.Final = types.MappingProxyType(
30 {ApplicationServerEnum.werkzeug_server: ("werkzeug", "run_simple")},
31)
32_SERVER_SIGNATURES: typing.Final = tuple(
33 _ServerSignature(
34 server_name=one_server.value,
35 prefilter_literals=_LITERALS_OF_SERVER.get(one_server, (one_server.value,)),
36 compiled_pattern=py_re.compile(
37 _PATTERN_OF_SERVER.get(
38 one_server,
39 rf"\b(?:from|import)\s+{one_server.value}\b|\b{one_server.value}\s+(?:--\w|[\w.]+:\w+)",
40 ),
41 flags=settings.TYPICAL_RE_FLAGS,
42 ),
43 )
44 for one_server in ApplicationServerEnum
45)
46_WORKER_CLASS_LITERALS: typing.Final = ("worker_class", "--worker-class")
47_WORKER_CLASS_PATTERN: typing.Final = py_re.compile(
48 r"(?:worker_class\s*=|--worker-class)[\s=\"']*(?P<worker_class>[\w.]+)",
49 flags=settings.TYPICAL_RE_FLAGS,
50)
51_WORKERS_COUNT_PATTERN: typing.Final = py_re.compile(
52 r"(?:workers\s*=|--workers|\s-w)[\s=\"']*(?P<found_number>\d{1,3})\b",
53 flags=settings.TYPICAL_RE_FLAGS,
54)
55_LISTEN_PORT_PATTERN: typing.Final = py_re.compile(
56 r"(?:(?:port\s*=|--port)[\s=\"']*|(?:bind|--bind)[\s=\"']*[\w.\[\]]*:)(?P<found_number>\d{2,5})\b",
57 flags=settings.TYPICAL_RE_FLAGS,
58)
59_TLS_PATTERN: typing.Final = py_re.compile(
60 r"\bssl_cert\w*\b|\bssl_key\w*\b|\bcertfile\b|\bkeyfile\b|\bssl_context\b|--ssl-\w+",
61 flags=settings.TYPICAL_RE_FLAGS,
62)
63_HTTP2_PATTERN: typing.Final = py_re.compile(
64 r"\bhttp2\b|\bhttp\s*=\s*[\"']?2\b|--http[\s=]+2\b",
65 flags=settings.TYPICAL_RE_FLAGS,
66)
69def _read_first_number(number_pattern: py_re.Pattern[str], raw_source: str, /) -> int:
70 number_match: typing.Final = number_pattern.search(raw_source)
71 if number_match is None:
72 return 0
73 return int(number_match.group("found_number"))
76def _find_servers_behind_worker_class(raw_source: str, lowered_source: str, /) -> set[str]:
77 if not prefilter.contains_any_literal(lowered_source, _WORKER_CLASS_LITERALS):
78 return set()
79 worker_class_match: typing.Final = _WORKER_CLASS_PATTERN.search(raw_source)
80 if worker_class_match is None:
81 return set()
82 worker_class_name: typing.Final = worker_class_match.group("worker_class").lower()
83 return {ApplicationServerEnum.gunicorn_server.value} | {
84 one_server.value for one_server in ApplicationServerEnum if one_server.value in worker_class_name
85 }
88def find_application_server_features(raw_source: str) -> ApplicationServerFeatures:
89 lowered_source: typing.Final = raw_source.lower()
90 servers_found: typing.Final = {
91 one_signature.server_name
92 for one_signature in _SERVER_SIGNATURES
93 if prefilter.contains_any_literal(lowered_source, one_signature.prefilter_literals)
94 and one_signature.compiled_pattern.search(raw_source)
95 } | _find_servers_behind_worker_class(raw_source, lowered_source)
96 if not servers_found:
97 return ApplicationServerFeatures()
98 return ApplicationServerFeatures(
99 servers_used=frozenset(servers_found),
100 workers_count=_read_first_number(_WORKERS_COUNT_PATTERN, raw_source),
101 listen_port=_read_first_number(_LISTEN_PORT_PATTERN, raw_source),
102 tls_enabled=bool(_TLS_PATTERN.search(raw_source)),
103 http2_enabled=bool(_HTTP2_PATTERN.search(raw_source)),
104 )