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: """
""" An open-source launcher for anime games.
Vollerei CLI """
""" utils.silent_message = silent
utils.silent_message = silent if noconfirm:
msg(f"Vollerei v{__version__}") utils.no_confirm = noconfirm
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,120 +1,142 @@
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=" ")
try:
self._patcher.update_patch()
except PatchUpdateError as e:
print("FAILED")
print(f"Patch update failed with following error: {e} ({e.__context__})")
return False
print("OK")
return True
def update_patch(self):
self.__update_patch()
def __patch_jadeite(self): @app.callback()
try: def callback(game_path: str = None, patch_type: str = None):
print("Installing patch...", end=" ") """
jadeite_dir = self._patcher.patch_game(game=self._game) Manages the Honkai: Star Rail installation
except PatcherError as e:
print("FAILED") This manages the game installation and handle the patching process automatically.
print(f"Patching failed with following error: {e}") """
return State.game: Game = Game(game_path)
print("OK") if patch_type is None:
exe_path = jadeite_dir.joinpath("jadeite.exe") patch_type = PatchType.Jadeite
print("Jadeite executable is located at:", exe_path) elif isinstance(patch_type, str):
print() patch_type = PatchType[patch_type]
print("=" * 15) elif isinstance(patch_type, int):
print( patch_type = PatchType(patch_type)
"Installation succeeded, but note that you need to run the game using " patcher.patch_type = patch_type
+ "Jadeite to use the patch."
@app.command()
def patch_type():
print("Patch type:", patcher.patch_type.name)
@app.command()
def update_patch():
patch_type()
msg("Updating patch...", end=" ")
try:
patcher.update_patch()
except PatchUpdateError as e:
print("FAILED")
print(f"Patch update failed with following error: {e} ({e.__context__})")
return False
msg("OK")
return True
def _patch_jadeite():
try:
msg("Installing patch...", end=" ")
jadeite_dir = patcher.patch_game(game=State.game)
except PatcherError as e:
print("FAILED")
print("Patching failed with following error:", e)
print_exc()
return
print("OK")
exe_path = jadeite_dir.joinpath("jadeite.exe")
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."
)
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."
)
msg("=" * 15)
def _patch_astra(self):
try:
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":
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
msg("Checking telemetry hosts...", end=" ")
telemetry_list = patcher.check_telemetry()
if telemetry_list:
msg("FOUND")
print("Telemetry hosts found:")
for host in telemetry_list:
print(f"{host}")
msg(
"To prevent the game from sending data about the patch, "
+ "we need to block these hosts."
) )
print() if not ask("Do you want to block these hosts?"):
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(
"And for your own sake, please only use testing accounts, as there is an "
+ "extremely high risk of getting banned."
)
print("=" * 15)
def __patch_astra(self):
try:
print("Patching game...", end=" ")
self._patcher.patch_game(game=self._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.")
if not ask("Do you want to patch the game?"):
print("Patching aborted.") print("Patching aborted.")
print("Please block these hosts manually then try again.")
return return
print("Checking telemetry hosts...", end=" ") try:
telemetry_list = self._patcher.check_telemetry() patcher.block_telemetry(telemetry_list=telemetry_list)
if telemetry_list: except Exception as e:
print("FOUND") print("Couldn't block telemetry hosts:", e)
print("Telemetry hosts found: ") if system() != "Windows":
for host in telemetry_list: print("Cannot continue, please block them manually then try again.")
print(f" - {host}")
print(
"To prevent the game from sending data about the patch, "
+ "we need to block these hosts."
)
if not ask("Do you want to block these hosts?"):
print("Patching aborted.")
print("Please block these hosts manually then try again.")
return return
try: print("Continuing anyway...")
self._patcher.block_telemetry(telemetry_list=telemetry_list) else:
except Exception as e: msg("OK")
print("Couldn't block telemetry hosts:", e) if not update_patch():
if system() != "Windows": return
print("Cannot continue, please block them manually then try again.") match patcher.patch_type:
return case PatchType.Jadeite:
print("Continuing anyway...") _patch_jadeite()
else: case PatchType.Astra:
print("OK") _patch_astra()
if not self.__update_patch():
return
match self._patcher.patch_type:
case PatchType.Jadeite:
self.__patch_jadeite()
case PatchType.Astra:
self.__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.
""" """
return self._path.joinpath("StarRail_Data") try:
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:
""" """