Beispiel #1
0
def new_config(config_path: Path, ) -> None:
    """
    Create a new configuration file with some pre-filled data.

    To avoid data loss, this path must not exist.

    This command is interactive, and will ask you some questions about your project.

    This command targeted towards existing projects that want to start using wap or
    projects that want to migrate from another packager.

    More than likely, you will need to edit this configuration file to fit to your
    project. This just provides a starting point.
    """
    if config_path.exists():
        raise NewConfigException('Path "' +
                                 click.style(f"{config_path}", fg="green") +
                                 '" exists. Aborting to avoid data loss.')

    project_name = Path.cwd().name

    config = guide(project_dir_name=project_name)
    log.info('\nCreating config file at "' +
             click.style(f"{config_path}", fg="green"))
    log.info(
        click.style("Make sure to edit it to fit your project!", fg="yellow"))
    config.to_path(config_path)
Beispiel #2
0
def zip_addon(addon_name: str, version: str, wow_version: WoWVersion) -> Path:
    package_path = get_package_path(addon_name=addon_name,
                                    version=version,
                                    wow_version=wow_version)
    zip_path = get_zip_path(addon_name=addon_name,
                            version=version,
                            wow_version=wow_version)

    if zip_path.exists():
        delete_path(zip_path)

    zip_path.parent.mkdir(parents=True, exist_ok=True)

    with ZipFile(zip_path, mode="w", compression=ZIP_DEFLATED) as zip_file:
        for path in package_path.rglob("*"):
            zip_file.write(
                filename=path,
                arcname=path.relative_to(package_path),
            )

    log.info("Zipped addon " + click.style(f"{addon_name}", fg="blue") + " (" +
             click.style(f"{wow_version.type()}", fg="magenta") + ') at "' +
             click.style(f"{zip_path}", fg="green") + '"')

    return zip_path
Beispiel #3
0
def package_addon(
    config_path: Path,
    config: Config,
    addon_name: str,
    dir_configs: Sequence[AddonConfig],
    version: str,
    wow_version: WoWVersion,
) -> Path:
    package_path = get_package_path(
        addon_name=addon_name,
        version=version,
        wow_version=wow_version,
    )

    if package_path.exists():
        delete_path(package_path)

    package_path.mkdir(parents=True)

    for dir_config in dir_configs:
        src_dir = config_path.parent / dir_config.path

        if not src_dir.is_dir():
            raise PackageException(
                f'Dir config has path "{src_dir}", but it is not a directory. '
                "This path must point to a directory, must be relative to "
                f'the parent of the config file ("{config_path.resolve()}") and, '
                'if it is in a subdirectory, must only use forward slashes ("/").'
            )

        dst_dir = package_path / src_dir.name

        shutil.copytree(src_dir, dst_dir)

        source_toc_path = src_dir / (src_dir.name + ".toc")
        toc_path = dst_dir / (src_dir.name + ".toc")

        if toc_path.exists():
            log.warn(f'TOC file "{source_toc_path}" exists, and will '
                     "be overwritten with a generated one.")
            delete_path(toc_path)

        write_toc(
            config=config,
            toc_config=dir_config.toc_config,
            dir_path=src_dir,
            write_path=toc_path,
            addon_version=version,
            wow_version=wow_version,
        )

    log.info("Built addon " + click.style(f"{addon_name}", fg="blue") + " (" +
             click.style(f"{wow_version.type()}", fg="magenta") + ') at "' +
             click.style(f"{package_path}", fg="green") + '"')

    return package_path
Beispiel #4
0
def _prompt_yes_no(text: str, **prompt_kwargs: Any) -> bool:
    yes_no_pattern = re.compile(r"^(y|(?:yes)|n|(?:no))$", flags=re.IGNORECASE)
    text = (click.style(text, fg="blue") + " <" +
            click.style("yes", fg="magenta") + "|" +
            click.style("y", fg="magenta") + "|" +
            click.style("no", fg="magenta") + "|" +
            click.style("n", fg="magenta") + ">")
    while True:
        value = cast(str, click.prompt(text=text, err=True, **prompt_kwargs))
        if (match := yes_no_pattern.match(value)) is not None:
            return "y" in match[0].lower()
        log.info(
            f'Value "{value}" does not match pattern: {yes_no_pattern.pattern}'
        )
Beispiel #5
0
def upload_addon(
    *,
    addon_name: str,
    curseforge_config: CurseforgeConfig,
    changelog: Changelog,
    wow_version: WoWVersion,
    version: str,
    release_type: str,
    curseforge_api: CurseForgeAPI,
) -> str:
    cf_wow_version_id = curseforge_api.get_version_id(
        version=wow_version.dot_version())

    zip_file_path = get_zip_path(addon_name=addon_name,
                                 version=version,
                                 wow_version=wow_version)

    if not zip_file_path.is_file():
        log.error(
            "Expected zip file not found. Have you run `" +
            click.style(f'wap package --version "{version}"', fg="blue") +
            "` yet?")
        raise UploadException(f'Zip file "{zip_file_path}" not found.')

    with zip_file_path.open("rb") as package_archive_file:
        file_id = curseforge_api.upload_addon_file(
            project_id=curseforge_config.project_id,
            archive_file=package_archive_file,
            display_name=f"{addon_name}-{version}-{wow_version.type()}",
            changelog=changelog,
            wow_version_id=cf_wow_version_id,
            release_type=release_type,
            file_name=zip_file_path.name,
        )

    url = curseforge_api.uploaded_file_url(
        slug=curseforge_config.project_slug,
        file_id=file_id,
    )
    log.info("Uploaded " + click.style(f"{addon_name}", fg="blue") + " (" +
             click.style(f"{wow_version.type()}", fg="magenta") +
             ") to CurseForge at " + click.style(f"{url}", fg="green"))

    return url
Beispiel #6
0
def _prompt_until_matching(
    pattern: Pattern[str],
    text: str,
    default: Optional[str] = None,
    **prompt_kwargs: Any,
) -> re.Match[str]:
    text = click.style(text, fg="blue")
    if default is not None:
        text += " [" + click.style(default, fg="green") + "]"
    while True:
        value = cast(
            str,
            click.prompt(text,
                         default=default,
                         show_default=False,
                         err=True,
                         **prompt_kwargs),
        )
        if (match := pattern.match(value)) is not None:
            return match
        log.info(f'Value "{value}" does not match pattern: {pattern.pattern}')
Beispiel #7
0
def watch_project(config_path: Path) -> Generator[set[FileState], None, None]:
    is_first_iter = True
    last_state: set[FileState] = set()

    log.info("Starting filesystem watcher. Press " +
             click.style("Ctrl-C", fg="red") + " at any time to stop.\n")

    while True:
        new_state = asyncio.run(
            _poll_for_change(config_path=config_path, last_state=last_state))

        if new_state != last_state:
            time_str = arrow.now().isoformat()
            if is_first_iter:
                log.info(
                    f"{time_str} - Running initial package and install...\n")
                is_first_iter = False
            else:
                log.info(
                    f"{time_str} - Changes detected. Repackaging and installing...\n"
                )

            diff = new_state - last_state
            last_state = new_state
            yield diff
Beispiel #8
0
def do_package_and_dev_install(
    ctx: click.Context,
    config_path: Path,
    version: str,
    wow_addons_path: Path,
) -> None:
    try:
        ctx.invoke(
            package,
            config_path=config_path,
            version=version,
        )
        ctx.invoke(
            dev_install,
            config_path=config_path,
            version=version,
            wow_addons_path=wow_addons_path,
        )
        log.info("")  # separate runs from one another
    except WAPException as we:
        log.error(we.message + "\n")
    except click.ClickException as ce:
        log.error(ce.message + "\n")
Beispiel #9
0
def dev_install_addon(
    *,
    addon_name: str,
    version: str,
    wow_addons_path: Path,
    wow_version: WoWVersion,
) -> Sequence[Path]:
    installed_paths = []

    package_path = get_package_path(addon_name=addon_name,
                                    version=version,
                                    wow_version=wow_version)

    if not package_path.is_dir():
        log.error("Expected package directory not found. Have you run `" +
                  click.style(f"wap package --version {version}", fg="blue") +
                  "` yet?")
        raise DevInstallException(
            f'Package directory "{package_path}" not found.')

    for addon_path in package_path.iterdir():
        install_addon_path = wow_addons_path / addon_path.name

        if install_addon_path.exists():
            delete_path(install_addon_path)

        shutil.copytree(addon_path, install_addon_path)

        installed_paths.append(install_addon_path)

        log.info("Installed addon directory " +
                 click.style(f"{addon_path.name}", fg="blue") + " (" +
                 click.style(f"{wow_version.type()}", fg="magenta") +
                 ') to "' + click.style(f"{install_addon_path}", fg="green") +
                 '"')

    return installed_paths
Beispiel #10
0
def validate(
    config_path: Path,
    show_json: bool,
) -> None:
    """
    Validates a wap configuration file.

    An exit code of 0 means the validation was successful. Otherwise, the error
    encountered is displayed and the exit code is non-zero.

    Successful validation does not indicate that you can use all the wap commands. It
    merely means that there were no errors parsing it.
    """
    try:
        config = Config.from_path(config_path)

        log.info(f'✔️ "{config_path}" is valid')

        if show_json:
            click.echo(json.dumps(config.to_python_object(), indent=2))

    except ConfigException:
        log.error(f'❌ "{config_path}" is not valid')
        raise
Beispiel #11
0
def base() -> None:
    """package and upload your WoW addons."""
    # always print out version info on run (will help with issue reports)
    log.info((VERSION_STRING_TEMPLATE + "\n") % {"version": __version__})
Beispiel #12
0
def guide(project_dir_name: str) -> Config:
    addon_name_pattern = re.compile(r"\S+")
    author_pattern = re.compile(r".+")
    notes_pattern = re.compile(r".+")
    project_id_pattern = re.compile(r"\d+")
    curseforge_url_pattern = re.compile(
        r"https:\/\/www\.curseforge\.com\/wow\/addons\/(?P<addon_name>\S+)")

    log.info(
        "This command will guide you through creating your " +
        click.style(".wap.yml", fg="blue") + " config.\n", )

    # addon name
    name = _prompt_until_matching(
        addon_name_pattern,
        text="Addon name",
        default=project_dir_name,
    )[0]

    # author for TOC
    author = _prompt_until_matching(
        author_pattern,
        text="Author name",
    )[0]

    # notes for TOC
    notes = _prompt_until_matching(
        notes_pattern,
        text="Addon description",
    )[0]

    toc_config = TocConfig(
        tags={
            "Title": name,
            "Author": author,
            "Notes": notes
        },
        files=[PurePosixPath("Init.lua")],
    )

    addon_configs = [
        AddonConfig(path=PurePosixPath(name), toc_config=toc_config)
    ]

    # wow versions supported
    wow_versions = []

    if _prompt_yes_no(text="Will this addon support retail WoW?"):
        wow_versions.append(LATEST_RETAIL_VERSION)
        if _prompt_yes_no(text="Will this addon support classic WoW?"):
            wow_versions.append(LATEST_CLASSIC_VERSION)
    else:
        wow_versions.append(LATEST_CLASSIC_VERSION)
        log.info("If not supporting retail, must be supporting classic")

    # curseforge
    curseforge_config = None
    if _prompt_yes_no(text="Do you have a CurseForge project?"):

        project_id = _prompt_until_matching(
            project_id_pattern,
            text="CurseForge project id (found in top-right of addon page)",
        )[0]

        project_slug = _prompt_until_matching(
            curseforge_url_pattern,
            text="CurseForge URL",
        )["addon_name"]

        curseforge_config = CurseforgeConfig(
            project_id=project_id,
            changelog_path=DEFAULT_CHANGELOG_PATH,
            project_slug=project_slug,
        )

    return Config(
        name=name,
        addon_configs=addon_configs,
        wow_versions=wow_versions,
        curseforge_config=curseforge_config,
    )
Beispiel #13
0
def quickstart(project_dir_path: Path, ) -> None:
    """
    Creates a new addon project directory at PROJECT_DIR_PATH with a default structure.
    PROJECT_DIR_PATH must not exist.

    This command is interactive, and will ask you some questions about your project.

    If you later decide you want to change something about how you answered these
    question, edit your config file.

    For example, running:

        wap quickstart MyAddon

    will create a directory structure like the following:

      \b
      MyAddon
      ├── MyAddon
      │   └── Init.lua
      ├── CHANGELOG.md
      ├── README.md
      └── .wap.yml
    """
    # the docstring above contains the \b escape. This prevents paragraph wrapping
    # https://click.palletsprojects.com/en/7.x/documentation/#preventing-rewrapping
    # i'm not sure why i had to adjust the indentation level to get it to line up
    # properly though

    if project_dir_path.exists():
        raise QuickstartException(
            f"{project_dir_path} exists. Choose a path that does not exist for your "
            "new project.")

    config = guide(project_dir_name=project_dir_path.resolve().name)
    project_name = config.name

    log.info('\nCreating project directory at "' +
             click.style(f"{project_dir_path}", fg="green") + '"')
    project_dir_path.mkdir(parents=True)
    config_path = project_dir_path / DEFAULT_CONFIG_PATH
    log.info('Writing config file at "' +
             click.style(f"{config_path}", fg="green") + '"')
    config.to_path(config_path)

    log.info("Creating changelog file at '" +
             click.style(f"{DEFAULT_CHANGELOG_PATH}", fg="green") + '"')
    write_changelog(project_dir_path / DEFAULT_CHANGELOG_PATH, project_name)

    readme_path = project_dir_path / "README.md"
    log.info('Creating readme at "' +
             click.style(f"{readme_path}", fg="green"))
    write_readme(readme_path, project_name)

    # guided config puts 1 starter lua file in the toc config's files.
    dir_config = config.addon_configs[0]
    starter_lua_file = dir_config.toc_config.files[0]
    lua_file = project_dir_path / dir_config.path / starter_lua_file
    log.info("Creating starter lua file at '" +
             click.style(f"{lua_file}", fg="green") + '"')
    lua_file.parent.mkdir()
    write_lua_file(lua_file, project_name)

    log.info(
        click.style(
            "\nProject created! You can now begin developing your project.\n",
            fg="green",
            bold=True,
        ))
    log.info(
        "After you `" + click.style(f'cd "{project_dir_path}"', fg="magenta") +
        "`, you can get started running some wap commands immediately, such as:"
    )
    log.info("  - " + click.style("wap package", fg="blue"))
    log.info("  - " + click.style(
        ("wap dev-install --wow-addons-path "
         f'"{default_wow_addons_path_for_system()}"'),
        fg="blue",
    ))
    if config.curseforge_config:
        log.info("  - " + click.style(
            'wap upload --version "dev" --curseforge-token "<your-token>"',
            fg="blue",
        ))

    log.info("\nHave fun!")