handler.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """An extension handler."""
  2. from __future__ import annotations
  3. from logging import Logger
  4. from typing import TYPE_CHECKING, Any, cast
  5. from jinja2 import Template
  6. from jinja2.exceptions import TemplateNotFound
  7. from jupyter_server.base.handlers import FileFindHandler
  8. if TYPE_CHECKING:
  9. from traitlets.config import Config
  10. from jupyter_server.extension.application import ExtensionApp
  11. from jupyter_server.serverapp import ServerApp
  12. class ExtensionHandlerJinjaMixin:
  13. """Mixin class for ExtensionApp handlers that use jinja templating for
  14. template rendering.
  15. """
  16. def get_template(self, name: str) -> Template:
  17. """Return the jinja template object for a given name"""
  18. try:
  19. env = f"{self.name}_jinja2_env" # type:ignore[attr-defined]
  20. template = cast(Template, self.settings[env].get_template(name)) # type:ignore[attr-defined]
  21. return template
  22. except TemplateNotFound:
  23. return cast(Template, super().get_template(name)) # type:ignore[misc]
  24. class ExtensionHandlerMixin:
  25. """Base class for Jupyter server extension handlers.
  26. Subclasses can serve static files behind a namespaced
  27. endpoint: "<base_url>/static/<name>/"
  28. This allows multiple extensions to serve static files under
  29. their own namespace and avoid intercepting requests for
  30. other extensions.
  31. """
  32. settings: dict[str, Any]
  33. def initialize(self, name: str, *args: Any, **kwargs: Any) -> None:
  34. self.name = name
  35. try:
  36. super().initialize(*args, **kwargs) # type:ignore[misc]
  37. except TypeError:
  38. pass
  39. @property
  40. def extensionapp(self) -> ExtensionApp:
  41. return cast("ExtensionApp", self.settings[self.name])
  42. @property
  43. def serverapp(self) -> ServerApp:
  44. key = "serverapp"
  45. return cast("ServerApp", self.settings[key])
  46. @property
  47. def log(self) -> Logger:
  48. if not hasattr(self, "name"):
  49. return cast(Logger, super().log) # type:ignore[misc]
  50. # Attempt to pull the ExtensionApp's log, otherwise fall back to ServerApp.
  51. try:
  52. return cast(Logger, self.extensionapp.log)
  53. except AttributeError:
  54. return cast(Logger, self.serverapp.log)
  55. @property
  56. def config(self) -> Config:
  57. return cast("Config", self.settings[f"{self.name}_config"])
  58. @property
  59. def server_config(self) -> Config:
  60. return cast("Config", self.settings["config"])
  61. @property
  62. def base_url(self) -> str:
  63. return cast(str, self.settings.get("base_url", "/"))
  64. def render_template(self, name: str, **ns) -> str:
  65. """Override render template to handle static_paths
  66. If render_template is called with a template from the base environment
  67. (e.g. default error pages)
  68. make sure our extension-specific static_url is _not_ used.
  69. """
  70. template = cast(Template, self.get_template(name)) # type:ignore[attr-defined]
  71. ns.update(self.template_namespace) # type:ignore[attr-defined]
  72. if template.environment is self.settings["jinja2_env"]:
  73. # default template environment, use default static_url
  74. ns["static_url"] = super().static_url # type:ignore[misc]
  75. return cast(str, template.render(**ns))
  76. @property
  77. def static_url_prefix(self) -> str:
  78. return self.extensionapp.static_url_prefix
  79. @property
  80. def static_path(self) -> str:
  81. return cast(str, self.settings[f"{self.name}_static_paths"])
  82. def static_url(self, path: str, include_host: bool | None = None, **kwargs: Any) -> str:
  83. """Returns a static URL for the given relative static file path.
  84. This method requires you set the ``{name}_static_path``
  85. setting in your extension (which specifies the root directory
  86. of your static files).
  87. This method returns a versioned url (by default appending
  88. ``?v=<signature>``), which allows the static files to be
  89. cached indefinitely. This can be disabled by passing
  90. ``include_version=False`` (in the default implementation;
  91. other static file implementations are not required to support
  92. this, but they may support other options).
  93. By default this method returns URLs relative to the current
  94. host, but if ``include_host`` is true the URL returned will be
  95. absolute. If this handler has an ``include_host`` attribute,
  96. that value will be used as the default for all `static_url`
  97. calls that do not pass ``include_host`` as a keyword argument.
  98. """
  99. key = f"{self.name}_static_paths"
  100. try:
  101. self.require_setting(key, "static_url") # type:ignore[attr-defined]
  102. except Exception as e:
  103. if key in self.settings:
  104. msg = (
  105. "This extension doesn't have any static paths listed. Check that the "
  106. "extension's `static_paths` trait is set."
  107. )
  108. raise Exception(msg) from None
  109. else:
  110. raise e
  111. get_url = self.settings.get("static_handler_class", FileFindHandler).make_static_url
  112. if include_host is None:
  113. include_host = getattr(self, "include_host", False)
  114. base = ""
  115. if include_host:
  116. base = self.request.protocol + "://" + self.request.host # type:ignore[attr-defined]
  117. # Hijack settings dict to send extension templates to extension
  118. # static directory.
  119. settings = {
  120. "static_path": self.static_path,
  121. "static_url_prefix": self.static_url_prefix,
  122. }
  123. return base + cast(str, get_url(settings, path, **kwargs))