pyserveX/pyserve/_routing_py.py
Илья Глазунов eeeccd57da
Some checks failed
Lint Code / lint (push) Failing after 44s
CI/CD Pipeline / lint (push) Successful in 0s
Run Tests / test (3.12) (push) Successful in 3m48s
Run Tests / test (3.13) (push) Successful in 3m7s
CI/CD Pipeline / test (push) Successful in 1s
CI/CD Pipeline / build-and-release (push) Has been skipped
CI/CD Pipeline / notify (push) Successful in 1s
Cython routing added
2026-01-31 02:44:50 +03:00

130 lines
4.1 KiB
Python

"""
Pure Python fallback for _routing when PCRE2/Cython is not available.
This module provides the same interface using the standard library `re` module.
It's slower than the Cython+PCRE2 implementation but works everywhere.
In future we may add pcre2.py library support for better performance in this module.
"""
import re
from typing import Any, Dict, List, Optional, Pattern, Tuple
class FastRouteMatch:
__slots__ = ("config", "params")
def __init__(self, config: Dict[str, Any], params: Optional[Dict[str, str]] = None):
self.config = config
self.params = params if params is not None else {}
class FastRouter:
"""
Router with regex pattern matching.
Matching order (nginx-like):
1. Exact routes (prefix "=") - O(1) dict lookup
2. Regex routes (prefix "~" or "~*") - linear scan
3. Default route (fallback)
"""
__slots__ = ("_exact_routes", "_regex_routes", "_default_route", "_has_default", "_regex_count")
def __init__(self) -> None:
self._exact_routes: Dict[str, Dict[str, Any]] = {}
self._regex_routes: List[Tuple[Pattern[str], Dict[str, Any]]] = []
self._default_route: Dict[str, Any] = {}
self._has_default: bool = False
self._regex_count: int = 0
def add_route(self, pattern: str, config: Dict[str, Any]) -> None:
if pattern.startswith("="):
exact_path = pattern[1:]
self._exact_routes[exact_path] = config
return
if pattern == "__default__":
self._default_route = config
self._has_default = True
return
if pattern.startswith("~"):
case_insensitive = pattern.startswith("~*")
regex_pattern = pattern[2:] if case_insensitive else pattern[1:]
flags = re.IGNORECASE if case_insensitive else 0
try:
compiled_pattern = re.compile(regex_pattern, flags)
self._regex_routes.append((compiled_pattern, config))
self._regex_count = len(self._regex_routes)
except re.error:
pass # Ignore invalid patterns
def match(self, path: str) -> Optional[FastRouteMatch]:
if path in self._exact_routes:
config = self._exact_routes[path]
return FastRouteMatch(config, {})
for pattern, config in self._regex_routes:
match_obj = pattern.search(path)
if match_obj is not None:
params = match_obj.groupdict()
return FastRouteMatch(config, params)
if self._has_default:
return FastRouteMatch(self._default_route, {})
return None
@property
def exact_routes(self) -> Dict[str, Dict[str, Any]]:
return self._exact_routes
@property
def routes(self) -> Dict[Pattern[str], Dict[str, Any]]:
return {p: c for p, c in self._regex_routes}
@property
def default_route(self) -> Optional[Dict[str, Any]]:
return self._default_route if self._has_default else None
def list_routes(self) -> List[Dict[str, Any]]:
result: List[Dict[str, Any]] = []
for path, config in self._exact_routes.items():
result.append({
"type": "exact",
"pattern": f"={path}",
"config": config,
})
for pattern, config in self._regex_routes:
result.append({
"type": "regex",
"pattern": pattern.pattern,
"config": config,
})
if self._has_default:
result.append({
"type": "default",
"pattern": "__default__",
"config": self._default_route,
})
return result
def fast_match(router: FastRouter, path: str) -> Optional[FastRouteMatch]:
"""
Convenience function for matching a path.
Args:
router: FastRouter instance
path: URL path to match
Returns:
FastRouteMatch or None
"""
return router.match(path)