Compare commits
2 commits
275f27155d
...
535f53c55a
Author | SHA1 | Date | |
---|---|---|---|
|
535f53c55a | ||
|
38a486131d |
5 changed files with 490 additions and 161 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
testdata/
|
testdata/
|
||||||
output/
|
packs/
|
|
@ -1,3 +1,10 @@
|
||||||
|
========== Banned alias positions ==========
|
||||||
|
* The
|
||||||
|
* #090
|
||||||
|
* もしン
|
||||||
|
* CAR????
|
||||||
|
* Non
|
||||||
|
東京 *
|
||||||
========== (R2) AGEN WIDA ==========
|
========== (R2) AGEN WIDA ==========
|
||||||
Angry Squid
|
Angry Squid
|
||||||
Blonde Fox
|
Blonde Fox
|
||||||
|
@ -657,4 +664,169 @@ StepMania Swearing
|
||||||
Flavorless Boys
|
Flavorless Boys
|
||||||
Bullet Refutal
|
Bullet Refutal
|
||||||
Vinyl Countdown
|
Vinyl Countdown
|
||||||
Blue Vocals
|
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
|
|
@ -18,13 +18,16 @@ from fs.base import FS
|
||||||
from fs.copy import copy_dir
|
from fs.copy import copy_dir
|
||||||
from fs.tempfs import TempFS
|
from fs.tempfs import TempFS
|
||||||
from fs.zipfs import ZipFS
|
from fs.zipfs import ZipFS
|
||||||
|
from msdparser import MSDParserError
|
||||||
from pathvalidate import sanitize_filename
|
from pathvalidate import sanitize_filename
|
||||||
import simfile
|
import simfile
|
||||||
from simfile.dir import SimfilePack, SimfileDirectory
|
from simfile.dir import SimfilePack, SimfileDirectory
|
||||||
|
from simfile.notes import NoteData
|
||||||
from simfile.sm import SMChart, SMSimfile
|
from simfile.sm import SMChart, SMSimfile
|
||||||
from simfile.ssc import SSCChart, SSCSimfile
|
from simfile.ssc import SSCChart, SSCSimfile
|
||||||
from simfile.timing import BeatValues, BeatValue
|
from simfile.tidy import tidy, Preset, RemoveComments
|
||||||
from simfile.types import Simfile
|
from simfile.timing import Beat, BeatValues, BeatValue
|
||||||
|
from simfile.types import Simfile, Chart, AttachedChart
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -38,7 +41,7 @@ class AnonymizeEntriesRawArgs:
|
||||||
file_uploads: str | None
|
file_uploads: str | None
|
||||||
deanonymized: bool
|
deanonymized: bool
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
emails: str
|
users: str
|
||||||
output: str | None
|
output: str | None
|
||||||
regenerate: bool
|
regenerate: bool
|
||||||
seed: str
|
seed: str
|
||||||
|
@ -112,10 +115,10 @@ def argparser():
|
||||||
help="skip anonymization of files, simply package them as-is",
|
help="skip anonymization of files, simply package them as-is",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-e",
|
"-u",
|
||||||
"--emails",
|
"--users",
|
||||||
type=str,
|
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(
|
parser.add_argument(
|
||||||
"-r",
|
"-r",
|
||||||
|
@ -142,9 +145,11 @@ CsvContents = list[dict[str, str]]
|
||||||
|
|
||||||
class KnownColumns(enum.StrEnum):
|
class KnownColumns(enum.StrEnum):
|
||||||
Timestamp = "Timestamp"
|
Timestamp = "Timestamp"
|
||||||
EmailAddress = "Email Address"
|
UserId = "Your gamer tag: (e.g. dimo)"
|
||||||
GeneratedAlias = "Generated Alias"
|
GeneratedAlias = "Generated Alias"
|
||||||
IgnoreFile = "Ignore File"
|
IgnoreFile = "Ignore File"
|
||||||
|
SongTitle = "Song Title"
|
||||||
|
SongArtist = "Song Artist"
|
||||||
# Not persisted:
|
# Not persisted:
|
||||||
ExtractedTo = "Extracted To"
|
ExtractedTo = "Extracted To"
|
||||||
|
|
||||||
|
@ -158,11 +163,32 @@ ChangedCsvContents = bool
|
||||||
|
|
||||||
|
|
||||||
def parse_timestamp(timestamp: str):
|
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:
|
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):
|
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))
|
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]
|
KnownColumns.Timestamp in csv_contents[0]
|
||||||
), f"Provided CSV file does not have a {repr(KnownColumns.Timestamp)} column"
|
), f"Provided CSV file does not have a {repr(KnownColumns.Timestamp)} column"
|
||||||
assert (
|
assert (
|
||||||
KnownColumns.EmailAddress in csv_contents[0]
|
KnownColumns.UserId in csv_contents[0]
|
||||||
), f"Provided CSV file does not have an {repr(KnownColumns.EmailAddress)} column"
|
), f"Provided CSV file does not have an {repr(KnownColumns.UserId)} column"
|
||||||
|
|
||||||
|
|
||||||
def detect_dynamic_columns(csv_contents: CsvContents) -> DynamicColumns:
|
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:
|
with open("aliases/suswords.txt", "r", encoding="utf-8") as suswords_file:
|
||||||
suswords = set(line.rstrip() for line in 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
|
seed = args.seed or args.csv
|
||||||
|
|
||||||
for row in csv_contents:
|
for row in csv_contents:
|
||||||
rnd = Random(",".join([row[KnownColumns.EmailAddress], seed]))
|
rnd = Random(",".join([row[KnownColumns.UserId], seed]))
|
||||||
while True:
|
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 (
|
if (
|
||||||
random_alias in alias_to_email_address
|
random_alias in alias_to_user_id
|
||||||
and alias_to_email_address[random_alias]
|
and alias_to_user_id[random_alias] != row[KnownColumns.UserId]
|
||||||
!= row[KnownColumns.EmailAddress]
|
|
||||||
):
|
):
|
||||||
print(
|
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(
|
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(
|
elif any(
|
||||||
random_part in suswords for random_part in random_alias.split(" ")
|
random_part in suswords for random_part in random_alias.split(" ")
|
||||||
):
|
):
|
||||||
print(
|
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:
|
else:
|
||||||
break
|
break
|
||||||
row[KnownColumns.GeneratedAlias] = random_alias
|
row[KnownColumns.GeneratedAlias] = random_alias
|
||||||
|
@ -321,7 +354,7 @@ def maybe_mark_resubmitted_entries(
|
||||||
resubmitted_total = 0
|
resubmitted_total = 0
|
||||||
for loop_pass in ("find", "mark"):
|
for loop_pass in ("find", "mark"):
|
||||||
for row in csv_contents:
|
for row in csv_contents:
|
||||||
user = row[KnownColumns.EmailAddress]
|
user = row[KnownColumns.UserId]
|
||||||
timestamp = parse_timestamp(row[KnownColumns.Timestamp])
|
timestamp = parse_timestamp(row[KnownColumns.Timestamp])
|
||||||
if loop_pass == "find":
|
if loop_pass == "find":
|
||||||
if user in most_recent_entry_per_user:
|
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:
|
if args.dry_run:
|
||||||
print("Dry run - not writing generated columns back to CSV")
|
print("Dry run - not writing generated columns back to CSV")
|
||||||
else:
|
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 = csv.DictWriter(csvfile, fieldnames=csv_contents[0].keys())
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for row in csv_contents:
|
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")
|
print("Wrote generated columns back to CSV")
|
||||||
|
|
||||||
|
|
||||||
def maybe_mark_unspecified_emails(
|
def maybe_mark_unspecified_user_ids(
|
||||||
args: AnonymizeEntriesArgs, csv_contents: CsvContents
|
args: AnonymizeEntriesArgs, csv_contents: CsvContents
|
||||||
):
|
):
|
||||||
if not args.emails:
|
if not args.users:
|
||||||
return
|
return
|
||||||
|
|
||||||
unspecified_total = 0
|
unspecified_total = 0
|
||||||
specified_total = 0
|
specified_total = 0
|
||||||
emails = set(args.emails.split(","))
|
users = set(args.users.split(","))
|
||||||
|
|
||||||
for row in csv_contents:
|
for row in csv_contents:
|
||||||
if not row[KnownColumns.IgnoreFile]:
|
if not row[KnownColumns.IgnoreFile]:
|
||||||
if row[KnownColumns.EmailAddress] not in emails:
|
if row[KnownColumns.UserId] not in users:
|
||||||
row[KnownColumns.IgnoreFile] = "unspecified"
|
row[KnownColumns.IgnoreFile] = "unspecified"
|
||||||
unspecified_total += 1
|
unspecified_total += 1
|
||||||
else:
|
else:
|
||||||
specified_total += 1
|
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 ""
|
s = "s" if specified_total != 1 else ""
|
||||||
print(
|
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
|
# Check all immediate subdirectories, followed by the root itself
|
||||||
root = "/"
|
root = "/"
|
||||||
contents = zip_fs.listdir(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:
|
for subdir in subdirs:
|
||||||
possible_path = fs.path.join(root, subdir)
|
possible_path = fs.path.join(root, subdir)
|
||||||
|
@ -401,34 +434,44 @@ def extract_entries_to_temporary_folder(
|
||||||
return (possible_path, possible_simfile_dir)
|
return (possible_path, possible_simfile_dir)
|
||||||
|
|
||||||
raise RuntimeError(
|
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, "
|
"Make sure the simfile is no more than one directory deep, "
|
||||||
'e.g. contains "Simfile/simfile.ssc".'
|
'e.g. contains "Simfile/simfile.ssc".'
|
||||||
)
|
)
|
||||||
|
|
||||||
def extract_simfile_dir(zip_fs: FS, temp_fs: FS) -> str:
|
def extract_simfile_dir(zip_fs: FS, temp_fs: FS) -> str:
|
||||||
zip_path, simfile_dir = find_simfile_dir_zip_path(zip_fs)
|
zip_path, simfile_dir = find_simfile_dir_zip_path(zip_fs)
|
||||||
canonical_filename = canonical_simfile_filename(simfile_dir.open())
|
try:
|
||||||
|
canonical_filename = canonical_simfile_filename(simfile_dir.open())
|
||||||
|
except MSDParserError:
|
||||||
|
print("Exception encountered while processing", simfile_dir)
|
||||||
|
raise
|
||||||
assert not temp_fs.exists(
|
assert not temp_fs.exists(
|
||||||
canonical_filename
|
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)
|
copy_dir(zip_fs, zip_path, temp_fs, canonical_filename)
|
||||||
return canonical_filename
|
return canonical_filename
|
||||||
|
|
||||||
temp_fs = TempFS(identifier="dimocracy-voucher")
|
temp_fs = TempFS(identifier="dimocracy-voucher")
|
||||||
|
|
||||||
for row in csv_contents:
|
for row in csv_contents:
|
||||||
if row[KnownColumns.IgnoreFile]:
|
try:
|
||||||
continue
|
if row[KnownColumns.IgnoreFile]:
|
||||||
zip_absolute_path = os.path.join(
|
continue
|
||||||
args.file_uploads, row[dynamic_columns.filename]
|
zip_absolute_path = os.path.join(
|
||||||
)
|
args.file_uploads, row[dynamic_columns.filename]
|
||||||
if os.path.isfile(zip_absolute_path):
|
)
|
||||||
with open(zip_absolute_path, "rb") as zip_file:
|
if os.path.isfile(zip_absolute_path):
|
||||||
zip_fs = ZipFS(zip_file)
|
with open(zip_absolute_path, "rb") as zip_file:
|
||||||
row[KnownColumns.ExtractedTo] = extract_simfile_dir(zip_fs, temp_fs)
|
zip_fs = ZipFS(zip_file)
|
||||||
else:
|
row[KnownColumns.ExtractedTo] = extract_simfile_dir(zip_fs, temp_fs)
|
||||||
print("WARNING: {zip_absolute_path} not found - skipping")
|
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}")
|
print(f"Extracted latest submissions to temporary directory {temp_fs.root_path}")
|
||||||
return temp_fs
|
return temp_fs
|
||||||
|
@ -450,7 +493,7 @@ def maybe_anonymize_entries(
|
||||||
)
|
)
|
||||||
os.rename(absolute_path, absolute_canonical_path)
|
os.rename(absolute_path, absolute_canonical_path)
|
||||||
print(
|
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):
|
def maybe_delete_file(absolute_path: str | None):
|
||||||
|
@ -458,16 +501,92 @@ def maybe_anonymize_entries(
|
||||||
os.remove(absolute_path)
|
os.remove(absolute_path)
|
||||||
print(f"Deleted {os.path.relpath(absolute_path, temp_fs.root_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)
|
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(
|
bpm_values.append(
|
||||||
BeatValue(
|
BeatValue(
|
||||||
beat=bpm_values[-1].beat + 10000,
|
beat=last_beat + 4,
|
||||||
value=bpm_values[-1].value + Decimal("0.001"),
|
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))}")
|
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:
|
for row in csv_contents:
|
||||||
if row[KnownColumns.IgnoreFile]:
|
if row[KnownColumns.IgnoreFile]:
|
||||||
|
@ -491,16 +610,7 @@ def maybe_anonymize_entries(
|
||||||
if simfile_dir.sm_path:
|
if simfile_dir.sm_path:
|
||||||
with simfile.mutate(simfile_dir.sm_path) as sm:
|
with simfile.mutate(simfile_dir.sm_path) as sm:
|
||||||
assert isinstance(sm, SMSimfile)
|
assert isinstance(sm, SMSimfile)
|
||||||
sm.credit = row[KnownColumns.GeneratedAlias]
|
anonymize_simfile(sm)
|
||||||
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]
|
|
||||||
maybe_rename_file(simfile_dir.sm_path, f"{canonical_filename}.sm")
|
maybe_rename_file(simfile_dir.sm_path, f"{canonical_filename}.sm")
|
||||||
print(
|
print(
|
||||||
f"Scrubbed {os.path.relpath(absolute_simfile_dir_path, temp_fs.root_path)}/{canonical_filename}.sm"
|
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:
|
if simfile_dir.ssc_path:
|
||||||
with simfile.mutate(simfile_dir.ssc_path) as ssc:
|
with simfile.mutate(simfile_dir.ssc_path) as ssc:
|
||||||
assert isinstance(ssc, SSCSimfile)
|
assert isinstance(ssc, SSCSimfile)
|
||||||
ssc.credit = row[KnownColumns.GeneratedAlias]
|
anonymize_simfile(ssc)
|
||||||
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)
|
|
||||||
maybe_rename_file(simfile_dir.ssc_path, f"{canonical_filename}.ssc")
|
maybe_rename_file(simfile_dir.ssc_path, f"{canonical_filename}.ssc")
|
||||||
print(
|
print(
|
||||||
f"Scrubbed {os.path.relpath(absolute_simfile_dir_path, temp_fs.root_path)}/{canonical_filename}.ssc"
|
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.is_file():
|
||||||
if (
|
if (
|
||||||
dir_entry.name.endswith(".old")
|
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(".txt")
|
||||||
or dir_entry.name.endswith(".zip")
|
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
|
# These are definitely safe to delete for distribution
|
||||||
os.remove(dir_entry.path)
|
os.remove(dir_entry.path)
|
||||||
|
@ -572,7 +668,7 @@ def maybe_save_anonymized_files(
|
||||||
args: AnonymizeEntriesArgs,
|
args: AnonymizeEntriesArgs,
|
||||||
csv_contents: CsvContents,
|
csv_contents: CsvContents,
|
||||||
temp_fs: TempFS,
|
temp_fs: TempFS,
|
||||||
):
|
) -> str | None:
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("Dry run - not saving files")
|
print("Dry run - not saving files")
|
||||||
return
|
return
|
||||||
|
@ -581,6 +677,39 @@ def maybe_save_anonymized_files(
|
||||||
output_path = f"{args.output}/{de}anonymized-{timestamp}"
|
output_path = f"{args.output}/{de}anonymized-{timestamp}"
|
||||||
shutil.copytree(temp_fs.root_path, output_path)
|
shutil.copytree(temp_fs.root_path, output_path)
|
||||||
print(f"Saved to {os.path.abspath(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)
|
maybe_save_generated_columns(args, csv_contents)
|
||||||
|
|
||||||
# Generate temporary CSV columns
|
# 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)
|
temp_fs = extract_entries_to_temporary_folder(args, csv_contents, dynamic_columns)
|
||||||
maybe_anonymize_entries(args, csv_contents, temp_fs)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
174
poetry.lock
generated
174
poetry.lock
generated
|
@ -13,33 +13,33 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "24.8.0"
|
version = "24.10.0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
|
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
|
||||||
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
|
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
|
||||||
{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.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
|
||||||
{file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
|
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
|
||||||
{file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
|
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
|
||||||
{file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
|
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
|
||||||
{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.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
|
||||||
{file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
|
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
|
||||||
{file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
|
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
|
||||||
{file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
|
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
|
||||||
{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.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
|
||||||
{file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
|
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
|
||||||
{file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
|
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
|
||||||
{file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
|
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
|
||||||
{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.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
|
||||||
{file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
|
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
|
||||||
{file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
|
{file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
|
||||||
{file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
|
{file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
|
||||||
{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.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
|
||||||
{file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
|
{file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
|
||||||
{file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
|
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
|
||||||
{file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
|
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -51,19 +51,19 @@ platformdirs = ">=2"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
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)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.8"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
|
||||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -101,13 +101,13 @@ scandir = ["scandir (>=1.5,<2.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "msdparser"
|
name = "msdparser"
|
||||||
version = "2.0.0"
|
version = "3.0.0a7"
|
||||||
description = "Robust & lightning fast MSD parser (StepMania file format)"
|
description = "Low-level parser for StepMania MSD files (SM, SSC, DWI...)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.10"
|
||||||
files = [
|
files = [
|
||||||
{file = "msdparser-2.0.0-py3-none-any.whl", hash = "sha256:bbef8c89b2dc16183ba714354b093fff11a43469971c42972284c5046826002d"},
|
{file = "msdparser-3.0.0a7-py3-none-any.whl", hash = "sha256:ef324aa943e325bc1c1ef6b363bf55433def3deb8a9f2fa608b14eff828ccf64"},
|
||||||
{file = "msdparser-2.0.0.tar.gz", hash = "sha256:9bb6cf4b705c76850b1ce22823c768b62ba2bb63d1c2407bbe3e2c23e38be8e3"},
|
{file = "msdparser-3.0.0a7.tar.gz", hash = "sha256:797bc6a99ae491cf2712483d2e0fb086ac4e047354cc8f8e4712bce95833c6c6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -123,13 +123,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "24.1"
|
version = "24.2"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -145,78 +145,100 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathvalidate"
|
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."
|
description = "pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "pathvalidate-3.2.0-py3-none-any.whl", hash = "sha256:cc593caa6299b22b37f228148257997e2fa850eea2daf7e4cc9205cef6908dee"},
|
{file = "pathvalidate-3.2.3-py3-none-any.whl", hash = "sha256:5eaf0562e345d4b6d0c0239d0f690c3bd84d2a9a3c4c73b99ea667401b27bee1"},
|
||||||
{file = "pathvalidate-3.2.0.tar.gz", hash = "sha256:5e8378cf6712bff67fbe7a8307d99fa8c1a0cb28aa477056f8fc374f0dff24ad"},
|
{file = "pathvalidate-3.2.3.tar.gz", hash = "sha256:59b5b9278e30382d6d213497623043ebe63f10e29055be4419a9c04c721739cb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["Sphinx (>=2.4)", "sphinx-rtd-theme (>=1.2.2)", "urllib3 (<2)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
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`."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
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 (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
|
||||||
type = ["mypy (>=1.8)"]
|
type = ["mypy (>=1.11.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "pyfakefs"
|
||||||
version = "72.1.0"
|
version = "4.5.6"
|
||||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
description = "pyfakefs implements a fake file system that mocks the Python file system modules."
|
||||||
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"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "simfile-2.1.1-py3-none-any.whl", hash = "sha256:133094a33cf6b841ca7e620880ef8354c56dd4aef6b2c77582de7f120f69c620"},
|
{file = "pyfakefs-4.5.6-py3-none-any.whl", hash = "sha256:6bb4e27457b0bc90e3ebfe5aed4f1b8c32a18713ba44e925f304bb9b9816a03c"},
|
||||||
{file = "simfile-2.1.1.tar.gz", hash = "sha256:a7885f8395cdd0f19cd79da34e5675da778aa0e8dda76543cf075e21b862c4b7"},
|
{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]
|
[package.dependencies]
|
||||||
fs = ">=2.4.15,<2.5.0"
|
fs = {version = "*", optional = true, markers = "extra == \"fs\""}
|
||||||
msdparser = ">=2.0.0,<2.1.0"
|
msdparser = ">=3.0.0a7,<3.1.0"
|
||||||
|
pyfakefs = "4.5.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
all = ["fs", "pillow"]
|
||||||
|
assets = ["pillow"]
|
||||||
|
fs = ["fs"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "cb0ec3bd6d2ec3fb7296e6dd4801035c044c88cc2e7da894954d5805603b9fee"
|
content-hash = "e855e50e2b9a7cbc4e17dbabdd57f17341c367540360e9ae990752eb0b481afa"
|
||||||
|
|
|
@ -7,7 +7,7 @@ readme = "README.md"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
simfile = "^2.1.1"
|
simfile = {version = "^3.0.0a4", extras = ["fs"]}
|
||||||
pathvalidate = "^3.2.0"
|
pathvalidate = "^3.2.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
Loading…
Reference in a new issue