feat/cli: migrate to typer

Fire is too hard to customize.
This commit is contained in:
tretrauit 2023-07-03 22:44:07 +07:00
parent 5b916025f3
commit 4db6e92b54
7 changed files with 699 additions and 582 deletions

174
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "certifi"
@ -106,6 +106,20 @@ files = [
{file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
@ -143,20 +157,6 @@ files = [
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]]
name = "fire"
version = "0.5.0"
description = "A library for automatically generating command line interfaces."
optional = false
python-versions = "*"
files = [
{file = "fire-0.5.0.tar.gz", hash = "sha256:a6b0d49e98c8963910021f92bba66f65ab440da2982b78eb1bbf95a0a34aacc6"},
]
[package.dependencies]
six = "*"
termcolor = "*"
[[package]]
name = "identify"
version = "2.5.24"
@ -193,6 +193,41 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "nodeenv"
version = "1.8.0"
@ -266,6 +301,20 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pygments"
version = "2.15.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.7"
files = [
{file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"},
{file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"},
]
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "pytest"
version = "7.3.2"
@ -356,6 +405,24 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rich"
version = "13.4.2"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"},
{file = "rich-13.4.2.tar.gz", hash = "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "setuptools"
version = "67.8.0"
@ -373,29 +440,70 @@ testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[l
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "termcolor"
version = "2.3.0"
description = "ANSI color formatting for output in terminal"
name = "shellingham"
version = "1.5.0.post1"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
files = [
{file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"},
{file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"},
{file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"},
{file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"},
]
[[package]]
name = "tqdm"
version = "4.65.0"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
files = [
{file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"},
{file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
tests = ["pytest", "pytest-cov"]
dev = ["py-make (>=0.1.0)", "twine", "wheel"]
notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
[[package]]
name = "typer"
version = "0.9.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.6"
files = [
{file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
{file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
]
[package.dependencies]
click = ">=7.1.1,<9.0.0"
colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""}
rich = {version = ">=10.11.0,<14.0.0", optional = true, markers = "extra == \"all\""}
shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""}
typing-extensions = ">=3.7.4.3"
[package.extras]
all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
[[package]]
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
optional = false
python-versions = ">=3.7"
files = [
{file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
{file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
]
[[package]]
name = "urllib3"
@ -437,4 +545,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "a96832cd8f35bc83d3041fd4ec46972877becf824e5a307207a48459260b5289"
content-hash = "e431d87194a6a74586b42518a010b7187edea973f9d7c41862987819dfebfbd2"

View File

@ -10,6 +10,7 @@ readme = "README.md"
python = "^3.11"
platformdirs = "^3.5.1"
requests = "^2.31.0"
typer = {extras = ["all"], version = "^0.9.0"}
[tool.poetry.group.dev.dependencies]
pytest = "^7.3.1"
@ -17,7 +18,7 @@ pre-commit = "^3.3.3"
[tool.poetry.group.cli.dependencies]
fire = "^0.5.0"
tqdm = "^4.65.0"
[build-system]
requires = ["poetry-core"]

View File

@ -1,5 +1,4 @@
from vollerei.cli import CLI
from fire import Fire
from vollerei.cli import app
Fire(CLI, name="vollerei")
app()

View File

@ -1,35 +1,18 @@
from pathlib import Path
from vollerei import __version__
from vollerei.cli.hsr import HSR
from vollerei.hsr import PatchType
from vollerei.cli import hsr
from vollerei.cli import utils
from vollerei.cli.utils import msg
import typer
class CLI:
def __init__(
self,
game_path: str = None,
patch_type=None,
noconfirm: bool = False,
silent: bool = False,
) -> None:
app = typer.Typer()
app.add_typer(hsr.app, name="hsr")
app.callback()
def callback(noconfirm: bool = False, silent: bool = False):
"""
Vollerei CLI
An open-source launcher for anime games.
"""
utils.silent_message = silent
msg(f"Vollerei v{__version__}")
if noconfirm:
msg("User requested to automatically answer yes to all questions.")
utils.no_confirm = noconfirm
if not game_path:
game_path = Path.cwd()
game_path = Path(game_path)
hsr_patch_type = patch_type
if patch_type is None:
hsr_patch_type = PatchType.Jadeite
elif isinstance(patch_type, str):
hsr_patch_type = PatchType[patch_type]
elif isinstance(patch_type, int):
hsr_patch_type = PatchType(patch_type)
self.hsr = HSR(game_path=game_path, patch_type=hsr_patch_type)

View File

@ -1,96 +1,113 @@
from traceback import print_exc
from platform import system
from vollerei.cli.utils import ask, msg
from vollerei.exceptions.game import GameError
from vollerei.hsr import Game, Patcher
from vollerei.exceptions.patcher import PatcherError, PatchUpdateError
from vollerei.hsr.patcher import PatchType
import typer
class HSR:
def __init__(
self, game_path=None, patch_type: PatchType = PatchType.Jadeite
) -> None:
self._game = Game(game_path)
msg("Game directory:", self._game.path)
msg("Game version:", self._game.get_version_str())
self._patcher = Patcher()
self._patcher.patch_type = patch_type
app = typer.Typer()
patcher = Patcher()
# Double _ means private to prevent Fire from invoking it
def patch_type(self):
print("Patch type:", self._patcher.patch_type.name)
def __update_patch(self):
self.patch_type()
print("Updating patch...", end=" ")
class State:
game: Game = None
@app.callback()
def callback(game_path: str = None, patch_type: str = None):
"""
Manages the Honkai: Star Rail installation
This manages the game installation and handle the patching process automatically.
"""
State.game: Game = Game(game_path)
if patch_type is None:
patch_type = PatchType.Jadeite
elif isinstance(patch_type, str):
patch_type = PatchType[patch_type]
elif isinstance(patch_type, int):
patch_type = PatchType(patch_type)
patcher.patch_type = patch_type
@app.command()
def patch_type():
print("Patch type:", patcher.patch_type.name)
@app.command()
def update_patch():
patch_type()
msg("Updating patch...", end=" ")
try:
self._patcher.update_patch()
patcher.update_patch()
except PatchUpdateError as e:
print("FAILED")
print(f"Patch update failed with following error: {e} ({e.__context__})")
return False
print("OK")
msg("OK")
return True
def update_patch(self):
self.__update_patch()
def __patch_jadeite(self):
def _patch_jadeite():
try:
print("Installing patch...", end=" ")
jadeite_dir = self._patcher.patch_game(game=self._game)
msg("Installing patch...", end=" ")
jadeite_dir = patcher.patch_game(game=State.game)
except PatcherError as e:
print("FAILED")
print(f"Patching failed with following error: {e}")
print("Patching failed with following error:", e)
print_exc()
return
print("OK")
exe_path = jadeite_dir.joinpath("jadeite.exe")
print("Jadeite executable is located at:", exe_path)
print()
print("=" * 15)
print(
msg("Jadeite executable is located at: ", end="")
print(exe_path)
msg()
msg("=" * 15)
msg(
"Installation succeeded, but note that you need to run the game using "
+ "Jadeite to use the patch."
)
print()
print(f'E.g: I_WANT_A_BAN=1 {exe_path} "{self._game.path}"')
print()
print(
"Please don't spread this project to public, we just want to play the game."
)
print(
msg()
msg(f'E.g: I_WANT_A_BAN=1 {exe_path} "{State.game.path}"')
msg()
msg("Please don't spread this project to public, we just want to play the game.")
msg(
"And for your own sake, please only use testing accounts, as there is an "
+ "extremely high risk of getting banned."
)
print("=" * 15)
msg("=" * 15)
def __patch_astra(self):
def _patch_astra(self):
try:
print("Patching game...", end=" ")
self._patcher.patch_game(game=self._game)
msg("Patching game...", end=" ")
patcher.patch_game(game=State.game)
except PatcherError as e:
print("FAILED")
print(f"Patching failed with following error: {e}")
return
print("OK")
def patch(self):
if system() == "Windows":
print(
"Windows is supported officialy by the game, so no patching is needed."
)
print("By patching you are breaking the ToS, use at your own risk.")
msg("Windows is supported officialy by the game, so no patching is needed.")
msg("By patching you are breaking the ToS, use at your own risk.")
if not ask("Do you want to patch the game?"):
print("Patching aborted.")
return
print("Checking telemetry hosts...", end=" ")
telemetry_list = self._patcher.check_telemetry()
msg("Checking telemetry hosts...", end=" ")
telemetry_list = patcher.check_telemetry()
if telemetry_list:
print("FOUND")
msg("FOUND")
print("Telemetry hosts found:")
for host in telemetry_list:
print(f" - {host}")
print(
print(f"{host}")
msg(
"To prevent the game from sending data about the patch, "
+ "we need to block these hosts."
)
@ -99,7 +116,7 @@ class HSR:
print("Please block these hosts manually then try again.")
return
try:
self._patcher.block_telemetry(telemetry_list=telemetry_list)
patcher.block_telemetry(telemetry_list=telemetry_list)
except Exception as e:
print("Couldn't block telemetry hosts:", e)
if system() != "Windows":
@ -107,14 +124,19 @@ class HSR:
return
print("Continuing anyway...")
else:
print("OK")
if not self.__update_patch():
msg("OK")
if not update_patch():
return
match self._patcher.patch_type:
match patcher.patch_type:
case PatchType.Jadeite:
self.__patch_jadeite()
_patch_jadeite()
case PatchType.Astra:
self.__patch_astra()
_patch_astra()
def get_version(self):
print(self._game.get_version_str())
@app.command()
def get_version():
try:
print(State.game.get_version_str())
except GameError as e:
print("Couldn't get game version:", e)

View File

@ -3,8 +3,8 @@ silent_message = False
def ask(question: str):
if no_confirm:
print(question + " [Y/n]: Y")
if no_confirm or silent_message:
msg(question + " [Y/n]: Y")
return True
while True:
answer = input(question + " [Y/n]: ")

View File

@ -1,6 +1,7 @@
from hashlib import md5
from os import PathLike
from pathlib import Path
from vollerei.exceptions.game import GameNotInstalledError
from vollerei.hsr.launcher.enums import GameChannel
from vollerei.common import ConfigFile
from vollerei.abc.launcher.game import GameABC
@ -67,7 +68,10 @@ class Game(GameABC):
"""
Path to the game data folder.
"""
try:
return self._path.joinpath("StarRail_Data")
except AttributeError:
raise GameNotInstalledError("Game path is not set.")
def is_installed(self) -> bool:
"""