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