From b701a910831fa1696ac36cd0bdcf5cc0aa73181e Mon Sep 17 00:00:00 2001
From: Firq <firelp42@gmail.com>
Date: Fri, 4 Apr 2025 11:33:49 +0200
Subject: [PATCH] Updated readme, fixed linting and type annotations

---
 README.md                     | 44 ++++++++++++++++++++++++++++++++---
 pyproject.toml                |  4 +++-
 skyeweave/__init__.py         |  2 ++
 skyeweave/cli/__init__.py     |  2 ++
 skyeweave/cli/cli.py          | 29 ++++++++++++-----------
 skyeweave/config/__init__.py  |  2 ++
 skyeweave/service/__init__.py |  2 ++
 skyeweave/service/atlas.py    |  4 ++--
 skyeweave/service/compose.py  | 16 ++++++-------
 skyeweave/utils/__init__.py   |  2 ++
 skyeweave/utils/filesystem.py |  2 +-
 skyeweave/utils/logger.py     |  8 +++----
 12 files changed, 84 insertions(+), 33 deletions(-)

diff --git a/README.md b/README.md
index 0b20ed7..b492eb3 100644
--- a/README.md
+++ b/README.md
@@ -60,10 +60,48 @@ If there are any issues with `skyeweave`, feel free to reach out to me using the
 
 If you want to help me debug when an issue occurs, set the environment variable `SKYEWEAVE_STDOUT_LEVEL` to `debug` and send me a copy of the log
 
-Example on Windows:
+Windows (PowerShell):
 
-```shell
+```powershell
 $env:SKYEWEAVE_STDOUT_LEVEL="debug"
 
-skyeweave --output out --id 70 > log.log
+skyeweave --output out --id 70 2>&1 log.log
 ```
+
+Linux (Bash):
+
+```bash
+SKYEWEAVE_STDOUT_LEVEL="debug"
+
+skyeweave --output out --id 70 2>&1 log.log
+```
+
+## Contributing
+
+Feel free to reach out if you want to help to improve skyeweave. I really appreachiate it.
+
+## FAQ
+
+> Q: Why Python
+
+A: Because it is the language I am the most familiar with, and I know my stuff. I also like not having to deal with too many restrictions when developing or the mess that is JS/TS.
+
+> Q: Why a CLI script?
+
+A: Because it felt like the appropriate solution for this problem. Writing a whole GUI application felt overkill, and for the most part having the tool just spittingt out the necessary files is more than enough.
+
+> Q: Where is the executable? Why do I have to install Python?
+
+A: ~~Because I said so!~~ In all honesty, I felt that it's too much work for too little return building executables for multiple systems, expecially with `pyinstaller` resulting in a large executable for what is actually happening. Installing Python is pretty straightforward, and I have attached guidance for setting up from scratch below. Also: See [this gem](https://www.reddit.com/r/github/comments/1at9br4) for another reason.
+
+> Q: How did this come to be?
+
+A: I am usually editing thumbnails for my videos with the expression sheets, and at some point I was just annoyed as the process of editing those for EVERY. SINGLE. SERVANT. IN. EVERY. THUMBNAIL.
+
+## How to install Python?
+
+1. Go to [the download page](https://www.python.org/downloads/) and download a current version of Python (must be version 3.10 or later for this package to work).
+2. Install Python as you would any other program, make sure it gets added to PATH
+3. Open a Terminal (cmd, poweshell on Windows; bash, sh, ... on Linux) and run `python --version` once to verify it installed correctly. There should be some output like `Python 3.11.9`
+
+After this, you can continue with the instructions above. I highly recommend you check out of virtual environments (venv) work beofre installing, so that you don't pollute the global package installation directory.
diff --git a/pyproject.toml b/pyproject.toml
index fb0f77f..4ccd7e2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,13 +4,14 @@ version = "1.0.0-c.4"
 requires-python = ">= 3.10"
 authors = [{name = "Firq", email = "firelp42@gmail.com"}]
 maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
-description = "Easily generate any FGO expression sheets from an id"
+description = "Helper script to easily generate experssions from FGO expression sheets"
 classifiers = [
     "Development Status :: 5 - Production/Stable",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
 ]
 
 dependencies = [
@@ -57,6 +58,7 @@ ignore-paths="test/*"
 python_version = "3.11"
 warn_return_any = true
 warn_unused_configs = true
+strict = true
 exclude = [ "test" ]
 
 [tool.pytest.ini_options]
diff --git a/skyeweave/__init__.py b/skyeweave/__init__.py
index 07a550a..a381fc2 100644
--- a/skyeweave/__init__.py
+++ b/skyeweave/__init__.py
@@ -3,3 +3,5 @@ import importlib.metadata
 __version__ = importlib.metadata.version(__package__ or "skyeweave")
 
 from .service import SkyeWeave
+
+__all__ = [ "SkyeWeave" ]
diff --git a/skyeweave/cli/__init__.py b/skyeweave/cli/__init__.py
index af3c5ad..f2cfb08 100644
--- a/skyeweave/cli/__init__.py
+++ b/skyeweave/cli/__init__.py
@@ -1 +1,3 @@
 from .cli import run
+
+__all__ = [ "run" ]
diff --git a/skyeweave/cli/cli.py b/skyeweave/cli/cli.py
index 4a18431..7c9b96d 100644
--- a/skyeweave/cli/cli.py
+++ b/skyeweave/cli/cli.py
@@ -2,7 +2,7 @@ import argparse
 import logging
 import pathlib
 import sys
-from typing import List
+from typing import List, Optional, Sequence, Tuple
 
 from .. import __version__
 from ..service import SkyeWeave
@@ -16,15 +16,15 @@ class CLIArguments(argparse.Namespace):
     """
     Default Arguments when calling the CLI
     """
-    output: str
-    id: int
-    reset: bool
-    nocache: bool
-    filter: List[str]
-    timeout: int
-    quiet: bool
+    output: Optional[str]
+    id: Optional[int]
+    reset: Optional[bool]
+    nocache: Optional[bool]
+    filter: Optional[List[str]]
+    timeout: Optional[int]
+    quiet: Optional[bool]
 
-def parse_arguments(arguments):
+def parse_arguments(arguments: Sequence[str]) -> Tuple[CLIArguments, list[str]]:
     """
     Create a parser and parse the arguments of sys.argv based on the Arguments Namespace
     Returns arguments and extra arguments separately
@@ -43,9 +43,9 @@ def parse_arguments(arguments):
     parser.add_argument("--reset", action="store_true", default=False, dest="reset", help="Delete any already downloaded assets")
     parser.add_argument("--quiet", "-q", action="store_true", default=False, dest="quiet", help="Mute output and hide progress bars")
 
-    return parser.parse_known_args(arguments, namespace=CLIArguments)
+    return parser.parse_known_args(arguments, namespace=CLIArguments())
 
-def __welcome():
+def __welcome() -> None:
     print("-------------------------------------------")
     print(" Welcome to skyeweave, an expression sheet ")
     print("       composer developed by Firq          ")
@@ -63,7 +63,7 @@ def validate_id(input_id: None | str) -> int:
 
     return int(input_id)
 
-def run():
+def run() -> None:
     args, _ = parse_arguments(sys.argv[1:])
     if args.output:
         PathConfig.OUTPUT = pathlib.Path(args.output)
@@ -75,7 +75,7 @@ def run():
     else:
         __welcome()
 
-    input_id = validate_id(args.id)
+    input_id = validate_id(str(args.id))
 
     if args.nocache:
         cachepath = PathConfig.IMAGES / str(input_id)
@@ -91,6 +91,7 @@ def run():
         rmdir(PathConfig.IMAGES)
         LOGGER.info("Successfully reset local storage")
 
-    weaver = SkyeWeave(input_id, args.filter)
+    filters = [int(f) for f in args.filter] if args.filter is not None else None
+    weaver = SkyeWeave(input_id,  filters=filters)
     weaver.download()
     weaver.compose()
diff --git a/skyeweave/config/__init__.py b/skyeweave/config/__init__.py
index 48e9947..ec28630 100644
--- a/skyeweave/config/__init__.py
+++ b/skyeweave/config/__init__.py
@@ -1 +1,3 @@
 from .config import AtlasAPIConfig, ExpressionDefaults, LoggingConfig, PathConfig
+
+__all__ = [ "AtlasAPIConfig", "ExpressionDefaults", "LoggingConfig", "PathConfig" ]
diff --git a/skyeweave/service/__init__.py b/skyeweave/service/__init__.py
index 2746888..3f15b8a 100644
--- a/skyeweave/service/__init__.py
+++ b/skyeweave/service/__init__.py
@@ -1 +1,3 @@
 from .compose import SkyeWeave
+
+__all__ = [ "SkyeWeave" ]
diff --git a/skyeweave/service/atlas.py b/skyeweave/service/atlas.py
index 5038c7e..09d5983 100644
--- a/skyeweave/service/atlas.py
+++ b/skyeweave/service/atlas.py
@@ -45,7 +45,7 @@ def fetch_config(chara_id: int) -> SpritesheetData:
     LOGGER.debug(returndata)
     return returndata
 
-def fetch_mstsvtjson():
+def fetch_mstsvtjson() -> None:
     url = AtlasAPIConfig.MST_SVT_JSON
     filelocation = PathConfig.IMAGES / "mstsvt.json"
 
@@ -65,7 +65,7 @@ def fetch_mstsvtjson():
                 break
             handle.write(block)
 
-def fetch_expression_sheets(tempfolder: pathlib.Path, imageid: int):
+def fetch_expression_sheets(tempfolder: pathlib.Path, imageid: int) -> pathlib.Path:
     atlasurl_base = f"https://static.atlasacademy.io/{AtlasAPIConfig.REGION}/CharaFigure/{imageid}"
 
     savefolder = tempfolder / str(imageid)
diff --git a/skyeweave/service/compose.py b/skyeweave/service/compose.py
index 7e5259f..e5fe74c 100644
--- a/skyeweave/service/compose.py
+++ b/skyeweave/service/compose.py
@@ -1,6 +1,6 @@
 import logging
 import pathlib
-from typing import Dict, List, Optional, TypedDict
+from typing import Dict, List, Optional, Tuple, TypedDict
 from collections import Counter
 
 from PIL import Image
@@ -19,7 +19,7 @@ class SkyeWeave:
     output_folder: pathlib.Path
     image_folder: pathlib.Path
     chara_ids: List[int]
-    chara_infos: Dict[str, CharaInfos]
+    chara_infos: Dict[int, CharaInfos]
 
     def __init__(self, input_id: int,  filters: Optional[List[int]] = None, output: Optional[pathlib.Path] = None, assets: Optional[pathlib.Path] = None):
         _output_folder = output or PathConfig.OUTPUT
@@ -42,20 +42,20 @@ class SkyeWeave:
         self.output_folder.mkdir(parents=True, exist_ok=True)
         self.image_folder.mkdir(parents=True, exist_ok=True)
 
-    def download(self):
+    def download(self) -> None:
         for char_id in self.chara_ids:
             expfolder = fetch_expression_sheets(self.image_folder, char_id)
             config = fetch_config(char_id)
             self.chara_infos.update({char_id : { "folder": expfolder, "config": config}})
         LOGGER.debug(self.chara_infos)
 
-    def compose(self):
+    def compose(self) -> None:
         for key, val in self.chara_infos.items():
             LOGGER.info(f"Processing sheet for {key}")
             self.process_sprite(val["folder"], val["config"], self.output_folder)
         LOGGER.info(f"Files have been saved at: {self.output_folder.absolute()}")
 
-    def process_sprite(self, images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path):
+    def process_sprite(self, images_folder: pathlib.Path, configdata: SpritesheetData, outputfolder: pathlib.Path) -> None:
         main_sprite = self._gen_main_sprite(images_folder / "0.png")
         image_idx = self._save_sprite(main_sprite, outputfolder, f"{images_folder.stem}")
 
@@ -79,14 +79,14 @@ class SkyeWeave:
                 LOGGER.debug(f"{x}/{y} - {'Invalid' if img is None else 'Valid'} image")
 
     @staticmethod
-    def _calculate_counts(width: int, height: int, facesize: tuple[int, int]):
+    def _calculate_counts(width: int, height: int, facesize: tuple[int, int]) -> Tuple[int, int]:
         rowcount, colcount = height // facesize[1], width // facesize[0]
         LOGGER.debug(f"{height} | {facesize[1]} --> {rowcount}")
         LOGGER.debug(f"{width} | {facesize[0]} --> {colcount}")
         return rowcount, colcount
 
     @staticmethod
-    def _gen_main_sprite(imagepath: pathlib.Path):
+    def _gen_main_sprite(imagepath: pathlib.Path) -> Image.Image:
         image = Image.open(imagepath)
         width, height = image.size
         LOGGER.debug(f"Main sprite ({imagepath}): {width}:{height}")
@@ -126,7 +126,7 @@ class SkyeWeave:
         return idx + 1
 
     @staticmethod
-    def _is_empty(img: Image.Image):
+    def _is_empty(img: Image.Image) -> bool:
         w, h = img.size
         croparea = (w * 0.375, h * 0.375, w * 0.625, h * 0.625 )
         data = Counter(img.crop(croparea).convert('LA').getdata())
diff --git a/skyeweave/utils/__init__.py b/skyeweave/utils/__init__.py
index d6ba198..e5d3dcd 100644
--- a/skyeweave/utils/__init__.py
+++ b/skyeweave/utils/__init__.py
@@ -1,2 +1,4 @@
 from .filesystem import rmdir
 from .logger import LOGGER, disable_tqdm
+
+__all__ = [ "rmdir", "LOGGER", "disable_tqdm" ]
diff --git a/skyeweave/utils/filesystem.py b/skyeweave/utils/filesystem.py
index 752ef02..1d3d0c5 100644
--- a/skyeweave/utils/filesystem.py
+++ b/skyeweave/utils/filesystem.py
@@ -1,6 +1,6 @@
 import pathlib
 
-def rmdir(directory: pathlib.Path):
+def rmdir(directory: pathlib.Path) -> None:
     """
     Recursively deletes all files and folders in a given directory
 
diff --git a/skyeweave/utils/logger.py b/skyeweave/utils/logger.py
index 11bec2e..dedf0bc 100644
--- a/skyeweave/utils/logger.py
+++ b/skyeweave/utils/logger.py
@@ -4,17 +4,17 @@ import sys
 
 from ..config import LoggingConfig
 
-def disable_tqdm():
+def disable_tqdm() -> None:
     from tqdm import tqdm
     from functools import partialmethod
-    tqdm.__init__ = partialmethod(tqdm.__init__, disable=True)
+    tqdm.__init__ = partialmethod(tqdm.__init__, disable=True) # type: ignore[method-assign,assignment]
 
-def __init_logger():
+def __init_logger() -> logging.Logger:
     if LoggingConfig.LEVEL == "DEBUG":
         disable_tqdm()
 
     default_logger = logging.getLogger(LoggingConfig.NAME)
-    default_logger.setLevel(LoggingConfig.LEVEL)
+    default_logger.setLevel(LoggingConfig.LEVEL) # type: ignore[arg-type]
 
     default_handler = logging.StreamHandler(stream=sys.stdout)
     default_formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')