from __future__ import annotations # noqa D100
from typing import TYPE_CHECKING, final, Self, Any
from urllib.parse import urlparse
import asyncclick as click
from typing_extensions import override
from betty.app.factory import AppDependentFactory
from betty.assertion import assert_str, assert_path, assert_locale
from betty.cli.commands import command, Command
from betty.cli.error import user_facing_error_to_bad_parameter
from betty.config import write_configuration_file
from betty.locale import get_display_name, DEFAULT_LOCALE
from betty.locale.localizable import _, StaticTranslations
from betty.machine_name import machinify, assert_machine_name
from betty.plugin import ShorthandPluginBase
from betty.plugin.config import PluginInstanceConfiguration
from betty.project.config import LocaleConfiguration, ProjectConfiguration
from betty.project.extension.deriver import Deriver
from betty.project.extension.gramps import Gramps
from betty.project.extension.gramps.config import (
FamilyTreeConfiguration,
GrampsConfiguration,
)
from betty.project.extension.http_api_doc import HttpApiDoc
from betty.project.extension.maps import Maps
from betty.project.extension.privatizer import Privatizer
from betty.project.extension.raspberry_mint import RaspberryMint
from betty.project.extension.trees import Trees
from betty.project.extension.webpack import Webpack
from betty.project.extension.wikipedia import Wikipedia
if TYPE_CHECKING:
from pathlib import Path
from collections.abc import Sequence, Callable
from betty.app import App
[docs]
@final
class New(ShorthandPluginBase, AppDependentFactory, Command):
"""
A command to create a new project.
"""
_plugin_id = "new"
_plugin_label = _("Create a new project")
[docs]
def __init__(self, app: App):
self._app = app
[docs]
@override
@classmethod
async def new_for_app(cls, app: App) -> Self:
return cls(app)
[docs]
@override
async def click_command(self) -> click.Command:
localizer = await self._app.localizer
description = self.plugin_description()
@command(
self.plugin_id(),
short_help=self.plugin_label().localize(localizer),
help=description.localize(localizer)
if description
else self.plugin_label().localize(localizer),
)
async def new() -> None:
configuration_file_path = click.prompt(
localizer._(
"Where do you want to save your project's configuration file?"
),
value_proc=user_facing_error_to_bad_parameter(localizer)(
_assert_project_configuration_file_path
),
)
configuration = await ProjectConfiguration.new(
configuration_file_path,
)
configuration.extensions.enable(
Deriver, Privatizer, RaspberryMint, Wikipedia
)
webpack_requirement = await Webpack.requirement()
if webpack_requirement.is_met():
configuration.extensions.enable(HttpApiDoc, Maps, Trees)
configuration.locales.replace(
LocaleConfiguration(
click.prompt(
localizer._(
"Which language should your project site be generated in? Enter an IETF BCP 47 language code."
),
default=DEFAULT_LOCALE,
value_proc=user_facing_error_to_bad_parameter(localizer)(
assert_locale()
),
)
)
)
while click.confirm(localizer._("Do you want to add another locale?")):
configuration.locales.append(
LocaleConfiguration(
click.prompt(
localizer._(
"Which language should your project site be generated in? Enter an IETF BCP 47 language code."
),
value_proc=user_facing_error_to_bad_parameter(localizer)(
assert_locale()
),
)
)
)
locales = list(configuration.locales)
configuration.title = _prompt_static_translations(
locales,
localizer._("What is your project called in {locale}?"),
)
configuration.name = click.prompt(
localizer._("What is your project's machine name?"),
default=machinify(
configuration.title.localize(
await self._app.localizers.get(
configuration.locales.default.locale
)
)
),
value_proc=user_facing_error_to_bad_parameter(localizer)(
assert_machine_name()
),
)
configuration.author = _prompt_static_translations(
locales,
localizer._("What is the project author called in {locale}?"),
)
configuration.url = click.prompt(
localizer._("At which URL will your site be published?"),
default="https://example.com",
value_proc=user_facing_error_to_bad_parameter(localizer)(_assert_url),
)
if click.confirm(localizer._("Do you want to load a Gramps family tree?")):
configuration.extensions.append(
PluginInstanceConfiguration(
Gramps,
configuration=GrampsConfiguration(
family_trees=[
FamilyTreeConfiguration(
click.prompt(
localizer._(
"What is the path to your exported Gramps family tree file?"
),
value_proc=user_facing_error_to_bad_parameter(
localizer
)(assert_path()),
)
)
]
),
)
)
await write_configuration_file(
configuration, configuration.configuration_file_path
)
click.secho(
localizer._("Saved your project to {configuration_file}.").format(
configuration_file=str(configuration_file_path)
),
fg="green",
)
return new
def _assert_project_configuration_file_path(value: Any) -> Path:
configuration_file_path = assert_path()(value)
if not configuration_file_path.suffix:
configuration_file_path /= "betty.yaml"
return configuration_file_path
def _assert_url(value: Any) -> str:
url = assert_str()(value)
parsed_url = urlparse(url)
scheme = parsed_url.scheme or "http"
return f"{scheme}://{parsed_url.netloc}{parsed_url.path}"
def _prompt_static_translations(
locales: Sequence[str],
text: str,
default: Any | None = None,
hide_input: bool = False,
confirmation_prompt: bool | str = False,
type: click.ParamType | Any | None = None, # noqa A002
value_proc: Callable[[str], Any] | None = None,
prompt_suffix: str = ": ",
show_default: bool = True,
err: bool = False,
show_choices: bool = True,
) -> StaticTranslations:
return {
locale: click.prompt(
text.format(locale=get_display_name(locale)),
default,
hide_input,
confirmation_prompt,
type,
value_proc,
prompt_suffix,
show_default,
err,
show_choices,
)
for locale in locales
}