Compare commits

...

2 commits

Author SHA1 Message Date
Ash Garcia
535f53c55a many, many changes. why wasn't i committing these 2025-07-22 14:49:28 -07:00
Ash Garcia
38a486131d d2024r1 changes 2024-09-23 21:10:31 -07:00
5 changed files with 490 additions and 161 deletions

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
testdata/
output/
packs/

View file

@ -1,3 +1,10 @@
========== Banned alias positions ==========
* The
* #090
* もしン
* CAR????
* Non
東京 *
========== (R2) AGEN WIDA ==========
Angry Squid
Blonde Fox
@ -658,3 +665,168 @@ Flavorless Boys
Bullet Refutal
Vinyl Countdown
Blue Vocals
========== dimocracy 2024 R1 - Bee, Ex, or Eff ==========
Swear Meteor
Enormous Glue
Extraterrestrial Jalapeño
Very Jazz
The もしン
Moonlight Eggo
Metal Murray
Wrist Swing
Team Brick
Two Island
Metal Banger
Slow Island
Lively Wave
None Emerald
Refridgerating Spoon
Legal Skirt
Leaping Pancake
Jungle Factory
Punishment Days
Disturbed Bag
Pink Committee
Activate Elf
Adventurous Ey
Obtain Thumping
Scream Cat
Last Head
Pink Opinion
Zodiac Literature
Redundant Sun
Spruious Salamander
Spiky Insurance
Boats Song
Makita Hi-Tech
Omega Rainbow
Steep Loss
Bubba Wave
Scatter Sorcerer
This Burst
Body Cucumbers
Teeny-Tiny Side
Expo Calendar
Smite Past
Alice Ten
Heat Oyster
Emotion Lotion
Spruious Hi-Tech
Adventurous Year
Volcano Lagoon
Cuba Shelter
Shiver Wave
Last Brother
Snail Okay
Nice Form
Hope Alternative
Attempt Jellyfish
Magic Breath
Makita Canine
Turnt Swung
Tekken Bubble
Obscene Loss
Extraterrestrial Glue
Geese Curtain
Repeat Balance
========== dimocracy 2024 R3 - Fantastic Artists ==========
Smite Power
Adaptable Graph
Blunt Spoon
Hop Lemons
Lethal Harpy
Collect Hill
Ripe Pants
Peaceful Chilver
Pioneer Volleyball
False Ray
Inconspicuous Fun
Adaptable Example
Pokemon Goggles
Fantastic Bear
Round Rat
Drayze Argument
Ghost Example
Salami Seagull
Fashionable Eleven
Tranquil Power
Troubleshoot Hi-Tech
Feldspar Tooth
Suspend On
Lively Poser
Strip Cannon
Lick Shoes
Fashionable Over
Happy Rest
Volcano Jambalaya
Two Thumping
Uncharacteristically Stream
Penitent Want
Stylish Idea
Sweet Stream
Try #090
Expect Rag
Nice Mania
Pleasant Solid
Adaptable Wolf
Hard-To-Find Sphere
Jellyfish Use
Alien #090
Copy Wave
Chew Haganuma
Purple Hacker
Sweet Music
========== dimocracy 2024 R4 - IdK! International dimocr4cy Karaoke! ==========
A Holds
Milky Wild
Basketball Bunions
Bite Dandy
Grateful Mage
New Impersonate
Down Off
Flam Expert
Devilish Scarecrow
Stocking Outside
Centrifuging Fight
Algebra Smuggler
Blind Fortress
Song Magenta
Temple Swords
Atlanta Hoagie
Seventeen Help
Modular Aftermath
Pixel Fantastics?
Disagreeable Days
Heat Hacker
Akiba CAR????
Marvelous Challenge
Blonde Rear
Courageous Dance
Disturbed Ey
Fretful Lemons
Peaceful Bane
Sixty Power
Scream Offset
Marvelous Gorilla
Construct Weevils
Ride Soup
Chief Time
Steep Bag
Simon #090
Dark Past
Cream Design
Pixel Floor
Palma NB
Modular Statement
Nice Gamer
Soothing Burst
Summer Style
Steep Gadzooks
Extraterrestrial Weevils
More Pickles
Wiggly Silver
Good Pumpkin
Whillikers #090
Construct Patient
Somber Axolotl
Milky Knot

View file

@ -18,13 +18,16 @@ from fs.base import FS
from fs.copy import copy_dir
from fs.tempfs import TempFS
from fs.zipfs import ZipFS
from msdparser import MSDParserError
from pathvalidate import sanitize_filename
import simfile
from simfile.dir import SimfilePack, SimfileDirectory
from simfile.notes import NoteData
from simfile.sm import SMChart, SMSimfile
from simfile.ssc import SSCChart, SSCSimfile
from simfile.timing import BeatValues, BeatValue
from simfile.types import Simfile
from simfile.tidy import tidy, Preset, RemoveComments
from simfile.timing import Beat, BeatValues, BeatValue
from simfile.types import Simfile, Chart, AttachedChart
####################
@ -38,7 +41,7 @@ class AnonymizeEntriesRawArgs:
file_uploads: str | None
deanonymized: bool
dry_run: bool
emails: str
users: str
output: str | None
regenerate: bool
seed: str
@ -112,10 +115,10 @@ def argparser():
help="skip anonymization of files, simply package them as-is",
)
parser.add_argument(
"-e",
"--emails",
"-u",
"--users",
type=str,
help="limit output to files from the specified emails (comma-separated)",
help="limit output to files from the specified users (comma-separated)",
)
parser.add_argument(
"-r",
@ -142,9 +145,11 @@ CsvContents = list[dict[str, str]]
class KnownColumns(enum.StrEnum):
Timestamp = "Timestamp"
EmailAddress = "Email Address"
UserId = "Your gamer tag: (e.g. dimo)"
GeneratedAlias = "Generated Alias"
IgnoreFile = "Ignore File"
SongTitle = "Song Title"
SongArtist = "Song Artist"
# Not persisted:
ExtractedTo = "Extracted To"
@ -158,11 +163,32 @@ ChangedCsvContents = bool
def parse_timestamp(timestamp: str):
return datetime.strptime(timestamp, "%m/%d/%Y %H:%M:%S")
return datetime.strptime(
timestamp,
"%Y/%m/%d %I:%M:%S %p" # it changed for some reason
# "%m/%d/%Y %H:%M:%S"
)
def canonical_simfile_filename(sm: Simfile) -> str:
return sanitize_filename(f"{sm.title} {sm.subtitle or ''}".rstrip())
title_name = f"{sm.titletranslit or sm.title} {sm.subtitletranslit or sm.subtitle or ''}".rstrip()
# HACK: replace this with proper duplicate handling later
title_name = (
title_name.replace("", "red_heart")
.replace("💚", "green_heart")
.replace("💛", "yellow_heart")
.replace("💙", "blue_heart")
.replace("💜", "purple_heart")
.replace("🖤", "black_heart")
.replace("🔶", "orange_diamond")
.replace("🔷", "blue_diamond")
.replace("🔴", "red_circle")
.replace("🔵", "blue_circle")
)
ascii_title_name = "".join([i if ord(i) < 128 else "_" for i in title_name])
if all((not c.isalnum() for c in ascii_title_name)):
return f"Non-ASCII Title {hash(title_name) % (2**16):x}"
return sanitize_filename(ascii_title_name)
################
@ -211,7 +237,7 @@ def assert_valid_file_paths(args: AnonymizeEntriesArgs):
def load_csv_contents(args: AnonymizeEntriesArgs):
with open(args.csv, "r") as csvfile:
with open(args.csv, "r", encoding="utf-8") as csvfile:
return list(csv.DictReader(csvfile))
@ -232,8 +258,8 @@ def assert_known_google_forms_columns_present(csv_contents: CsvContents):
KnownColumns.Timestamp in csv_contents[0]
), f"Provided CSV file does not have a {repr(KnownColumns.Timestamp)} column"
assert (
KnownColumns.EmailAddress in csv_contents[0]
), f"Provided CSV file does not have an {repr(KnownColumns.EmailAddress)} column"
KnownColumns.UserId in csv_contents[0]
), f"Provided CSV file does not have an {repr(KnownColumns.UserId)} column"
def detect_dynamic_columns(csv_contents: CsvContents) -> DynamicColumns:
@ -272,33 +298,40 @@ def maybe_generate_aliases(
with open("aliases/suswords.txt", "r", encoding="utf-8") as suswords_file:
suswords = set(line.rstrip() for line in suswords_file)
alias_to_email_address = {}
alias_to_user_id = {}
seed = args.seed or args.csv
for row in csv_contents:
rnd = Random(",".join([row[KnownColumns.EmailAddress], seed]))
rnd = Random(",".join([row[KnownColumns.UserId], seed]))
while True:
random_alias = f"{rnd.choice(alias_parts[0])} {rnd.choice(alias_parts[1])}"
# ad-hoc code for round 2 (3-way collab):
# either_part = [*alias_parts[0], *alias_parts[1]]
# random_alias = f"{rnd.choice(alias_parts[0])} {rnd.choice(either_part)} {rnd.choice(alias_parts[1])}"
random_alias_left = rnd.choice(alias_parts[0])
random_alias_right = rnd.choice(alias_parts[1])
random_alias = f"{random_alias_left} {random_alias_right}"
if (
random_alias in alias_to_email_address
and alias_to_email_address[random_alias]
!= row[KnownColumns.EmailAddress]
random_alias in alias_to_user_id
and alias_to_user_id[random_alias] != row[KnownColumns.UserId]
):
print(
f"Rerolling alias for {row[KnownColumns.EmailAddress]} because {repr(random_alias)} is already being used by {alias_to_email_address[random_alias]}"
f"Rerolling alias for {row[KnownColumns.UserId]} because {repr(random_alias)} is already being used by {alias_to_user_id[random_alias]}"
)
elif random_alias in usedaliases:
elif (
random_alias.lower() in usedaliases
or f"* {random_alias_right.lower()}" in usedaliases
or f"{random_alias_left.lower()} *" in usedaliases
):
print(
f"Rerolling alias for {row[KnownColumns.EmailAddress]} because {repr(random_alias)} has already been used"
f"Rerolling alias for {row[KnownColumns.UserId]} because {repr(random_alias)} has already been used"
)
elif any(
random_part in suswords for random_part in random_alias.split(" ")
):
print(
f"WARNING: alias for {row[KnownColumns.EmailAddress]} {repr(random_alias)} contains a sus word"
f"WARNING: alias for {row[KnownColumns.UserId]} {repr(random_alias)} contains a sus word"
)
break
else:
break
row[KnownColumns.GeneratedAlias] = random_alias
@ -321,7 +354,7 @@ def maybe_mark_resubmitted_entries(
resubmitted_total = 0
for loop_pass in ("find", "mark"):
for row in csv_contents:
user = row[KnownColumns.EmailAddress]
user = row[KnownColumns.UserId]
timestamp = parse_timestamp(row[KnownColumns.Timestamp])
if loop_pass == "find":
if user in most_recent_entry_per_user:
@ -343,7 +376,7 @@ def maybe_save_generated_columns(args: AnonymizeEntriesArgs, csv_contents: CsvCo
if args.dry_run:
print("Dry run - not writing generated columns back to CSV")
else:
with open(args.csv, "w", newline="") as csvfile:
with open(args.csv, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=csv_contents[0].keys())
writer.writeheader()
for row in csv_contents:
@ -351,29 +384,29 @@ def maybe_save_generated_columns(args: AnonymizeEntriesArgs, csv_contents: CsvCo
print("Wrote generated columns back to CSV")
def maybe_mark_unspecified_emails(
def maybe_mark_unspecified_user_ids(
args: AnonymizeEntriesArgs, csv_contents: CsvContents
):
if not args.emails:
if not args.users:
return
unspecified_total = 0
specified_total = 0
emails = set(args.emails.split(","))
users = set(args.users.split(","))
for row in csv_contents:
if not row[KnownColumns.IgnoreFile]:
if row[KnownColumns.EmailAddress] not in emails:
if row[KnownColumns.UserId] not in users:
row[KnownColumns.IgnoreFile] = "unspecified"
unspecified_total += 1
else:
specified_total += 1
assert specified_total > 0, "No responses were found from the specified emails"
assert specified_total > 0, "No responses were found from the specified users"
s = "s" if specified_total != 1 else ""
print(
f"Processing {specified_total} file{s} for specified emails & ignoring {unspecified_total} others"
f"Processing {specified_total} file{s} for specified users & ignoring {unspecified_total} others"
)
@ -389,7 +422,7 @@ def extract_entries_to_temporary_folder(
# Check all immediate subdirectories, followed by the root itself
root = "/"
contents = zip_fs.listdir(root)
subdirs = [item for item in contents if zip_fs.isdir(item)]
subdirs = [item for item in contents if zip_fs.isdir(item)] + [root]
for subdir in subdirs:
possible_path = fs.path.join(root, subdir)
@ -401,23 +434,28 @@ def extract_entries_to_temporary_folder(
return (possible_path, possible_simfile_dir)
raise RuntimeError(
"Unable to find a suitable simfile directory in the ZIP. "
"Unable to find a suitable simfile directory in ZIP. "
"Make sure the simfile is no more than one directory deep, "
'e.g. contains "Simfile/simfile.ssc".'
)
def extract_simfile_dir(zip_fs: FS, temp_fs: FS) -> str:
zip_path, simfile_dir = find_simfile_dir_zip_path(zip_fs)
try:
canonical_filename = canonical_simfile_filename(simfile_dir.open())
except MSDParserError:
print("Exception encountered while processing", simfile_dir)
raise
assert not temp_fs.exists(
canonical_filename
), "ERROR: trying to extract {canonical_filename} but it's already present in the temp folder"
), f"ERROR: trying to extract {canonical_filename} but it's already present in the temp folder"
copy_dir(zip_fs, zip_path, temp_fs, canonical_filename)
return canonical_filename
temp_fs = TempFS(identifier="dimocracy-voucher")
for row in csv_contents:
try:
if row[KnownColumns.IgnoreFile]:
continue
zip_absolute_path = os.path.join(
@ -429,6 +467,11 @@ def extract_entries_to_temporary_folder(
row[KnownColumns.ExtractedTo] = extract_simfile_dir(zip_fs, temp_fs)
else:
print("WARNING: {zip_absolute_path} not found - skipping")
except:
print(
f"Exception encountered while processing row {row[KnownColumns.UserId]}"
)
raise
print(f"Extracted latest submissions to temporary directory {temp_fs.root_path}")
return temp_fs
@ -450,7 +493,7 @@ def maybe_anonymize_entries(
)
os.rename(absolute_path, absolute_canonical_path)
print(
f"Renamed {os.path.relpath(absolute_path, temp_fs.root_path)} to {os.path.relpath(absolute_canonical_path, temp_fs.root_path)}"
f"Renaming {os.path.relpath(absolute_path, temp_fs.root_path)} to {os.path.relpath(absolute_canonical_path, temp_fs.root_path)}"
)
def maybe_delete_file(absolute_path: str | None):
@ -458,16 +501,92 @@ def maybe_anonymize_entries(
os.remove(absolute_path)
print(f"Deleted {os.path.relpath(absolute_path, temp_fs.root_path)}")
def anonymize_bpms(bpm_str: str | None) -> str:
def anonymize_bpms(obj: Simfile | SSCChart, last_beat: Beat) -> None:
bpm_str = obj.bpms
bpm_values = BeatValues.from_str(bpm_str)
if not obj.displaybpm and len(bpm_values) == 1:
obj.displaybpm = str(int(bpm_values[0].value))
bpm_values.append(
BeatValue(
beat=bpm_values[-1].beat + 10000,
beat=last_beat + 4,
value=bpm_values[-1].value + Decimal("0.001"),
)
)
obj.bpms = str(bpm_values)
print(f"Anonymized BPMs from {repr(bpm_str)} to {repr(str(bpm_values))}")
return str(bpm_values)
def clean_up_difficulties(sf: Simfile) -> Chart:
charts_to_remove: list[AttachedChart] = []
chart_with_notes = None
for _chart in sf.charts:
chart: Chart = _chart # typing workaround
notedata = NoteData(_chart)
if next(iter(notedata), None) is None:
charts_to_remove.append(_chart)
continue
if chart_with_notes is not None:
raise RuntimeError(
f"{canonical_filename} contains multiple charts with notes"
)
chart_with_notes = chart
if chart.difficulty != "Challenge":
print(
f"WARNING: forced difficulty of chart in {canonical_filename} to Challenge"
)
chart.difficulty = "Challenge"
if chart_with_notes is None:
raise RuntimeError(f"{canonical_filename} has no charts with notes")
for chart_to_remove in charts_to_remove:
print(
f"WARNING: removing {chart_to_remove.difficulty} chart with no notes from {canonical_filename}"
)
sm.charts.remove(chart_to_remove) # type: ignore
return chart_with_notes
def anonymize_simfile(sf: Simfile):
if sf.titletranslit == "Chong wo":
print(f"Assigning credit to {row[KnownColumns.GeneratedAlias]}")
sf.credit = row[KnownColumns.GeneratedAlias]
sf.music = f"{canonical_filename}.ogg"
sf.background = ""
sf.banner = ""
sf.cdtitle = ""
sf.genre = ""
sf.music = f"{canonical_filename}.ogg"
if isinstance(sf, SSCSimfile):
sf.jacket = ""
sf.cdimage = ""
sf.discimage = ""
sf.labels = ""
chart = clean_up_difficulties(sf)
last_beat = Beat(0)
for note in NoteData(chart):
last_beat = note.beat
anonymize_bpms(sf, last_beat)
if isinstance(chart, SMChart):
chart.description = row[KnownColumns.GeneratedAlias]
else:
chart.credit = row[KnownColumns.GeneratedAlias]
chart.description = ""
chart.chartname = ""
chart.chartstyle = ""
if chart.bpms:
anonymize_bpms(chart, last_beat)
tidy(sf, Preset.RECOMMENDED, remove_comments=RemoveComments.ALL)
for row in csv_contents:
if row[KnownColumns.IgnoreFile]:
@ -491,16 +610,7 @@ def maybe_anonymize_entries(
if simfile_dir.sm_path:
with simfile.mutate(simfile_dir.sm_path) as sm:
assert isinstance(sm, SMSimfile)
sm.credit = row[KnownColumns.GeneratedAlias]
sm.background = ""
sm.banner = ""
sm.cdtitle = ""
sm.genre = ""
sm.music = f"{canonical_filename}.ogg"
sm.bpms = anonymize_bpms(sm.bpms)
for _chart in sm.charts:
sm_chart: SMChart = _chart # typing workaround
sm_chart.description = row[KnownColumns.GeneratedAlias]
anonymize_simfile(sm)
maybe_rename_file(simfile_dir.sm_path, f"{canonical_filename}.sm")
print(
f"Scrubbed {os.path.relpath(absolute_simfile_dir_path, temp_fs.root_path)}/{canonical_filename}.sm"
@ -509,25 +619,7 @@ def maybe_anonymize_entries(
if simfile_dir.ssc_path:
with simfile.mutate(simfile_dir.ssc_path) as ssc:
assert isinstance(ssc, SSCSimfile)
ssc.credit = row[KnownColumns.GeneratedAlias]
ssc.music = f"{canonical_filename}.ogg"
ssc.background = ""
ssc.banner = ""
ssc.cdtitle = ""
ssc.genre = ""
ssc.jacket = ""
ssc.cdimage = ""
ssc.discimage = ""
ssc.labels = ""
ssc.bpms = anonymize_bpms(ssc.bpms)
for _chart in ssc.charts:
ssc_chart: SSCChart = _chart # typing workaround
ssc_chart.description = ""
ssc_chart.chartname = ""
ssc_chart.chartstyle = ""
ssc_chart.credit = row[KnownColumns.GeneratedAlias]
if ssc_chart.bpms:
ssc_chart.bpms = anonymize_bpms(ssc_chart.bpms)
anonymize_simfile(ssc)
maybe_rename_file(simfile_dir.ssc_path, f"{canonical_filename}.ssc")
print(
f"Scrubbed {os.path.relpath(absolute_simfile_dir_path, temp_fs.root_path)}/{canonical_filename}.ssc"
@ -537,8 +629,12 @@ def maybe_anonymize_entries(
if dir_entry.is_file():
if (
dir_entry.name.endswith(".old")
or dir_entry.name.endswith(".sm~")
or dir_entry.name.endswith(".ssc~")
or dir_entry.name.endswith(".txt")
or dir_entry.name.endswith(".zip")
or dir_entry.name == ".DS_Store"
or dir_entry.name == "desktop.ini"
):
# These are definitely safe to delete for distribution
os.remove(dir_entry.path)
@ -572,7 +668,7 @@ def maybe_save_anonymized_files(
args: AnonymizeEntriesArgs,
csv_contents: CsvContents,
temp_fs: TempFS,
):
) -> str | None:
if args.dry_run:
print("Dry run - not saving files")
return
@ -581,6 +677,39 @@ def maybe_save_anonymized_files(
output_path = f"{args.output}/{de}anonymized-{timestamp}"
shutil.copytree(temp_fs.root_path, output_path)
print(f"Saved to {os.path.abspath(output_path)}")
return output_path
def maybe_generate_song_metadata(
args: AnonymizeEntriesArgs,
csv_contents: CsvContents,
output_path: str,
):
reuse_song_metadata = (
not args.regenerate
and KnownColumns.SongTitle in csv_contents[0]
and KnownColumns.SongArtist in csv_contents[0]
)
if reuse_song_metadata:
print("Reusing generated song metadata")
return False
alias_to_simfile: dict[str, Simfile] = {}
for sf, sf_path in simfile.openpack(output_path):
assert sf.credit, f"{sf_path} was generated without a credit?!"
alias_to_simfile[sf.credit] = sf
for row in csv_contents:
alias = row[KnownColumns.GeneratedAlias]
sf = alias_to_simfile[alias]
row[KnownColumns.SongTitle] = f"{sf.title} {sf.subtitle or ''}".rstrip()
row[KnownColumns.SongArtist] = sf.artist or ""
print(
f"Mapped {alias} to {row[KnownColumns.SongTitle]} by {row[KnownColumns.SongArtist]}"
)
return True
###############
@ -604,11 +733,17 @@ def main(argv: list[str]):
maybe_save_generated_columns(args, csv_contents)
# Generate temporary CSV columns
maybe_mark_unspecified_emails(args, csv_contents)
maybe_mark_unspecified_user_ids(args, csv_contents)
temp_fs = extract_entries_to_temporary_folder(args, csv_contents, dynamic_columns)
maybe_anonymize_entries(args, csv_contents, temp_fs)
maybe_save_anonymized_files(args, csv_contents, temp_fs)
output_path = maybe_save_anonymized_files(args, csv_contents, temp_fs)
if output_path:
csv_contents_changed = maybe_generate_song_metadata(
args, csv_contents, output_path
)
if csv_contents_changed:
maybe_save_generated_columns(args, csv_contents)
if __name__ == "__main__":

174
poetry.lock generated
View file

@ -13,33 +13,33 @@ files = [
[[package]]
name = "black"
version = "24.8.0"
version = "24.10.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
{file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"},
{file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
{file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
{file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
{file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"},
{file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
{file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
{file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
{file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"},
{file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
{file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
{file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
{file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"},
{file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
{file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
{file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
{file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"},
{file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
{file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
{file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
{file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
{file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
{file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
{file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
{file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
{file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
{file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
{file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
]
[package.dependencies]
@ -51,19 +51,19 @@ platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.7"
version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
]
[package.dependencies]
@ -101,13 +101,13 @@ scandir = ["scandir (>=1.5,<2.0)"]
[[package]]
name = "msdparser"
version = "2.0.0"
description = "Robust & lightning fast MSD parser (StepMania file format)"
version = "3.0.0a7"
description = "Low-level parser for StepMania MSD files (SM, SSC, DWI...)"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.10"
files = [
{file = "msdparser-2.0.0-py3-none-any.whl", hash = "sha256:bbef8c89b2dc16183ba714354b093fff11a43469971c42972284c5046826002d"},
{file = "msdparser-2.0.0.tar.gz", hash = "sha256:9bb6cf4b705c76850b1ce22823c768b62ba2bb63d1c2407bbe3e2c23e38be8e3"},
{file = "msdparser-3.0.0a7-py3-none-any.whl", hash = "sha256:ef324aa943e325bc1c1ef6b363bf55433def3deb8a9f2fa608b14eff828ccf64"},
{file = "msdparser-3.0.0a7.tar.gz", hash = "sha256:797bc6a99ae491cf2712483d2e0fb086ac4e047354cc8f8e4712bce95833c6c6"},
]
[[package]]
@ -123,13 +123,13 @@ files = [
[[package]]
name = "packaging"
version = "24.1"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
@ -145,78 +145,100 @@ files = [
[[package]]
name = "pathvalidate"
version = "3.2.0"
version = "3.2.3"
description = "pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
files = [
{file = "pathvalidate-3.2.0-py3-none-any.whl", hash = "sha256:cc593caa6299b22b37f228148257997e2fa850eea2daf7e4cc9205cef6908dee"},
{file = "pathvalidate-3.2.0.tar.gz", hash = "sha256:5e8378cf6712bff67fbe7a8307d99fa8c1a0cb28aa477056f8fc374f0dff24ad"},
{file = "pathvalidate-3.2.3-py3-none-any.whl", hash = "sha256:5eaf0562e345d4b6d0c0239d0f690c3bd84d2a9a3c4c73b99ea667401b27bee1"},
{file = "pathvalidate-3.2.3.tar.gz", hash = "sha256:59b5b9278e30382d6d213497623043ebe63f10e29055be4419a9c04c721739cb"},
]
[package.extras]
docs = ["Sphinx (>=2.4)", "sphinx-rtd-theme (>=1.2.2)", "urllib3 (<2)"]
test = ["Faker (>=1.0.8)", "allpairspy (>=2)", "click (>=6.2)", "pytest (>=6.0.1)", "pytest-discord (>=0.1.4)", "pytest-md-report (>=0.4.1)"]
docs = ["Sphinx (>=2.4)", "sphinx_rtd_theme (>=1.2.2)", "urllib3 (<2)"]
readme = ["path (>=13,<18)", "readmemaker (>=1.2.0)"]
test = ["Faker (>=1.0.8)", "allpairspy (>=2)", "click (>=6.2)", "pytest (>=6.0.1)", "pytest-md-report (>=0.6.2)"]
[[package]]
name = "platformdirs"
version = "4.2.2"
version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.11.2)"]
[[package]]
name = "setuptools"
version = "72.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"},
{file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"},
]
[package.extras]
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "simfile"
version = "2.1.1"
description = "Modern simfile library for Python"
name = "pyfakefs"
version = "4.5.6"
description = "pyfakefs implements a fake file system that mocks the Python file system modules."
optional = false
python-versions = ">=3.6"
files = [
{file = "simfile-2.1.1-py3-none-any.whl", hash = "sha256:133094a33cf6b841ca7e620880ef8354c56dd4aef6b2c77582de7f120f69c620"},
{file = "simfile-2.1.1.tar.gz", hash = "sha256:a7885f8395cdd0f19cd79da34e5675da778aa0e8dda76543cf075e21b862c4b7"},
{file = "pyfakefs-4.5.6-py3-none-any.whl", hash = "sha256:6bb4e27457b0bc90e3ebfe5aed4f1b8c32a18713ba44e925f304bb9b9816a03c"},
{file = "pyfakefs-4.5.6.tar.gz", hash = "sha256:914d7bf994406cfbefee0b4d45918f60c15b406afe93f8194a804da5a450a822"},
]
[[package]]
name = "setuptools"
version = "75.8.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
files = [
{file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"},
{file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "simfile"
version = "3.0.0a4"
description = "Simfile parsing & editing library"
optional = false
python-versions = ">=3.10"
files = [
{file = "simfile-3.0.0a4-py3-none-any.whl", hash = "sha256:dc86236debdecf257065c5614397655503d78e8679c801215b3b3a93bdb2ef9c"},
{file = "simfile-3.0.0a4.tar.gz", hash = "sha256:c20e5c8aa7833ce48d54e2f42ad068466271209861630f4d2e17a9cc6f0b3460"},
]
[package.dependencies]
fs = ">=2.4.15,<2.5.0"
msdparser = ">=2.0.0,<2.1.0"
fs = {version = "*", optional = true, markers = "extra == \"fs\""}
msdparser = ">=3.0.0a7,<3.1.0"
pyfakefs = "4.5.6"
[package.extras]
all = ["fs", "pillow"]
assets = ["pillow"]
fs = ["fs"]
[[package]]
name = "six"
version = "1.16.0"
version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "cb0ec3bd6d2ec3fb7296e6dd4801035c044c88cc2e7da894954d5805603b9fee"
content-hash = "e855e50e2b9a7cbc4e17dbabdd57f17341c367540360e9ae990752eb0b481afa"

View file

@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
simfile = "^2.1.1"
simfile = {version = "^3.0.0a4", extras = ["fs"]}
pathvalidate = "^3.2.0"
[tool.poetry.group.dev.dependencies]