Spaces:
Sleeping
Sleeping
File size: 7,961 Bytes
bcc8074 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
"""
Route Matcher - URL pattern matching for service configuration.
Provides flexible URL matching capabilities for services to define
which routes require auth, credits, etc.
Supported patterns:
- Exact match: "/api/users"
- Prefix match: "/api/*" (matches /api/anything)
- Wildcard match: "/api/users/*/posts" (matches /api/users/123/posts)
- Deep wildcard: "/api/**" (matches /api/users/123/posts/456)
- Regex match: "^/api/v[0-9]+/.*$"
Usage:
matcher = RouteMatcher(["/api/*", "/admin/**"])
if matcher.matches("/api/users"):
# Route requires auth
pass
"""
import re
import logging
from typing import List, Set, Optional, Pattern
from fnmatch import fnmatch
logger = logging.getLogger(__name__)
class RouteMatcher:
"""
Flexible URL pattern matcher for route configuration.
Supports exact matches, glob patterns, and regex patterns.
"""
def __init__(self, patterns: List[str]):
"""
Initialize route matcher with patterns.
Args:
patterns: List of URL patterns to match
"""
self.patterns = patterns
self._exact_matches: Set[str] = set()
self._prefix_patterns: List[str] = []
self._glob_patterns: List[str] = []
self._regex_patterns: List[Pattern] = []
# Classify patterns for performance
self._classify_patterns()
def _classify_patterns(self) -> None:
"""
Classify patterns by type for optimal matching performance.
Order of matching:
1. Exact matches (fastest - O(1))
2. Prefix patterns (fast - string startswith)
3. Glob patterns (medium - fnmatch)
4. Regex patterns (slowest - regex matching)
"""
for pattern in self.patterns:
# Empty pattern
if not pattern:
continue
# Regex pattern (starts with ^)
if pattern.startswith("^"):
try:
compiled = re.compile(pattern)
self._regex_patterns.append(compiled)
logger.debug(f"Classified as regex: {pattern}")
except re.error as e:
logger.warning(f"Invalid regex pattern '{pattern}': {e}")
continue
# Glob pattern (contains * or ?)
if "*" in pattern or "?" in pattern:
# Simple prefix wildcard: /api/*
if pattern.endswith("/*") and "*" not in pattern[:-2]:
prefix = pattern[:-2] # Remove /*
self._prefix_patterns.append(prefix)
logger.debug(f"Classified as prefix: {prefix}")
else:
# Complex glob: /api/*/users or /api/**
self._glob_patterns.append(pattern)
logger.debug(f"Classified as glob: {pattern}")
continue
# Exact match
self._exact_matches.add(pattern)
logger.debug(f"Classified as exact: {pattern}")
def matches(self, path: str) -> bool:
"""
Check if a URL path matches any configured pattern.
Args:
path: URL path to check (e.g., "/api/users/123")
Returns:
True if path matches any pattern, False otherwise
"""
# Strip query parameters and fragments
path = path.split("?")[0].split("#")[0]
# Normalize path (remove trailing slash unless it's just "/")
if path != "/" and path.endswith("/"):
path = path.rstrip("/")
# 1. Exact match (O(1))
if path in self._exact_matches:
return True
# 2. Prefix match (O(n) but fast)
for prefix in self._prefix_patterns:
if path.startswith(prefix + "/") or path == prefix:
return True
# 3. Glob match (O(n))
for pattern in self._glob_patterns:
if fnmatch(path, pattern):
return True
# 4. Regex match (O(n) but slower)
for regex in self._regex_patterns:
if regex.match(path):
return True
return False
def get_matching_pattern(self, path: str) -> Optional[str]:
"""
Get the first pattern that matches the given path.
Useful for debugging or determining which rule matched.
Args:
path: URL path to check
Returns:
Matching pattern string or None
"""
# Strip query parameters and fragments
path = path.split("?")[0].split("#")[0]
# Normalize path
if path != "/" and path.endswith("/"):
path = path.rstrip("/")
# Exact match
if path in self._exact_matches:
return path
# Prefix match
for prefix in self._prefix_patterns:
if path.startswith(prefix + "/") or path == prefix:
return prefix + "/*"
# Glob match
for pattern in self._glob_patterns:
if fnmatch(path, pattern):
return pattern
# Regex match
for regex in self._regex_patterns:
if regex.match(path):
return regex.pattern
return None
def __repr__(self) -> str:
"""String representation for debugging."""
return (
f"RouteMatcher("
f"exact={len(self._exact_matches)}, "
f"prefix={len(self._prefix_patterns)}, "
f"glob={len(self._glob_patterns)}, "
f"regex={len(self._regex_patterns)})"
)
class RouteConfig:
"""
Route configuration helper for services.
Manages multiple route lists (required, optional, public) with
precedence and exclusion logic.
"""
def __init__(
self,
required: List[str] = None,
optional: List[str] = None,
public: List[str] = None,
):
"""
Initialize route configuration.
Args:
required: Routes that REQUIRE the service (e.g., auth required)
optional: Routes where service is OPTIONAL (e.g., auth optional)
public: Routes that are PUBLIC (e.g., no auth needed)
Precedence: public > required > optional (for conflict resolution)
"""
self.required_matcher = RouteMatcher(required or [])
self.optional_matcher = RouteMatcher(optional or [])
self.public_matcher = RouteMatcher(public or [])
def is_required(self, path: str) -> bool:
"""
Check if service is REQUIRED for this path.
Returns False if path is public (public takes precedence).
"""
if self.is_public(path):
return False
return self.required_matcher.matches(path)
def is_optional(self, path: str) -> bool:
"""
Check if service is OPTIONAL for this path.
Returns False if path is public or required.
"""
if self.is_public(path):
return False
if self.required_matcher.matches(path):
return False
return self.optional_matcher.matches(path)
def is_public(self, path: str) -> bool:
"""
Check if path is PUBLIC (service not needed).
Public takes highest precedence.
"""
return self.public_matcher.matches(path)
def requires_service(self, path: str) -> bool:
"""
Check if service is needed (required OR optional) for this path.
Returns False if path is not matched by any configuration.
"""
return self.is_required(path) or self.is_optional(path)
__all__ = [
'RouteMatcher',
'RouteConfig',
]
|