Coverage for archdocs/features/sqlalchemy/parser.py: 100%
17 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 typing
4from archdocs import prefilter, settings
5from archdocs.features.sqlalchemy.const import SQLAlchemyFeatures
8_ASYNC_ENGINE_PATTERN: typing.Final = py_re.compile(r"sqlalchemy\.ext\.asyncio", settings.TYPICAL_RE_FLAGS)
9# The windows are what keep both patterns honest on somebody's whole project: an unbounded gap
10# reads "pool_ anywhere later in the file" as pooling and costs quadratic time on sources where
11# `create_engine(` repeats without a match, a killer measured in minutes on a single file.
12_POOLING_PATTERN: typing.Final = py_re.compile(r"create_engine\(.{0,2000}?pool_", settings.TYPICAL_RE_FLAGS)
13_MULTIPLE_HOSTS_PATTERN: typing.Final = py_re.compile(
14 r"create_engine\([^)'\"]{0,200}['\"](?:postgresql|mysql|mariadb|oracle|mssql)\+[^'\"]*://[^/,]+(?:,[^/,]+)+/",
15 settings.TYPICAL_RE_FLAGS,
16)
17# The attribute is written three ways and the value is hyphenated in the two that matter:
18# `?target_session_attrs=read-write` inside a dsn, `"target_session_attrs": "read-write"` in
19# `connect_args`, and a bare setting. Quotes around the key and around the value are optional
20# on purpose — demanding them is what used to make this pattern unable to match a dsn at all.
21_TARGET_SESSION_ATTRS_PATTERN: typing.Final = py_re.compile(
22 r"target_session_attrs['\"]?\s*[=:]\s*['\"]?(?P<attrs_value>[\w-]+)",
23 settings.TYPICAL_RE_FLAGS,
24)
25_DB_TYPE_PATTERN: typing.Final = py_re.compile(
26 r"['\"](postgresql(?:\+[^'\"]*)?|mysql(?:\+[^'\"]*)?|sqlite(?:\+[^'\"]*)?|oracle(?:\+[^'\"]*)?|"
27 r"mssql(?:\+[^'\"]*)?|mariadb(?:\+[^'\"]*)?|cockroachdb(?:\+[^'\"]*)?)['\"]",
28 settings.TYPICAL_RE_FLAGS,
29)
30# One union of literals for the whole feature: an engine is found not only by the package
31# name but by a bare DSN in a string, so the database schemes of `_DB_TYPE_PATTERN` belong here too.
32_SQLALCHEMY_LITERALS: typing.Final = (
33 "sqlalchemy",
34 "create_engine",
35 "target_session_attrs",
36 "postgresql",
37 "mysql",
38 "sqlite",
39 "oracle",
40 "mssql",
41 "mariadb",
42 "cockroachdb",
43)
44_EMPTY_FEATURES: typing.Final = SQLAlchemyFeatures(
45 async_used=False,
46 pooling_used=False,
47 multiple_hosts=False,
48 target_session_attrs="",
49 database_type="",
50)
53def find_sqlalchemy_features(raw_source: str) -> SQLAlchemyFeatures:
54 if not prefilter.contains_any_literal(raw_source.lower(), _SQLALCHEMY_LITERALS):
55 return _EMPTY_FEATURES
56 target_session_attrs_match: typing.Final = _TARGET_SESSION_ATTRS_PATTERN.search(raw_source)
57 database_type_match: typing.Final = _DB_TYPE_PATTERN.search(raw_source)
58 return SQLAlchemyFeatures(
59 async_used=_ASYNC_ENGINE_PATTERN.search(raw_source) is not None,
60 pooling_used=_POOLING_PATTERN.search(raw_source) is not None,
61 multiple_hosts=_MULTIPLE_HOSTS_PATTERN.search(raw_source) is not None,
62 target_session_attrs=target_session_attrs_match.group("attrs_value") if target_session_attrs_match else "",
63 database_type=database_type_match.group(1) if database_type_match else "",
64 )