Skip to content

Commit

Permalink
Add seamless update framework. (#90)
Browse files Browse the repository at this point in the history
* Update config.py

* Update main.py

* fix incorrect release version/beta version check

* Update misc.py

* Add test results, prepare README

* Update README.md status badges.

* Get most of it working.

* Add test results, prepare README

* Update README.md status badges.

* more implementation, fix many problems

* Update config.py

* Add test results, prepare README

* Update README.md status badges.

* remaining fixes

* Add test results, prepare README

* Update README.md status badges.

* move relevant code to utility libs

* fix

* Add test results, prepare README

* Update README.md status badges.

* remaining fixes

* Add test results, prepare README

* Update README.md status badges.

* implement into HoloPatcher

* Update __main__.py

* Update requirements.txt

* Add test results, prepare README

* Update README.md status badges.

* fix import order

* sort around dependencies

don't require requests/pycyrptodome for update logic... should always be an optional feature.

* Add test results, prepare README

* Update README.md status badges.

* fix the unix-related issues

* add missing unix dep to toolset

* remaining cleanup

* Add test results, prepare README

* Update README.md status badges.

* have logger output to console

* Add test results, prepare README

* Update README.md status badges.

* Ensure new app has executable permission

* Add test results, prepare README

* Update README.md status badges.

* Update config.py

* Update os_helper.py

* Add test results, prepare README

* Update README.md status badges.

* Got stuff done

* Add test results, prepare README

* Update README.md status badges.

* fix the regex

* Fix trailing comma in the json.

* it is ready

---------

Co-authored-by: GitHub Action <[email protected]>
  • Loading branch information
th3w1zard1 and actions-user authored Mar 24, 2024
1 parent 17855a3 commit 82d01a4
Show file tree
Hide file tree
Showing 91 changed files with 10,647 additions and 8,470 deletions.
4 changes: 2 additions & 2 deletions Libraries/PyKotor/recommended.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
defusedxml~=0.7
charset-normalizer>=2.0,<3.4
defusedxml>=0.7
charset-normalizer>=2.0
4 changes: 2 additions & 2 deletions Libraries/PyKotor/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def main():
)


import contextlib
from contextlib import suppress
import datetime
import re

Expand Down Expand Up @@ -891,7 +891,7 @@ def loads(s, _dict=dict, decoder=None):
currentlevel[group] = [decoder.get_empty_table()]
currentlevel = currentlevel[group]
if arrayoftables:
with contextlib.suppress(KeyError):
with suppress(KeyError):
currentlevel = currentlevel[-1]
elif line[0] == "{":
if line[-1] != "}":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pykotor.common.misc import ResRef
from pykotor.resource.type import ResourceType
from utility.error_handling import safe_repr
from utility.string import format_text
from utility.string_util import format_text
from utility.system.path import PureWindowsPath

if TYPE_CHECKING:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pykotor.common.language import Language
from pykotor.common.misc import ResRef
from pykotor.resource.type import ResourceType
from utility.string import compare_and_format, format_text
from utility.string_util import compare_and_format, format_text

if TYPE_CHECKING:
from collections.abc import Callable
Expand Down
32 changes: 16 additions & 16 deletions Libraries/PyKotor/src/pykotor/resource/resource_auto.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

import contextlib
from contextlib import suppress
import os

from typing import TYPE_CHECKING
Expand Down Expand Up @@ -44,7 +44,7 @@ def read_resource(source: SOURCE_TYPES, resource_type: ResourceType | None = Non
bytes: The resource data as bytes
"""
source_path: os.PathLike | str | None = None
with contextlib.suppress(Exception):
with suppress(Exception):
if isinstance(source, (os.PathLike, str)):
source_path = source
if not resource_type:
Expand Down Expand Up @@ -94,33 +94,33 @@ def read_resource(source: SOURCE_TYPES, resource_type: ResourceType | None = Non


def read_unknown_resource(source: SOURCE_TYPES) -> bytes: # noqa: PLR0911
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_tlk(read_tlk(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_ssf(read_ssf(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_2da(read_2da(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_lip(read_lip(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_tpc(read_tpc(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_erf(read_erf(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_rim(read_rim(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_ncs(read_ncs(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_gff(read_gff(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_mdl(read_mdl(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_vis(read_vis(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_lyt(read_lyt(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_ltr(read_ltr(source))
with contextlib.suppress(OSError, ValueError):
with suppress(OSError, ValueError):
return bytes_bwm(read_bwm(source))
msg = "Source resource data not recognized as any kotor file formats."
raise ValueError(msg)
2 changes: 1 addition & 1 deletion Libraries/PyKotor/src/pykotor/resource/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from pykotor.common.stream import BinaryReader, BinaryWriter
from utility.error_handling import format_exception_with_variables
from utility.string import WrappedStr
from utility.string_util import WrappedStr

if TYPE_CHECKING:
from collections.abc import Callable, Iterable
Expand Down
8 changes: 4 additions & 4 deletions Libraries/PyKotor/src/pykotor/tools/encoding.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import codecs
import contextlib

from contextlib import suppress
from typing import TYPE_CHECKING

if TYPE_CHECKING:
Expand Down Expand Up @@ -68,7 +68,7 @@ def _decode_attempt(attempt_errors: str) -> str:

# Attempt decoding with provided encoding
if provided_encoding is not None:
with contextlib.suppress(UnicodeDecodeError):
with suppress(UnicodeDecodeError):
return byte_content.decode(provided_encoding, errors=attempt_errors)

# Detect encoding using charset_normalizer
Expand All @@ -87,7 +87,7 @@ def _decode_attempt(attempt_errors: str) -> str:
result_detect: CharsetMatch | None = detected_encodings.best()
if result_detect is None:
# Semi-Final fallback (utf-8) if no encoding is detected
with contextlib.suppress(UnicodeDecodeError):
with suppress(UnicodeDecodeError):
return byte_content.decode(encoding="utf-8", errors=attempt_errors)
# Final fallback (latin1) if no encoding is detected
return byte_content.decode(encoding="latin1", errors=attempt_errors)
Expand All @@ -110,7 +110,7 @@ def _decode_attempt(attempt_errors: str) -> str:
return byte_content.decode(encoding=best_encoding, errors=attempt_errors)

# Attempt strict first for more accurate results.
with contextlib.suppress(UnicodeDecodeError):
with suppress(UnicodeDecodeError):
return _decode_attempt(attempt_errors="strict")
return _decode_attempt(attempt_errors=errors)

Expand Down
2 changes: 1 addition & 1 deletion Libraries/PyKotor/src/pykotor/tools/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from pykotor.tools.misc import is_mod_file
from pykotor.tools.path import CaseAwarePath
from utility.error_handling import assert_with_variable_trace
from utility.string import ireplace
from utility.string_util import ireplace

if TYPE_CHECKING:
import os
Expand Down
2 changes: 1 addition & 1 deletion Libraries/PyKotor/src/pykotor/tools/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING, Any

from pykotor.tools.registry import find_software_key, winreg_key
from utility.string import ireplace
from utility.string_util import ireplace
from utility.system.path import (
Path as InternalPath,
PosixPath as InternalPosixPath,
Expand Down
4 changes: 2 additions & 2 deletions Libraries/PyKotor/src/pykotor/tools/registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

import contextlib
from contextlib import suppress
import os

from pykotor.common.misc import Game
Expand Down Expand Up @@ -51,7 +51,7 @@ def find_software_key(software_name: str) -> str | None:
# Enumerate through the SIDs
sid: str = winreg.EnumKey(hkey_users, i)
software_path = f"{sid}\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{software_name}"
with contextlib.suppress(FileNotFoundError), winreg.OpenKey(hkey_users, software_path) as software_key:
with suppress(FileNotFoundError), winreg.OpenKey(hkey_users, software_path) as software_key:
# If this point is reached, the software is installed under this SID
return winreg.QueryValue(software_key, "InstallLocation")
i += 1
Expand Down
2 changes: 1 addition & 1 deletion Libraries/PyKotor/src/pykotor/tslpatcher/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime, timezone
from enum import IntEnum

from utility.event import Observable
from utility.event_util import Observable


class LogType(IntEnum):
Expand Down
4 changes: 2 additions & 2 deletions Libraries/PyKotorFont/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def main():
)


import contextlib
from contextlib import suppress
import datetime
import re

Expand Down Expand Up @@ -874,7 +874,7 @@ def loads(s, _dict=dict, decoder=None):
currentlevel[group] = [decoder.get_empty_table()]
currentlevel = currentlevel[group]
if arrayoftables:
with contextlib.suppress(KeyError):
with suppress(KeyError):
currentlevel = currentlevel[-1]
elif line[0] == "{":
if line[-1] != "}":
Expand Down
4 changes: 2 additions & 2 deletions Libraries/PyKotorGL/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def main():
)


import contextlib
from contextlib import suppress
import datetime
import re

Expand Down Expand Up @@ -906,7 +906,7 @@ def loads(s, _dict=dict, decoder=None):
currentlevel[group] = [decoder.get_empty_table()]
currentlevel = currentlevel[group]
if arrayoftables:
with contextlib.suppress(KeyError):
with suppress(KeyError):
currentlevel = currentlevel[-1]
elif line[0] == "{":
if line[-1] != "}":
Expand Down
File renamed without changes.
131 changes: 131 additions & 0 deletions Libraries/Utility/src/utility/logger_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from __future__ import annotations

import json
import logging
import sys

from logging.handlers import RotatingFileHandler
from typing import ClassVar

from utility.error_handling import format_exception_with_variables


class ColoredConsoleHandler(logging.StreamHandler):
try:
import colorama # type: ignore[import-untyped, reportMissingModuleSource]
colorama.init()
USING_COLORAMA = True
except ImportError:
USING_COLORAMA = False

COLOR_CODES: ClassVar[dict[int, str]] = {
logging.DEBUG: "\033[0;36m" if not USING_COLORAMA else colorama.Fore.CYAN, # Cyan
logging.INFO: "\033[0;37m" if not USING_COLORAMA else colorama.Fore.WHITE, # White
logging.WARNING: "\033[0;33m" if not USING_COLORAMA else colorama.Fore.YELLOW, # Yellow
logging.ERROR: "\033[0;31m" if not USING_COLORAMA else colorama.Fore.RED, # Red
logging.CRITICAL: "\033[1;41m" if not USING_COLORAMA else colorama.Back.RED, # Red background
}

RESET_CODE = "\033[0m" if not USING_COLORAMA else colorama.Style.RESET_ALL

def format(self, record):
msg = super().format(record)
return f"{self.COLOR_CODES.get(record.levelno, '')}{msg}{self.RESET_CODE}"

class CustomExceptionFormatter(logging.Formatter):
def formatException(self, ei):
etype, value, tb = ei
return format_exception_with_variables(value, etype=etype, tb=tb)

def format(self, record):
result = super().format(record)
if record.exc_info:
# Here we use our custom exception formatting
result += "\n" + self.formatException(record.exc_info)
return result

class JSONFormatter(logging.Formatter):
def format(self, record):
log_record = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"name": record.name,
"message": record.getMessage(),
}
if record.exc_info:
# Your custom exception formatting is called here
formatted_exception = super().formatException(record.exc_info) # Adjust this call as necessary
log_record["exception"] = formatted_exception
return json.dumps(log_record)

class LogLevelFilter(logging.Filter):
"""Filters (allows) all the log messages at or above a specific level."""
def __init__(self, passlevel: int, reject: bool = False): # noqa: FBT001, FBT002
super().__init__()
self.passlevel: int = passlevel
self.reject: bool = reject

def filter(self, record):
if self.reject:
return record.levelno < self.passlevel
return record.levelno >= self.passlevel

def get_root_logger() -> logging.Logger:
logger = logging.getLogger()
if not logger.handlers:
logger.setLevel(logging.DEBUG)

log_levels = {
logging.DEBUG: "pykotor_debug.log",
logging.INFO: "pykotor_info.log",
logging.WARNING: "pykotor_warning.log",
logging.ERROR: "pykotor_error.log",
logging.CRITICAL: "pykotor_critical.log",
}

for level, filename in log_levels.items():
handler = RotatingFileHandler(filename, maxBytes=1048576, backupCount=5)
handler.setLevel(level)

# Apply JSON formatting for DEBUG, CustomExceptionFormatter for others
if level == logging.DEBUG:
handler.setFormatter(JSONFormatter())
else:
handler.setFormatter(CustomExceptionFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))

# Replacing StreamHandler with ColoredConsoleHandler
console_handler = ColoredConsoleHandler()
formatter = logging.Formatter("%(levelname)s(%(name)s): %(message)s")
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)

# Exclude lower level logs for handlers above DEBUG
if level > logging.DEBUG:
handler.addFilter(LogLevelFilter(level))

logger.addHandler(handler)

return logger

# Modify the handle_exception function if necessary
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return

logger = get_root_logger()
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))

sys.excepthook = handle_exception

# Example usage
if __name__ == "__main__":
logger = get_root_logger()
logger.debug("This is a debug message.")
logger.info("This is an info message.")
logger.warning("This is a warning message.")
logger.error("This is an error message.")
logger.critical("This is a critical message.")

# Uncomment to test uncaught exception logging
# raise RuntimeError("Test uncaught exception")
File renamed without changes.
Loading

0 comments on commit 82d01a4

Please sign in to comment.