/
liquid-g
/
liquid-code
Обзор
Документация
Войти
/
liquid-g
/
liquid-code
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/liquidcode/routing.py
390 строк
15 KB
User
0.3.0-RC - дописаны тесты и мелкие исправления
04 июл 2026, 13:07
04 июл 2026, 13:07
40ad8b8
Код
Авторство
О чём код?
""" Маршрутизация LiquidCode. Содержит классы Route, Router, RouteGroup и декоратор route для привязки методов контроллеров к URL-путям. """ import inspect import re from typing import ( Any, Callable, Dict, List, Optional, Pattern, Tuple, Type, Union, ) class Route: """ Объект маршрута, содержащий информацию о пути, HTTP-методах и связанном контроллере. """ def __init__( self, path: str, methods: List[str], controller: Any, group_middlewares: Optional[List[Callable]] = None, ) -> None: """ Args: path: Строка пути с возможными параметрами {name} или {name:regex}. methods: Список HTTP-методов (например, ['GET', 'POST']). controller: Callable-объект или кортеж (класс, имя_метода). group_middlewares: Список middleware для группы маршрутов. """ self.path = path self.methods = [m.upper() for m in methods] self.controller = controller self.group_middlewares = group_middlewares or [] self.pattern: Pattern = self._compile_pattern(path) def _compile_pattern(self, path: str) -> Pattern: """ Преобразует путь с параметрами в регулярное выражение. Поддерживает: - {name} -> именованная группа (?P<name>[^/]+) - {name:regex} -> именованная группа с указанным regex Returns: Скомпилированное регулярное выражение. """ def repl(match: re.Match) -> str: name = match.group(1) pattern = match.group(2) or "[^/]+" return f"(?P<{name}>{pattern})" # Убираем избыточное экранирование '}' regex = re.sub(r"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?\}", repl, path) regex = f"^{regex}$" return re.compile(regex) def match(self, path: str) -> Optional[Dict[str, str]]: """ Проверяет, соответствует ли переданный путь данному маршруту. Returns: Словарь с извлечёнными параметрами или None, если путь не совпадает. """ match = self.pattern.match(path) if match: return match.groupdict() return None class Router: """ Хранилище маршрутов с возможностью поиска по пути и методу. """ def __init__(self) -> None: self._routes: List[Route] = [] def add_route( self, path: str, controller: Union[Callable, Tuple[Type, str]], methods: List[str], group_middlewares: Optional[List[Callable]] = None, ) -> None: """ Добавляет новый маршрут в коллекцию. Автоматически создаёт вариант с завершающим слешем, если путь не заканчивается на '/'. Args: path: Строка пути с параметрами. controller: Callback-функция или кортеж (класс, имя_метода). methods: Список HTTP-методов. group_middlewares: Список middleware для группы маршрутов. Raises: TypeError: Если controller не является callable или кортежем (класс, метод). ValueError: Если methods не список строк или возникают конфликты. """ # Валидация controller if not callable(controller) and not ( isinstance(controller, tuple) and len(controller) == 2 and isinstance(controller[0], type) and isinstance(controller[1], str) ): raise TypeError("controller must be callable or tuple (class, method_name)") # Нормализация методов if not methods: methods = ["GET"] methods = [m.upper() for m in methods] if not all(isinstance(m, str) for m in methods): raise ValueError("methods must be a list of strings") # Проверка конфликтов (одинаковый путь и пересекающиеся методы) for route in self._routes: if route.path == path and set(route.methods) & set(methods): raise ValueError( f"Route conflict: path '{path}' " f"with methods {methods} already exists" ) # Добавляем основной маршрут route = Route(path, methods, controller, group_middlewares) self._routes.append(route) # Если путь не заканчивается на '/' и не равен '/', добавляем вариант со слешем if path != "/" and not path.endswith("/"): path_with_slash = path + "/" # Проверяем конфликт для варианта со слешем for existing_route in self._routes: if existing_route.path == path_with_slash and set( existing_route.methods ) & set(methods): # Если такой маршрут уже существует, пропускаем return route_slash = Route(path_with_slash, methods, controller, group_middlewares) self._routes.append(route_slash) def add_routes_from_controller(self, controller_class: Type) -> None: """ Сканирует все методы (включая унаследованные) и регистрирует те, которые помечены декоратором @route. В качестве контроллера сохраняется кортеж (controller_class, method_name). """ for _, method in inspect.getmembers( controller_class, predicate=inspect.isfunction ): if hasattr(method, "_route_path") and hasattr(method, "_route_methods"): path = method._route_path methods = method._route_methods if not isinstance(methods, list): methods = list(methods) if methods else ["GET"] self.add_route( path, (controller_class, method.__name__), methods, group_middlewares=None, # пока нет поддержки для контроллеров в группах ) def match( self, path: str, method: str ) -> Optional[ Tuple[Union[Callable, Tuple[Type, str]], Dict[str, str], List[Callable]] ]: """ Ищет маршрут, соответствующий переданным пути и методу. Returns: Кортеж (контроллер, параметры, group_middlewares) или None, если маршрут не найден. """ method = method.upper() for route in self._routes: if method not in route.methods: continue params = route.match(path) if params is not None: return route.controller, params, route.group_middlewares return None def has_route(self, path: str, method: str) -> bool: """ Проверяет, существует ли маршрут для указанного пути и метода. """ method = method.upper() for route in self._routes: if route.path == path and method in route.methods: return True return False def all_routes(self) -> List[Dict[str, Any]]: """ Возвращает список всех зарегистрированных маршрутов в удобочитаемом виде. Полезно для документации, отладки или генерации OpenAPI-спецификации. """ result = [] for route in self._routes: controller_info = ( route.controller.__qualname__ if callable(route.controller) else f"{route.controller[0].__name__}.{route.controller[1]}" ) result.append( { "path": route.path, "methods": route.methods, "controller": controller_info, } ) return result def group( self, prefix: str, *args, middlewares: Optional[List[Callable]] = None, **kwargs, ) -> "RouteGroup": """ Создаёт группу маршрутов с общим префиксом и middleware. Args: prefix: Префикс URL для всех маршрутов в группе. middlewares: Список middleware, применяемых ко всем маршрутам группы. Returns: Объект RouteGroup. """ return RouteGroup(self, prefix, middlewares) def route(self, path: str, methods: Optional[List[str]] = None): """ Декоратор для добавления маршрута. Args: path: Путь URL. methods: Список HTTP-методов (по умолчанию ['GET']). Returns: Декоратор, который регистрирует маршрут. """ def decorator(func): self.add_route(path, func, methods or ["GET"]) return func return decorator class RouteGroup: """ Группа маршрутов с общим префиксом и middleware. Пример: api = router.group('/api', middlewares=[AuthMiddleware()]) @api.route('/users') def get_users(request): return {'users': []} """ def __init__( self, router: Router, prefix: str = "", middlewares: Optional[List[Callable]] = None, ): """ Args: router: Экземпляр роутера. prefix: Префикс URL для группы. middlewares: Список middleware для группы. """ self.router = router # Нормализуем префикс: убираем завершающий слеш, добавляем начальный self.prefix = prefix.rstrip("/") if self.prefix and not self.prefix.startswith("/"): self.prefix = "/" + self.prefix self.middlewares = middlewares or [] def route(self, path: str, methods: Optional[List[str]] = None): """ Декоратор для добавления маршрута в группу. Args: path: Путь (относительно префикса группы). methods: Список HTTP-методов (по умолчанию ['GET']). Returns: Декоратор, который регистрирует маршрут в роутере с префиксом группы. """ def decorator(func): full_path = self.prefix + path if not full_path.startswith("/"): full_path = "/" + full_path self.router.add_route( path=full_path, controller=func, methods=methods or ["GET"], group_middlewares=self.middlewares, ) return func return decorator def add_route( self, path: str, controller: Callable, methods: Optional[List[str]] = None, ): """ Явное добавление маршрута в группу (без декоратора). """ full_path = self.prefix + path if not full_path.startswith("/"): full_path = "/" + full_path self.router.add_route( path=full_path, controller=controller, methods=methods or ["GET"], group_middlewares=self.middlewares, ) def group( self, prefix: str, *args, middlewares: Optional[List[Callable]] = None, **kwargs, ) -> "RouteGroup": """ Создаёт вложенную группу с объединённым префиксом и middleware. Args: prefix: Дополнительный префикс. middlewares: Дополнительные middleware для вложенной группы. *args, **kwargs: Дополнительные аргументы (пока игнорируются, но могут быть использованы в будущем). Returns: Новая группа маршрутов. """ new_prefix = self.prefix + prefix combined_middlewares = self.middlewares + (middlewares or []) return RouteGroup(self.router, new_prefix, combined_middlewares) def route(path: str, methods: Optional[List[str]] = None) -> Callable: """ Декоратор для привязки метода контроллера к маршруту. Пример: @route('/users/{id}', methods=['GET']) def get_user(self, id: int): return {'user': id} Args: path: Шаблон пути (может содержать параметры вида {name}). methods: Список HTTP-методов. Если None или пустой список, используется ['GET']. Returns: Декорированная функция с атрибутами _route_path и _route_methods. """ if not methods: methods = ["GET"] methods = [m.upper() for m in methods] def decorator(func: Callable) -> Callable: func._route_path = path func._route_methods = methods return func return decorator