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]] [[package]]
name = "certifi" name = "certifi"
@ -106,6 +106,20 @@ files = [
{file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, {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]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" 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)"] 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)"] 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]] [[package]]
name = "identify" name = "identify"
version = "2.5.24" version = "2.5.24"
@ -193,6 +193,41 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {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]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.8.0" version = "1.8.0"
@ -266,6 +301,20 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1" pyyaml = ">=5.1"
virtualenv = ">=20.10.0" 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]] [[package]]
name = "pytest" name = "pytest"
version = "7.3.2" version = "7.3.2"
@ -356,6 +405,24 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 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]] [[package]]
name = "setuptools" name = "setuptools"
version = "67.8.0" 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"] 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]] [[package]]
name = "six" name = "shellingham"
version = "1.16.0" version = "1.5.0.post1"
description = "Python 2 and 3 compatibility utilities" description = "Tool to Detect Surrounding Shell"
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"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"}, {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"},
{file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"}, {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] [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]] [[package]]
name = "urllib3" name = "urllib3"
@ -437,4 +545,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "a96832cd8f35bc83d3041fd4ec46972877becf824e5a307207a48459260b5289" content-hash = "e431d87194a6a74586b42518a010b7187edea973f9d7c41862987819dfebfbd2"

View File

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

View File

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

View File

@ -1,35 +1,18 @@
from pathlib import Path from vollerei.cli import hsr
from vollerei import __version__
from vollerei.cli.hsr import HSR
from vollerei.hsr import PatchType
from vollerei.cli import utils from vollerei.cli import utils
from vollerei.cli.utils import msg import typer
class CLI: app = typer.Typer()
def __init__( app.add_typer(hsr.app, name="hsr")
self,
game_path: str = None, app.callback()
patch_type=None,
noconfirm: bool = False,
silent: bool = False, def callback(noconfirm: bool = False, silent: bool = False):
) -> None:
""" """
Vollerei CLI An open-source launcher for anime games.
""" """
utils.silent_message = silent utils.silent_message = silent
msg(f"Vollerei v{__version__}")
if noconfirm: if noconfirm:
msg("User requested to automatically answer yes to all questions.")
utils.no_confirm = noconfirm 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 traceback import print_exc
from platform import system from platform import system
from vollerei.cli.utils import ask, msg from vollerei.cli.utils import ask, msg
from vollerei.exceptions.game import GameError
from vollerei.hsr import Game, Patcher from vollerei.hsr import Game, Patcher
from vollerei.exceptions.patcher import PatcherError, PatchUpdateError from vollerei.exceptions.patcher import PatcherError, PatchUpdateError
from vollerei.hsr.patcher import PatchType from vollerei.hsr.patcher import PatchType
import typer
class HSR: app = typer.Typer()
def __init__( patcher = Patcher()
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
# 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): class State:
self.patch_type() game: Game = None
print("Updating patch...", end=" ")
@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: try:
self._patcher.update_patch() patcher.update_patch()
except PatchUpdateError as e: except PatchUpdateError as e:
print("FAILED") print("FAILED")
print(f"Patch update failed with following error: {e} ({e.__context__})") print(f"Patch update failed with following error: {e} ({e.__context__})")
return False return False
print("OK") msg("OK")
return True return True
def update_patch(self):
self.__update_patch()
def __patch_jadeite(self): def _patch_jadeite():
try: try:
print("Installing patch...", end=" ") msg("Installing patch...", end=" ")
jadeite_dir = self._patcher.patch_game(game=self._game) jadeite_dir = patcher.patch_game(game=State.game)
except PatcherError as e: except PatcherError as e:
print("FAILED") print("FAILED")
print(f"Patching failed with following error: {e}") print("Patching failed with following error:", e)
print_exc()
return return
print("OK") print("OK")
exe_path = jadeite_dir.joinpath("jadeite.exe") exe_path = jadeite_dir.joinpath("jadeite.exe")
print("Jadeite executable is located at:", exe_path) msg("Jadeite executable is located at: ", end="")
print() print(exe_path)
print("=" * 15) msg()
print( msg("=" * 15)
msg(
"Installation succeeded, but note that you need to run the game using " "Installation succeeded, but note that you need to run the game using "
+ "Jadeite to use the patch." + "Jadeite to use the patch."
) )
print() msg()
print(f'E.g: I_WANT_A_BAN=1 {exe_path} "{self._game.path}"') msg(f'E.g: I_WANT_A_BAN=1 {exe_path} "{State.game.path}"')
print() msg()
print( msg("Please don't spread this project to public, we just want to play the game.")
"Please don't spread this project to public, we just want to play the game." msg(
)
print(
"And for your own sake, please only use testing accounts, as there is an " "And for your own sake, please only use testing accounts, as there is an "
+ "extremely high risk of getting banned." + "extremely high risk of getting banned."
) )
print("=" * 15) msg("=" * 15)
def __patch_astra(self):
def _patch_astra(self):
try: try:
print("Patching game...", end=" ") msg("Patching game...", end=" ")
self._patcher.patch_game(game=self._game) patcher.patch_game(game=State.game)
except PatcherError as e: except PatcherError as e:
print("FAILED") print("FAILED")
print(f"Patching failed with following error: {e}") print(f"Patching failed with following error: {e}")
return return
print("OK") print("OK")
def patch(self): def patch(self):
if system() == "Windows": if system() == "Windows":
print( msg("Windows is supported officialy by the game, so no patching is needed.")
"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.")
)
print("By patching you are breaking the ToS, use at your own risk.")
if not ask("Do you want to patch the game?"): if not ask("Do you want to patch the game?"):
print("Patching aborted.") print("Patching aborted.")
return return
print("Checking telemetry hosts...", end=" ") msg("Checking telemetry hosts...", end=" ")
telemetry_list = self._patcher.check_telemetry() telemetry_list = patcher.check_telemetry()
if telemetry_list: if telemetry_list:
print("FOUND") msg("FOUND")
print("Telemetry hosts found:") print("Telemetry hosts found:")
for host in telemetry_list: for host in telemetry_list:
print(f" - {host}") print(f"{host}")
print( msg(
"To prevent the game from sending data about the patch, " "To prevent the game from sending data about the patch, "
+ "we need to block these hosts." + "we need to block these hosts."
) )
@ -99,7 +116,7 @@ class HSR:
print("Please block these hosts manually then try again.") print("Please block these hosts manually then try again.")
return return
try: try:
self._patcher.block_telemetry(telemetry_list=telemetry_list) patcher.block_telemetry(telemetry_list=telemetry_list)
except Exception as e: except Exception as e:
print("Couldn't block telemetry hosts:", e) print("Couldn't block telemetry hosts:", e)
if system() != "Windows": if system() != "Windows":
@ -107,14 +124,19 @@ class HSR:
return return
print("Continuing anyway...") print("Continuing anyway...")
else: else:
print("OK") msg("OK")
if not self.__update_patch(): if not update_patch():
return return
match self._patcher.patch_type: match patcher.patch_type:
case PatchType.Jadeite: case PatchType.Jadeite:
self.__patch_jadeite() _patch_jadeite()
case PatchType.Astra: 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): def ask(question: str):
if no_confirm: if no_confirm or silent_message:
print(question + " [Y/n]: Y") msg(question + " [Y/n]: Y")
return True return True
while True: while True:
answer = input(question + " [Y/n]: ") answer = input(question + " [Y/n]: ")

View File

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