Compare commits
10 Commits
main
...
console-on
Author | SHA1 | Date | |
---|---|---|---|
d8bf7239f6 | |||
eabd2f0f78 | |||
25da1593d1 | |||
a10fa2be5c | |||
18f52a2e47 | |||
681b894384 | |||
594767e4d4 | |||
5851c6bf22 | |||
de8630bd45 | |||
e464078293 |
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
main.build
|
||||
main.dist
|
||||
main.onefile-build
|
||||
main.bin
|
||||
__pycache__
|
||||
|
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
83
config.py
Normal file
83
config.py
Normal file
@ -0,0 +1,83 @@
|
||||
import tkinter as tk
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
APP_NAME = "SC KF"
|
||||
|
||||
|
||||
class PersistentConfig:
|
||||
def __init__(self, db_path: Path):
|
||||
self.table_name = "properties"
|
||||
self.db = sqlite3.connect(
|
||||
str(db_path), isolation_level=None, check_same_thread=False, autocommit=True
|
||||
)
|
||||
self.db.execute("pragma journal_mode=wal;")
|
||||
self.db.execute(
|
||||
f"create table if not exists {self.table_name} (key text primary key, value_str text, value_int int) strict;"
|
||||
)
|
||||
|
||||
def set_int(self, key: str, value: int) -> None:
|
||||
self.db.execute(
|
||||
f"insert into {self.table_name} (key, value_int) values (:key, :value) on conflict (key) do update set value_int = :value;",
|
||||
{"key": key, "value": value},
|
||||
)
|
||||
|
||||
def get_int(self, key: str, default=None) -> int | None:
|
||||
res = self.db.execute(
|
||||
f"select value_int from {self.table_name} where key = ?;", (key,)
|
||||
)
|
||||
return next(iter(res), (default,))[0]
|
||||
|
||||
def set_str(self, key: str, value: str) -> None:
|
||||
self.db.execute(
|
||||
f"insert into {self.table_name} (key, value_str) values (:key, :value) on conflict (key) do update set value_str = :value;",
|
||||
{"key": key, "value": value},
|
||||
)
|
||||
|
||||
def get_str(self, key: str, default=None) -> str | None:
|
||||
res = self.db.execute(
|
||||
f"select value_str from {self.table_name} where key = ?;", (key,)
|
||||
)
|
||||
return next(iter(res), (default,))[0]
|
||||
|
||||
def set_bool(self, key: str, value: bool) -> None:
|
||||
self.set_int(key, int(value))
|
||||
|
||||
def get_bool(self, key: str, default=None) -> bool | None:
|
||||
return bool(self.get_int(key, default))
|
||||
|
||||
def get_tk_boolean(self, name: str, default: bool) -> tk.BooleanVar:
|
||||
value = self.get_bool(name, default)
|
||||
var = tk.BooleanVar(name=name, value=value)
|
||||
var.trace_add("write", lambda a, b, c: self.set_bool(name, var.get()))
|
||||
return var
|
||||
|
||||
def get_tk_str(self, name: str, default: str | None) -> tk.StringVar:
|
||||
value = self.get_str(name, default)
|
||||
var = tk.StringVar(name=name, value=value)
|
||||
var.trace_add("write", lambda a, b, c: self.set_str(name, var.get()))
|
||||
return var
|
||||
|
||||
def get_tk_int(self, name: str, default: int | None) -> tk.IntVar:
|
||||
value = self.get_int(name, default)
|
||||
var = tk.IntVar(name=name, value=value)
|
||||
var.trace_add("write", lambda a, b, c: self.set_int(name, var.get()))
|
||||
return var
|
||||
|
||||
|
||||
def get_app_data_dir() -> Path:
|
||||
if os.name == "nt":
|
||||
directory = Path.home() / "AppData/Roaming/SCKF"
|
||||
|
||||
elif os.name == "posix":
|
||||
directory = Path.home() / ".config/SCKF"
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported os: {os.name}")
|
||||
|
||||
directory.mkdir(exist_ok=True, parents=True)
|
||||
return directory
|
||||
|
||||
|
||||
db = PersistentConfig(get_app_data_dir() / "config.sqlite")
|
34
discord_subscriber.py
Normal file
34
discord_subscriber.py
Normal file
@ -0,0 +1,34 @@
|
||||
import requests
|
||||
from monitor import Event, SubscriberABC
|
||||
from loguru import logger
|
||||
from monitor import Filter
|
||||
|
||||
|
||||
class DiscordSubscriber(SubscriberABC):
|
||||
def __init__(self, event_filter: Filter):
|
||||
super().__init__(event_filter)
|
||||
self.webhook_url: str | None = None
|
||||
self.enabled: bool = True
|
||||
|
||||
def set_url(self, webhook_url: str) -> None:
|
||||
self.webhook_url = webhook_url
|
||||
|
||||
def set_enabled(self, value: bool) -> None:
|
||||
self.enabled = value
|
||||
|
||||
def consume_event(self, event: Event) -> None:
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if not self.filter.filter_event(event):
|
||||
return
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self.webhook_url,
|
||||
json={"content": event.to_str(include_ts=False, rich_text=True)},
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
except Exception:
|
||||
logger.opt(exception=True).warning(f"Error sending to Discord: {event}")
|
376
main.py
376
main.py
@ -1,377 +1,17 @@
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from ui import DeathLogGUI
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, scrolledtext
|
||||
import queue
|
||||
import requests
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator, Self
|
||||
|
||||
|
||||
# Default values
|
||||
DEFAULT_DISCORD_WEBHOOK_URL = "HOOOOOOOOOOOOOOOOK"
|
||||
DEFAULT_LOG_FILE_PATH = Path(r"E:\RSI\StarCitizen\LIVE\Game.log")
|
||||
DEBUG = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Line:
|
||||
line: str
|
||||
is_realtime: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Kill:
|
||||
killer: str
|
||||
victim: str
|
||||
zone: str
|
||||
killed_by: str
|
||||
damage_type: str
|
||||
|
||||
@staticmethod
|
||||
def from_line(line: str) -> "Self":
|
||||
# <2025-05-23T16:48:53.672Z> [Notice] <Actor Death> CActor::Kill: 'Neon0ne' [1895570109956] in zone 'ANVL_Ballista_3700492224326' killed by 'Aleksey31' [1978962253870] using 'MRCK_S07_ANVL_Ballista_Duel_3700492224372' [Class unknown] with damage type 'VehicleDestruction' from direction x: 0.000000, y: 0.000000, z: 0.000000 [Team_ActorTech][Actor]
|
||||
splited = [i.strip("'") for i in line.split()]
|
||||
return Kill(
|
||||
victim=splited[5],
|
||||
zone=splited[9],
|
||||
killer=splited[12],
|
||||
killed_by=splited[15],
|
||||
damage_type=splited[21],
|
||||
)
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(
|
||||
self,
|
||||
log_file_path: Path,
|
||||
discord_webhook_url: str,
|
||||
do_backread: bool = False,
|
||||
do_mock_discord: bool = False,
|
||||
hide_npc_kills: bool = False,
|
||||
kill_callback=None,
|
||||
):
|
||||
self.log_file_path = log_file_path
|
||||
self.do_backread: bool = do_backread
|
||||
self.do_mock_discord: bool = do_mock_discord
|
||||
self.discord_webhook_url: str = discord_webhook_url
|
||||
self.nickname: str | None = None
|
||||
self.kill_callback = kill_callback
|
||||
self.hide_npc_kills = hide_npc_kills
|
||||
self.running = True
|
||||
|
||||
if not (discord_webhook_url.startswith("https://") or do_mock_discord):
|
||||
msg = f"{discord_webhook_url} не вебхук, так то"
|
||||
print(msg)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def try_set_self_name(self, line: str) -> None:
|
||||
# <2025-05-23T16:19:10.244Z> [Notice] <AccountLoginCharacterStatus_Character> Character: createdAt 1747220301029 - updatedAt 1747220301703 - geid 1978962253870 - accountId 2261831 - name Aleksey31 - state STATE_CURRENT [Team_GameServices][Login]
|
||||
if self.nickname is not None:
|
||||
print("Warning, AccountLoginCharacterStatus_Character duplicate", line)
|
||||
return
|
||||
|
||||
try:
|
||||
self.nickname = line.split()[17]
|
||||
print(f"Extracted {self.nickname=}")
|
||||
|
||||
except IndexError:
|
||||
print("Failed to extract nickname from line", line)
|
||||
|
||||
def process_lines(self) -> None:
|
||||
for line in self.monitor_log_file():
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
process_important_realtime_events = self.do_backread or line.is_realtime
|
||||
|
||||
if "<AccountLoginCharacterStatus_Character>" in line.line:
|
||||
self.try_set_self_name(line.line)
|
||||
continue
|
||||
|
||||
if (
|
||||
"[Notice] <Actor Death> CActor::Kill:" in line.line
|
||||
and process_important_realtime_events
|
||||
):
|
||||
kill = Kill.from_line(line.line)
|
||||
# Skip processing if this is an NPC kill and hide_npc_kills is enabled
|
||||
if self.hide_npc_kills and "_NPC_" in kill.victim:
|
||||
continue
|
||||
self.send_kill(kill)
|
||||
|
||||
def send_kill(self, kill: Kill) -> None:
|
||||
if kill.victim == kill.killer:
|
||||
msg = f"🤦 **{kill.victim}** РКН"
|
||||
else:
|
||||
msg = f"💀 **{kill.victim}** killed by 🔫 **{kill.killer}**"
|
||||
|
||||
self.send(msg)
|
||||
|
||||
# Call the callback if provided
|
||||
if self.kill_callback:
|
||||
self.kill_callback(kill, msg)
|
||||
|
||||
def send(self, message: str) -> None:
|
||||
try:
|
||||
if self.do_mock_discord:
|
||||
print("Sending message to discord", message)
|
||||
else:
|
||||
requests.post(self.discord_webhook_url, json={"content": message})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending to Discord: {e}")
|
||||
|
||||
def monitor_log_file(self) -> Generator[Line, None, None]:
|
||||
is_realtime = False
|
||||
try:
|
||||
with self.log_file_path.open("r", encoding="utf-8") as file:
|
||||
while self.running:
|
||||
current_position = file.tell()
|
||||
line = file.readline()
|
||||
|
||||
if not line:
|
||||
is_realtime = True
|
||||
time.sleep(5)
|
||||
file.seek(current_position)
|
||||
if DEBUG is True:
|
||||
return
|
||||
|
||||
line = line.removesuffix("\n")
|
||||
yield Line(line, is_realtime)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Log file not found: {self.log_file_path}")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error monitoring log file: {e}")
|
||||
return
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
|
||||
class DeathLogGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Star Citizen Death Log Monitor")
|
||||
self.root.geometry("800x600")
|
||||
self.root.minsize(600, 400)
|
||||
|
||||
self.log_path = tk.StringVar(value=str(DEFAULT_LOG_FILE_PATH))
|
||||
self.webhook_url = tk.StringVar(value=DEFAULT_DISCORD_WEBHOOK_URL)
|
||||
self.do_backread = tk.BooleanVar(value=False)
|
||||
self.do_mock_discord = tk.BooleanVar(value=False)
|
||||
self.hide_npc_kills = tk.BooleanVar(value=True)
|
||||
|
||||
self.app = None
|
||||
self.monitor_thread = None
|
||||
self.kill_queue = queue.Queue()
|
||||
self.recent_kills: list[str] = []
|
||||
self.max_kills_to_display = 100
|
||||
|
||||
self.create_widgets()
|
||||
self.update_kill_display()
|
||||
|
||||
def create_widgets(self):
|
||||
# Create main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Configuration section
|
||||
config_frame = ttk.LabelFrame(main_frame, text="Configuration", padding="10")
|
||||
config_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
# Log file path
|
||||
ttk.Label(config_frame, text="Log File Path:").grid(
|
||||
row=0, column=0, sticky=tk.W, pady=2
|
||||
)
|
||||
ttk.Entry(config_frame, textvariable=self.log_path, width=50).grid(
|
||||
row=0, column=1, sticky=tk.W + tk.E, pady=2, padx=5
|
||||
)
|
||||
ttk.Button(config_frame, text="Browse", command=self.browse_log_file).grid(
|
||||
row=0, column=2, pady=2
|
||||
)
|
||||
|
||||
# Discord webhook URL
|
||||
ttk.Label(config_frame, text="Discord Webhook URL:").grid(
|
||||
row=1, column=0, sticky=tk.W, pady=2
|
||||
)
|
||||
ttk.Entry(config_frame, textvariable=self.webhook_url, width=50).grid(
|
||||
row=1, column=1, sticky=tk.W + tk.E, pady=2, padx=5
|
||||
)
|
||||
|
||||
# Checkboxes
|
||||
ttk.Checkbutton(
|
||||
config_frame, text="Process existing log entries", variable=self.do_backread
|
||||
).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Mock Discord (don't send actual messages)",
|
||||
variable=self.do_mock_discord,
|
||||
).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Hide NPC kills (_NPC_ in name)",
|
||||
variable=self.hide_npc_kills,
|
||||
).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
# Control buttons
|
||||
control_frame = ttk.Frame(main_frame)
|
||||
control_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
self.start_button = ttk.Button(
|
||||
control_frame, text="Start Monitoring", command=self.start_monitoring
|
||||
)
|
||||
self.start_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.stop_button = ttk.Button(
|
||||
control_frame,
|
||||
text="Stop Monitoring",
|
||||
command=self.stop_monitoring,
|
||||
state=tk.DISABLED,
|
||||
)
|
||||
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Status indicator
|
||||
self.status_var = tk.StringVar(value="Not monitoring")
|
||||
status_frame = ttk.Frame(main_frame)
|
||||
status_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(status_frame, text="Status:").pack(side=tk.LEFT)
|
||||
self.status_label = ttk.Label(
|
||||
status_frame, textvariable=self.status_var, foreground="red"
|
||||
)
|
||||
self.status_label.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Kill events display
|
||||
kills_frame = ttk.LabelFrame(
|
||||
main_frame, text="Recent Kill Events", padding="10"
|
||||
)
|
||||
kills_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||
|
||||
self.kill_display = scrolledtext.ScrolledText(
|
||||
kills_frame, wrap=tk.WORD, height=10
|
||||
)
|
||||
self.kill_display.pack(fill=tk.BOTH, expand=True)
|
||||
self.kill_display.config(state=tk.DISABLED)
|
||||
|
||||
def browse_log_file(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Star Citizen Log File",
|
||||
filetypes=[("Log Files", "*.log"), ("All Files", "*.*")],
|
||||
)
|
||||
if filename:
|
||||
self.log_path.set(filename)
|
||||
|
||||
def start_monitoring(self):
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
return
|
||||
|
||||
try:
|
||||
log_path = Path(self.log_path.get())
|
||||
webhook_url = self.webhook_url.get()
|
||||
|
||||
self.app = App(
|
||||
log_file_path=log_path,
|
||||
discord_webhook_url=webhook_url,
|
||||
do_backread=self.do_backread.get(),
|
||||
do_mock_discord=self.do_mock_discord.get(),
|
||||
hide_npc_kills=self.hide_npc_kills.get(),
|
||||
kill_callback=self.on_kill,
|
||||
)
|
||||
|
||||
self.monitor_thread = threading.Thread(
|
||||
target=self.app.process_lines, daemon=True
|
||||
)
|
||||
self.monitor_thread.start()
|
||||
|
||||
self.status_var.set("Monitoring active")
|
||||
self.status_label.config(foreground="green")
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Error: {str(e)}")
|
||||
self.status_label.config(foreground="red")
|
||||
|
||||
def stop_monitoring(self):
|
||||
if self.app:
|
||||
self.app.stop()
|
||||
|
||||
if self.monitor_thread:
|
||||
self.monitor_thread.join(timeout=1.0)
|
||||
|
||||
self.status_var.set("Monitoring stopped")
|
||||
self.status_label.config(foreground="red")
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
|
||||
def on_kill(self, kill: Kill, message: str):
|
||||
# No need to filter here as it's already filtered in the App class
|
||||
self.kill_queue.put((kill, message))
|
||||
|
||||
def update_kill_display(self):
|
||||
while not self.kill_queue.empty():
|
||||
try:
|
||||
kill, message = self.kill_queue.get_nowait()
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
# Simplified message with only nicknames and emojis
|
||||
if kill.victim == kill.killer:
|
||||
formatted_message = f"[{timestamp}] 🤦 {kill.victim} (self-kill)\n"
|
||||
else:
|
||||
formatted_message = (
|
||||
f"[{timestamp}] 💀 {kill.victim} killed by 🔫 {kill.killer}\n"
|
||||
)
|
||||
|
||||
self.recent_kills.append(formatted_message)
|
||||
if len(self.recent_kills) > self.max_kills_to_display:
|
||||
self.recent_kills.pop(0)
|
||||
|
||||
self.kill_display.config(state=tk.NORMAL)
|
||||
self.kill_display.delete(1.0, tk.END)
|
||||
for msg in self.recent_kills:
|
||||
self.kill_display.insert(tk.END, msg)
|
||||
self.kill_display.config(state=tk.DISABLED)
|
||||
self.kill_display.see(tk.END)
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
self.root.after(100, self.update_kill_display)
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Check if we should run in GUI mode or CLI mode
|
||||
import sys
|
||||
with suppress(Exception):
|
||||
from ctypes import windll
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--cli":
|
||||
# CLI mode
|
||||
if DEBUG is True:
|
||||
app = App(
|
||||
Path("./Game.log"),
|
||||
DEFAULT_DISCORD_WEBHOOK_URL,
|
||||
do_mock_discord=True,
|
||||
do_backread=True,
|
||||
hide_npc_kills=False,
|
||||
)
|
||||
windll.shcore.SetProcessDpiAwareness(1)
|
||||
|
||||
else:
|
||||
app = App(
|
||||
DEFAULT_LOG_FILE_PATH,
|
||||
DEFAULT_DISCORD_WEBHOOK_URL,
|
||||
hide_npc_kills=False,
|
||||
)
|
||||
app.process_lines()
|
||||
|
||||
else:
|
||||
# GUI mode
|
||||
root = tk.Tk()
|
||||
app = DeathLogGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nMonitoring stopped")
|
||||
root = tk.Tk()
|
||||
app = DeathLogGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
179
monitor.py
Normal file
179
monitor.py
Normal file
@ -0,0 +1,179 @@
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator, Self
|
||||
from loguru import logger
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
def parse_timestamp(line: str) -> datetime.datetime | None:
|
||||
# <2025-05-23T16:48:53.672Z>
|
||||
try:
|
||||
line = line.strip("<>").replace("Z", "+00:00")
|
||||
return datetime.datetime.fromisoformat(line)
|
||||
|
||||
except Exception:
|
||||
logger.warning(f"Failed to parse timestamp from {line!r}")
|
||||
return None
|
||||
|
||||
|
||||
damage_type_to_emoji = {
|
||||
"ElectricArc": ":cloud_lightning:",
|
||||
"Explosion": ":boom:",
|
||||
"VehicleDestruction": ":boom: :airplane:",
|
||||
"TakeDown": ":martial_arts_uniform: :face_with_peeking_eye:",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Line:
|
||||
line: str
|
||||
is_realtime: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Event:
|
||||
timestamp: datetime.datetime | None
|
||||
|
||||
def to_str(self, include_ts: bool = False, rich_text: bool = False) -> str:
|
||||
return str(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Kill(Event):
|
||||
killer: str
|
||||
victim: str
|
||||
zone: str
|
||||
killed_by: str
|
||||
damage_type: str
|
||||
|
||||
@staticmethod
|
||||
def from_line(line: str) -> "Self":
|
||||
# <2025-05-23T16:48:53.672Z> [Notice] <Actor Death> CActor::Kill: 'Neon0ne' [1895570109956] in zone 'ANVL_Ballista_3700492224326' killed by 'Aleksey31' [1978962253870] using 'MRCK_S07_ANVL_Ballista_Duel_3700492224372' [Class unknown] with damage type 'VehicleDestruction' from direction x: 0.000000, y: 0.000000, z: 0.000000 [Team_ActorTech][Actor]
|
||||
splited = [i.strip("'") for i in line.split()]
|
||||
return Kill(
|
||||
timestamp=parse_timestamp(splited[0]),
|
||||
victim=splited[5],
|
||||
zone=splited[9],
|
||||
killer=splited[12],
|
||||
killed_by=splited[15],
|
||||
damage_type=splited[21],
|
||||
)
|
||||
|
||||
def to_str(self, include_ts: bool = False, rich_text: bool = True) -> str:
|
||||
ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts
|
||||
if self.victim == self.killer:
|
||||
return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} РКН{' 🤦' * rich_text}"
|
||||
|
||||
emoji = " " + damage_type_to_emoji.get(self.damage_type, "")
|
||||
return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} killed by {'🔫 ' * rich_text}{'**' * rich_text}{self.killer}{'**' * rich_text}{emoji * rich_text}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Filter:
|
||||
only_personal_feed: bool
|
||||
include_npc: bool
|
||||
nickname: str | None = None
|
||||
|
||||
def filter_event(self, event: Event) -> bool:
|
||||
if isinstance(event, Kill):
|
||||
if "_NPC_" in event.killer + event.victim and not self.include_npc:
|
||||
return False
|
||||
|
||||
if all(
|
||||
[
|
||||
self.nickname not in (event.killer, event.victim),
|
||||
self.only_personal_feed is True,
|
||||
self.nickname is not None,
|
||||
]
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
f"Don't know how to filter event {type(event)}, letting through"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class SubscriberABC(ABC):
|
||||
def __init__(self, event_filter: Filter):
|
||||
self.filter = event_filter
|
||||
|
||||
@abstractmethod
|
||||
def consume_event(self, event: Event) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class Monitor:
|
||||
log_file_path: Path
|
||||
subscribers: set[SubscriberABC]
|
||||
do_backread: bool = False
|
||||
nickname: str | None = None
|
||||
running: bool = False
|
||||
|
||||
def try_set_self_name(self, line: str) -> None:
|
||||
# <2025-05-23T16:19:10.244Z> [Notice] <AccountLoginCharacterStatus_Character> Character: createdAt 1747220301029 - updatedAt 1747220301703 - geid 1978962253870 - accountId 2261831 - name Aleksey31 - state STATE_CURRENT [Team_GameServices][Login]
|
||||
if self.nickname is not None:
|
||||
logger.warning(
|
||||
"Warning, AccountLoginCharacterStatus_Character duplicate", line
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
self.nickname = line.split()[17]
|
||||
logger.info(f"Extracted {self.nickname=}")
|
||||
|
||||
except IndexError:
|
||||
logger.warning("Failed to extract nickname from line", line)
|
||||
|
||||
def call_subscribers(self, event: Event) -> None:
|
||||
logger.info(f"Passing event: {event}")
|
||||
if len(self.subscribers) == 0:
|
||||
logger.warning("set of subscribers is empty")
|
||||
|
||||
for subscriber in self.subscribers:
|
||||
subscriber.consume_event(event)
|
||||
|
||||
def process_lines(self) -> None:
|
||||
logger.info("Beginning lines consuming")
|
||||
self.running = True
|
||||
for line in self.monitor_log_file():
|
||||
process_important_realtime_events = self.do_backread or line.is_realtime
|
||||
|
||||
if "<AccountLoginCharacterStatus_Character>" in line.line:
|
||||
self.try_set_self_name(line.line)
|
||||
continue
|
||||
|
||||
if (
|
||||
"[Notice] <Actor Death> CActor::Kill:" in line.line
|
||||
and process_important_realtime_events
|
||||
):
|
||||
kill = Kill.from_line(line.line)
|
||||
self.call_subscribers(kill)
|
||||
|
||||
def monitor_log_file(self) -> Generator[Line, None, None]:
|
||||
is_realtime = False
|
||||
with self.log_file_path.open("r", encoding="utf-8") as file:
|
||||
while self.running:
|
||||
current_position = file.tell()
|
||||
line = file.readline()
|
||||
|
||||
if not line:
|
||||
if not is_realtime:
|
||||
logger.info("Reached edge of the file")
|
||||
|
||||
is_realtime = True
|
||||
time.sleep(0.15)
|
||||
file.seek(current_position)
|
||||
|
||||
line = line.removesuffix("\n")
|
||||
yield Line(line, is_realtime)
|
||||
|
||||
logger.info("Exiting reading loop")
|
@ -4,6 +4,13 @@ version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"pyinstaller>=6.13.0",
|
||||
"loguru>=0.7.3",
|
||||
"pystray>=0.19.5",
|
||||
"requests>=2.32.3",
|
||||
"sv-ttk>=2.6.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"nuitka>=2.7.3",
|
||||
]
|
||||
|
322
ui.py
Normal file
322
ui.py
Normal file
@ -0,0 +1,322 @@
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
import pystray
|
||||
from pystray import Menu, MenuItem
|
||||
from PIL import Image, ImageDraw
|
||||
import sv_ttk
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, scrolledtext
|
||||
import config
|
||||
from discord_subscriber import DiscordSubscriber
|
||||
from monitor import Monitor, Event, SubscriberABC, Filter
|
||||
from loguru import logger
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class Feed(SubscriberABC):
|
||||
def __init__(
|
||||
self, event_filter: Filter, callback_data_changed: Callable | None = None
|
||||
):
|
||||
super().__init__(event_filter)
|
||||
self.ring_buffer: deque[Event] = deque(maxlen=512)
|
||||
self.ring_buffer_lock = threading.Lock()
|
||||
self.callback = callback_data_changed
|
||||
|
||||
self.trace_callback = lambda name, b, c: self._notify_data_changed()
|
||||
|
||||
self.include_ts: tk.BooleanVar = config.db.get_tk_boolean("INCLUDE_TS", True)
|
||||
self.include_ts.trace_add("write", self.trace_callback)
|
||||
|
||||
def consume_event(self, event: Event) -> None:
|
||||
with self.ring_buffer_lock:
|
||||
self.ring_buffer.append(event)
|
||||
|
||||
self._notify_data_changed()
|
||||
|
||||
def render(self) -> str:
|
||||
with self.ring_buffer_lock:
|
||||
return "\n".join(
|
||||
event.to_str(include_ts=self.include_ts.get(), rich_text=False)
|
||||
for event in self.ring_buffer
|
||||
if self.filter.filter_event(event)
|
||||
)
|
||||
|
||||
def clear(self) -> None:
|
||||
with self.ring_buffer_lock:
|
||||
self.ring_buffer.clear()
|
||||
|
||||
self._notify_data_changed()
|
||||
|
||||
def _notify_data_changed(self) -> None:
|
||||
if callable(self.callback):
|
||||
self.callback()
|
||||
|
||||
|
||||
def get_image(
|
||||
width: int = 64, height: int = 64, color1: str = "black", color2: str = "green"
|
||||
) -> Image:
|
||||
image = Image.new("RGB", (width, height), color1)
|
||||
dc = ImageDraw.Draw(image)
|
||||
dc.rectangle((width // 2, 0, width, height // 2), fill=color2)
|
||||
dc.rectangle((0, height // 2, width // 2, height), fill=color2)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
class DeathLogGUI:
|
||||
def __init__(self, root: tk.Tk):
|
||||
self.root = root
|
||||
self.root.title(config.APP_NAME)
|
||||
self.root.geometry(config.db.get_str("GEOMETRY", "850x600"))
|
||||
self.root.minsize(850, 600)
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.handle_close_window)
|
||||
|
||||
theme_mapping = {True: "dark", False: "light"}
|
||||
sv_ttk.set_theme(theme_mapping.get(config.db.get_bool("IS_DARK_MODE", False)))
|
||||
|
||||
self.log_path = config.db.get_tk_str(
|
||||
"LOG_FILE_PATH", r"E:\RSI\StarCitizen\LIVE\Game.log"
|
||||
)
|
||||
|
||||
self.do_backread = config.db.get_tk_boolean("DO_BACKREAD", False)
|
||||
|
||||
self.include_npc_kills = config.db.get_tk_boolean("INCLUDE_NPC_KILLS", True)
|
||||
self.personal_feed = config.db.get_tk_boolean("PERSONAL_FEED", False)
|
||||
|
||||
self.filter = Filter(
|
||||
only_personal_feed=self.personal_feed.get(),
|
||||
include_npc=self.include_npc_kills.get(),
|
||||
)
|
||||
|
||||
def filter_changed(a, b, c) -> None:
|
||||
self.filter.include_npc = self.include_npc_kills.get()
|
||||
self.filter.only_personal_feed = self.personal_feed.get()
|
||||
logger.debug("Filter props changed")
|
||||
self.feed.callback()
|
||||
|
||||
self.include_npc_kills.trace_add("write", filter_changed)
|
||||
self.personal_feed.trace_add("write", filter_changed)
|
||||
|
||||
self.is_dark_mode = config.db.get_tk_boolean("IS_DARK_MODE", False)
|
||||
self.is_dark_mode.trace_add(
|
||||
"write",
|
||||
lambda a, b, c: sv_ttk.set_theme(
|
||||
theme_mapping.get(self.is_dark_mode.get()), self.root
|
||||
),
|
||||
)
|
||||
|
||||
self.feed = Feed(
|
||||
callback_data_changed=partial(self.root.after, 0, self.update_feed_display),
|
||||
event_filter=self.filter,
|
||||
)
|
||||
self.enable_discord = config.db.get_tk_boolean("ENABLE_DISCORD", False)
|
||||
self.webhook_url = config.db.get_tk_str(
|
||||
"DISCORD_WEBHOOK_URL", "https://discord.com/..."
|
||||
)
|
||||
self.discord = DiscordSubscriber(event_filter=self.filter)
|
||||
self.discord.enabled = self.enable_discord.get()
|
||||
self.discord.webhook_url = self.webhook_url.get()
|
||||
|
||||
self.webhook_url.trace_add(
|
||||
"write", lambda a, b, c: self.discord.set_url(self.webhook_url.get())
|
||||
)
|
||||
|
||||
self.enable_discord.trace_add(
|
||||
"write", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get())
|
||||
)
|
||||
|
||||
self.monitor: Monitor | None = None
|
||||
self.monitor_thread: threading.Thread | None = None
|
||||
|
||||
self.create_widgets()
|
||||
self.update_feed_display()
|
||||
|
||||
self.tray = pystray.Icon(
|
||||
name=config.APP_NAME,
|
||||
icon=get_image(),
|
||||
menu=Menu(
|
||||
MenuItem("Show window", self.handle_open_window, default=True),
|
||||
MenuItem("Exit", self.shutdown),
|
||||
),
|
||||
)
|
||||
|
||||
self.tray.visible = False
|
||||
self.tray_thread = threading.Thread(
|
||||
target=self.tray.run, kwargs={"setup": lambda _: None}
|
||||
)
|
||||
self.tray_thread.start()
|
||||
|
||||
def shutdown(self):
|
||||
logger.info("Beginning GUI shutdown")
|
||||
self.stop_monitoring()
|
||||
logger.info(self.root.winfo_geometry())
|
||||
config.db.set_str("GEOMETRY", self.root.winfo_geometry())
|
||||
self.tray.stop()
|
||||
config.db.db.close()
|
||||
self.root.quit()
|
||||
|
||||
def handle_close_window(self) -> None:
|
||||
self.root.withdraw()
|
||||
self.tray.visible = True
|
||||
|
||||
def handle_open_window(self) -> None:
|
||||
self.root.deiconify()
|
||||
self.tray.visible = False
|
||||
|
||||
def create_widgets(self):
|
||||
# Create main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Configuration section
|
||||
config_frame = ttk.LabelFrame(main_frame, text="Configuration", padding="10")
|
||||
config_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
# Log file path
|
||||
ttk.Label(config_frame, text="Log File Path:").grid(
|
||||
row=0, column=0, sticky=tk.W, pady=2
|
||||
)
|
||||
ttk.Entry(config_frame, textvariable=self.log_path, width=50).grid(
|
||||
row=0, column=1, sticky=tk.W + tk.E, pady=2, padx=5
|
||||
)
|
||||
ttk.Button(config_frame, text="Browse", command=self.browse_log_file).grid(
|
||||
row=0, column=2, pady=2
|
||||
)
|
||||
|
||||
# Discord webhook URL
|
||||
ttk.Label(config_frame, text="Discord Webhook URL:").grid(
|
||||
row=1, column=0, sticky=tk.W, pady=2
|
||||
)
|
||||
ttk.Entry(config_frame, textvariable=self.webhook_url, width=50).grid(
|
||||
row=1, column=1, sticky=tk.W + tk.E, pady=2, padx=5
|
||||
)
|
||||
|
||||
# Checkboxes
|
||||
ttk.Checkbutton(
|
||||
config_frame, text="Process existing log entries", variable=self.do_backread
|
||||
).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Do send events to discord",
|
||||
variable=self.enable_discord,
|
||||
).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Only personal events",
|
||||
variable=self.personal_feed,
|
||||
).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Hide NPC kills (_NPC_ in name)",
|
||||
variable=self.include_npc_kills,
|
||||
).grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Include timestamps",
|
||||
variable=self.feed.include_ts,
|
||||
).grid(row=6, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
ttk.Checkbutton(
|
||||
config_frame,
|
||||
text="Enable dark mode",
|
||||
variable=self.is_dark_mode,
|
||||
).grid(row=7, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
# Control buttons
|
||||
control_frame = ttk.Frame(main_frame)
|
||||
control_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
self.start_button = ttk.Button(
|
||||
control_frame, text="Start Monitoring", command=self.start_monitoring
|
||||
)
|
||||
self.start_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.stop_button = ttk.Button(
|
||||
control_frame,
|
||||
text="Stop Monitoring",
|
||||
command=self.stop_monitoring,
|
||||
state=tk.DISABLED,
|
||||
)
|
||||
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Status indicator
|
||||
self.status_var = tk.StringVar(value="Not monitoring")
|
||||
status_frame = ttk.Frame(main_frame)
|
||||
status_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(status_frame, text="Status:").pack(side=tk.LEFT)
|
||||
self.status_label = ttk.Label(
|
||||
status_frame, textvariable=self.status_var, foreground="red"
|
||||
)
|
||||
self.status_label.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
kills_frame = ttk.LabelFrame(
|
||||
main_frame, text="Recent Kill Events", padding="10"
|
||||
)
|
||||
kills_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||
|
||||
self.feed_display = scrolledtext.ScrolledText(
|
||||
kills_frame, wrap=tk.WORD, height=10
|
||||
)
|
||||
self.feed_display.pack(fill=tk.BOTH, expand=True)
|
||||
self.feed_display.config(state=tk.DISABLED)
|
||||
|
||||
def browse_log_file(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select Star Citizen Log File",
|
||||
filetypes=[("Log Files", "*.log"), ("All Files", "*.*")],
|
||||
)
|
||||
if filename:
|
||||
self.log_path.set(filename)
|
||||
|
||||
def start_monitoring(self):
|
||||
if self.monitor_thread and self.monitor_thread.is_alive():
|
||||
return
|
||||
|
||||
try:
|
||||
self.monitor = Monitor(
|
||||
log_file_path=Path(self.log_path.get()),
|
||||
do_backread=self.do_backread.get(),
|
||||
subscribers={self.feed, self.discord},
|
||||
)
|
||||
|
||||
self.monitor_thread = threading.Thread(
|
||||
target=self.monitor.process_lines, daemon=True
|
||||
)
|
||||
self.monitor_thread.start()
|
||||
self.feed.clear()
|
||||
|
||||
self.status_var.set("Monitoring active")
|
||||
self.status_label.config(foreground="green")
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Error: {str(e)}")
|
||||
self.status_label.config(foreground="red")
|
||||
logger.opt(exception=True).exception("Failed to start monitoring")
|
||||
|
||||
def stop_monitoring(self):
|
||||
if self.monitor:
|
||||
self.monitor.running = False
|
||||
self.monitor_thread.join(timeout=1.0)
|
||||
|
||||
self.status_var.set("Monitoring stopped")
|
||||
self.status_label.config(foreground="red")
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
|
||||
def update_feed_display(self):
|
||||
with suppress(AttributeError):
|
||||
self.filter.nickname = self.monitor.nickname
|
||||
|
||||
self.feed_display.config(state=tk.NORMAL)
|
||||
self.feed_display.delete("1.0", tk.END)
|
||||
self.feed_display.insert("1.0", self.feed.render())
|
||||
self.feed_display.config(state=tk.DISABLED)
|
||||
self.feed_display.see(tk.END)
|
271
uv.lock
generated
271
uv.lock
generated
@ -2,15 +2,6 @@ version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
@ -20,6 +11,28 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
@ -42,6 +55,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@ -52,83 +74,137 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2023.2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.13.0"
|
||||
name = "nuitka"
|
||||
version = "2.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyinstaller-hooks-contrib" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "ordered-set" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/b1/2949fe6d3874e961898ca5cfc1bf2cf13bdeea488b302e74a745bc28c8ba/pyinstaller-6.13.0.tar.gz", hash = "sha256:38911feec2c5e215e5159a7e66fdb12400168bd116143b54a8a7a37f08733456", size = 4276427 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/40/7fd16fffa8bbf78f9f9b1abe75ac123e3cc46740c6142c4db4e29b32ce4a/Nuitka-2.7.3.tar.gz", hash = "sha256:bd3dd12c5f0dfec0208f88c14ef3bbfc4ba3495eafc62c31b4e1f71171536e47", size = 3884010 }
|
||||
|
||||
[[package]]
|
||||
name = "ordered-set"
|
||||
version = "4.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/02/d1a347d35b1b627da1e148159e617576555619ac3bb8bbd5fed661fc7bb5/pyinstaller-6.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:aa404f0b02cd57948098055e76ee190b8e65ccf7a2a3f048e5000f668317069f", size = 1001923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/80/6da39f7aeac65c9ca5afad0fac37887d75fdfd480178a7077c9d30b0704c/pyinstaller-6.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:92efcf2f09e78f07b568c5cb7ed48c9940f5dad627af4b49bede6320fab2a06e", size = 718135 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/2c/d21d31f780a489609e7bf6385c0f7635238dc98b37cba8645b53322b7450/pyinstaller-6.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:9f82f113c463f012faa0e323d952ca30a6f922685d9636e754bd3a256c7ed200", size = 728543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/20/e6ca87bbed6c0163533195707f820f05e10b8da1223fc6972cfe3c3c50c7/pyinstaller-6.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:db0e7945ebe276f604eb7c36e536479556ab32853412095e19172a5ec8fca1c5", size = 726868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d5/53b19285f8817ab6c4b07c570208d62606bab0e5a049d50c93710a1d9dc6/pyinstaller-6.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:92fe7337c5aa08d42b38d7a79614492cb571489f2cb0a8f91dc9ef9ccbe01ed3", size = 725037 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/5b/08e0b305ba71e6d7cb247e27d714da7536895b0283132d74d249bf662366/pyinstaller-6.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bc09795f5954135dd4486c1535650958c8218acb954f43860e4b05fb515a21c0", size = 721027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/9c/d8d0a7120103471be8dbe1c5419542aa794b9b9ec2ef628b542f9e6f9ef0/pyinstaller-6.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:589937548d34978c568cfdc39f31cf386f45202bc27fdb8facb989c79dfb4c02", size = 723443 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/c7/8a9d81569dda2352068ecc6ee779d5feff6729569dd1b4ffd1236ecd38fe/pyinstaller-6.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b7260832f7501ba1d2ce1834d4cddc0f2b94315282bc89c59333433715015447", size = 719915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e6/cccadb02b90198c7ed4ffb8bc34d420efb72b996f47cbd4738067a602d65/pyinstaller-6.13.0-py3-none-win32.whl", hash = "sha256:80c568848529635aa7ca46d8d525f68486d53e03f68b7bb5eba2c88d742e302c", size = 1294997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/06/15cbe0e25d1e73d5b981fa41ff0bb02b15e924e30b8c61256f4a28c4c837/pyinstaller-6.13.0-py3-none-win_amd64.whl", hash = "sha256:8d4296236b85aae570379488c2da833b28828b17c57c2cc21fccd7e3811fe372", size = 1352714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ef/74379298d46e7caa6aa7ceccc865106d3d4b15ac487ffdda2a35bfb6fe79/pyinstaller-6.13.0-py3-none-win_arm64.whl", hash = "sha256:d9f21d56ca2443aa6a1e255e7ad285c76453893a454105abe1b4d45e92bb9a20", size = 1293589 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2025.4"
|
||||
name = "pillow"
|
||||
version = "11.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/94/a111239b98260869780a5767e5d74bfd3a8c13a40457f479c28dcd91f89d/pyobjc_core-11.0.tar.gz", hash = "sha256:63bced211cb8a8fb5c8ff46473603da30e51112861bd02c438fbbbc8578d9a70", size = 994931 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/16/0c468e73dbecb821e3da8819236fe832dfc53eb5f66a11775b055a7589ea/pyobjc_core-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c338c1deb7ab2e9436d4175d1127da2eeed4a1b564b3d83b9f3ae4844ba97e86", size = 743900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/88/cecec88fd51f62a6cd7775cc4fb6bfde16652f97df88d28c84fb77ca0c18/pyobjc_core-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b4e9dc4296110f251a4033ff3f40320b35873ea7f876bd29a1c9705bb5e08c59", size = 791905 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "pyobjc-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/94/dfc5c7903306211798f990e6794c2eb7b8685ac487b26979e9255790419c/pyinstaller_hooks_contrib-2025.4.tar.gz", hash = "sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446", size = 162628 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/32/53809096ad5fc3e7a2c5ddea642590a5f2cb5b81d0ad6ea67fdb2263d9f9/pyobjc_framework_cocoa-11.0.tar.gz", hash = "sha256:00346a8cb81ad7b017b32ff7bf596000f9faa905807b1bd234644ebd47f692c5", size = 6173848 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/e1/ed48c7074145898e5c5b0072e87be975c5bd6a1d0f08c27a1daa7064fca0/pyinstaller_hooks_contrib-2025.4-py3-none-any.whl", hash = "sha256:6c2d73269b4c484eb40051fc1acee0beb113c2cfb3b37437b8394faae6f0d072", size = 434451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a5/609281a7e89efefbef9db1d8fe66bc0458c3b4e74e2227c644f9c18926fa/pyobjc_framework_Cocoa-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15b2bd977ed340074f930f1330f03d42912d5882b697d78bd06f8ebe263ef92e", size = 385889 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/f6/2d5a863673ef7b85a3cba875c43e6c495fb1307427a6801001ae94bb5e54/pyobjc_framework_Cocoa-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5750001db544e67f2b66f02067d8f0da96bb2ef71732bde104f01b8628f9d7ea", size = 389831 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
name = "pyobjc-framework-quartz"
|
||||
version = "11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/ad/f00f3f53387c23bbf4e0bb1410e11978cbf87c82fa6baff0ee86f74c5fb6/pyobjc_framework_quartz-11.0.tar.gz", hash = "sha256:3205bf7795fb9ae34747f701486b3db6dfac71924894d1f372977c4d70c3c619", size = 3952463 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/9e/54c48fe8faab06ee5eb80796c8c17ec61fc313d84398540ee70abeaf7070/pyobjc_framework_Quartz-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:973b4f9b8ab844574461a038bd5269f425a7368d6e677e3cc81fcc9b27b65498", size = 212478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/28/456b54a59bfe11a91b7b4e94f8ffdcf174ffd1efa169f4283e5b3bc10194/pyobjc_framework_Quartz-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:66ab58d65348863b8707e63b2ec5cdc54569ee8189d1af90d52f29f5fdf6272c", size = 217973 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pystray"
|
||||
version = "0.19.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pillow" },
|
||||
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "python-xlib", marker = "sys_platform == 'linux'" },
|
||||
{ name = "six" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-xlib"
|
||||
version = "0.33"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -151,23 +227,44 @@ name = "sc-death-log"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "loguru" },
|
||||
{ name = "pystray" },
|
||||
{ name = "requests" },
|
||||
{ name = "sv-ttk" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "nuitka" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "pyinstaller", specifier = ">=6.13.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "pystray", specifier = ">=0.19.5" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "sv-ttk", specifier = ">=2.6.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "nuitka", specifier = ">=2.7.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.8.0"
|
||||
name = "sv-ttk"
|
||||
version = "2.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/da/6ad667a4bad4d66ec2d15206c1a1ad81d572679e516aae078824a6f35870/sv_ttk-2.6.0.tar.gz", hash = "sha256:3fd440396c95e30e88f686fcf28be425480f7320d6bf346f9cea5d6f56702cc2", size = 47214 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/3d/be0abc3202e90f282ad465f4e7c6e41bc8dce810ce5d1611566a1e7dfba8/sv_ttk-2.6.0-py3-none-any.whl", hash = "sha256:4319c52edf2e14732fe84bdc9788e26f9e9a1ad79451ec0f89f0120ffc8105d9", size = 49097 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -178,3 +275,39 @@ sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstandard"
|
||||
version = "0.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 },
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user