Source code for betty.project.url

"""
URL generators for project resources.
"""

from __future__ import annotations

from typing import final, Any, Self, TYPE_CHECKING
from urllib.parse import quote

from typing_extensions import override

from betty.media_type.media_types import HTML, JSON, JSON_LD
from betty.project.factory import ProjectDependentFactory
from betty.string import camel_case_to_kebab_case
from betty.typing import private
from betty.url import (
    generate_from_path,
    LocalizedUrlGenerator as StdLocalizedUrlGenerator,
    StaticUrlGenerator as StdStaticUrlGenerator,
)
from betty.url.proxy import ProxyLocalizedUrlGenerator
from betty.model import Entity

if TYPE_CHECKING:
    from betty.media_type import MediaType
    from betty.project import Project
    from betty.locale import Localey
    from collections.abc import Mapping


class _ProjectUrlGenerator:
    def __init__(
        self,
        base_url: str,
        root_path: str,
        locales: Mapping[str, str],
        clean_urls: bool,
    ):
        self._base_url = base_url
        self._root_path = root_path
        self._locales = locales
        assert len(locales)
        self._default_locale = next(iter(locales))
        self._clean_urls = clean_urls

    def _generate_from_path(
        self, path: str, *, absolute: bool = False, locale: Localey | None = None
    ) -> str:
        return generate_from_path(
            path,
            absolute=absolute,
            locale=locale,
            base_url=self._base_url,
            root_path=self._root_path,
            locales=self._locales,
            clean_urls=self._clean_urls,
        )


def _supports_path(resource: Any) -> bool:
    return isinstance(resource, str) and resource.startswith("/")


@final
class _LocalizedPathUrlGenerator(_ProjectUrlGenerator, StdLocalizedUrlGenerator):
    @override
    def supports(self, resource: Any) -> bool:
        return _supports_path(resource)

    @override
    def generate(
        self,
        resource: Any,
        media_type: MediaType,
        *,
        absolute: bool = False,
        locale: Localey | None = None,
    ) -> str:
        assert self.supports(resource)
        return self._generate_from_path(
            resource,
            absolute=absolute,
            locale=locale or self._default_locale,
        )


[docs] @final class StaticUrlGenerator( ProjectDependentFactory, _ProjectUrlGenerator, StdStaticUrlGenerator ): """ Generate URLs for static (non-localized) file paths. """
[docs] @override @classmethod async def new_for_project(cls, project: Project) -> Self: return cls( project.configuration.base_url, project.configuration.root_path, { locale_configuration.locale: locale_configuration.alias for locale_configuration in project.configuration.locales.values() }, project.configuration.clean_urls, )
[docs] @override def supports(self, resource: Any) -> bool: return _supports_path(resource)
[docs] @override def generate( self, resource: Any, *, absolute: bool = False, ) -> str: assert self.supports(resource) return self._generate_from_path(resource, absolute=absolute)
class _EntityTypeDependentUrlGenerator(_ProjectUrlGenerator, StdLocalizedUrlGenerator): _pattern: str def __init__( self, base_url: str, root_path: str, locales: Mapping[str, str], clean_urls: bool, ): super().__init__(base_url, root_path, locales, clean_urls) def _get_extension_and_locale( self, media_type: MediaType, *, locale: Localey | None ) -> tuple[str, Localey | None]: if media_type == HTML: return "html", locale or self._default_locale elif media_type in (JSON, JSON_LD): return "json", None else: raise ValueError(f'Unknown entity media type "{media_type}".') @final class _EntityTypeUrlGenerator(_EntityTypeDependentUrlGenerator): _pattern = "/{entity_type}/index.{extension}" @override def supports(self, resource: Any) -> bool: return isinstance(resource, type) and issubclass(resource, Entity) @override def generate( self, resource: Entity, media_type: MediaType, *, absolute: bool = False, locale: Localey | None = None, ) -> str: assert self.supports(resource) extension, locale = self._get_extension_and_locale(media_type, locale=locale) return self._generate_from_path( self._pattern.format( entity_type=camel_case_to_kebab_case(resource.plugin_id()), extension=extension, ), absolute=absolute, locale=locale, ) @final class _EntityUrlGenerator(_EntityTypeDependentUrlGenerator): _pattern = "/{entity_type}/{entity_id}/index.{extension}" @override def supports(self, resource: Any) -> bool: return isinstance(resource, Entity) @override def generate( self, resource: Entity, media_type: MediaType, *, absolute: bool = False, locale: Localey | None = None, ) -> str: assert self.supports(resource) extension, locale = self._get_extension_and_locale(media_type, locale=locale) return self._generate_from_path( self._pattern.format( entity_type=camel_case_to_kebab_case(resource.plugin_id()), entity_id=quote(resource.id), extension=extension, ), absolute=absolute, locale=locale, )
[docs] @final class LocalizedUrlGenerator(StdLocalizedUrlGenerator, ProjectDependentFactory): """ Generate URLs for all resources provided by a Betty project. """
[docs] @private def __init__( self, *upstreams: StdLocalizedUrlGenerator, ): self._upstream = ProxyLocalizedUrlGenerator(*upstreams)
[docs] @override @classmethod async def new_for_project(cls, project: Project) -> Self: args = ( project.configuration.base_url, project.configuration.root_path, { locale_configuration.locale: locale_configuration.alias for locale_configuration in project.configuration.locales.values() }, project.configuration.clean_urls, ) return cls( _EntityTypeUrlGenerator(*args), _EntityUrlGenerator(*args), _LocalizedPathUrlGenerator(*args), )
[docs] @override def supports(self, resource: Any) -> bool: return self._upstream.supports(resource)
[docs] @override def generate( self, resource: Any, media_type: MediaType, *, absolute: bool = False, locale: Localey | None = None, ) -> str: return self._upstream.generate( resource, media_type, absolute=absolute, locale=locale )