aio-libs / aiohttp
1 10
import abc
2 10
import base64
3 10
import hashlib
4 10
import keyword
5 10
import os
6 10
import re
7 10
from contextlib import contextmanager
8 10
from pathlib import Path
9 10
from types import MappingProxyType
10 10
from typing import (
11
    TYPE_CHECKING,
12
    Any,
13
    Awaitable,
14
    Callable,
15
    Container,
16
    Dict,
17
    Generator,
18
    Iterable,
19
    Iterator,
20
    List,
21
    Mapping,
22
    Optional,
23
    Pattern,
24
    Set,
25
    Sized,
26
    Tuple,
27
    Type,
28
    Union,
29
    cast,
30
)
31

32 10
from typing_extensions import Final, TypedDict
33 10
from yarl import URL, __version__ as yarl_version  # type: ignore[attr-defined]
34

35 10
from . import hdrs
36 10
from .abc import AbstractMatchInfo, AbstractRouter, AbstractView
37 10
from .helpers import DEBUG, iscoroutinefunction
38 10
from .http import HttpVersion11
39 10
from .typedefs import PathLike
40 10
from .web_exceptions import (
41
    HTTPException,
42
    HTTPExpectationFailed,
43
    HTTPForbidden,
44
    HTTPMethodNotAllowed,
45
    HTTPNotFound,
46
)
47 10
from .web_fileresponse import FileResponse
48 10
from .web_request import Request
49 10
from .web_response import Response, StreamResponse
50 10
from .web_routedef import AbstractRouteDef
51

52 10
__all__ = (
53
    "UrlDispatcher",
54
    "UrlMappingMatchInfo",
55
    "AbstractResource",
56
    "Resource",
57
    "PlainResource",
58
    "DynamicResource",
59
    "AbstractRoute",
60
    "ResourceRoute",
61
    "StaticResource",
62
    "View",
63
)
64

65

66
if TYPE_CHECKING:  # pragma: no cover
67
    from .web_app import Application
68

69
    BaseDict = Dict[str, str]
70
else:
71 10
    BaseDict = dict
72

73 10
YARL_VERSION: Final[Tuple[int, ...]] = tuple(map(int, yarl_version.split(".")[:2]))
74

75 10
HTTP_METHOD_RE: Final[Pattern[str]] = re.compile(
76
    r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$"
77
)
78 10
ROUTE_RE: Final[Pattern[str]] = re.compile(
79
    r"(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})"
80
)
81 10
PATH_SEP: Final[str] = re.escape("/")
82

83

84 10
_WebHandler = Callable[[Request], Awaitable[StreamResponse]]
85 10
_ExpectHandler = Callable[[Request], Awaitable[None]]
86 10
_Resolve = Tuple[Optional[AbstractMatchInfo], Set[str]]
87

88

89 10
class _InfoDict(TypedDict, total=False):
90 10
    path: str
91

92 10
    formatter: str
93 10
    pattern: Pattern[str]
94

95 10
    directory: Path
96 10
    prefix: str
97 10
    routes: Mapping[str, "AbstractRoute"]
98

99 10
    app: "Application"
100

101 10
    domain: str
102

103 10
    rule: "AbstractRuleMatching"
104

105 10
    http_exception: HTTPException
106

107

108 10
class AbstractResource(Sized, Iterable["AbstractRoute"]):
109 10
    def __init__(self, *, name: Optional[str] = None) -> None:
110 10
        self._name = name
111

112 10
    @property
113 10
    def name(self) -> Optional[str]:
114 10
        return self._name
115

116 10
    @property
117 10
    @abc.abstractmethod
118 10
    def canonical(self) -> str:
119
        """Exposes the resource's canonical path.
120

121
        For example '/foo/bar/{name}'
122

123
        """
124

125 10
    @abc.abstractmethod  # pragma: no branch
126 10
    def url_for(self, **kwargs: str) -> URL:
127
        """Construct url for resource with additional params."""
128

129 10
    @abc.abstractmethod  # pragma: no branch
130 10
    async def resolve(self, request: Request) -> _Resolve:
131
        """Resolve resource
132

133
        Return (UrlMappingMatchInfo, allowed_methods) pair."""
134

135 10
    @abc.abstractmethod
136 10
    def add_prefix(self, prefix: str) -> None:
137
        """Add a prefix to processed URLs.
138

139
        Required for subapplications support.
140

141
        """
142

143 10
    @abc.abstractmethod
144 10
    def get_info(self) -> _InfoDict:
145
        """Return a dict with additional info useful for introspection"""
146

147 10
    def freeze(self) -> None:
148 10
        pass
149

150 10
    @abc.abstractmethod
151 10
    def raw_match(self, path: str) -> bool:
152
        """Perform a raw match against path"""
153

154

155 10
class AbstractRoute(abc.ABC):
156 10
    def __init__(
157
        self,
158
        method: str,
159
        handler: Union[_WebHandler, Type[AbstractView]],
160
        *,
161
        expect_handler: Optional[_ExpectHandler] = None,
162
        resource: Optional[AbstractResource] = None,
163
    ) -> None:
164

165 10
        if expect_handler is None:
166 10
            expect_handler = _default_expect_handler
167

168 10
        assert iscoroutinefunction(
169
            expect_handler
170
        ), f"Coroutine is expected, got {expect_handler!r}"
171

172 10
        method = method.upper()
173 10
        if not HTTP_METHOD_RE.match(method):
174 10
            raise ValueError(f"{method} is not allowed HTTP method")
175

176 10
        if iscoroutinefunction(handler):
177 10
            pass
178 10
        elif isinstance(handler, type) and issubclass(handler, AbstractView):
179 10
            pass
180
        else:
181 10
            raise TypeError(
182
                "Only async functions are allowed as web-handlers "
183
                ", got {!r}".format(handler)
184
            )
185

186 10
        self._method = method
187 10
        self._handler = handler
188 10
        self._expect_handler = expect_handler
189 10
        self._resource = resource
190

191 10
    @property
192 10
    def method(self) -> str:
193 10
        return self._method
194

195 10
    @property
196 10
    def handler(self) -> _WebHandler:
197 10
        return self._handler
198

199 10
    @property
200 10
    @abc.abstractmethod
201 10
    def name(self) -> Optional[str]:
202
        """Optional route's name, always equals to resource's name."""
203

204 10
    @property
205 10
    def resource(self) -> Optional[AbstractResource]:
206 10
        return self._resource
207

208 10
    @abc.abstractmethod
209 10
    def get_info(self) -> _InfoDict:
210
        """Return a dict with additional info useful for introspection"""
211

212 10
    @abc.abstractmethod  # pragma: no branch
213 10
    def url_for(self, *args: str, **kwargs: str) -> URL:
214
        """Construct url for route with additional params."""
215

216 10
    async def handle_expect_header(self, request: Request) -> None:
217 10
        await self._expect_handler(request)
218

219

220 10
class UrlMappingMatchInfo(BaseDict, AbstractMatchInfo):
221 10
    def __init__(self, match_dict: Dict[str, str], route: AbstractRoute):
222 10
        super().__init__(match_dict)
223 10
        self._route = route
224 10
        self._apps = []  # type: List[Application]
225 10
        self._current_app = None  # type: Optional[Application]
226 10
        self._frozen = False
227

228 10
    @property
229 10
    def handler(self) -> _WebHandler:
230 10
        return self._route.handler
231

232 10
    @property
233 10
    def route(self) -> AbstractRoute:
234 10
        return self._route
235

236 10
    @property
237 10
    def expect_handler(self) -> _ExpectHandler:
238 10
        return self._route.handle_expect_header
239

240 10
    @property
241 10
    def http_exception(self) -> Optional[HTTPException]:
242 10
        return None
243

244 10
    def get_info(self) -> _InfoDict:  # type: ignore[override]
245 10
        return self._route.get_info()
246

247 10
    @property
248 10
    def apps(self) -> Tuple["Application", ...]:
249 10
        return tuple(self._apps)
250

251 10
    def add_app(self, app: "Application") -> None:
252 10
        if self._frozen:
253 10
            raise RuntimeError("Cannot change apps stack after .freeze() call")
254 10
        if self._current_app is None:
255 10
            self._current_app = app
256 10
        self._apps.insert(0, app)
257

258 10
    @property
259 10
    def current_app(self) -> "Application":
260 10
        app = self._current_app
261 10
        assert app is not None
262 10
        return app
263

264 10
    @contextmanager
265 10
    def set_current_app(self, app: "Application") -> Generator[None, None, None]:
266
        if DEBUG:  # pragma: no cover
267
            if app not in self._apps:
268
                raise RuntimeError(
269
                    "Expected one of the following apps {!r}, got {!r}".format(
270
                        self._apps, app
271
                    )
272
                )
273 10
        prev = self._current_app
274 10
        self._current_app = app
275 10
        try:
276 10
            yield
277
        finally:
278 10
            self._current_app = prev
279

280 10
    def freeze(self) -> None:
281 10
        self._frozen = True
282

283 10
    def __repr__(self) -> str:
284 10
        return f"<MatchInfo {super().__repr__()}: {self._route}>"
285

286

287 10
class MatchInfoError(UrlMappingMatchInfo):
288 10
    def __init__(self, http_exception: HTTPException) -> None:
289 10
        self._exception = http_exception
290 10
        super().__init__({}, SystemRoute(self._exception))
291

292 10
    @property
293 10
    def http_exception(self) -> HTTPException:
294 10
        return self._exception
295

296 10
    def __repr__(self) -> str:
297 10
        return "<MatchInfoError {}: {}>".format(
298
            self._exception.status, self._exception.reason
299
        )
300

301

302 10
async def _default_expect_handler(request: Request) -> None:
303
    """Default handler for Expect header.
304

305
    Just send "100 Continue" to client.
306
    raise HTTPExpectationFailed if value of header is not "100-continue"
307
    """
308 10
    expect = request.headers.get(hdrs.EXPECT, "")
309 10
    if request.version == HttpVersion11:
310 10
        if expect.lower() == "100-continue":
311 10
            await request.writer.write(b"HTTP/1.1 100 Continue\r\n\r\n")
312
        else:
313 10
            raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)
314

315

316 10
class Resource(AbstractResource):
317 10
    def __init__(self, *, name: Optional[str] = None) -> None:
318 10
        super().__init__(name=name)
319 10
        self._routes = []  # type: List[ResourceRoute]
320

321 10
    def add_route(
322
        self,
323
        method: str,
324
        handler: Union[Type[AbstractView], _WebHandler],
325
        *,
326
        expect_handler: Optional[_ExpectHandler] = None,
327
    ) -> "ResourceRoute":
328

329 10
        for route_obj in self._routes:
330 10
            if route_obj.method == method or route_obj.method == hdrs.METH_ANY:
331 10
                raise RuntimeError(
332
                    "Added route will never be executed, "
333
                    "method {route.method} is already "
334
                    "registered".format(route=route_obj)
335
                )
336

337 10
        route_obj = ResourceRoute(method, handler, self, expect_handler=expect_handler)
338 10
        self.register_route(route_obj)
339 10
        return route_obj
340

341 10
    def register_route(self, route: "ResourceRoute") -> None:
342 10
        assert isinstance(
343
            route, ResourceRoute
344
        ), f"Instance of Route class is required, got {route!r}"
345 10
        self._routes.append(route)
346

347 10
    async def resolve(self, request: Request) -> _Resolve:
348 10
        allowed_methods = set()  # type: Set[str]
349

350 10
        match_dict = self._match(request.rel_url.raw_path)
351 10
        if match_dict is None:
352 10
            return None, allowed_methods
353

354 10
        for route_obj in self._routes:
355 10
            route_method = route_obj.method
356 10
            allowed_methods.add(route_method)
357

358 10
            if route_method == request.method or route_method == hdrs.METH_ANY:
359 10
                return (UrlMappingMatchInfo(match_dict, route_obj), allowed_methods)
360
        else:
361 10
            return None, allowed_methods
362

363 10
    @abc.abstractmethod
364 10
    def _match(self, path: str) -> Optional[Dict[str, str]]:
365
        pass  # pragma: no cover
366

367 10
    def __len__(self) -> int:
368 10
        return len(self._routes)
369

370 10
    def __iter__(self) -> Iterator[AbstractRoute]:
371 10
        return iter(self._routes)
372

373
    # TODO: implement all abstract methods
374

375

376 10
class PlainResource(Resource):
377 10
    def __init__(self, path: str, *, name: Optional[str] = None) -> None:
378 10
        super().__init__(name=name)
379 10
        assert not path or path.startswith("/")
380 10
        self._path = path
381

382 10
    @property
383 10
    def canonical(self) -> str:
384 10
        return self._path
385

386 10
    def freeze(self) -> None:
387 10
        if not self._path:
388 10
            self._path = "/"
389

390 10
    def add_prefix(self, prefix: str) -> None:
391 10
        assert prefix.startswith("/")
392 10
        assert not prefix.endswith("/")
393 10
        assert len(prefix) > 1
394 10
        self._path = prefix + self._path
395

396 10
    def _match(self, path: str) -> Optional[Dict[str, str]]:
397
        # string comparison is about 10 times faster than regexp matching
398 10
        if self._path == path:
399 10
            return {}
400
        else:
401 10
            return None
402

403 10
    def raw_match(self, path: str) -> bool:
404 10
        return self._path == path
405

406 10
    def get_info(self) -> _InfoDict:
407 10
        return {"path": self._path}
408

409 10
    def url_for(self) -> URL:  # type: ignore[override]
410 10
        return URL.build(path=self._path, encoded=True)
411

412 10
    def __repr__(self) -> str:
413 10
        name = "'" + self.name + "' " if self.name is not None else ""
414 10
        return f"<PlainResource {name} {self._path}>"
415

416

417 10
class DynamicResource(Resource):
418

419 10
    DYN = re.compile(r"\{(?P<var>[_a-zA-Z][_a-zA-Z0-9]*)\}")
420 10
    DYN_WITH_RE = re.compile(r"\{(?P<var>[_a-zA-Z][_a-zA-Z0-9]*):(?P<re>.+)\}")
421 10
    GOOD = r"[^{}/]+"
422

423 10
    def __init__(self, path: str, *, name: Optional[str] = None) -> None:
424 10
        super().__init__(name=name)
425 10
        pattern = ""
426 10
        formatter = ""
427 10
        for part in ROUTE_RE.split(path):
428 10
            match = self.DYN.fullmatch(part)
429 10
            if match:
430 10
                pattern += "(?P<{}>{})".format(match.group("var"), self.GOOD)
431 10
                formatter += "{" + match.group("var") + "}"
432 10
                continue
433

434 10
            match = self.DYN_WITH_RE.fullmatch(part)
435 10
            if match:
436 10
                pattern += "(?P<{var}>{re})".format(**match.groupdict())
437 10
                formatter += "{" + match.group("var") + "}"
438 10
                continue
439

440 10
            if "{" in part or "}" in part:
441 10
                raise ValueError(f"Invalid path '{path}'['{part}']")
442

443 10
            part = _requote_path(part)
444 10
            formatter += part
445 10
            pattern += re.escape(part)
446

447 10
        try:
448 10
            compiled = re.compile(pattern)
449 10
        except re.error as exc:
450 10
            raise ValueError(f"Bad pattern '{pattern}': {exc}") from None
451 10
        assert compiled.pattern.startswith(PATH_SEP)
452 10
        assert formatter.startswith("/")
453 10
        self._pattern = compiled
454 10
        self._formatter = formatter
455

456 10
    @property
457 10
    def canonical(self) -> str:
458 10
        return self._formatter
459

460 10
    def add_prefix(self, prefix: str) -> None:
461 10
        assert prefix.startswith("/")
462 10
        assert not prefix.endswith("/")
463 10
        assert len(prefix) > 1
464 10
        self._pattern = re.compile(re.escape(prefix) + self._pattern.pattern)
465 10
        self._formatter = prefix + self._formatter
466

467 10
    def _match(self, path: str) -> Optional[Dict[str, str]]:
468 10
        match = self._pattern.fullmatch(path)
469 10
        if match is None:
470 10
            return None
471
        else:
472 10
            return {
473
                key: _unquote_path(value) for key, value in match.groupdict().items()
474
            }
475

476 10
    def raw_match(self, path: str) -> bool:
477 10
        return self._formatter == path
478

479 10
    def get_info(self) -> _InfoDict:
480 10
        return {"formatter": self._formatter, "pattern": self._pattern}
481

482 10
    def url_for(self, **parts: str) -> URL:
483 10
        url = self._formatter.format_map({k: _quote_path(v) for k, v in parts.items()})
484 10
        return URL.build(path=url, encoded=True)
485

486 10
    def __repr__(self) -> str:
487 10
        name = "'" + self.name + "' " if self.name is not None else ""
488 10
        return "<DynamicResource {name} {formatter}>".format(
489
            name=name, formatter=self._formatter
490
        )
491

492

493 10
class PrefixResource(AbstractResource):
494 10
    def __init__(self, prefix: str, *, name: Optional[str] = None) -> None:
495 10
        assert not prefix or prefix.startswith("/"), prefix
496 10
        assert prefix in ("", "/") or not prefix.endswith("/"), prefix
497 10
        super().__init__(name=name)
498 10
        self._prefix = _requote_path(prefix)
499

500 10
    @property
501 10
    def canonical(self) -> str:
502 10
        return self._prefix
503

504 10
    def add_prefix(self, prefix: str) -> None:
505 10
        assert prefix.startswith("/")
506 10
        assert not prefix.endswith("/")
507 10
        assert len(prefix) > 1
508 10
        self._prefix = prefix + self._prefix
509

510 10
    def raw_match(self, prefix: str) -> bool:
511 10
        return False
512

513
    # TODO: impl missing abstract methods
514

515

516 10
class StaticResource(PrefixResource):
517 10
    VERSION_KEY = "v"
518

519 10
    def __init__(
520
        self,
521
        prefix: str,
522
        directory: PathLike,
523
        *,
524
        name: Optional[str] = None,
525
        expect_handler: Optional[_ExpectHandler] = None,
526
        chunk_size: int = 256 * 1024,
527
        show_index: bool = False,
528
        follow_symlinks: bool = False,
529
        append_version: bool = False,
530
    ) -> None:
531 10
        super().__init__(prefix, name=name)
532 10
        try:
533 10
            directory = Path(directory)
534 10
            if str(directory).startswith("~"):
535 7
                directory = Path(os.path.expanduser(str(directory)))
536 10
            directory = directory.resolve()
537 10
            if not directory.is_dir():
538 10
                raise ValueError("Not a directory")
539 10
        except (FileNotFoundError, ValueError) as error:
540 10
            raise ValueError(f"No directory exists at '{directory}'") from error
541 10
        self._directory = directory
542 10
        self._show_index = show_index
543 10
        self._chunk_size = chunk_size
544 10
        self._follow_symlinks = follow_symlinks
545 10
        self._expect_handler = expect_handler
546 10
        self._append_version = append_version
547

548 10
        self._routes = {
549
            "GET": ResourceRoute(
550
                "GET", self._handle, self, expect_handler=expect_handler
551
            ),
552
            "HEAD": ResourceRoute(
553
                "HEAD", self._handle, self, expect_handler=expect_handler
554
            ),
555
        }
556

557 10
    def url_for(  # type: ignore[override]
558
        self,
559
        *,
560
        filename: Union[str, Path],
561
        append_version: Optional[bool] = None,
562
    ) -> URL:
563 10
        if append_version is None:
564 10
            append_version = self._append_version
565 10
        if isinstance(filename, Path):
566 10
            filename = str(filename)
567 10
        filename = filename.lstrip("/")
568

569 10
        url = URL.build(path=self._prefix, encoded=True)
570
        # filename is not encoded
571 10
        if YARL_VERSION < (1, 6):
572 0
            url = url / filename.replace("%", "%25")
573
        else:
574 10
            url = url / filename
575

576 10
        if append_version:
577 10
            try:
578 10
                filepath = self._directory.joinpath(filename).resolve()
579 10
                if not self._follow_symlinks:
580 10
                    filepath.relative_to(self._directory)
581 10
            except (ValueError, FileNotFoundError):
582
                # ValueError for case when path point to symlink
583
                # with follow_symlinks is False
584 10
                return url  # relatively safe
585 10
            if filepath.is_file():
586
                # TODO cache file content
587
                # with file watcher for cache invalidation
588 10
                with filepath.open("rb") as f:
589 10
                    file_bytes = f.read()
590 10
                h = self._get_file_hash(file_bytes)
591 10
                url = url.with_query({self.VERSION_KEY: h})
592 10
                return url
593 10
        return url
594

595 10
    @staticmethod
596 10
    def _get_file_hash(byte_array: bytes) -> str:
597 10
        m = hashlib.sha256()  # todo sha256 can be configurable param
598 10
        m.update(byte_array)
599 10
        b64 = base64.urlsafe_b64encode(m.digest())
600 10
        return b64.decode("ascii")
601

602 10
    def get_info(self) -> _InfoDict:
603 10
        return {
604
            "directory": self._directory,
605
            "prefix": self._prefix,
606
            "routes": self._routes,
607
        }
608

609 10
    def set_options_route(self, handler: _WebHandler) -> None:
610 10
        if "OPTIONS" in self._routes:
611 10
            raise RuntimeError("OPTIONS route was set already")
612 10
        self._routes["OPTIONS"] = ResourceRoute(
613
            "OPTIONS", handler, self, expect_handler=self._expect_handler
614
        )
615

616 10
    async def resolve(self, request: Request) -> _Resolve:
617 10
        path = request.rel_url.raw_path
618 10
        method = request.method
619 10
        allowed_methods = set(self._routes)
620 10
        if not path.startswith(self._prefix):
621 10
            return None, set()
622

623 10
        if method not in allowed_methods:
624 10
            return None, allowed_methods
625

626 10
        match_dict = {"filename": _unquote_path(path[len(self._prefix) + 1 :])}
627 10
        return (UrlMappingMatchInfo(match_dict, self._routes[method]), allowed_methods)
628

629 10
    def __len__(self) -> int:
630 10
        return len(self._routes)
631

632 10
    def __iter__(self) -> Iterator[AbstractRoute]:
633 10
        return iter(self._routes.values())
634

635 10
    async def _handle(self, request: Request) -> StreamResponse:
636 10
        rel_url = request.match_info["filename"]
637 10
        try:
638 10
            filename = Path(rel_url)
639 10
            if filename.anchor:
640
                # rel_url is an absolute name like
641
                # /static/\\machine_name\c$ or /static/D:\path
642
                # where the static dir is totally different
643 10
                raise HTTPForbidden()
644 10
            filepath = self._directory.joinpath(filename).resolve()
645 10
            if not self._follow_symlinks:
646 10
                filepath.relative_to(self._directory)
647 10
        except (ValueError, FileNotFoundError) as error:
648
            # relatively safe
649 0
            raise HTTPNotFound() from error
650 10
        except HTTPForbidden:
651 10
            raise
652 10
        except Exception as error:
653
            # perm error or other kind!
654 10
            request.app.logger.exception(error)
655 10
            raise HTTPNotFound() from error
656

657
        # on opening a dir, load its contents if allowed
658 10
        if filepath.is_dir():
659 10
            if self._show_index:
660 10
                try:
661 10
                    return Response(
662
                        text=self._directory_as_html(filepath), content_type="text/html"
663
                    )
664 0
                except PermissionError:
665 0
                    raise HTTPForbidden()
666
            else:
667 10
                raise HTTPForbidden()
668 10
        elif filepath.is_file():
669 10
            return FileResponse(filepath, chunk_size=self._chunk_size)
670
        else:
671 10
            raise HTTPNotFound
672

673 10
    def _directory_as_html(self, filepath: Path) -> str:
674
        # returns directory's index as html
675

676
        # sanity check
677 10
        assert filepath.is_dir()
678

679 10
        relative_path_to_dir = filepath.relative_to(self._directory).as_posix()
680 10
        index_of = f"Index of /{relative_path_to_dir}"
681 10
        h1 = f"<h1>{index_of}</h1>"
682

683 10
        index_list = []
684 10
        dir_index = filepath.iterdir()
685 10
        for _file in sorted(dir_index):
686
            # show file url as relative to static path
687 10
            rel_path = _file.relative_to(self._directory).as_posix()
688 10
            file_url = self._prefix + "/" + rel_path
689

690
            # if file is a directory, add '/' to the end of the name
691 10
            if _file.is_dir():
692 10
                file_name = f"{_file.name}/"
693
            else:
694 10
                file_name = _file.name
695

696 10
            index_list.append(
697
                '<li><a href="{url}">{name}</a></li>'.format(
698
                    url=file_url, name=file_name
699
                )
700
            )
701 10
        ul = "<ul>\n{}\n</ul>".format("\n".join(index_list))
702 10
        body = f"<body>\n{h1}\n{ul}\n</body>"
703

704 10
        head_str = f"<head>\n<title>{index_of}</title>\n</head>"
705 10
        html = f"<html>\n{head_str}\n{body}\n</html>"
706

707 10
        return html
708

709 10
    def __repr__(self) -> str:
710 10
        name = "'" + self.name + "'" if self.name is not None else ""
711 10
        return "<StaticResource {name} {path} -> {directory!r}>".format(
712
            name=name, path=self._prefix, directory=self._directory
713
        )
714

715

716 10
class PrefixedSubAppResource(PrefixResource):
717 10
    def __init__(self, prefix: str, app: "Application") -> None:
718 10
        super().__init__(prefix)
719 10
        self._app = app
720 10
        for resource in app.router.resources():
721 10
            resource.add_prefix(prefix)
722

723 10
    def add_prefix(self, prefix: str) -> None:
724 10
        super().add_prefix(prefix)
725 10
        for resource in self._app.router.resources():
726 10
            resource.add_prefix(prefix)
727

728 10
    def url_for(self, *args: str, **kwargs: str) -> URL:
729 10
        raise RuntimeError(".url_for() is not supported " "by sub-application root")
730

731 10
    def get_info(self) -> _InfoDict:
732 10
        return {"app": self._app, "prefix": self._prefix}
733

734 10
    async def resolve(self, request: Request) -> _Resolve:
735 10
        if (
736
            not request.url.raw_path.startswith(self._prefix + "/")
737
            and request.url.raw_path != self._prefix
738
        ):
739 10
            return None, set()
740 10
        match_info = await self._app.router.resolve(request)
741 10
        match_info.add_app(self._app)
742 10
        if isinstance(match_info.http_exception, HTTPMethodNotAllowed):
743 10
            methods = match_info.http_exception.allowed_methods
744
        else:
745 10
            methods = set()
746 10
        return match_info, methods
747

748 10
    def __len__(self) -> int:
749 10
        return len(self._app.router.routes())
750

751 10
    def __iter__(self) -> Iterator[AbstractRoute]:
752 10
        return iter(self._app.router.routes())
753

754 10
    def __repr__(self) -> str:
755 10
        return "<PrefixedSubAppResource {prefix} -> {app!r}>".format(
756
            prefix=self._prefix, app=self._app
757
        )
758

759

760 10
class AbstractRuleMatching(abc.ABC):
761 10
    @abc.abstractmethod  # pragma: no branch
762 10
    async def match(self, request: Request) -> bool:
763
        """Return bool if the request satisfies the criteria"""
764

765 10
    @abc.abstractmethod  # pragma: no branch
766 10
    def get_info(self) -> _InfoDict:
767
        """Return a dict with additional info useful for introspection"""
768

769 10
    @property
770 10
    @abc.abstractmethod  # pragma: no branch
771 10
    def canonical(self) -> str:
772
        """Return a str"""
773

774

775 10
class Domain(AbstractRuleMatching):
776 10
    re_part = re.compile(r"(?!-)[a-z\d-]{1,63}(?<!-)")
777

778 10
    def __init__(self, domain: str) -> None:
779 10
        super().__init__()
780 10
        self._domain = self.validation(domain)
781

782 10
    @property
783 10
    def canonical(self) -> str:
784 10
        return self._domain
785

786 10
    def validation(self, domain: str) -> str:
787 10
        if not isinstance(domain, str):
788 10
            raise TypeError("Domain must be str")
789 10
        domain = domain.rstrip(".").lower()
790 10
        if not domain:
791 10
            raise ValueError("Domain cannot be empty")
792 10
        elif "://" in domain:
793 10
            raise ValueError("Scheme not supported")
794 10
        url = URL("http://" + domain)
795 10
        assert url.raw_host is not None
796 10
        if not all(self.re_part.fullmatch(x) for x in url.raw_host.split(".")):
797 10
            raise ValueError("Domain not valid")
798 10
        if url.port == 80:
799 10
            return url.raw_host
800 10
        return f"{url.raw_host}:{url.port}"
801

802 10
    async def match(self, request: Request) -> bool:
803 10
        host = request.headers.get(hdrs.HOST)
804 10
        if not host:
805 0
            return False
806 10
        return self.match_domain(host)
807

808 10
    def match_domain(self, host: str) -> bool:
809 10
        return host.lower() == self._domain
810

811 10
    def get_info(self) -> _InfoDict:
812 10
        return {"domain": self._domain}
813

814

815 10
class MaskDomain(Domain):
816 10
    re_part = re.compile(r"(?!-)[a-z\d\*-]{1,63}(?<!-)")
817

818 10
    def __init__(self, domain: str) -> None:
819 10
        super().__init__(domain)
820 10
        mask = self._domain.replace(".", r"\.").replace("*", ".*")
821 10
        self._mask = re.compile(mask)
822

823 10
    @property
824 10
    def canonical(self) -> str:
825 10
        return self._mask.pattern
826

827 10
    def match_domain(self, host: str) -> bool:
828 10
        return self._mask.fullmatch(host) is not None
829

830

831 10
class MatchedSubAppResource(PrefixedSubAppResource):
832 10
    def __init__(self, rule: AbstractRuleMatching, app: "Application") -> None:
833 10
        AbstractResource.__init__(self)
834 10
        self._prefix = ""
835 10
        self._app = app
836 10
        self._rule = rule
837

838 10
    @property
839 10
    def canonical(self) -> str:
840 10
        return self._rule.canonical
841

842 10
    def get_info(self) -> _InfoDict:
843 10
        return {"app": self._app, "rule": self._rule}
844

845 10
    async def resolve(self, request: Request) -> _Resolve:
846 10
        if not await self._rule.match(request):
847 10
            return None, set()
848 10
        match_info = await self._app.router.resolve(request)
849 10
        match_info.add_app(self._app)
850 10
        if isinstance(match_info.http_exception, HTTPMethodNotAllowed):
851 10
            methods = match_info.http_exception.allowed_methods
852
        else:
853 10
            methods = set()
854 10
        return match_info, methods
855

856 10
    def __repr__(self) -> str:
857 10
        return "<MatchedSubAppResource -> {app!r}>" "".format(app=self._app)
858

859

860 10
class ResourceRoute(AbstractRoute):
861
    """A route with resource"""
862

863 10
    def __init__(
864
        self,
865
        method: str,
866
        handler: Union[_WebHandler, Type[AbstractView]],
867
        resource: AbstractResource,
868
        *,
869
        expect_handler: Optional[_ExpectHandler] = None,
870
    ) -> None:
871 10
        super().__init__(
872
            method, handler, expect_handler=expect_handler, resource=resource
873
        )
874

875 10
    def __repr__(self) -> str:
876 10
        return "<ResourceRoute [{method}] {resource} -> {handler!r}".format(
877
            method=self.method, resource=self._resource, handler=self.handler
878
        )
879

880 10
    @property
881 10
    def name(self) -> Optional[str]:
882 10
        if self._resource is None:
883 0
            return None
884 10
        return self._resource.name
885

886 10
    def url_for(self, *args: str, **kwargs: str) -> URL:
887
        """Construct url for route with additional params."""
888 10
        assert self._resource is not None
889 10
        return self._resource.url_for(*args, **kwargs)
890

891 10
    def get_info(self) -> _InfoDict:
892 10
        assert self._resource is not None
893 10
        return self._resource.get_info()
894

895

896 10
class SystemRoute(AbstractRoute):
897 10
    def __init__(self, http_exception: HTTPException) -> None:
898 10
        super().__init__(hdrs.METH_ANY, self._handle)
899 10
        self._http_exception = http_exception
900

901 10
    def url_for(self, *args: str, **kwargs: str) -> URL:
902 10
        raise RuntimeError(".url_for() is not allowed for SystemRoute")
903

904 10
    @property
905 10
    def name(self) -> Optional[str]:
906 10
        return None
907

908 10
    def get_info(self) -> _InfoDict:
909 10
        return {"http_exception": self._http_exception}
910

911 10
    async def _handle(self, request: Request) -> StreamResponse:
912 10
        raise self._http_exception
913

914 10
    @property
915 10
    def status(self) -> int:
916 10
        return self._http_exception.status
917

918 10
    @property
919 10
    def reason(self) -> str:
920 10
        return self._http_exception.reason
921

922 10
    def __repr__(self) -> str:
923 10
        return "<SystemRoute {self.status}: {self.reason}>".format(self=self)
924

925

926 10
class View(AbstractView):
927 10
    async def _iter(self) -> StreamResponse:
928 10
        if self.request.method not in hdrs.METH_ALL:
929 10
            self._raise_allowed_methods()
930 10
        method: Callable[[], Awaitable[StreamResponse]] = getattr(
931
            self, self.request.method.lower(), None
932
        )
933 10
        if method is None:
934 10
            self._raise_allowed_methods()
935 10
        resp = await method()
936 10
        return resp
937

938 10
    def __await__(self) -> Generator[Any, None, StreamResponse]:
939 10
        return self._iter().__await__()
940

941 10
    def _raise_allowed_methods(self) -> None:
942 10
        allowed_methods = {m for m in hdrs.METH_ALL if hasattr(self, m.lower())}
943 10
        raise HTTPMethodNotAllowed(self.request.method, allowed_methods)
944

945

946 10
class ResourcesView(Sized, Iterable[AbstractResource], Container[AbstractResource]):
947 10
    def __init__(self, resources: List[AbstractResource]) -> None:
948 10
        self._resources = resources
949

950 10
    def __len__(self) -> int:
951 10
        return len(self._resources)
952

953 10
    def __iter__(self) -> Iterator[AbstractResource]:
954 10
        yield from self._resources
955

956 10
    def __contains__(self, resource: object) -> bool:
957 10
        return resource in self._resources
958

959

960 10
class RoutesView(Sized, Iterable[AbstractRoute], Container[AbstractRoute]):
961 10
    def __init__(self, resources: List[AbstractResource]):
962 10
        self._routes = []  # type: List[AbstractRoute]
963 10
        for resource in resources:
964 10
            for route in resource:
965 10
                self._routes.append(route)
966

967 10
    def __len__(self) -> int:
968 10
        return len(self._routes)
969

970 10
    def __iter__(self) -> Iterator[AbstractRoute]:
971 10
        yield from self._routes
972

973 10
    def __contains__(self, route: object) -> bool:
974 10
        return route in self._routes
975

976

977 10
class UrlDispatcher(AbstractRouter, Mapping[str, AbstractResource]):
978

979 10
    NAME_SPLIT_RE = re.compile(r"[.:-]")
980

981 10
    def __init__(self) -> None:
982 10
        super().__init__()
983 10
        self._resources = []  # type: List[AbstractResource]
984 10
        self._named_resources = {}  # type: Dict[str, AbstractResource]
985

986 10
    async def resolve(self, request: Request) -> AbstractMatchInfo:
987 10
        method = request.method
988 10
        allowed_methods = set()  # type: Set[str]
989

990 10
        for resource in self._resources:
991 10
            match_dict, allowed = await resource.resolve(request)
992 10
            if match_dict is not None:
993 10
                return match_dict
994
            else:
995 10
                allowed_methods |= allowed
996
        else:
997 10
            if allowed_methods:
998 10
                return MatchInfoError(HTTPMethodNotAllowed(method, allowed_methods))
999
            else:
1000 10
                return MatchInfoError(HTTPNotFound())
1001

1002 10
    def __iter__(self) -> Iterator[str]:
1003 10
        return iter(self._named_resources)
1004

1005 10
    def __len__(self) -> int:
1006 10
        return len(self._named_resources)
1007

1008 10
    def __contains__(self, resource: object) -> bool:
1009 10
        return resource in self._named_resources
1010

1011 10
    def __getitem__(self, name: str) -> AbstractResource:
1012 10
        return self._named_resources[name]
1013

1014 10
    def resources(self) -> ResourcesView:
1015 10
        return ResourcesView(self._resources)
1016

1017 10
    def routes(self) -> RoutesView:
1018 10
        return RoutesView(self._resources)
1019

1020 10
    def named_resources(self) -> Mapping[str, AbstractResource]:
1021 10
        return MappingProxyType(self._named_resources)
1022

1023 10
    def register_resource(self, resource: AbstractResource) -> None:
1024 10
        assert isinstance(
1025
            resource, AbstractResource
1026
        ), f"Instance of AbstractResource class is required, got {resource!r}"
1027 10
        if self.frozen:
1028 10
            raise RuntimeError("Cannot register a resource into frozen router.")
1029

1030 10
        name = resource.name
1031

1032 10
        if name is not None:
1033 10
            parts = self.NAME_SPLIT_RE.split(name)
1034 10
            for part in parts:
1035 10
                if keyword.iskeyword(part):
1036 10
                    raise ValueError(
1037
                        f"Incorrect route name {name!r}, "
1038
                        "python keywords cannot be used "
1039
                        "for route name"
1040
                    )
1041 10
                if not part.isidentifier():
1042 0
                    raise ValueError(
1043
                        "Incorrect route name {!r}, "
1044
                        "the name should be a sequence of "
1045
                        "python identifiers separated "
1046
                        "by dash, dot or column".format(name)
1047
                    )
1048 10
            if name in self._named_resources:
1049 10
                raise ValueError(
1050
                    "Duplicate {!r}, "
1051
                    "already handled by {!r}".format(name, self._named_resources[name])
1052
                )
1053 10
            self._named_resources[name] = resource
1054 10
        self._resources.append(resource)
1055

1056 10
    def add_resource(self, path: str, *, name: Optional[str] = None) -> Resource:
1057 10
        if path and not path.startswith("/"):
1058 10
            raise ValueError("path should be started with / or be empty")
1059
        # Reuse last added resource if path and name are the same
1060 10
        if self._resources:
1061 10
            resource = self._resources[-1]
1062 10
            if resource.name == name and resource.raw_match(path):
1063 10
                return cast(Resource, resource)
1064 10
        if not ("{" in path or "}" in path or ROUTE_RE.search(path)):
1065 10
            resource = PlainResource(_requote_path(path), name=name)
1066 10
            self.register_resource(resource)
1067 10
            return resource
1068 10
        resource = DynamicResource(path, name=name)
1069 10
        self.register_resource(resource)
1070 10
        return resource
1071

1072 10
    def add_route(
1073
        self,
1074
        method: str,
1075
        path: str,
1076
        handler: Union[_WebHandler, Type[AbstractView]],
1077
        *,
1078
        name: Optional[str] = None,
1079
        expect_handler: Optional[_ExpectHandler] = None,
1080
    ) -> AbstractRoute:
1081 10
        resource = self.add_resource(path, name=name)
1082 10
        return resource.add_route(method, handler, expect_handler=expect_handler)
1083

1084 10
    def add_static(
1085
        self,
1086
        prefix: str,
1087
        path: PathLike,
1088
        *,
1089
        name: Optional[str] = None,
1090
        expect_handler: Optional[_ExpectHandler] = None,
1091
        chunk_size: int = 256 * 1024,
1092
        show_index: bool = False,
1093
        follow_symlinks: bool = False,
1094
        append_version: bool = False,
1095
    ) -> AbstractResource:
1096
        """Add static files view.
1097

1098
        prefix - url prefix
1099
        path - folder with files
1100

1101
        """
1102 10
        assert prefix.startswith("/")
1103 10
        if prefix.endswith("/"):
1104 10
            prefix = prefix[:-1]
1105 10
        resource = StaticResource(
1106
            prefix,
1107
            path,
1108
            name=name,
1109
            expect_handler=expect_handler,
1110
            chunk_size=chunk_size,
1111
            show_index=show_index,
1112
            follow_symlinks=follow_symlinks,
1113
            append_version=append_version,
1114
        )
1115 10
        self.register_resource(resource)
1116 10
        return resource
1117

1118 10
    def add_head(self, path: str, handler: _WebHandler, **kwargs: Any) -> AbstractRoute:
1119
        """
1120
        Shortcut for add_route with method HEAD
1121
        """
1122 10
        return self.add_route(hdrs.METH_HEAD, path, handler, **kwargs)
1123

1124 10
    def add_options(
1125
        self, path: str, handler: _WebHandler, **kwargs: Any
1126
    ) -> AbstractRoute:
1127
        """
1128
        Shortcut for add_route with method OPTIONS
1129
        """
1130 10
        return self.add_route(hdrs.METH_OPTIONS, path, handler, **kwargs)
1131

1132 10
    def add_get(
1133
        self,
1134
        path: str,
1135
        handler: _WebHandler,
1136
        *,
1137
        name: Optional[str] = None,
1138
        allow_head: bool = True,
1139
        **kwargs: Any,
1140
    ) -> AbstractRoute:
1141
        """
1142
        Shortcut for add_route with method GET, if allow_head is true another
1143
        route is added allowing head requests to the same endpoint
1144
        """
1145 10
        resource = self.add_resource(path, name=name)
1146 10
        if allow_head:
1147 10
            resource.add_route(hdrs.METH_HEAD, handler, **kwargs)
1148 10
        return resource.add_route(hdrs.METH_GET, handler, **kwargs)
1149

1150 10
    def add_post(self, path: str, handler: _WebHandler, **kwargs: Any) -> AbstractRoute:
1151
        """
1152
        Shortcut for add_route with method POST
1153
        """
1154 10
        return self.add_route(hdrs.METH_POST, path, handler, **kwargs)
1155

1156 10
    def add_put(self, path: str, handler: _WebHandler, **kwargs: Any) -> AbstractRoute:
1157
        """
1158
        Shortcut for add_route with method PUT
1159
        """
1160 10
        return self.add_route(hdrs.METH_PUT, path, handler, **kwargs)
1161

1162 10
    def add_patch(
1163
        self, path: str, handler: _WebHandler, **kwargs: Any
1164
    ) -> AbstractRoute:
1165
        """
1166
        Shortcut for add_route with method PATCH
1167
        """
1168 10
        return self.add_route(hdrs.METH_PATCH, path, handler, **kwargs)
1169

1170 10
    def add_delete(
1171
        self, path: str, handler: _WebHandler, **kwargs: Any
1172
    ) -> AbstractRoute:
1173
        """
1174
        Shortcut for add_route with method DELETE
1175
        """
1176 10
        return self.add_route(hdrs.METH_DELETE, path, handler, **kwargs)
1177

1178 10
    def add_view(
1179
        self, path: str, handler: Type[AbstractView], **kwargs: Any
1180
    ) -> AbstractRoute:
1181
        """
1182
        Shortcut for add_route with ANY methods for a class-based view
1183
        """
1184 10
        return self.add_route(hdrs.METH_ANY, path, handler, **kwargs)
1185

1186 10
    def freeze(self) -> None:
1187 10
        super().freeze()
1188 10
        for resource in self._resources:
1189 10
            resource.freeze()
1190

1191 10
    def add_routes(self, routes: Iterable[AbstractRouteDef]) -> List[AbstractRoute]:
1192
        """Append routes to route table.
1193

1194
        Parameter should be a sequence of RouteDef objects.
1195

1196
        Returns a list of registered AbstractRoute instances.
1197
        """
1198 10
        registered_routes = []
1199 10
        for route_def in routes:
1200 10
            registered_routes.extend(route_def.register(self))
1201 10
        return registered_routes
1202

1203

1204 10
def _quote_path(value: str) -> str:
1205 10
    if YARL_VERSION < (1, 6):
1206 0
        value = value.replace("%", "%25")
1207 10
    return URL.build(path=value, encoded=False).raw_path
1208

1209

1210 10
def _unquote_path(value: str) -> str:
1211 10
    return URL.build(path=value, encoded=True).path
1212

1213

1214 10
def _requote_path(value: str) -> str:
1215
    # Quote non-ascii characters and other characters which must be quoted,
1216
    # but preserve existing %-sequences.
1217 10
    result = _quote_path(value)
1218 10
    if "%" in value:
1219 10
        result = result.replace("%25", "%")
1220 10
    return result

Read our documentation on viewing source code .

Loading