You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

966 lines
30 KiB

4 years ago
  1. import abc
  2. import asyncio
  3. import base64
  4. import collections
  5. import hashlib
  6. import inspect
  7. import keyword
  8. import os
  9. import re
  10. import warnings
  11. from collections.abc import Container, Iterable, Sized
  12. from contextlib import contextmanager
  13. from functools import wraps
  14. from pathlib import Path
  15. from types import MappingProxyType
  16. from yarl import URL
  17. from . import hdrs
  18. from .abc import AbstractMatchInfo, AbstractRouter, AbstractView
  19. from .helpers import DEBUG
  20. from .http import HttpVersion11
  21. from .web_exceptions import (HTTPExpectationFailed, HTTPForbidden,
  22. HTTPMethodNotAllowed, HTTPNotFound)
  23. from .web_fileresponse import FileResponse
  24. from .web_response import Response
  25. __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo',
  26. 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource',
  27. 'AbstractRoute', 'ResourceRoute',
  28. 'StaticResource', 'View')
  29. HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$")
  30. ROUTE_RE = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
  31. PATH_SEP = re.escape('/')
  32. class AbstractResource(Sized, Iterable):
  33. def __init__(self, *, name=None):
  34. self._name = name
  35. @property
  36. def name(self):
  37. return self._name
  38. @property
  39. @abc.abstractmethod
  40. def canonical(self):
  41. """Exposes the resource's canonical path.
  42. For example '/foo/bar/{name}'
  43. """
  44. @abc.abstractmethod # pragma: no branch
  45. def url_for(self, **kwargs):
  46. """Construct url for resource with additional params."""
  47. @abc.abstractmethod # pragma: no branch
  48. async def resolve(self, request):
  49. """Resolve resource
  50. Return (UrlMappingMatchInfo, allowed_methods) pair."""
  51. @abc.abstractmethod
  52. def add_prefix(self, prefix):
  53. """Add a prefix to processed URLs.
  54. Required for subapplications support.
  55. """
  56. @abc.abstractmethod
  57. def get_info(self):
  58. """Return a dict with additional info useful for introspection"""
  59. def freeze(self):
  60. pass
  61. @abc.abstractmethod
  62. def raw_match(self, path):
  63. """Perform a raw match against path"""
  64. class AbstractRoute(abc.ABC):
  65. def __init__(self, method, handler, *,
  66. expect_handler=None,
  67. resource=None):
  68. if expect_handler is None:
  69. expect_handler = _default_expect_handler
  70. assert asyncio.iscoroutinefunction(expect_handler), \
  71. 'Coroutine is expected, got {!r}'.format(expect_handler)
  72. method = method.upper()
  73. if not HTTP_METHOD_RE.match(method):
  74. raise ValueError("{} is not allowed HTTP method".format(method))
  75. assert callable(handler), handler
  76. if asyncio.iscoroutinefunction(handler):
  77. pass
  78. elif inspect.isgeneratorfunction(handler):
  79. warnings.warn("Bare generators are deprecated, "
  80. "use @coroutine wrapper", DeprecationWarning)
  81. elif (isinstance(handler, type) and
  82. issubclass(handler, AbstractView)):
  83. pass
  84. else:
  85. warnings.warn("Bare functions are deprecated, "
  86. "use async ones", DeprecationWarning)
  87. @wraps(handler)
  88. async def handler_wrapper(*args, **kwargs):
  89. result = old_handler(*args, **kwargs)
  90. if asyncio.iscoroutine(result):
  91. result = await result
  92. return result
  93. old_handler = handler
  94. handler = handler_wrapper
  95. self._method = method
  96. self._handler = handler
  97. self._expect_handler = expect_handler
  98. self._resource = resource
  99. @property
  100. def method(self):
  101. return self._method
  102. @property
  103. def handler(self):
  104. return self._handler
  105. @property
  106. @abc.abstractmethod
  107. def name(self):
  108. """Optional route's name, always equals to resource's name."""
  109. @property
  110. def resource(self):
  111. return self._resource
  112. @abc.abstractmethod
  113. def get_info(self):
  114. """Return a dict with additional info useful for introspection"""
  115. @abc.abstractmethod # pragma: no branch
  116. def url_for(self, *args, **kwargs):
  117. """Construct url for route with additional params."""
  118. async def handle_expect_header(self, request):
  119. return await self._expect_handler(request)
  120. class UrlMappingMatchInfo(dict, AbstractMatchInfo):
  121. def __init__(self, match_dict, route):
  122. super().__init__(match_dict)
  123. self._route = route
  124. self._apps = []
  125. self._current_app = None
  126. self._frozen = False
  127. @property
  128. def handler(self):
  129. return self._route.handler
  130. @property
  131. def route(self):
  132. return self._route
  133. @property
  134. def expect_handler(self):
  135. return self._route.handle_expect_header
  136. @property
  137. def http_exception(self):
  138. return None
  139. def get_info(self):
  140. return self._route.get_info()
  141. @property
  142. def apps(self):
  143. return tuple(self._apps)
  144. def add_app(self, app):
  145. if self._frozen:
  146. raise RuntimeError("Cannot change apps stack after .freeze() call")
  147. if self._current_app is None:
  148. self._current_app = app
  149. self._apps.insert(0, app)
  150. @property
  151. def current_app(self):
  152. return self._current_app
  153. @contextmanager
  154. def set_current_app(self, app):
  155. if DEBUG: # pragma: no cover
  156. if app not in self._apps:
  157. raise RuntimeError(
  158. "Expected one of the following apps {!r}, got {!r}"
  159. .format(self._apps, app))
  160. prev = self._current_app
  161. self._current_app = app
  162. try:
  163. yield
  164. finally:
  165. self._current_app = prev
  166. def freeze(self):
  167. self._frozen = True
  168. def __repr__(self):
  169. return "<MatchInfo {}: {}>".format(super().__repr__(), self._route)
  170. class MatchInfoError(UrlMappingMatchInfo):
  171. def __init__(self, http_exception):
  172. self._exception = http_exception
  173. super().__init__({}, SystemRoute(self._exception))
  174. @property
  175. def http_exception(self):
  176. return self._exception
  177. def __repr__(self):
  178. return "<MatchInfoError {}: {}>".format(self._exception.status,
  179. self._exception.reason)
  180. async def _default_expect_handler(request):
  181. """Default handler for Expect header.
  182. Just send "100 Continue" to client.
  183. raise HTTPExpectationFailed if value of header is not "100-continue"
  184. """
  185. expect = request.headers.get(hdrs.EXPECT)
  186. if request.version == HttpVersion11:
  187. if expect.lower() == "100-continue":
  188. await request.writer.write(
  189. b"HTTP/1.1 100 Continue\r\n\r\n", drain=False)
  190. else:
  191. raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)
  192. class Resource(AbstractResource):
  193. def __init__(self, *, name=None):
  194. super().__init__(name=name)
  195. self._routes = []
  196. def add_route(self, method, handler, *,
  197. expect_handler=None):
  198. for route_obj in self._routes:
  199. if route_obj.method == method or route_obj.method == hdrs.METH_ANY:
  200. raise RuntimeError("Added route will never be executed, "
  201. "method {route.method} is already "
  202. "registered".format(route=route_obj))
  203. route_obj = ResourceRoute(method, handler, self,
  204. expect_handler=expect_handler)
  205. self.register_route(route_obj)
  206. return route_obj
  207. def register_route(self, route):
  208. assert isinstance(route, ResourceRoute), \
  209. 'Instance of Route class is required, got {!r}'.format(route)
  210. self._routes.append(route)
  211. async def resolve(self, request):
  212. allowed_methods = set()
  213. match_dict = self._match(request.rel_url.raw_path)
  214. if match_dict is None:
  215. return None, allowed_methods
  216. for route_obj in self._routes:
  217. route_method = route_obj.method
  218. allowed_methods.add(route_method)
  219. if (route_method == request.method or
  220. route_method == hdrs.METH_ANY):
  221. return (UrlMappingMatchInfo(match_dict, route_obj),
  222. allowed_methods)
  223. else:
  224. return None, allowed_methods
  225. def __len__(self):
  226. return len(self._routes)
  227. def __iter__(self):
  228. return iter(self._routes)
  229. # TODO: implement all abstract methods
  230. class PlainResource(Resource):
  231. def __init__(self, path, *, name=None):
  232. super().__init__(name=name)
  233. assert not path or path.startswith('/')
  234. self._path = path
  235. @property
  236. def canonical(self):
  237. return self._path
  238. def freeze(self):
  239. if not self._path:
  240. self._path = '/'
  241. def add_prefix(self, prefix):
  242. assert prefix.startswith('/')
  243. assert not prefix.endswith('/')
  244. assert len(prefix) > 1
  245. self._path = prefix + self._path
  246. def _match(self, path):
  247. # string comparison is about 10 times faster than regexp matching
  248. if self._path == path:
  249. return {}
  250. else:
  251. return None
  252. def raw_match(self, path):
  253. return self._path == path
  254. def get_info(self):
  255. return {'path': self._path}
  256. def url_for(self):
  257. return URL.build(path=self._path, encoded=True)
  258. def __repr__(self):
  259. name = "'" + self.name + "' " if self.name is not None else ""
  260. return "<PlainResource {name} {path}>".format(name=name,
  261. path=self._path)
  262. class DynamicResource(Resource):
  263. DYN = re.compile(r'\{(?P<var>[_a-zA-Z][_a-zA-Z0-9]*)\}')
  264. DYN_WITH_RE = re.compile(
  265. r'\{(?P<var>[_a-zA-Z][_a-zA-Z0-9]*):(?P<re>.+)\}')
  266. GOOD = r'[^{}/]+'
  267. def __init__(self, path, *, name=None):
  268. super().__init__(name=name)
  269. pattern = ''
  270. formatter = ''
  271. for part in ROUTE_RE.split(path):
  272. match = self.DYN.fullmatch(part)
  273. if match:
  274. pattern += '(?P<{}>{})'.format(match.group('var'), self.GOOD)
  275. formatter += '{' + match.group('var') + '}'
  276. continue
  277. match = self.DYN_WITH_RE.fullmatch(part)
  278. if match:
  279. pattern += '(?P<{var}>{re})'.format(**match.groupdict())
  280. formatter += '{' + match.group('var') + '}'
  281. continue
  282. if '{' in part or '}' in part:
  283. raise ValueError("Invalid path '{}'['{}']".format(path, part))
  284. path = URL.build(path=part).raw_path
  285. formatter += path
  286. pattern += re.escape(path)
  287. try:
  288. compiled = re.compile(pattern)
  289. except re.error as exc:
  290. raise ValueError(
  291. "Bad pattern '{}': {}".format(pattern, exc)) from None
  292. assert compiled.pattern.startswith(PATH_SEP)
  293. assert formatter.startswith('/')
  294. self._pattern = compiled
  295. self._formatter = formatter
  296. @property
  297. def canonical(self):
  298. return self._formatter
  299. def add_prefix(self, prefix):
  300. assert prefix.startswith('/')
  301. assert not prefix.endswith('/')
  302. assert len(prefix) > 1
  303. self._pattern = re.compile(re.escape(prefix)+self._pattern.pattern)
  304. self._formatter = prefix + self._formatter
  305. def _match(self, path):
  306. match = self._pattern.fullmatch(path)
  307. if match is None:
  308. return None
  309. else:
  310. return {key: URL.build(path=value, encoded=True).path
  311. for key, value in match.groupdict().items()}
  312. def raw_match(self, path):
  313. return self._formatter == path
  314. def get_info(self):
  315. return {'formatter': self._formatter,
  316. 'pattern': self._pattern}
  317. def url_for(self, **parts):
  318. url = self._formatter.format_map({k: URL.build(path=v).raw_path
  319. for k, v in parts.items()})
  320. return URL.build(path=url)
  321. def __repr__(self):
  322. name = "'" + self.name + "' " if self.name is not None else ""
  323. return ("<DynamicResource {name} {formatter}>"
  324. .format(name=name, formatter=self._formatter))
  325. class PrefixResource(AbstractResource):
  326. def __init__(self, prefix, *, name=None):
  327. assert not prefix or prefix.startswith('/'), prefix
  328. assert prefix in ('', '/') or not prefix.endswith('/'), prefix
  329. super().__init__(name=name)
  330. self._prefix = URL.build(path=prefix).raw_path
  331. @property
  332. def canonical(self):
  333. return self._prefix
  334. def add_prefix(self, prefix):
  335. assert prefix.startswith('/')
  336. assert not prefix.endswith('/')
  337. assert len(prefix) > 1
  338. self._prefix = prefix + self._prefix
  339. def raw_match(self, prefix):
  340. return False
  341. # TODO: impl missing abstract methods
  342. class StaticResource(PrefixResource):
  343. VERSION_KEY = 'v'
  344. def __init__(self, prefix, directory, *, name=None,
  345. expect_handler=None, chunk_size=256 * 1024,
  346. show_index=False, follow_symlinks=False,
  347. append_version=False):
  348. super().__init__(prefix, name=name)
  349. try:
  350. directory = Path(directory)
  351. if str(directory).startswith('~'):
  352. directory = Path(os.path.expanduser(str(directory)))
  353. directory = directory.resolve()
  354. if not directory.is_dir():
  355. raise ValueError('Not a directory')
  356. except (FileNotFoundError, ValueError) as error:
  357. raise ValueError(
  358. "No directory exists at '{}'".format(directory)) from error
  359. self._directory = directory
  360. self._show_index = show_index
  361. self._chunk_size = chunk_size
  362. self._follow_symlinks = follow_symlinks
  363. self._expect_handler = expect_handler
  364. self._append_version = append_version
  365. self._routes = {'GET': ResourceRoute('GET', self._handle, self,
  366. expect_handler=expect_handler),
  367. 'HEAD': ResourceRoute('HEAD', self._handle, self,
  368. expect_handler=expect_handler)}
  369. def url_for(self, *, filename, append_version=None):
  370. if append_version is None:
  371. append_version = self._append_version
  372. if isinstance(filename, Path):
  373. filename = str(filename)
  374. while filename.startswith('/'):
  375. filename = filename[1:]
  376. filename = '/' + filename
  377. # filename is not encoded
  378. url = URL.build(path=self._prefix + filename)
  379. if append_version is True:
  380. try:
  381. if filename.startswith('/'):
  382. filename = filename[1:]
  383. filepath = self._directory.joinpath(filename).resolve()
  384. if not self._follow_symlinks:
  385. filepath.relative_to(self._directory)
  386. except (ValueError, FileNotFoundError):
  387. # ValueError for case when path point to symlink
  388. # with follow_symlinks is False
  389. return url # relatively safe
  390. if filepath.is_file():
  391. # TODO cache file content
  392. # with file watcher for cache invalidation
  393. with open(str(filepath), mode='rb') as f:
  394. file_bytes = f.read()
  395. h = self._get_file_hash(file_bytes)
  396. url = url.with_query({self.VERSION_KEY: h})
  397. return url
  398. return url
  399. @staticmethod
  400. def _get_file_hash(byte_array):
  401. m = hashlib.sha256() # todo sha256 can be configurable param
  402. m.update(byte_array)
  403. b64 = base64.urlsafe_b64encode(m.digest())
  404. return b64.decode('ascii')
  405. def get_info(self):
  406. return {'directory': self._directory,
  407. 'prefix': self._prefix}
  408. def set_options_route(self, handler):
  409. if 'OPTIONS' in self._routes:
  410. raise RuntimeError('OPTIONS route was set already')
  411. self._routes['OPTIONS'] = ResourceRoute(
  412. 'OPTIONS', handler, self,
  413. expect_handler=self._expect_handler)
  414. async def resolve(self, request):
  415. path = request.rel_url.raw_path
  416. method = request.method
  417. allowed_methods = set(self._routes)
  418. if not path.startswith(self._prefix):
  419. return None, set()
  420. if method not in allowed_methods:
  421. return None, allowed_methods
  422. match_dict = {'filename': URL.build(path=path[len(self._prefix)+1:],
  423. encoded=True).path}
  424. return (UrlMappingMatchInfo(match_dict, self._routes[method]),
  425. allowed_methods)
  426. def __len__(self):
  427. return len(self._routes)
  428. def __iter__(self):
  429. return iter(self._routes.values())
  430. async def _handle(self, request):
  431. rel_url = request.match_info['filename']
  432. try:
  433. filename = Path(rel_url)
  434. if filename.anchor:
  435. # rel_url is an absolute name like
  436. # /static/\\machine_name\c$ or /static/D:\path
  437. # where the static dir is totally different
  438. raise HTTPForbidden()
  439. filepath = self._directory.joinpath(filename).resolve()
  440. if not self._follow_symlinks:
  441. filepath.relative_to(self._directory)
  442. except (ValueError, FileNotFoundError) as error:
  443. # relatively safe
  444. raise HTTPNotFound() from error
  445. except HTTPForbidden:
  446. raise
  447. except Exception as error:
  448. # perm error or other kind!
  449. request.app.logger.exception(error)
  450. raise HTTPNotFound() from error
  451. # on opening a dir, load it's contents if allowed
  452. if filepath.is_dir():
  453. if self._show_index:
  454. try:
  455. ret = Response(text=self._directory_as_html(filepath),
  456. content_type="text/html")
  457. except PermissionError:
  458. raise HTTPForbidden()
  459. else:
  460. raise HTTPForbidden()
  461. elif filepath.is_file():
  462. ret = FileResponse(filepath, chunk_size=self._chunk_size)
  463. else:
  464. raise HTTPNotFound
  465. return ret
  466. def _directory_as_html(self, filepath):
  467. # returns directory's index as html
  468. # sanity check
  469. assert filepath.is_dir()
  470. relative_path_to_dir = filepath.relative_to(self._directory).as_posix()
  471. index_of = "Index of /{}".format(relative_path_to_dir)
  472. h1 = "<h1>{}</h1>".format(index_of)
  473. index_list = []
  474. dir_index = filepath.iterdir()
  475. for _file in sorted(dir_index):
  476. # show file url as relative to static path
  477. rel_path = _file.relative_to(self._directory).as_posix()
  478. file_url = self._prefix + '/' + rel_path
  479. # if file is a directory, add '/' to the end of the name
  480. if _file.is_dir():
  481. file_name = "{}/".format(_file.name)
  482. else:
  483. file_name = _file.name
  484. index_list.append(
  485. '<li><a href="{url}">{name}</a></li>'.format(url=file_url,
  486. name=file_name)
  487. )
  488. ul = "<ul>\n{}\n</ul>".format('\n'.join(index_list))
  489. body = "<body>\n{}\n{}\n</body>".format(h1, ul)
  490. head_str = "<head>\n<title>{}</title>\n</head>".format(index_of)
  491. html = "<html>\n{}\n{}\n</html>".format(head_str, body)
  492. return html
  493. def __repr__(self):
  494. name = "'" + self.name + "'" if self.name is not None else ""
  495. return "<StaticResource {name} {path} -> {directory!r}>".format(
  496. name=name, path=self._prefix, directory=self._directory)
  497. class PrefixedSubAppResource(PrefixResource):
  498. def __init__(self, prefix, app):
  499. super().__init__(prefix)
  500. self._app = app
  501. for resource in app.router.resources():
  502. resource.add_prefix(prefix)
  503. def add_prefix(self, prefix):
  504. super().add_prefix(prefix)
  505. for resource in self._app.router.resources():
  506. resource.add_prefix(prefix)
  507. def url_for(self, *args, **kwargs):
  508. raise RuntimeError(".url_for() is not supported "
  509. "by sub-application root")
  510. def get_info(self):
  511. return {'app': self._app,
  512. 'prefix': self._prefix}
  513. async def resolve(self, request):
  514. if not request.url.raw_path.startswith(self._prefix):
  515. return None, set()
  516. match_info = await self._app.router.resolve(request)
  517. match_info.add_app(self._app)
  518. if isinstance(match_info.http_exception, HTTPMethodNotAllowed):
  519. methods = match_info.http_exception.allowed_methods
  520. else:
  521. methods = set()
  522. return match_info, methods
  523. def __len__(self):
  524. return len(self._app.router.routes())
  525. def __iter__(self):
  526. return iter(self._app.router.routes())
  527. def __repr__(self):
  528. return "<PrefixedSubAppResource {prefix} -> {app!r}>".format(
  529. prefix=self._prefix, app=self._app)
  530. class ResourceRoute(AbstractRoute):
  531. """A route with resource"""
  532. def __init__(self, method, handler, resource, *,
  533. expect_handler=None):
  534. super().__init__(method, handler, expect_handler=expect_handler,
  535. resource=resource)
  536. def __repr__(self):
  537. return "<ResourceRoute [{method}] {resource} -> {handler!r}".format(
  538. method=self.method, resource=self._resource,
  539. handler=self.handler)
  540. @property
  541. def name(self):
  542. return self._resource.name
  543. def url_for(self, *args, **kwargs):
  544. """Construct url for route with additional params."""
  545. return self._resource.url_for(*args, **kwargs)
  546. def get_info(self):
  547. return self._resource.get_info()
  548. class SystemRoute(AbstractRoute):
  549. def __init__(self, http_exception):
  550. super().__init__(hdrs.METH_ANY, self._handler)
  551. self._http_exception = http_exception
  552. def url_for(self, *args, **kwargs):
  553. raise RuntimeError(".url_for() is not allowed for SystemRoute")
  554. @property
  555. def name(self):
  556. return None
  557. def get_info(self):
  558. return {'http_exception': self._http_exception}
  559. async def _handler(self, request):
  560. raise self._http_exception
  561. @property
  562. def status(self):
  563. return self._http_exception.status
  564. @property
  565. def reason(self):
  566. return self._http_exception.reason
  567. def __repr__(self):
  568. return "<SystemRoute {self.status}: {self.reason}>".format(self=self)
  569. class View(AbstractView):
  570. async def _iter(self):
  571. if self.request.method not in hdrs.METH_ALL:
  572. self._raise_allowed_methods()
  573. method = getattr(self, self.request.method.lower(), None)
  574. if method is None:
  575. self._raise_allowed_methods()
  576. resp = await method()
  577. return resp
  578. def __await__(self):
  579. return self._iter().__await__()
  580. def _raise_allowed_methods(self):
  581. allowed_methods = {
  582. m for m in hdrs.METH_ALL if hasattr(self, m.lower())}
  583. raise HTTPMethodNotAllowed(self.request.method, allowed_methods)
  584. class ResourcesView(Sized, Iterable, Container):
  585. def __init__(self, resources):
  586. self._resources = resources
  587. def __len__(self):
  588. return len(self._resources)
  589. def __iter__(self):
  590. yield from self._resources
  591. def __contains__(self, resource):
  592. return resource in self._resources
  593. class RoutesView(Sized, Iterable, Container):
  594. def __init__(self, resources):
  595. self._routes = []
  596. for resource in resources:
  597. for route_obj in resource:
  598. self._routes.append(route_obj)
  599. def __len__(self):
  600. return len(self._routes)
  601. def __iter__(self):
  602. yield from self._routes
  603. def __contains__(self, route_obj):
  604. return route_obj in self._routes
  605. class UrlDispatcher(AbstractRouter, collections.abc.Mapping):
  606. NAME_SPLIT_RE = re.compile(r'[.:-]')
  607. def __init__(self):
  608. super().__init__()
  609. self._resources = []
  610. self._named_resources = {}
  611. async def resolve(self, request):
  612. method = request.method
  613. allowed_methods = set()
  614. for resource in self._resources:
  615. match_dict, allowed = await resource.resolve(request)
  616. if match_dict is not None:
  617. return match_dict
  618. else:
  619. allowed_methods |= allowed
  620. else:
  621. if allowed_methods:
  622. return MatchInfoError(HTTPMethodNotAllowed(method,
  623. allowed_methods))
  624. else:
  625. return MatchInfoError(HTTPNotFound())
  626. def __iter__(self):
  627. return iter(self._named_resources)
  628. def __len__(self):
  629. return len(self._named_resources)
  630. def __contains__(self, name):
  631. return name in self._named_resources
  632. def __getitem__(self, name):
  633. return self._named_resources[name]
  634. def resources(self):
  635. return ResourcesView(self._resources)
  636. def routes(self):
  637. return RoutesView(self._resources)
  638. def named_resources(self):
  639. return MappingProxyType(self._named_resources)
  640. def register_resource(self, resource):
  641. assert isinstance(resource, AbstractResource), \
  642. 'Instance of AbstractResource class is required, got {!r}'.format(
  643. resource)
  644. if self.frozen:
  645. raise RuntimeError(
  646. "Cannot register a resource into frozen router.")
  647. name = resource.name
  648. if name is not None:
  649. parts = self.NAME_SPLIT_RE.split(name)
  650. for part in parts:
  651. if not part.isidentifier() or keyword.iskeyword(part):
  652. raise ValueError('Incorrect route name {!r}, '
  653. 'the name should be a sequence of '
  654. 'python identifiers separated '
  655. 'by dash, dot or column'.format(name))
  656. if name in self._named_resources:
  657. raise ValueError('Duplicate {!r}, '
  658. 'already handled by {!r}'
  659. .format(name, self._named_resources[name]))
  660. self._named_resources[name] = resource
  661. self._resources.append(resource)
  662. def add_resource(self, path, *, name=None):
  663. if path and not path.startswith('/'):
  664. raise ValueError("path should be started with / or be empty")
  665. # Reuse last added resource if path and name are the same
  666. if self._resources:
  667. resource = self._resources[-1]
  668. if resource.name == name and resource.raw_match(path):
  669. return resource
  670. if not ('{' in path or '}' in path or ROUTE_RE.search(path)):
  671. url = URL.build(path=path)
  672. resource = PlainResource(url.raw_path, name=name)
  673. self.register_resource(resource)
  674. return resource
  675. resource = DynamicResource(path, name=name)
  676. self.register_resource(resource)
  677. return resource
  678. def add_route(self, method, path, handler,
  679. *, name=None, expect_handler=None):
  680. resource = self.add_resource(path, name=name)
  681. return resource.add_route(method, handler,
  682. expect_handler=expect_handler)
  683. def add_static(self, prefix, path, *, name=None, expect_handler=None,
  684. chunk_size=256 * 1024,
  685. show_index=False, follow_symlinks=False,
  686. append_version=False):
  687. """Add static files view.
  688. prefix - url prefix
  689. path - folder with files
  690. """
  691. assert prefix.startswith('/')
  692. if prefix.endswith('/'):
  693. prefix = prefix[:-1]
  694. resource = StaticResource(prefix, path,
  695. name=name,
  696. expect_handler=expect_handler,
  697. chunk_size=chunk_size,
  698. show_index=show_index,
  699. follow_symlinks=follow_symlinks,
  700. append_version=append_version)
  701. self.register_resource(resource)
  702. return resource
  703. def add_head(self, path, handler, **kwargs):
  704. """
  705. Shortcut for add_route with method HEAD
  706. """
  707. return self.add_route(hdrs.METH_HEAD, path, handler, **kwargs)
  708. def add_options(self, path, handler, **kwargs):
  709. """
  710. Shortcut for add_route with method OPTIONS
  711. """
  712. return self.add_route(hdrs.METH_OPTIONS, path, handler, **kwargs)
  713. def add_get(self, path, handler, *, name=None, allow_head=True, **kwargs):
  714. """
  715. Shortcut for add_route with method GET, if allow_head is true another
  716. route is added allowing head requests to the same endpoint
  717. """
  718. resource = self.add_resource(path, name=name)
  719. if allow_head:
  720. resource.add_route(hdrs.METH_HEAD, handler, **kwargs)
  721. return resource.add_route(hdrs.METH_GET, handler, **kwargs)
  722. def add_post(self, path, handler, **kwargs):
  723. """
  724. Shortcut for add_route with method POST
  725. """
  726. return self.add_route(hdrs.METH_POST, path, handler, **kwargs)
  727. def add_put(self, path, handler, **kwargs):
  728. """
  729. Shortcut for add_route with method PUT
  730. """
  731. return self.add_route(hdrs.METH_PUT, path, handler, **kwargs)
  732. def add_patch(self, path, handler, **kwargs):
  733. """
  734. Shortcut for add_route with method PATCH
  735. """
  736. return self.add_route(hdrs.METH_PATCH, path, handler, **kwargs)
  737. def add_delete(self, path, handler, **kwargs):
  738. """
  739. Shortcut for add_route with method DELETE
  740. """
  741. return self.add_route(hdrs.METH_DELETE, path, handler, **kwargs)
  742. def add_view(self, path, handler, **kwargs):
  743. """
  744. Shortcut for add_route with ANY methods for a class-based view
  745. """
  746. return self.add_route(hdrs.METH_ANY, path, handler, **kwargs)
  747. def freeze(self):
  748. super().freeze()
  749. for resource in self._resources:
  750. resource.freeze()
  751. def add_routes(self, routes):
  752. """Append routes to route table.
  753. Parameter should be a sequence of RouteDef objects.
  754. """
  755. for route_obj in routes:
  756. route_obj.register(self)