Skip to content

Commit c897e5f

Browse files
authored
Merge pull request #58 from crackhex/zip
zip files implementation + refactor
2 parents d98d360 + 2e19bef commit c897e5f

7 files changed

Lines changed: 141 additions & 39 deletions

File tree

‎adapters/discord/commands/host/set_file_command.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,19 @@
2626
from discord.ext import commands
2727

2828
from adapters.discord.checks import host_only
29+
from application.parsers.registry import EXTENSION_STRATEGIES
2930
from application.parsers.rkg_parser_strategy import RkgParserStrategy
3031
from application.parsers.rksys_parser_strategy import RksysParserStrategy
3132
from application.parsers.null_parser_strategy import NullParserStrategy
3233
from application.parsers.parser_strategy import ParserStrategy
34+
from application.parsers.zip_parser_strategy import ZipParserStrategy
3335
from application.services.config_service import ConfigService
3436

3537
# --------------------------------------------------------------------------- #
3638
# Map extensions → concrete strategy classes. #
3739
# This is to be extended by third party comps (sm64, nsmbw, etc) #
3840
# --------------------------------------------------------------------------- #
39-
EXTENSION_STRATEGIES: Dict[str, Type[ParserStrategy]] = {
40-
"rkg": RkgParserStrategy,
41-
"dat": RksysParserStrategy, # rksys.dat
42-
}
41+
4342

4443
ALLOWED_CHOICES = [
4544
app_commands.Choice(name=f".{ext}", value=ext) for ext in EXTENSION_STRATEGIES

‎application/parsers/registry.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Registry
3+
===========
4+
5+
Module path:
6+
src/application/parsers/registry.py
7+
8+
Summary:
9+
Maps file extension to parsing strategies and provides helper methods
10+
"""
11+
12+
from typing import Dict, Type, Optional
13+
14+
from application.parsers.parser_strategy import ParserStrategy
15+
from application.parsers.rkg_parser_strategy import RkgParserStrategy
16+
from application.parsers.rksys_parser_strategy import RksysParserStrategy
17+
from application.parsers.zip_parser_strategy import ZipParserStrategy
18+
19+
EXTENSION_STRATEGIES: Dict[str, Type[ParserStrategy]] = {
20+
"rkg": RkgParserStrategy,
21+
"dat": RksysParserStrategy,
22+
"zip": ZipParserStrategy,
23+
}
24+
25+
def get_strategy_cls(ext: str) -> Optional[Type[ParserStrategy]]:
26+
return EXTENSION_STRATEGIES.get(ext.lower())
27+
28+
def new_strategy(ext: str) -> Optional[ParserStrategy]:
29+
cls = get_strategy_cls(ext)
30+
return cls() if cls else None
31+
32+
def candidates_excluding(current: ParserStrategy) -> list[ParserStrategy]:
33+
"""Helper for fallback loop in parse_file()."""
34+
current_cls = type(current)
35+
return [cls() for cls in EXTENSION_STRATEGIES.values() if cls is not current_cls]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Zip File Parser Strategy
3+
=============================
4+
5+
Module path:
6+
src/application/parsers/zip_parser_strategy.py
7+
8+
Summary:
9+
ParserStrategy implementation for Zip files, used for special types of tasks that require more than a type of file.
10+
11+
Responsibilities:
12+
- Check if the file bytes are indeed a zip by verifying its magic headers
13+
- Produce a ZipFile object with default run_time=0
14+
"""
15+
16+
from domain.entities import ZipFile
17+
from io import BytesIO
18+
import zipfile
19+
from .parser_strategy import ParserStrategy
20+
21+
22+
class ZipParserStrategy(ParserStrategy):
23+
"""
24+
Concrete ParserStrategy for .zip files.
25+
26+
supports(): accepts any zip
27+
parse(): returns a ZipFile
28+
"""
29+
30+
def supports(self, file_bytes: bytes) -> bool:
31+
"""
32+
Under the hood, this checks for the magic headers
33+
"""
34+
try:
35+
return zipfile.is_zipfile(BytesIO(file_bytes))
36+
except Exception:
37+
return False
38+
39+
def parse(self, file_bytes: bytes, uploaded_at: int) -> ZipFile:
40+
"""
41+
Parse the Zip file into a ZipFile domain object.
42+
43+
Args:
44+
file_bytes (bytes): raw file content
45+
uploaded_at (int): UNIX timestamp when file was received
46+
47+
Returns:
48+
ZipFile: with run_time=0.0
49+
"""
50+
return ZipFile(
51+
path="",
52+
uploaded_at=uploaded_at,
53+
)

‎application/services/submission_services/mkw_submission_service.py‎

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
11
"""
2-
MKW Wii Submission Service
2+
MKW Submission Service
33
==========================
44
55
Module path:
6-
src/application/services/mkw_submission_service.py
6+
src/application/services/submission_services/mkw_submission_service.py
77
8-
Summary
9-
-------
10-
Domain-specific implementation of ``AbstractSubmissionService`` for the
11-
**Mario Kart Wii TAS Competition**.
12-
It understands two file formats:
8+
Summary:
139
14-
* ``.rkg`` – single-track ghost file (Time Trials).
15-
* ``.rksys`` – multi-track system save dump (Grand Prix / TT batch).
10+
Game-specific implementation of the base submission service for the
11+
Mario Kart Wii TAS Competition.
12+
It understands the following file formats:
13+
14+
* .rkg – single-track ghost file (Time Trials).
15+
* .rksys – game file, used for ghosts across multiple tracks.
16+
* .zip - sending multiple files, of different type.
1617
1718
The service chooses the proper parser **at run-time**, based on the single
18-
“accepted file extension” stored in the guild/competition configuration.
19+
accepted file extension stored in the guild/competition configuration.
1920
20-
If a user submits multiple times, previous runs are automatically replaced
21-
according to the base‐class algorithm.
2221
"""
2322

2423
from __future__ import annotations
2524

26-
from typing import Optional
25+
from typing import Optional, Dict, Type
2726

27+
from application.parsers.parser_strategy import ParserStrategy
28+
from application.parsers.registry import candidates_excluding
2829
from application.parsers.rkg_parser_strategy import RkgParserStrategy
2930
from application.parsers.rksys_parser_strategy import RksysParserStrategy
3031
from application.parsers.file_parser import FileParser
3132
from application.parsers.null_parser_strategy import NullParserStrategy # safety
33+
from application.parsers.zip_parser_strategy import ZipParserStrategy
3234

3335
from application.services.submission_services.base_submission_service import BaseSubmissionService
3436

@@ -43,6 +45,7 @@
4345
from domain.entities import SubmissionFile, Submission
4446

4547

48+
4649
class MKWSubmissionService(BaseSubmissionService):
4750
"""
4851
Concrete submission service for Mario Kart Wii competitions.
@@ -74,7 +77,7 @@ def __init__(
7477
speed_repo=speed_repo
7578
)
7679

77-
# ────────────────────────── required hooks ────────────────────────────
80+
# ────────────────────────── required methods ────────────────────────────
7881
# Pick the correct parser & return the parsed value object
7982
def parse_file(
8083
self,
@@ -88,9 +91,9 @@ def parse_file(
8891
Raises
8992
------
9093
ValueError
91-
If the bytes do not match either .rkg nor .rksys.
94+
If the bytes do not match any of the accepted formats.
9295
"""
93-
# ``FileParser`` should already have the right strategy
96+
# FileParser should already have the right strategy
9497
# (because /set-file set it), but we double-check.
9598
strat = self._parser.strategy
9699
if isinstance(strat, NullParserStrategy):
@@ -102,12 +105,16 @@ def parse_file(
102105
if strat.supports(file_bytes):
103106
return strat.parse(file_bytes, uploaded_at_epoch)
104107

105-
# Otherwise, fall back to the other parser
106-
alt_strat = RkgParserStrategy() if isinstance(strat, RksysParserStrategy) else RksysParserStrategy()
107-
if alt_strat.supports(file_bytes):
108-
# Swap strategy for the rest of the runtime
109-
self._parser.set_strategy(alt_strat)
110-
return alt_strat.parse(file_bytes, uploaded_at_epoch)
108+
109+
# Otherwise, try all other known strategies
110+
strat = self._parser.strategy
111+
if strat.supports(file_bytes):
112+
return strat.parse(file_bytes, uploaded_at_epoch)
113+
114+
for alt in candidates_excluding(strat):
115+
if alt.supports(file_bytes):
116+
self._parser.set_strategy(alt)
117+
return alt.parse(file_bytes, uploaded_at_epoch)
111118

112119
raise ValueError("Unsupported MKW submission file format")
113120

@@ -118,12 +125,12 @@ def populate_metadata(
118125
file_obj: SubmissionFile,
119126
) -> None:
120127
"""
121-
Extract MKW-specific metadata and copy it onto ``submission``.
128+
Extract MKW-specific metadata and copy it onto submission.
122129
123130
* run_time → submission.time
124131
* character / vehicle if available (only on RKG), else None
125132
"""
126-
# If rkg, retrieve run_time, else set to 0 if rksys (or time isn't found)
133+
# If rkg, retrieve run_time, else set to 0 if not an rkg (or time isn't found)
127134
if hasattr(file_obj, "run_time"):
128135
submission.time = file_obj.run_time or 0.0
129136
else:

‎application/services/submission_services/service_factory.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
1313
Adding support for a new game requires:
1414
15-
1. Implementing a concrete XXXSubmissionService that derives fromBaseSubmissionService`.
15+
1. Implementing a concrete XXXSubmissionService that derives from BaseSubmissionService.
1616
2. Importing it here and registering a new key in
1717
SubmissionServiceFactory – the rest of the bot remains unchanged.
1818
3. Register the file extension in main, and in entities.

‎domain/entities.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
Responsibilities:
1313
- Task: competition with lifecycle methods (open, publish, close)
1414
- SpeedTaskSession: individual user session for a speed-task
15-
- SubmissionFile, RKGFile, RKSysFile: file metadata value objects
15+
- SubmissionFile, RKGFile, RKSysFile, ZipFile: file metadata value objects
1616
- User: Discord user
1717
- Team: group of users who collaborate in a task
1818
- Submission: run submission tied to a task, user/team, and file
@@ -206,6 +206,14 @@ def __init__(self, path: str, uploaded_at: int):
206206
super().__init__(path, uploaded_at)
207207
self.run_time = 0.0 # no run time available
208208

209+
class ZipFile(SubmissionFile):
210+
"""
211+
Represents a .zip file.
212+
"""
213+
def __init__(self, path: str, uploaded_at: int):
214+
super().__init__(path, uploaded_at)
215+
self.run_time = 0.0 # no run time available
216+
209217

210218
class User:
211219
"""

‎main.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
from dotenv import load_dotenv
1212

1313
from application.parsers.null_parser_strategy import NullParserStrategy
14+
from application.parsers.registry import new_strategy
1415
from application.parsers.rkg_parser_strategy import RkgParserStrategy
1516
from application.parsers.rksys_parser_strategy import RksysParserStrategy
17+
from application.parsers.zip_parser_strategy import ZipParserStrategy
1618
from application.services.submission_services.service_factory import build_submission_service
1719
from infrastructure.db import init_db
1820
from infrastructure.repositories.sqlalchemy_task_repo import SqlAlchemyTaskRepository
@@ -153,19 +155,17 @@ async def _bootstrap() -> None:
153155
guild_mappings = await config_service.list_guild_configs()
154156
if guild_mappings:
155157
comp_key = guild_mappings[0].comp # 'mkw', 'sm64', ...
156-
ext_cfg = await config_service.get_submission_file_extension(comp_key)
157-
158-
if ext_cfg and ext_cfg.ext == "rkg":
159-
file_parser.set_strategy(RkgParserStrategy())
160-
elif ext_cfg and ext_cfg.ext == "rksys":
161-
file_parser.set_strategy(RksysParserStrategy())
162158

163-
# insert other comps here... (sm64, nsmbw)
159+
# Get the parser strategy by getting the current file extension (if set)
160+
ext_cfg = await config_service.get_submission_file_extension(comp_key)
161+
if ext_cfg and ext_cfg.ext:
162+
if strat := new_strategy(ext_cfg.ext):
163+
file_parser.set_strategy(strat)
164164

165-
# else: keep NullParser until /set-file is run
165+
# implicit else: keep NullParser until /set-file is run
166166

167167
submission_service = build_submission_service(
168-
comp=comp_key, # None ⇒ generic/Null service
168+
comp=comp_key,
169169
user_svc=user_service,
170170
submission_repo=submission_repo,
171171
task_repo=task_repo,

0 commit comments

Comments
 (0)