1
0
mirror of synced 2024-12-03 18:57:16 +01:00

Add support for memon v1.0.0

This commit is contained in:
Stepland 2021-12-27 02:05:27 +01:00
parent 1d9704e214
commit 503ba1d528
40 changed files with 1526 additions and 809 deletions

View File

@ -32,6 +32,4 @@ per-file-ignores =
example*.py: F405, F403
# Silence weird false positive on inline comments ...
jubeatools/formats/jubeat_analyser/symbols.py: E262
# there's a field named "l" in a marshmallow schema and I don't want te rename it
jubeatools/formats/memon/v0.py: E741
max-line-length = 120

View File

@ -1,3 +1,11 @@
# v1.4.0
## Added
- Jubeatools can now handle HAKUs, in the following formats :
- [memon:v1.0.0]
- [memon] 🎉 inital support for v1.0.0 !
## Changed
- Improved the merging procedure for song objects
# v1.3.0
## Added
- [memon] 🎉 v0.3.0 support

View File

@ -17,4 +17,6 @@ def test_that_ommiting_beat_snap_works() -> None:
result = runner.invoke(
convert, [str(p.resolve(strict=True)), "out.txt", "-f", "memo2"]
)
if result.exception:
raise result.exception
assert result.exit_code == 0

View File

@ -2,4 +2,4 @@
Module containing all the load/dump code for all file formats
"""
from .enum import Format
from .formats import DUMPERS, LOADERS
from .loaders_and_dumpers import DUMPERS, LOADERS

View File

@ -9,6 +9,7 @@ class Format(str, Enum):
MEMON_0_1_0 = "memon:v0.1.0"
MEMON_0_2_0 = "memon:v0.2.0"
MEMON_0_3_0 = "memon:v0.3.0"
MEMON_1_0_0 = "memon:v1.0.0"
MONO_COLUMN = "mono-column"
MEMO = "memo"
MEMO_1 = "memo1"

View File

@ -0,0 +1,16 @@
from enum import Enum
class Format(str, Enum):
EVE = "eve"
JBSQ = "jbsq"
MALODY = "malody"
MEMON_LEGACY = "memon:legacy"
MEMON_0_1_0 = "memon:v0.1.0"
MEMON_0_2_0 = "memon:v0.2.0"
MEMON_0_3_0 = "memon:v0.3.0"
MEMON_1_0_0 = "memon:v1.0.0"
MONO_COLUMN = "mono-column"
MEMO = "memo"
MEMO_1 = "memo1"
MEMO_2 = "memo2"

View File

@ -35,7 +35,7 @@ def recognize_json_formats(path: Path) -> Format:
if not isinstance(obj, dict):
raise ValueError("Top level value is not an object")
if obj.keys() >= {"metadata", "data"}:
if obj.keys() & {"metadata", "data", "version"}:
return recognize_memon_version(obj)
elif obj.keys() >= {"meta", "time", "note"}:
return Format.MALODY
@ -55,6 +55,8 @@ def recognize_memon_version(obj: dict) -> Format:
return Format.MEMON_0_2_0
elif version == "0.3.0":
return Format.MEMON_0_3_0
elif version == "1.0.0":
return Format.MEMON_1_0_0
else:
raise ValueError(f"Unsupported memon version : {version}")

View File

@ -275,7 +275,7 @@ def make_full_dumper_from_jubeat_analyser_chart_dumper(
song: Song, *, circle_free: bool = False, **kwargs: Any
) -> List[ChartFile]:
files: List[ChartFile] = []
for difficulty, chart, timing in song.iter_charts_with_timing():
for difficulty, chart, timing in song.iter_charts_with_applicable_timing():
chart_file = chart_dumper(
difficulty,
chart,

View File

@ -2,7 +2,6 @@ from collections import ChainMap
from copy import deepcopy
from dataclasses import astuple, dataclass
from decimal import Decimal
from functools import reduce
from itertools import product
from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Set, Tuple, Union
@ -350,4 +349,4 @@ def _load_memo_file(lines: List[str]) -> Song:
def load_memo(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_memo_file(lines) for _, lines in files.items()]
return reduce(Song.merge, charts)
return Song.from_monochart_instances(charts)

View File

@ -1,7 +1,6 @@
from copy import deepcopy
from dataclasses import astuple, dataclass
from decimal import Decimal
from functools import reduce
from itertools import product
from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Set, Tuple, Union
@ -341,4 +340,4 @@ def _load_memo1_file(lines: List[str]) -> Song:
def load_memo1(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_memo1_file(lines) for _, lines in files.items()]
return reduce(Song.merge, charts)
return Song.from_monochart_instances(charts)

View File

@ -1,6 +1,5 @@
from dataclasses import astuple, dataclass
from decimal import Decimal
from functools import reduce
from itertools import product, zip_longest
from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union
@ -460,4 +459,4 @@ def _load_memo2_file(lines: List[str]) -> Song:
def load_memo2(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_memo2_file(lines) for _, lines in files.items()]
return reduce(Song.merge, charts)
return Song.from_monochart_instances(charts)

View File

@ -1,7 +1,6 @@
from copy import deepcopy
from dataclasses import astuple, dataclass
from decimal import Decimal
from functools import reduce
from itertools import product
from pathlib import Path
from typing import Any, Dict, Iterator, List, Set, Tuple, Union
@ -247,7 +246,7 @@ class MonoColumnParser(JubeatAnalyserParser):
def load_mono_column(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_mono_column_file(lines) for _, lines in files.items()]
return reduce(Song.merge, charts)
return Song.from_monochart_instances(charts)
def _load_mono_column_file(lines: List[str]) -> Song:

View File

@ -10,7 +10,7 @@ from ..dump_tools import make_events_from_chart
def _dump_eve(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_timing():
for dif, chart, timing in song.iter_charts_with_applicable_timing():
events = make_events_from_chart(chart.notes, timing)
chart_text = "\n".join(e.dump() for e in events)
chart_bytes = chart_text.encode("ascii")

View File

@ -1,4 +1,3 @@
from functools import reduce
from pathlib import Path
from typing import Any, Iterator, List, Optional
@ -12,7 +11,7 @@ from ..load_tools import make_chart_from_events
def load_eve(path: Path, *, beat_snap: int = 240, **kwargs: Any) -> song.Song:
files = load_folder(path)
charts = [_load_eve(l, p, beat_snap=beat_snap) for p, l in files.items()]
return reduce(song.Song.merge, charts)
return song.Song.from_monochart_instances(charts)
def load_file(path: Path) -> List[str]:

View File

@ -15,7 +15,7 @@ from . import construct
def _dump_jbsq(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_timing():
for dif, chart, timing in song.iter_charts_with_applicable_timing():
events = make_events_from_chart(chart.notes, timing)
jbsq_chart = make_jbsq_chart(events, chart.notes)
chart_bytes = construct.jbsq.build(jbsq_chart)

View File

@ -1,4 +1,3 @@
from functools import reduce
from pathlib import Path
from typing import Any, Optional
@ -16,7 +15,7 @@ def load_jbsq(path: Path, *, beat_snap: int = 240, **kwargs: Any) -> song.Song:
load_jbsq_file(bytes_, path, beat_snap=beat_snap)
for path, bytes_ in files.items()
]
return reduce(song.Song.merge, charts)
return song.Song.from_monochart_instances(charts)
def load_file(path: Path) -> bytes:

View File

@ -12,6 +12,7 @@ LOADERS: Dict[Format, Loader] = {
Format.MEMON_0_1_0: memon.load_memon_0_1_0,
Format.MEMON_0_2_0: memon.load_memon_0_2_0,
Format.MEMON_0_3_0: memon.load_memon_0_3_0,
Format.MEMON_1_0_0: memon.load_memon_1_0_0,
Format.MONO_COLUMN: jubeat_analyser.load_mono_column,
Format.MEMO: jubeat_analyser.load_memo,
Format.MEMO_1: jubeat_analyser.load_memo1,
@ -26,6 +27,7 @@ DUMPERS: Dict[Format, Dumper] = {
Format.MEMON_0_1_0: memon.dump_memon_0_1_0,
Format.MEMON_0_2_0: memon.dump_memon_0_2_0,
Format.MEMON_0_3_0: memon.dump_memon_0_3_0,
Format.MEMON_1_0_0: memon.dump_memon_1_0_0,
Format.MONO_COLUMN: jubeat_analyser.dump_mono_column,
Format.MEMO: jubeat_analyser.dump_memo,
Format.MEMO_1: jubeat_analyser.dump_memo1,

View File

@ -15,7 +15,7 @@ from . import schema as malody
def dump_malody_song(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_timing():
for dif, chart, timing in song.iter_charts_with_applicable_timing():
malody_chart = dump_malody_chart(song.metadata, dif, chart, timing)
json_chart = malody.CHART_SCHEMA.dump(malody_chart)
chart_bytes = json.dumps(json_chart, indent=4, use_decimal=True).encode("utf-8")
@ -64,7 +64,7 @@ def dump_timing(timing: song.Timing) -> List[malody.BPMEvent]:
def dump_bpm_change(b: song.BPMEvent) -> malody.BPMEvent:
return malody.BPMEvent(
beat=beats_to_tuple(b.time),
beat=beats_to_fraction_tuple(b.time),
bpm=b.BPM,
)
@ -83,7 +83,7 @@ def dump_note(
@dump_note.register
def dump_tap_note(n: song.TapNote) -> malody.TapNote:
return malody.TapNote(
beat=beats_to_tuple(n.time),
beat=beats_to_fraction_tuple(n.time),
index=n.position.index,
)
@ -91,16 +91,16 @@ def dump_tap_note(n: song.TapNote) -> malody.TapNote:
@dump_note.register
def dump_long_note(n: song.LongNote) -> malody.LongNote:
return malody.LongNote(
beat=beats_to_tuple(n.time),
beat=beats_to_fraction_tuple(n.time),
index=n.position.index,
endbeat=beats_to_tuple(n.time + n.duration),
endbeat=beats_to_fraction_tuple(n.time + n.duration),
endindex=n.tail_tip.index,
)
def dump_bgm(audio: Path, timing: song.Timing) -> malody.Sound:
return malody.Sound(
beat=beats_to_tuple(song.BeatsTime(0)),
beat=beats_to_fraction_tuple(song.BeatsTime(0)),
sound=str(audio),
vol=100,
offset=-int(timing.beat_zero_offset * 1000),
@ -110,7 +110,7 @@ def dump_bgm(audio: Path, timing: song.Timing) -> malody.Sound:
)
def beats_to_tuple(b: song.BeatsTime) -> Tuple[int, int, int]:
def beats_to_fraction_tuple(b: song.BeatsTime) -> Tuple[int, int, int]:
integer_part = int(b)
remainder = b % 1
return (

View File

@ -1,7 +1,7 @@
import warnings
from decimal import Decimal
from fractions import Fraction
from functools import reduce, singledispatch
from functools import singledispatch
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union
@ -18,7 +18,7 @@ from . import schema as malody
def load_malody(path: Path, **kwargs: Any) -> song.Song:
files = load_folder(path)
charts = [load_malody_file(d) for d in files.values()]
return reduce(song.Song.merge, charts)
return song.Song.from_monochart_instances(charts)
def load_file(path: Path) -> Any:

View File

@ -65,7 +65,7 @@ def test_that_none_values_in_metadata_dont_appear_in_dumped_json(
@given(malody_song())
def test_that_field_are_ordered(s: song.Song) -> None:
def test_that_fields_are_ordered(s: song.Song) -> None:
dif, chart = next(iter(s.charts.items()))
assert chart.timing is not None
malody_chart = dump_malody_chart(s.metadata, dif, chart, chart.timing)

View File

@ -8,13 +8,17 @@ parse than existing "memo-like" formats (memo, youbeat, etc ...).
https://github.com/Stepland/memon
"""
from .v0 import (
from .v0.dump import (
dump_memon_0_1_0,
dump_memon_0_2_0,
dump_memon_0_3_0,
dump_memon_legacy,
)
from .v0.load import (
load_memon_0_1_0,
load_memon_0_2_0,
load_memon_0_3_0,
load_memon_legacy,
)
from .v1.dump import dump_memon_1_0_0
from .v1.load import load_memon_1_0_0

View File

@ -0,0 +1,46 @@
from pathlib import Path
from typing import Any, Callable, Dict
import simplejson as json
from jubeatools import song as jbt
from jubeatools.formats.dump_tools import FileNameFormat
from jubeatools.formats.load_tools import FolderLoader, make_folder_loader
from jubeatools.formats.typing import Dumper, Loader, SongFileDumper
def _load_raw_memon(path: Path) -> Any:
with path.open() as f:
return json.load(f, use_decimal=True)
load_folder: FolderLoader[Any] = make_folder_loader("*.memon", _load_raw_memon)
def make_memon_folder_loader(memon_loader: Callable[[Any], jbt.Song]) -> Loader:
"""Create memon folder loader from the given file loader"""
def load(path: Path, merge: bool = False, **kwargs: Any) -> jbt.Song:
files = load_folder(path)
if not merge and len(files) > 1:
raise ValueError(
"Multiple .memon files were found in the given folder, "
"use the --merge option if you want to make a single memon file "
"out of several that each containt a different chart (or set of "
"charts) for the same song"
)
charts = [memon_loader(d) for d in files.values()]
return jbt.Song.from_monochart_instances(charts)
return load
def make_memon_dumper(internal_dumper: SongFileDumper) -> Dumper:
def dump(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
name_format = FileNameFormat(Path("{title}.memon"), suggestion=path)
songfile = internal_dumper(song, **kwargs)
filepath = name_format.available_filename_for(songfile)
return {filepath: songfile.contents}
return dump

View File

@ -1,596 +0,0 @@
from functools import reduce
from io import StringIO
from itertools import chain
from pathlib import Path
from typing import Any, Callable, Dict, List, Union
import simplejson as json
from marshmallow import (
RAISE,
Schema,
ValidationError,
fields,
validate,
validates_schema,
)
from multidict import MultiDict
from jubeatools import song as jbt
from jubeatools.formats.dump_tools import FileNameFormat
from jubeatools.formats.filetypes import SongFile
from jubeatools.formats.load_tools import FolderLoader, make_folder_loader
from jubeatools.formats.typing import Dumper, Loader
from jubeatools.utils import lcm, none_or
# v0.x.x long note value :
#
# 8
# 4
# 0
# 11 7 3 . 1 5 9
# 2
# 6
# 10
X_Y_OFFSET_TO_P_VALUE = {
(0, -1): 0,
(0, -2): 4,
(0, -3): 8,
(0, 1): 2,
(0, 2): 6,
(0, 3): 10,
(1, 0): 1,
(2, 0): 5,
(3, 0): 9,
(-1, 0): 3,
(-2, 0): 7,
(-3, 0): 11,
}
P_VALUE_TO_X_Y_OFFSET = {v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items()}
class StrictSchema(Schema):
class Meta:
unknown = RAISE
class MemonNote(StrictSchema):
n = fields.Integer(required=True, validate=validate.Range(min=0, max=15))
t = fields.Integer(required=True, validate=validate.Range(min=0))
l = fields.Integer(required=True, validate=validate.Range(min=0))
p = fields.Integer(required=True, validate=validate.Range(min=0, max=11))
@validates_schema
def validate_tail_tip_position(self, data: Dict[str, int], **kwargs: Any) -> None:
if data["l"] > 0:
x = data["n"] % 4
y = data["n"] // 4
dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]]
if not (0 <= x + dx < 4 and 0 <= y + dy < 4):
raise ValidationError("Invalid tail position : {data}")
class MemonChart_0_1_0(StrictSchema):
level = fields.Decimal(required=True)
resolution = fields.Integer(required=True, validate=validate.Range(min=1))
notes = fields.Nested(MemonNote, many=True)
class MemonChart_legacy(MemonChart_0_1_0):
dif_name = fields.String(required=True)
class MemonMetadata_legacy(StrictSchema):
title = fields.String(required=True, data_key="song title")
artist = fields.String(required=True)
audio = fields.String(required=True, data_key="music path")
cover = fields.String(required=True, data_key="jacket path")
BPM = fields.Decimal(
required=True, validate=validate.Range(min=0, min_inclusive=False)
)
offset = fields.Decimal(required=True)
class MemonMetadata_0_1_0(MemonMetadata_legacy):
cover = fields.String(required=True, data_key="album cover path")
class MemonPreview(StrictSchema):
position = fields.Decimal(required=True, validate=validate.Range(min=0))
length = fields.Decimal(
required=True, validate=validate.Range(min=0, min_inclusive=False)
)
class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
preview = fields.Nested(MemonPreview)
class MemonMetadata_0_3_0(MemonMetadata_0_2_0):
audio = fields.String(required=False, data_key="music path")
cover = fields.String(required=False, data_key="album cover path")
preview_path = fields.String(data_key="preview path")
class Memon_legacy(StrictSchema):
metadata = fields.Nested(MemonMetadata_legacy, required=True)
data = fields.Nested(MemonChart_legacy, required=True, many=True)
class Memon_0_1_0(StrictSchema):
version = fields.String(required=True, validate=validate.OneOf(["0.1.0"]))
metadata = fields.Nested(MemonMetadata_0_1_0, required=True)
data = fields.Dict(
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
)
class Memon_0_2_0(StrictSchema):
version = fields.String(required=True, validate=validate.OneOf(["0.2.0"]))
metadata = fields.Nested(MemonMetadata_0_2_0, required=True)
data = fields.Dict(
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
)
class Memon_0_3_0(StrictSchema):
version = fields.String(required=True, validate=validate.OneOf(["0.3.0"]))
metadata = fields.Nested(MemonMetadata_0_3_0, required=True)
data = fields.Dict(
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
)
def _load_raw_memon(path: Path) -> Any:
with path.open() as f:
return json.load(f, use_decimal=True)
load_folder: FolderLoader[Any] = make_folder_loader("*.memon", _load_raw_memon)
def make_folder_loader_with_optional_merge(
memon_loader: Callable[[Any], jbt.Song]
) -> Loader:
def load(path: Path, merge: bool = False, **kwargs: Any) -> jbt.Song:
files = load_folder(path)
if not merge and len(files) > 1:
raise ValueError(
"Multiple .memon files were found in the given folder, "
"use the --merge option if you want to make a single memon file "
"out of several that each containt a different chart (or set of "
"charts) for the same song"
)
charts = [memon_loader(d) for d in files.values()]
return reduce(jbt.Song.merge, charts)
return load
def _load_memon_note_v0(
note: dict, resolution: int
) -> Union[jbt.TapNote, jbt.LongNote]:
position = jbt.NotePosition.from_index(note["n"])
time = jbt.beats_time_from_ticks(ticks=note["t"], resolution=resolution)
if note["l"] > 0:
duration = jbt.beats_time_from_ticks(ticks=note["l"], resolution=resolution)
p_value = note["p"]
𝛿x, 𝛿y = P_VALUE_TO_X_Y_OFFSET[p_value]
tail_tip = jbt.NotePosition.from_raw_position(position + jbt.Position(𝛿x, 𝛿y))
return jbt.LongNote(time, position, duration, tail_tip)
else:
return jbt.TapNote(time, position)
def _load_memon_legacy(raw_memon: Any) -> jbt.Song:
schema = Memon_legacy()
memon = schema.load(raw_memon)
metadata = jbt.Metadata(
title=memon["metadata"]["title"],
artist=memon["metadata"]["artist"],
audio=Path(memon["metadata"]["audio"]),
cover=Path(memon["metadata"]["cover"]),
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=memon["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-memon["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for memon_chart in memon["data"]:
charts.add(
memon_chart["dif_name"],
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_legacy = make_folder_loader_with_optional_merge(_load_memon_legacy)
def _load_memon_0_1_0(raw_memon: Any) -> jbt.Song:
schema = Memon_0_1_0()
memon = schema.load(raw_memon)
metadata = jbt.Metadata(
title=memon["metadata"]["title"],
artist=memon["metadata"]["artist"],
audio=Path(memon["metadata"]["audio"]),
cover=Path(memon["metadata"]["cover"]),
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=memon["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-memon["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for difficulty, memon_chart in memon["data"].items():
charts.add(
difficulty,
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_0_1_0 = make_folder_loader_with_optional_merge(_load_memon_0_1_0)
def _load_memon_0_2_0(raw_memon: Any) -> jbt.Song:
schema = Memon_0_2_0()
memon = schema.load(raw_memon)
preview = None
if "preview" in memon["metadata"]:
start = memon["metadata"]["preview"]["position"]
length = memon["metadata"]["preview"]["length"]
preview = jbt.Preview(start, length)
metadata = jbt.Metadata(
title=memon["metadata"]["title"],
artist=memon["metadata"]["artist"],
audio=Path(memon["metadata"]["audio"]),
cover=Path(memon["metadata"]["cover"]),
preview=preview,
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=memon["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-memon["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for difficulty, memon_chart in memon["data"].items():
charts.add(
difficulty,
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_0_2_0 = make_folder_loader_with_optional_merge(_load_memon_0_2_0)
def _load_memon_0_3_0(raw_memon: Any) -> jbt.Song:
schema = Memon_0_3_0()
memon = schema.load(raw_memon)
preview = None
if "preview" in memon["metadata"]:
start = memon["metadata"]["preview"]["position"]
length = memon["metadata"]["preview"]["length"]
preview = jbt.Preview(start, length)
metadata = jbt.Metadata(
title=memon["metadata"]["title"],
artist=memon["metadata"]["artist"],
audio=none_or(Path, memon["metadata"].get("audio")),
cover=none_or(Path, memon["metadata"].get("cover")),
preview=preview,
preview_file=none_or(Path, memon["metadata"].get("preview_path")),
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=memon["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-memon["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for difficulty, memon_chart in memon["data"].items():
charts.add(
difficulty,
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_0_3_0 = make_folder_loader_with_optional_merge(_load_memon_0_3_0)
def _long_note_tail_value_v0(note: jbt.LongNote) -> int:
dx = note.tail_tip.x - note.position.x
dy = note.tail_tip.y - note.position.y
try:
return X_Y_OFFSET_TO_P_VALUE[dx, dy]
except KeyError:
raise ValueError(
f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note"
) from None
def _get_timing(song: jbt.Song) -> jbt.Timing:
if song.common_timing is not None:
return song.common_timing
else:
return next(
chart.timing for chart in song.charts.values() if chart.timing is not None
)
def _raise_if_unfit_for_v0(song: jbt.Song, version: str) -> None:
"""Raises an exception if the Song object is ill-formed or contains information
that cannot be represented in a memon v0.x.y file (includes legacy)"""
if song.common_timing is None and all(
chart.timing is None for chart in song.charts.values()
):
raise ValueError("The song has no timing information")
chart_timings = [
chart.timing for chart in song.charts.values() if chart.timing is not None
]
if chart_timings:
first_one = chart_timings[0]
if any(t != first_one for t in chart_timings):
raise ValueError(
f"memon:{version} cannot represent a song with per-chart timing"
)
timing = _get_timing(song)
number_of_timing_events = len(timing.events)
if number_of_timing_events != 1:
if number_of_timing_events == 0:
raise ValueError("The song has no BPM")
else:
raise ValueError(f"memon:{version} does not handle BPM changes")
event = timing.events[0]
if event.BPM <= 0:
raise ValueError(f"memon:{version} only accepts strictly positive BPMs")
if event.time != 0:
raise ValueError(f"memon:{version} only accepts a BPM on the first beat")
for difficulty, chart in song.charts.items():
if len(set(chart.notes)) != len(chart.notes):
raise ValueError(
f"{difficulty} chart has duplicate notes, these cannot be represented"
)
def _dump_to_json(memon: dict) -> bytes:
memon_fp = StringIO()
json.dump(memon, memon_fp, use_decimal=True, indent=4)
return memon_fp.getvalue().encode("utf-8")
def _compute_resolution(notes: List[Union[jbt.TapNote, jbt.LongNote]]) -> int:
return lcm(
*chain(
iter(note.time.denominator for note in notes),
iter(
note.duration.denominator
for note in notes
if isinstance(note, jbt.LongNote)
),
)
)
def _dump_memon_note_v0(
note: Union[jbt.TapNote, jbt.LongNote], resolution: int
) -> Dict[str, int]:
"""converts a note into the {n, t, l, p} form"""
memon_note = {
"n": note.position.index,
"t": note.time.numerator * (resolution // note.time.denominator),
"l": 0,
"p": 0,
}
if isinstance(note, jbt.LongNote):
memon_note["l"] = note.duration.numerator * (
resolution // note.duration.denominator
)
memon_note["p"] = _long_note_tail_value_v0(note)
return memon_note
def _dump_memon_legacy(song: jbt.Song) -> SongFile:
_raise_if_unfit_for_v0(song, "legacy")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"music path": str(song.metadata.audio),
"jacket path": str(song.metadata.cover),
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": [],
}
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"].append(
{
"dif_name": difficulty,
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(
set(chart.notes), key=lambda n: (n.time, n.position)
)
],
}
)
return SongFile(contents=_dump_to_json(memon), song=song)
def make_memon_dumper(internal_dumper: Callable[[jbt.Song], SongFile]) -> Dumper:
def dump(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
name_format = FileNameFormat(Path("{title}.memon"), suggestion=path)
songfile = internal_dumper(song)
filepath = name_format.available_filename_for(songfile)
return {filepath: songfile.contents}
return dump
dump_memon_legacy = make_memon_dumper(_dump_memon_legacy)
def _dump_memon_0_1_0(song: jbt.Song) -> SongFile:
_raise_if_unfit_for_v0(song, "v0.1.0")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"version": "0.1.0",
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"music path": str(song.metadata.audio),
"album cover path": str(song.metadata.cover),
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": dict(),
}
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"][difficulty] = {
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
],
}
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_0_1_0 = make_memon_dumper(_dump_memon_0_1_0)
def _dump_memon_0_2_0(song: jbt.Song) -> SongFile:
_raise_if_unfit_for_v0(song, "v0.2.0")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"version": "0.2.0",
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"music path": str(song.metadata.audio),
"album cover path": str(song.metadata.cover),
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": {},
}
if song.metadata.preview is not None:
memon["metadata"]["preview"] = {
"position": song.metadata.preview.start,
"length": song.metadata.preview.length,
}
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"][difficulty] = {
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
],
}
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_0_2_0 = make_memon_dumper(_dump_memon_0_2_0)
def _dump_memon_0_3_0(song: jbt.Song) -> SongFile:
_raise_if_unfit_for_v0(song, "v0.3.0")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"version": "0.3.0",
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": {},
}
if song.metadata.audio is not None:
memon["metadata"]["music path"] = str(song.metadata.audio)
if song.metadata.cover is not None:
memon["metadata"]["album cover path"] = str(song.metadata.cover)
if song.metadata.preview is not None:
memon["metadata"]["preview"] = {
"position": song.metadata.preview.start,
"length": song.metadata.preview.length,
}
if song.metadata.preview_file is not None:
memon["metadata"]["preview path"] = str(song.metadata.preview_file)
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"][difficulty] = {
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
],
}
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_0_3_0 = make_memon_dumper(_dump_memon_0_3_0)

View File

View File

@ -0,0 +1,269 @@
from io import StringIO
from itertools import chain
from typing import Any, Dict, List, Union
import simplejson as json
from jubeatools import song as jbt
from jubeatools.formats.filetypes import SongFile
from jubeatools.utils import lcm
from ..tools import make_memon_dumper
from . import schema
def _long_note_tail_value_v0(note: jbt.LongNote) -> int:
dx = note.tail_tip.x - note.position.x
dy = note.tail_tip.y - note.position.y
try:
return schema.X_Y_OFFSET_TO_P_VALUE[dx, dy]
except KeyError:
raise ValueError(
f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note"
) from None
def _get_timing(song: jbt.Song) -> jbt.Timing:
if song.common_timing is not None:
return song.common_timing
else:
return next(
chart.timing for chart in song.charts.values() if chart.timing is not None
)
def _raise_if_unfit_for_v0(song: jbt.Song, version: str) -> None:
"""Raises an exception if the Song object is ill-formed or contains information
that cannot be represented in a memon v0.x.y file (includes legacy)"""
if song.common_timing is None and all(
chart.timing is None for chart in song.charts.values()
):
raise ValueError("The song has no timing information")
chart_timings = [
chart.timing for chart in song.charts.values() if chart.timing is not None
]
if chart_timings:
first_one = chart_timings[0]
if any(t != first_one for t in chart_timings):
raise ValueError(
f"memon:{version} cannot represent a song with per-chart timing"
)
timing = _get_timing(song)
number_of_timing_events = len(timing.events)
if number_of_timing_events != 1:
if number_of_timing_events == 0:
raise ValueError("The song has no BPM")
else:
raise ValueError(f"memon:{version} does not handle BPM changes")
event = timing.events[0]
if event.BPM <= 0:
raise ValueError(f"memon:{version} only accepts strictly positive BPMs")
if event.time != 0:
raise ValueError(f"memon:{version} only accepts a BPM on the first beat")
for difficulty, chart in song.charts.items():
if len(set(chart.notes)) != len(chart.notes):
raise ValueError(
f"{difficulty} chart has duplicate notes, these cannot be represented"
)
def _dump_to_json(memon: dict) -> bytes:
memon_fp = StringIO()
json.dump(memon, memon_fp, use_decimal=True, indent=4)
return memon_fp.getvalue().encode("utf-8")
def _compute_resolution(notes: List[Union[jbt.TapNote, jbt.LongNote]]) -> int:
return lcm(
*chain(
iter(note.time.denominator for note in notes),
iter(
note.duration.denominator
for note in notes
if isinstance(note, jbt.LongNote)
),
)
)
def _dump_memon_note_v0(
note: Union[jbt.TapNote, jbt.LongNote], resolution: int
) -> Dict[str, int]:
"""converts a note into the {n, t, l, p} form"""
memon_note = {
"n": note.position.index,
"t": note.time.numerator * (resolution // note.time.denominator),
"l": 0,
"p": 0,
}
if isinstance(note, jbt.LongNote):
memon_note["l"] = note.duration.numerator * (
resolution // note.duration.denominator
)
memon_note["p"] = _long_note_tail_value_v0(note)
return memon_note
def _dump_memon_legacy(song: jbt.Song, **kwargs: Any) -> SongFile:
_raise_if_unfit_for_v0(song, "legacy")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"music path": str(song.metadata.audio),
"jacket path": str(song.metadata.cover),
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": [],
}
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"].append(
{
"dif_name": difficulty,
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(
set(chart.notes), key=lambda n: (n.time, n.position)
)
],
}
)
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_legacy = make_memon_dumper(_dump_memon_legacy)
def _dump_memon_0_1_0(song: jbt.Song, **kwargs: Any) -> SongFile:
_raise_if_unfit_for_v0(song, "v0.1.0")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"version": "0.1.0",
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"music path": str(song.metadata.audio),
"album cover path": str(song.metadata.cover),
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": dict(),
}
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"][difficulty] = {
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
],
}
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_0_1_0 = make_memon_dumper(_dump_memon_0_1_0)
def _dump_memon_0_2_0(song: jbt.Song, **kwargs: Any) -> SongFile:
_raise_if_unfit_for_v0(song, "v0.2.0")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"version": "0.2.0",
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"music path": str(song.metadata.audio),
"album cover path": str(song.metadata.cover),
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": {},
}
if song.metadata.preview is not None:
memon["metadata"]["preview"] = {
"position": song.metadata.preview.start,
"length": song.metadata.preview.length,
}
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"][difficulty] = {
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
],
}
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_0_2_0 = make_memon_dumper(_dump_memon_0_2_0)
def _dump_memon_0_3_0(song: jbt.Song, **kwargs: Any) -> SongFile:
_raise_if_unfit_for_v0(song, "v0.3.0")
timing = _get_timing(song)
memon: Dict[str, Any] = {
"version": "0.3.0",
"metadata": {
"song title": song.metadata.title,
"artist": song.metadata.artist,
"BPM": timing.events[0].BPM,
"offset": -timing.beat_zero_offset,
},
"data": {},
}
if song.metadata.audio is not None:
memon["metadata"]["music path"] = str(song.metadata.audio)
if song.metadata.cover is not None:
memon["metadata"]["album cover path"] = str(song.metadata.cover)
if song.metadata.preview is not None:
memon["metadata"]["preview"] = {
"position": song.metadata.preview.start,
"length": song.metadata.preview.length,
}
if song.metadata.preview_file is not None:
memon["metadata"]["preview path"] = str(song.metadata.preview_file)
for difficulty, chart in song.charts.items():
resolution = _compute_resolution(chart.notes)
memon["data"][difficulty] = {
"level": chart.level,
"resolution": resolution,
"notes": [
_dump_memon_note_v0(note, resolution)
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
],
}
return SongFile(contents=_dump_to_json(memon), song=song)
dump_memon_0_3_0 = make_memon_dumper(_dump_memon_0_3_0)

View File

@ -0,0 +1,168 @@
from pathlib import Path
from typing import Any, Union
from multidict import MultiDict
from jubeatools import song as jbt
from jubeatools.utils import none_or
from ..tools import make_memon_folder_loader
from . import schema as memon
def _load_memon_note_v0(
note: dict, resolution: int
) -> Union[jbt.TapNote, jbt.LongNote]:
position = jbt.NotePosition.from_index(note["n"])
time = jbt.beats_time_from_ticks(ticks=note["t"], resolution=resolution)
if note["l"] > 0:
duration = jbt.beats_time_from_ticks(ticks=note["l"], resolution=resolution)
p_value = note["p"]
𝛿x, 𝛿y = memon.P_VALUE_TO_X_Y_OFFSET[p_value]
tail_tip = jbt.NotePosition.from_raw_position(position + jbt.Position(𝛿x, 𝛿y))
return jbt.LongNote(time, position, duration, tail_tip)
else:
return jbt.TapNote(time, position)
def _load_memon_legacy(raw_memon: Any) -> jbt.Song:
schema = memon.Memon_legacy()
file = schema.load(raw_memon)
metadata = jbt.Metadata(
title=file["metadata"]["title"],
artist=file["metadata"]["artist"],
audio=Path(file["metadata"]["audio"]),
cover=Path(file["metadata"]["cover"]),
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for memon_chart in file["data"]:
charts.add(
memon_chart["dif_name"],
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_legacy = make_memon_folder_loader(_load_memon_legacy)
def _load_memon_0_1_0(raw_memon: Any) -> jbt.Song:
schema = memon.Memon_0_1_0()
file = schema.load(raw_memon)
metadata = jbt.Metadata(
title=file["metadata"]["title"],
artist=file["metadata"]["artist"],
audio=Path(file["metadata"]["audio"]),
cover=Path(file["metadata"]["cover"]),
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for difficulty, memon_chart in file["data"].items():
charts.add(
difficulty,
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_0_1_0 = make_memon_folder_loader(_load_memon_0_1_0)
def _load_memon_0_2_0(raw_memon: Any) -> jbt.Song:
schema = memon.Memon_0_2_0()
file = schema.load(raw_memon)
preview = None
if "preview" in file["metadata"]:
start = file["metadata"]["preview"]["position"]
length = file["metadata"]["preview"]["length"]
preview = jbt.Preview(start, length)
metadata = jbt.Metadata(
title=file["metadata"]["title"],
artist=file["metadata"]["artist"],
audio=Path(file["metadata"]["audio"]),
cover=Path(file["metadata"]["cover"]),
preview=preview,
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for difficulty, memon_chart in file["data"].items():
charts.add(
difficulty,
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_0_2_0 = make_memon_folder_loader(_load_memon_0_2_0)
def _load_memon_0_3_0(raw_memon: Any) -> jbt.Song:
schema = memon.Memon_0_3_0()
file = schema.load(raw_memon)
preview = None
if "preview" in file["metadata"]:
start = file["metadata"]["preview"]["position"]
length = file["metadata"]["preview"]["length"]
preview = jbt.Preview(start, length)
metadata = jbt.Metadata(
title=file["metadata"]["title"],
artist=file["metadata"]["artist"],
audio=none_or(Path, file["metadata"].get("audio")),
cover=none_or(Path, file["metadata"].get("cover")),
preview=preview,
preview_file=none_or(Path, file["metadata"].get("preview_path")),
)
common_timing = jbt.Timing(
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
for difficulty, memon_chart in file["data"].items():
charts.add(
difficulty,
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
)
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
load_memon_0_3_0 = make_memon_folder_loader(_load_memon_0_3_0)

View File

@ -0,0 +1,131 @@
from typing import Any, Dict
from marshmallow import (
RAISE,
Schema,
ValidationError,
fields,
validate,
validates_schema,
)
# v0.x.x long note value :
#
# 8
# 4
# 0
# 11 7 3 . 1 5 9
# 2
# 6
# 10
X_Y_OFFSET_TO_P_VALUE = {
(0, -1): 0,
(0, -2): 4,
(0, -3): 8,
(0, 1): 2,
(0, 2): 6,
(0, 3): 10,
(1, 0): 1,
(2, 0): 5,
(3, 0): 9,
(-1, 0): 3,
(-2, 0): 7,
(-3, 0): 11,
}
P_VALUE_TO_X_Y_OFFSET = {v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items()}
class StrictSchema(Schema):
class Meta:
ordered = True
unknown = RAISE
class MemonNote(StrictSchema):
n = fields.Integer(required=True, validate=validate.Range(min=0, max=15))
t = fields.Integer(required=True, validate=validate.Range(min=0))
# flake8 doesn't like "l" as a name here, so I silence the precise warning
l = fields.Integer(required=True, validate=validate.Range(min=0)) # noqa: E741
p = fields.Integer(required=True, validate=validate.Range(min=0, max=11))
@validates_schema
def validate_tail_tip_position(self, data: Dict[str, int], **kwargs: Any) -> None:
if data["l"] > 0:
x = data["n"] % 4
y = data["n"] // 4
dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]]
if not (0 <= x + dx < 4 and 0 <= y + dy < 4):
raise ValidationError("Invalid tail position : {data}")
class MemonChart_0_1_0(StrictSchema):
level = fields.Decimal(required=True)
resolution = fields.Integer(required=True, validate=validate.Range(min=1))
notes = fields.Nested(MemonNote, many=True)
class MemonChart_legacy(MemonChart_0_1_0):
dif_name = fields.String(required=True)
class MemonMetadata_legacy(StrictSchema):
title = fields.String(required=True, data_key="song title")
artist = fields.String(required=True)
audio = fields.String(required=True, data_key="music path")
cover = fields.String(required=True, data_key="jacket path")
BPM = fields.Decimal(
required=True, validate=validate.Range(min=0, min_inclusive=False)
)
offset = fields.Decimal(required=True)
class MemonMetadata_0_1_0(MemonMetadata_legacy):
cover = fields.String(required=True, data_key="album cover path")
class MemonPreview(StrictSchema):
position = fields.Decimal(required=True, validate=validate.Range(min=0))
length = fields.Decimal(
required=True, validate=validate.Range(min=0, min_inclusive=False)
)
class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
preview = fields.Nested(MemonPreview)
class MemonMetadata_0_3_0(MemonMetadata_0_2_0):
audio = fields.String(required=False, data_key="music path")
cover = fields.String(required=False, data_key="album cover path")
preview_path = fields.String(data_key="preview path")
class Memon_legacy(StrictSchema):
metadata = fields.Nested(MemonMetadata_legacy, required=True)
data = fields.Nested(MemonChart_legacy, required=True, many=True)
class Memon_0_1_0(StrictSchema):
version = fields.String(required=True, validate=validate.OneOf(["0.1.0"]))
metadata = fields.Nested(MemonMetadata_0_1_0, required=True)
data = fields.Dict(
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
)
class Memon_0_2_0(StrictSchema):
version = fields.String(required=True, validate=validate.OneOf(["0.2.0"]))
metadata = fields.Nested(MemonMetadata_0_2_0, required=True)
data = fields.Dict(
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
)
class Memon_0_3_0(StrictSchema):
version = fields.String(required=True, validate=validate.OneOf(["0.3.0"]))
metadata = fields.Nested(MemonMetadata_0_3_0, required=True)
data = fields.Dict(
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
)

View File

@ -32,11 +32,6 @@ def memon_legacy_compatible_song(draw: st.DrawFn) -> song.Song:
diffs_strat=memon_diffs(),
chart_strat=jbst.chart(timing_strat=st.none()),
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
metadata_strat=jbst.metadata(
text_strat=st.text(
alphabet=st.characters(blacklist_categories=("Cc", "Cs")),
),
),
)
)
random_song.metadata.preview = None
@ -65,11 +60,6 @@ def memon_0_2_0_compatible_song(draw: st.DrawFn) -> song.Song:
diffs_strat=memon_diffs(),
chart_strat=jbst.chart(timing_strat=st.none()),
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
metadata_strat=jbst.metadata(
text_strat=st.text(
alphabet=st.characters(blacklist_categories=("Cc", "Cs")),
),
),
)
)
random_song.metadata.preview_file = None
@ -88,11 +78,6 @@ def memon_0_3_0_compatible_song(draw: st.DrawFn) -> song.Song:
diffs_strat=memon_diffs(),
chart_strat=jbst.chart(timing_strat=st.none()),
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
metadata_strat=jbst.metadata(
text_strat=st.text(
alphabet=st.characters(blacklist_categories=("Cc", "Cs")),
),
),
)
)

View File

View File

@ -0,0 +1,185 @@
from collections import Counter
from functools import singledispatch
from typing import Any, Iterable, List, Optional, Set, Tuple, TypeVar, Union
import simplejson as json
from jubeatools import song as jbt
from jubeatools.formats.filetypes import SongFile
from jubeatools.utils import none_or
from ..tools import make_memon_dumper
from . import schema as memon
def _dump_memon_1_0_0(
song: jbt.Song, use_fractions: bool = False, **kwargs: Any
) -> SongFile:
metadata = dump_metadata(song.metadata)
common_timing = dump_file_timing(song)
charts = {
diff: dump_chart(chart, common_timing) for diff, chart in song.charts.items()
}
file = memon.File(
version="1.0.0",
metadata=metadata,
timing=common_timing,
data=charts,
)
json_file = memon.FILE_SCHEMA.dump(file)
file_bytes = json.dumps(json_file, indent=4, use_decimal=True).encode("utf-8")
return SongFile(contents=file_bytes, song=song)
dump_memon_1_0_0 = make_memon_dumper(_dump_memon_1_0_0)
def dump_metadata(metadata: jbt.Metadata) -> memon.Metadata:
return memon.Metadata(
title=metadata.title,
artist=metadata.artist,
audio=none_or(str, metadata.audio),
jacket=none_or(str, metadata.cover),
preview=(
none_or(str, metadata.preview_file)
or none_or(dump_preview, metadata.preview)
),
)
def dump_preview(preview: jbt.Preview) -> memon.PreviewSample:
return memon.PreviewSample(
start=preview.start,
duration=preview.length,
)
def dump_file_timing(song: jbt.Song) -> Optional[memon.Timing]:
events = get_common_value(
t.events for _, _, t in song.iter_charts_with_applicable_timing()
)
beat_zero_offset = get_common_value(
t.beat_zero_offset for _, _, t in song.iter_charts_with_applicable_timing()
)
hakus = get_common_value(
none_or(frozenset.__call__, c.hakus or song.common_hakus)
for c in song.charts.values()
)
timing = dump_timing(
events=events,
beat_zero_offset=beat_zero_offset,
hakus=hakus,
)
timing.remove_default_values()
return timing or None
T = TypeVar("T")
def get_common_value(values: Iterable[T]) -> Optional[T]:
possible_values = Counter(values)
value, count = possible_values.most_common(1).pop()
if count >= 2:
return value
else:
return None
def dump_timing(
events: Optional[Iterable[jbt.BPMEvent]],
beat_zero_offset: Optional[jbt.SecondsTime],
hakus: Optional[Set[jbt.BeatsTime]],
) -> memon.Timing:
return memon.Timing(
offset=beat_zero_offset,
resolution=None,
bpms=none_or(dump_bpms, events),
hakus=none_or(dump_hakus, hakus),
)
def dump_bpms(bpms: Iterable[jbt.BPMEvent]) -> List[memon.BPMEvent]:
return [dump_bpm(b) for b in bpms]
def dump_bpm(bpm: jbt.BPMEvent) -> memon.BPMEvent:
return memon.BPMEvent(beat=beats_to_best_form(bpm.time), bpm=bpm.BPM)
def beats_to_best_form(b: jbt.BeatsTime) -> memon.SymbolicTime:
if is_expressible_as_240th(b):
return (240 * b.numerator) // b.denominator
else:
return beat_to_fraction_tuple(b)
def is_expressible_as_240th(b: jbt.BeatsTime) -> bool:
return (240 * b.numerator) % b.denominator == 0
def beat_to_fraction_tuple(b: jbt.BeatsTime) -> Tuple[int, int, int]:
integer_part = int(b)
remainder = b % 1
return (
integer_part,
remainder.numerator,
remainder.denominator,
)
def dump_hakus(hakus: Set[jbt.BeatsTime]) -> List[memon.SymbolicTime]:
return [beats_to_best_form(b) for b in sorted(hakus)]
def dump_chart(chart: jbt.Chart, file_timing: Optional[memon.Timing]) -> memon.Chart:
return memon.Chart(
level=chart.level,
resolution=None,
timing=dump_chart_timing(chart.timing, chart.hakus, file_timing),
notes=[dump_note(n) for n in chart.notes],
)
def dump_chart_timing(
chart_timing: Optional[jbt.Timing],
chart_hakus: Optional[Set[jbt.BeatsTime]],
file_timing: Optional[memon.Timing],
) -> Optional[memon.Timing]:
if chart_timing is None:
events = None
beat_zero_offset = None
else:
events = chart_timing.events
beat_zero_offset = chart_timing.beat_zero_offset
res = dump_timing(events, beat_zero_offset, chart_hakus)
fallback = memon.Timing.fill_in_defaults(file_timing)
return res.remove_common_values(fallback)
@singledispatch
def dump_note(n: Union[jbt.TapNote, jbt.LongNote]) -> memon.Note:
...
@dump_note.register
def dump_tap_note(tap: jbt.TapNote) -> memon.TapNote:
return memon.TapNote(n=tap.position.index, t=beats_to_best_form(tap.time))
@dump_note.register
def dump_long_note(long: jbt.LongNote) -> memon.LongNote:
return memon.LongNote(
n=long.position.index,
t=beats_to_best_form(long.time),
l=beats_to_best_form(long.duration),
p=tail_as_6_notation(long),
)
def tail_as_6_notation(long: jbt.LongNote) -> int:
if long.tail_tip.y == long.position.y:
return long.tail_tip.x - int(long.tail_tip.x > long.position.x)
else:
return 3 + long.tail_tip.y - int(long.tail_tip.y > long.position.y)

View File

@ -0,0 +1,157 @@
from dataclasses import replace
from decimal import Decimal
from functools import partial, singledispatch
from pathlib import Path
from typing import Any, List, Set, Tuple, Union
from jubeatools import song as jbt
from jubeatools.utils import none_or
from ..tools import make_memon_folder_loader
from . import schema as memon
def _load_memon_1_0_0(raw_json: Any) -> jbt.Song:
file: memon.File = memon.FILE_SCHEMA.load(raw_json)
metadata = none_or(load_metadata, file.metadata) or jbt.Metadata()
charts = {diff: load_chart(chart, file) for diff, chart in file.data.items()}
if not file.timing:
common_timing = None
common_hakus = None
else:
timing = memon.Timing.fill_in_defaults(file.timing)
common_timing = load_timing(timing)
resolution = timing.resolution or 240
load_hakus_with_res = partial(load_hakus, resolution=resolution)
common_hakus = none_or(load_hakus_with_res, file.timing.hakus)
return jbt.Song(
metadata=metadata,
charts=charts,
common_timing=common_timing,
common_hakus=common_hakus,
)
load_memon_1_0_0 = make_memon_folder_loader(_load_memon_1_0_0)
def load_metadata(m: memon.Metadata) -> jbt.Metadata:
result = jbt.Metadata(
title=m.title,
artist=m.artist,
audio=none_or(Path, m.audio),
cover=none_or(Path, m.jacket),
)
if m.preview is None:
return result
elif isinstance(m.preview, str):
return replace(
result,
preview_file=Path(m.preview),
)
elif isinstance(m.preview, memon.PreviewSample):
return replace(result, preview=jbt.Preview(m.preview.start, m.preview.duration))
def load_chart(c: memon.Chart, m: memon.File) -> jbt.Chart:
applicable_timing = memon.Timing.fill_in_defaults(c.timing, m.timing)
if not c.timing:
timing = None
hakus = None
else:
timing = load_timing(applicable_timing)
resolution = applicable_timing.resolution or 240
load_hakus_with_res = partial(load_hakus, resolution=resolution)
hakus = none_or(load_hakus_with_res, c.timing.hakus)
return jbt.Chart(
level=c.level,
timing=timing,
hakus=hakus,
notes=[load_note(n, c.resolution or 240) for n in c.notes],
)
def load_hakus(h: List[memon.SymbolicTime], resolution: int) -> Set[jbt.BeatsTime]:
return set(load_symbolic_time(t, resolution) for t in h)
def load_timing(t: memon.Timing) -> jbt.Timing:
return jbt.Timing(
events=load_bpms(t.bpms or [], t.resolution or 240),
beat_zero_offset=t.offset or Decimal(0),
)
def load_bpms(bpms: List[memon.BPMEvent], resolution: int) -> List[jbt.BPMEvent]:
return [load_bpm(b, resolution) for b in bpms]
def load_bpm(bpm: memon.BPMEvent, resolution: int) -> jbt.BPMEvent:
return jbt.BPMEvent(
time=load_symbolic_time(bpm.beat, resolution),
BPM=bpm.bpm,
)
@singledispatch
def load_symbolic_time(
t: Union[int, Tuple[int, int, int]], resolution: int
) -> jbt.BeatsTime:
...
@load_symbolic_time.register
def load_symbolic_time_int(t: int, resolution: int) -> jbt.BeatsTime:
return jbt.BeatsTime(t, resolution)
@load_symbolic_time.register(tuple)
def load_symbolic_time_tuple(t: Tuple[int, int, int], resolution: int) -> jbt.BeatsTime:
return t[0] + jbt.BeatsTime(t[1], t[2])
@singledispatch
def load_note(note: memon.Note, resolution: int) -> Union[jbt.TapNote, jbt.LongNote]:
...
@load_note.register
def load_tap_note(note: memon.TapNote, resolution: int) -> jbt.TapNote:
return jbt.TapNote(
time=load_symbolic_time(note.t, resolution),
position=jbt.NotePosition.from_index(note.n),
)
@load_note.register
def load_long_note(note: memon.LongNote, resolution: int) -> jbt.LongNote:
position = jbt.NotePosition.from_index(note.n)
return jbt.LongNote(
time=load_symbolic_time(note.t, resolution),
position=position,
duration=load_symbolic_time(note.l, resolution),
tail_tip=convert_6_notation_to_position(position, note.p),
)
def convert_6_notation_to_position(pos: jbt.NotePosition, p: int) -> jbt.NotePosition:
# horizontal
if p < 3:
if p < pos.x:
x = p
else:
x = p + 1
y = pos.y
else:
p -= 3
x = pos.x
if p < pos.y:
y = p
else:
y = p + 1
return jbt.NotePosition(x, y)

View File

@ -0,0 +1,186 @@
from dataclasses import astuple, dataclass, field
from decimal import Decimal
from functools import partial
from typing import Any, Dict, List, Optional, Tuple, Union
from marshmallow import EXCLUDE, Schema, ValidationError, post_dump, validate
from marshmallow_dataclass import NewType, class_schema
PositiveDecimal = NewType("PositiveDecimal", Decimal, validate=validate.Range(min=0))
StrictlyPositiveDecimal = NewType(
"StrictlyPositiveDecimal",
Decimal,
validate=validate.Range(min=0, min_inclusive=False),
)
@dataclass
class PreviewSample:
start: PositiveDecimal
duration: StrictlyPositiveDecimal
Preview = Union[str, PreviewSample]
@dataclass
class Metadata:
title: Optional[str]
artist: Optional[str]
audio: Optional[str]
jacket: Optional[str]
preview: Optional[Preview]
PositiveInt = NewType("PositiveInt", int, validate=validate.Range(min=0))
StrictlyPositiveInt = NewType(
"StrictlyPositiveInteger", int, validate=validate.Range(min=0, min_inclusive=False)
)
MixedNumber = Tuple[int, int, int]
SymbolicTime = Union[int, MixedNumber]
def validate_symbolic_time(t: SymbolicTime) -> None:
if isinstance(t, int):
if t < 0:
raise ValidationError("Negative ticks are not allowed")
elif isinstance(t, tuple):
validate_mixed_number(t)
def validate_mixed_number(m: MixedNumber) -> None:
if m[0] < 0:
raise ValidationError("First number in fraction tuple can't be negative")
elif m[1] < 0:
raise ValidationError("Second number in fraction tuple can't be negative")
elif m[2] < 1:
raise ValidationError("Third number in fraction tuple can't be less than 1")
def validate_strictly_positive_mixed_number(m: MixedNumber) -> None:
if (m[0], m[1]) == (0, 0):
raise ValidationError("The tuple must represent a strictly positive number")
@dataclass
class BPMEvent:
beat: SymbolicTime = field(metadata={"validate": validate_symbolic_time})
bpm: StrictlyPositiveDecimal
@dataclass
class Timing:
offset: Optional[Decimal]
resolution: Optional[StrictlyPositiveInt]
bpms: Optional[List[BPMEvent]]
hakus: Optional[List[SymbolicTime]] = field(
metadata={"validate": partial(map, validate_symbolic_time)}
)
def remove_default_values(self) -> "Timing":
return self.remove_common_values(DEFAULT_TIMING)
def remove_common_values(self, other: "Timing") -> "Timing":
return Timing(
offset=None if self.offset == other.offset else self.offset,
resolution=None if self.resolution == other.resolution else self.resolution,
bpms=None if self.bpms == other.bpms else self.bpms,
hakus=None if self.hakus == other.hakus else self.hakus,
)
def __bool__(self) -> bool:
return any(f is not None for f in astuple(self))
@classmethod
def fill_in_defaults(cls, *timings: Optional["Timing"]) -> "Timing":
ordered = [*timings, DEFAULT_TIMING]
return cls.merge(*(t for t in ordered if t is not None))
@classmethod
def merge(cls, *timings: "Timing") -> "Timing":
offset = next((t.offset for t in timings if t.offset is not None), None)
resolution = next(
(t.resolution for t in timings if t.resolution is not None), None
)
bpms = next((t.bpms for t in timings if t.bpms is not None), None)
hakus = next((t.hakus for t in timings if t.hakus is not None), None)
return cls(
offset=offset,
resolution=resolution,
bpms=bpms,
hakus=hakus,
)
DEFAULT_TIMING = Timing(
offset=Decimal(0),
resolution=240,
bpms=[BPMEvent(0, Decimal(120))],
hakus=None,
)
Button = NewType("Button", int, validate=validate.Range(min=0, max=15))
@dataclass
class TapNote:
n: Button
t: SymbolicTime = field(metadata={"validate": validate_symbolic_time})
def validate_symbolic_duration(t: SymbolicTime) -> None:
if isinstance(t, int):
if t < 1:
raise ValidationError("Duration has to be positive and non-zero")
elif isinstance(t, tuple):
validate_mixed_number(t)
validate_strictly_positive_mixed_number(t)
TailIn6Notation = NewType("TailIn6Notation", int, validate=validate.Range(min=0, max=5))
@dataclass
class LongNote(TapNote):
l: SymbolicTime = field(metadata={"validate": validate_symbolic_duration})
p: TailIn6Notation
# LongNote first otherwise long notes get interpreted as tap notes
Note = Union[LongNote, TapNote]
@dataclass
class Chart:
level: Optional[Decimal]
resolution: Optional[StrictlyPositiveInt]
timing: Optional[Timing]
notes: List[Note]
Version = NewType("Version", str, validate=validate.Equal("1.0.0"))
@dataclass
class File:
version: Version
metadata: Optional[Metadata]
timing: Optional[Timing]
data: Dict[str, Chart]
class BaseSchema(Schema):
class Meta:
ordered = True
unknown = EXCLUDE
@post_dump
def _remove_none_values(self, data: dict, **kwargs: Any) -> dict:
return remove_none_values(data)
def remove_none_values(data: dict) -> dict:
return {key: value for key, value in data.items() if value is not None}
FILE_SCHEMA = class_schema(File, base_schema=BaseSchema)()

View File

@ -0,0 +1,53 @@
from pathlib import Path
from typing import Set
import hypothesis.strategies as st
from hypothesis import given
from jubeatools import song
from jubeatools.formats.enum import Format
from jubeatools.testutils import strategies as jbst
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
@st.composite
def memon_diffs(draw: st.DrawFn) -> Set[str]:
simple_diff_names = st.sampled_from(list(d.value for d in song.Difficulty))
diff_names = st.one_of(
simple_diff_names,
st.text(
alphabet=st.characters(min_codepoint=0x20, max_codepoint=0x7E),
min_size=1,
max_size=20,
),
)
s: Set[str] = draw(st.sets(diff_names, min_size=1, max_size=10))
return s
@st.composite
def memon_1_0_0_compatible_song(draw: st.DrawFn) -> song.Song:
"""Memon v1.0.0 only support one kind of metadata at once"""
random_song: song.Song = draw(
jbst.song(
diffs_strat=memon_diffs(),
common_hakus_strat=st.one_of(st.none(), jbst.hakus()),
chart_strat=jbst.chart(
hakus_strat=st.one_of(st.none(), jbst.hakus()),
),
)
)
preview = draw(st.one_of(jbst.metadata_path_strat(), jbst.preview()))
if isinstance(preview, str):
random_song.metadata.preview = None
random_song.metadata.preview_file = Path(preview)
else:
random_song.metadata.preview = preview
random_song.metadata.preview_file = None
return random_song
@given(memon_1_0_0_compatible_song())
def test_memon_1_0_0(song: song.Song) -> None:
dump_and_load_then_compare(Format.MEMON_1_0_0, song)

View File

@ -1,7 +1,7 @@
from pathlib import Path
from typing import Any, Dict, List, Protocol
from jubeatools.formats.filetypes import ChartFile
from jubeatools.formats.filetypes import ChartFile, SongFile
from jubeatools.song import Song
@ -24,6 +24,14 @@ class ChartFileDumper(Protocol):
...
class SongFileDumper(Protocol):
"""Generic signature of internal dumper for formats that use a single file
to hold all charts of a song"""
def __call__(self, song: Song, **kwargs: Any) -> SongFile:
...
class Loader(Protocol):
"""A Loader deserializes a Path to a Song object and possibly takes in
some options via the kwargs.

View File

@ -7,16 +7,31 @@ number of seconds is used"""
from __future__ import annotations
from dataclasses import astuple, dataclass, field
from collections import Counter
from dataclasses import Field, astuple, dataclass, field, fields
from decimal import Decimal
from enum import Enum, auto
from fractions import Fraction
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Iterator, List, Mapping, Optional, Tuple, Union
from typing import (
Any,
Callable,
Iterable,
Iterator,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from multidict import MultiDict
from jubeatools.utils import none_or
BeatsTime = Fraction
SecondsTime = Decimal
@ -174,19 +189,26 @@ class BPMEvent:
@dataclass(unsafe_hash=True)
class Timing:
events: List[BPMEvent]
events: Sequence[BPMEvent]
beat_zero_offset: SecondsTime
def __post_init__(self) -> None:
self.events = tuple(self.events)
@dataclass
class Chart:
level: Optional[Decimal]
timing: Optional[Timing] = None
hakus: Optional[Set[BeatsTime]] = None
notes: List[Union[TapNote, LongNote]] = field(default_factory=list)
@dataclass
@dataclass(frozen=True)
class Preview:
"""Frozen so it can be hashed to be deduped using a set when merging
Metadata instances"""
start: SecondsTime
length: SecondsTime
@ -200,6 +222,51 @@ class Metadata:
preview: Optional[Preview] = None
preview_file: Optional[Path] = None
@classmethod
def permissive_merge(cls, metadatas: Iterable["Metadata"]) -> "Metadata":
"""Make the "sum" of all the given metadata instances, if possible. If
several instances have different defined values for the same field,
merging will fail. Fields with Noneor empty values (empty string or
empty path) are conscidered undefined and their values can be replaced
by an actual value if supplied by at least one object from the given
iterable."""
metadatas = list(metadatas)
return cls(
**{f.name: _get_common_value(f, metadatas) for f in fields(cls)},
)
def _get_common_value(field_: Field, metadatas: Iterable[Metadata]) -> Any:
raw_values = []
empty_values = []
for m in metadatas:
value = getattr(m, field_.name)
if value is None:
continue
elif not value:
empty_values.append(value)
else:
raw_values.append(value)
real_values = set(raw_values)
if len(real_values) > 1:
raise ValueError(
f"Can't merge metadata, the {field_.name} field has "
f"conflicting possible values : {real_values}"
)
try:
return real_values.pop()
except KeyError:
pass
try:
return empty_values.pop()
except IndexError:
pass
return None
class Difficulty(str, Enum):
BASIC = "BSC"
@ -209,34 +276,74 @@ class Difficulty(str, Enum):
@dataclass
class Song:
"""The abstract representation format for all jubeat chart sets.
A Song is a set of charts with associated metadata"""
metadata: Metadata
charts: Mapping[str, Chart] = field(default_factory=MultiDict)
common_timing: Optional[Timing] = None
common_hakus: Optional[Set[BeatsTime]] = None
def merge(self, other: "Song") -> "Song":
if self.metadata != other.metadata:
raise ValueError(
"Merge conflit in song metadata :\n"
f"{self.metadata}\n"
f"{other.metadata}"
)
@classmethod
def from_monochart_instances(cls, songs: Iterable["Song"]) -> "Song":
metadata = Metadata.permissive_merge(song.metadata for song in songs)
charts: MultiDict[Chart] = MultiDict()
charts.extend(self.charts)
charts.extend(other.charts)
if (
self.common_timing is not None
and other.common_timing is not None
and self.common_timing != other.common_timing
):
raise ValueError("Can't merge songs with differing global timings")
common_timing = self.common_timing or other.common_timing
return Song(self.metadata, charts, common_timing)
for song in songs:
song.remove_common_timing()
song.remove_common_hakus()
charts.extend(song.charts)
def iter_charts_with_timing(self) -> Iterator[Tuple[str, Chart, Timing]]:
merged = cls(
metadata=metadata,
charts=charts,
)
merged.minimize_timings()
merged.minimize_hakus()
return merged
def minimize_timings(self) -> None:
"""Turn timings into minimal form : Use the most common timing object
as the common timing, if it is used by more than two charts, otherwise
each chart gets its own timing object and no common timing is defined"""
self.remove_common_timing()
counts = Counter(c.timing for c in self.charts.values())
((most_used, count),) = counts.most_common(1)
if count >= 2:
self.common_timing = most_used
for chart in self.charts.values():
if chart.timing == most_used:
chart.timing = None
def remove_common_timing(self) -> None:
"""Modify the song object so that no chart relies on the common timing
object, charts that previously did rely on it now have a chart-specific
timing object equal to the old common timing"""
for _, chart, applicable_timing in self.iter_charts_with_applicable_timing():
chart.timing = applicable_timing
self.common_timing = None
def minimize_hakus(self) -> None:
"""Same deal as "minimize_timings" but with hakus"""
self.remove_common_hakus()
counts = Counter(
none_or(frozenset.__call__, c.hakus) for c in self.charts.values()
)
most_used, count = counts.most_common(1).pop()
if count >= 2:
self.common_hakus = most_used
for chart in self.charts.values():
if chart.hakus == most_used:
chart.hakus = None
def remove_common_hakus(self) -> None:
for chart in self.charts.values():
if chart.hakus is None:
chart.hakus = self.common_hakus
self.common_hakus = None
def iter_charts_with_applicable_timing(self) -> Iterator[Tuple[str, Chart, Timing]]:
for dif, chart in self.charts.items():
timing = chart.timing or self.common_timing
if timing is None:

View File

@ -4,6 +4,7 @@ Hypothesis strategies to generate notes and charts
from decimal import Decimal
from enum import Flag, auto
from functools import partial
from itertools import product
from pathlib import Path
from typing import Dict, Iterable, Optional, Set, Union
@ -210,19 +211,25 @@ def level(draw: st.DrawFn) -> Union[int, Decimal]:
return d
hakus = partial(st.sets, beat_time())
@st.composite
def chart(
draw: st.DrawFn,
timing_strat: st.SearchStrategy[Timing] = timing_info(),
hakus_strat: st.SearchStrategy[Optional[Set[BeatsTime]]] = st.none(),
notes_strat: st.SearchStrategy[Iterable[Union[TapNote, LongNote]]] = notes(),
level_strat: st.SearchStrategy[Union[int, Decimal]] = level(),
) -> Chart:
level = Decimal(draw(level_strat))
timing = draw(timing_strat)
hakus = draw(hakus_strat)
notes = draw(notes_strat)
return Chart(
level=level,
timing=timing,
hakus=hakus,
notes=sorted(notes, key=lambda n: (n.time, n.position)),
)
@ -238,11 +245,19 @@ def preview(draw: st.DrawFn) -> Preview:
return Preview(start, length)
metadata_text_strat = partial(
st.text, alphabet=st.characters(blacklist_categories=("Cc", "Cs"))
)
metadata_path_strat = partial(
st.text, alphabet=st.characters(blacklist_categories=("Cc", "Cs"))
)
@st.composite
def metadata(
draw: st.DrawFn,
text_strat: st.SearchStrategy[str] = st.text(),
path_strat: st.SearchStrategy[str] = st.text(),
text_strat: st.SearchStrategy[str] = metadata_text_strat(),
path_strat: st.SearchStrategy[str] = metadata_path_strat(),
) -> Metadata:
return Metadata(
title=draw(text_strat),
@ -267,6 +282,7 @@ def song(
st.sampled_from(list(d.value for d in Difficulty)), min_size=1, max_size=3
),
common_timing_strat: st.SearchStrategy[Optional[Timing]] = timing_info(),
common_hakus_strat: st.SearchStrategy[Optional[Set[BeatsTime]]] = st.none(),
chart_strat: st.SearchStrategy[Chart] = chart(),
metadata_strat: st.SearchStrategy[Metadata] = metadata(),
) -> Song:
@ -279,4 +295,5 @@ def song(
metadata=draw(metadata_strat),
charts=charts,
common_timing=draw(common_timing_strat),
common_hakus=draw(common_hakus_strat),
)

View File

@ -29,6 +29,8 @@ def dump_and_load_then_compare(
dump_options = dump_options or {}
loader = LOADERS[format_]
dumper = DUMPERS[format_]
song.minimize_timings()
song.minimize_hakus()
with temp_path() as folder_path:
files = dumper(song, folder_path, **dump_options)
for file_path, bytes_ in files.items():
@ -36,4 +38,6 @@ def dump_and_load_then_compare(
note(f"Wrote to {file_path} :\n{bytes_decoder(bytes_)}")
assert guess_format(file_path) == format_
recovered_song = loader(folder_path, **load_options)
recovered_song.minimize_timings()
recovered_song.minimize_hakus()
assert recovered_song == song

252
poetry.lock generated
View File

@ -47,7 +47,7 @@ sh = "*"
[[package]]
name = "black"
version = "21.9b0"
version = "21.12b0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
@ -58,7 +58,6 @@ click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0,<1"
platformdirs = ">=2"
regex = ">=2020.1.8"
tomli = ">=0.2.6,<2.0.0"
typing-extensions = [
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
@ -67,9 +66,9 @@ typing-extensions = [
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
python2 = ["typed-ast (>=1.4.2)"]
python2 = ["typed-ast (>=1.4.3)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
@ -128,22 +127,22 @@ pyflakes = ">=2.3.0,<2.4.0"
[[package]]
name = "hypothesis"
version = "6.23.4"
version = "6.32.1"
description = "A library for property-based testing"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
attrs = ">=19.2.0"
sortedcontainers = ">=2.1.0,<3.0.0"
[package.extras]
all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"]
all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2021.5)"]
cli = ["click (>=7.0)", "black (>=19.10b0)", "rich (>=9.0.0)"]
codemods = ["libcst (>=0.3.16)"]
dateutil = ["python-dateutil (>=1.4)"]
django = ["pytz (>=2014.1)", "django (>=2.2)"]
django = ["django (>=2.2)"]
dpcontracts = ["dpcontracts (>=0.4)"]
ghostwriter = ["black (>=19.10b0)"]
lark = ["lark-parser (>=0.6.5)"]
@ -152,7 +151,7 @@ pandas = ["pandas (>=0.25)"]
pytest = ["pytest (>=4.6)"]
pytz = ["pytz (>=2014.1)"]
redis = ["redis (>=3.0.0)"]
zoneinfo = ["importlib-resources (>=3.3.0)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"]
zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2021.5)"]
[[package]]
name = "iniconfig"
@ -178,7 +177,7 @@ xdg_home = ["appdirs (>=1.4.0)"]
[[package]]
name = "marshmallow"
version = "3.14.0"
version = "3.14.1"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
category = "main"
optional = false
@ -186,7 +185,7 @@ python-versions = ">=3.6"
[package.extras]
dev = ["pytest", "pytz", "simplejson", "mypy (==0.910)", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["sphinx (==4.2.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.7)"]
docs = ["sphinx (==4.3.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.7)"]
lint = ["mypy (==0.910)", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
@ -233,7 +232,7 @@ python-versions = "*"
[[package]]
name = "more-itertools"
version = "8.10.0"
version = "8.12.0"
description = "More routines for operating on iterables, beyond itertools"
category = "main"
optional = false
@ -274,14 +273,14 @@ python-versions = "*"
[[package]]
name = "packaging"
version = "21.0"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2"
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "parsimonious"
@ -340,11 +339,11 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.10.0"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycodestyle"
@ -364,11 +363,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
version = "2.4.7"
version = "3.0.6"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pyprojroot"
@ -411,14 +413,6 @@ python-versions = "*"
dev = ["check-manifest", "nose"]
test = ["coverage", "nose"]
[[package]]
name = "regex"
version = "2021.10.23"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "rope"
version = "0.17.0"
@ -440,7 +434,7 @@ python-versions = "*"
[[package]]
name = "simplejson"
version = "3.17.5"
version = "3.17.6"
description = "Simple, fast, extensible JSON encoder/decoder for Python"
category = "main"
optional = false
@ -472,7 +466,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "1.2.1"
version = "1.2.3"
description = "A lil' TOML parser"
category = "dev"
optional = false
@ -480,7 +474,7 @@ python-versions = ">=3.6"
[[package]]
name = "typeguard"
version = "2.13.0"
version = "2.13.3"
description = "Run-time type checker for Python"
category = "main"
optional = false
@ -492,7 +486,7 @@ test = ["pytest", "typing-extensions", "mypy"]
[[package]]
name = "types-simplejson"
version = "3.17.1"
version = "3.17.2"
description = "Typing stubs for simplejson"
category = "dev"
optional = false
@ -508,11 +502,11 @@ python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
version = "4.0.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
python-versions = "*"
python-versions = ">=3.6"
[[package]]
name = "typing-inspect"
@ -529,7 +523,7 @@ typing-extensions = ">=3.7.4"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "025ff93ec3b033c89bdaaeab7905dc10ff450ed70695bcbd863a10e6af6fa9ea"
content-hash = "41155bca4070edc9eb6e6369890af55397251caeab4e5c2db62ef7785410853e"
[metadata.files]
atomicwrites = [
@ -548,8 +542,8 @@ autoimport = [
{file = "autoimport-0.7.5.tar.gz", hash = "sha256:327391b21eb6f0ce14ad20af33a55e7a951681f3514bab5b840b9887fc187913"},
]
black = [
{file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
{file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
{file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"},
{file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
@ -571,8 +565,8 @@ flake8 = [
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
]
hypothesis = [
{file = "hypothesis-6.23.4-py3-none-any.whl", hash = "sha256:0996bc15581f8b810af1921a0814b3efee0c095f4f041597512733dac327d23a"},
{file = "hypothesis-6.23.4.tar.gz", hash = "sha256:1a93f81c95bf585cf091866a0daf7765fa489f3b69091ab00e949756718ae872"},
{file = "hypothesis-6.32.1-py3-none-any.whl", hash = "sha256:67e4fcf1da355a8996c3e1ae3e0d4c325cf318f766638a34626bf1cb906a69df"},
{file = "hypothesis-6.32.1.tar.gz", hash = "sha256:7cbd02e78807208a56dce822d39d0d0b43cc3603258d175d22c8b7875683a742"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -583,8 +577,8 @@ isort = [
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
]
marshmallow = [
{file = "marshmallow-3.14.0-py3-none-any.whl", hash = "sha256:6d00e42d6d6289f8cd3e77618a01689d57a078fe324ee579b00fa206d32e9b07"},
{file = "marshmallow-3.14.0.tar.gz", hash = "sha256:bba1a940985c052c5cc7849f97da196ebc81f3b85ec10c56ef1f3228aa9cbe74"},
{file = "marshmallow-3.14.1-py3-none-any.whl", hash = "sha256:04438610bc6dadbdddb22a4a55bcc7f6f8099e69580b2e67f5a681933a1f4400"},
{file = "marshmallow-3.14.1.tar.gz", hash = "sha256:4c05c1684e0e97fe779c62b91878f173b937fe097b356cd82f793464f5bc6138"},
]
marshmallow-dataclass = [
{file = "marshmallow_dataclass-8.5.3-py3-none-any.whl", hash = "sha256:eefeff62ee975c64d293d2db9370e7e748a2ff83dcb5109416b75e087a2ac02e"},
@ -599,8 +593,8 @@ mccabe = [
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
more-itertools = [
{file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"},
{file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"},
{file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"},
{file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"},
]
multidict = [
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"},
@ -706,8 +700,8 @@ mypy-extensions = [
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
parsimonious = [
{file = "parsimonious-0.8.1.tar.gz", hash = "sha256:3add338892d580e0cb3b1a39e4a1b427ff9f687858fdd61097053742391a9f6b"},
@ -729,8 +723,8 @@ pluggy = [
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycodestyle = [
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
@ -741,8 +735,8 @@ pyflakes = [
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
{file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
{file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
]
pyprojroot = [
{file = "pyprojroot-0.2.0-py3-none-any.whl", hash = "sha256:741e8b4878a0d6bb6b06ec09aa05797130289e2127aa595b8f1cbadce697909f"},
@ -755,44 +749,6 @@ pytest = [
python-constraint = [
{file = "python-constraint-1.4.0.tar.bz2", hash = "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e"},
]
regex = [
{file = "regex-2021.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45b65d6a275a478ac2cbd7fdbf7cc93c1982d613de4574b56fd6972ceadb8395"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74d071dbe4b53c602edd87a7476ab23015a991374ddb228d941929ad7c8c922e"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34d870f9f27f2161709054d73646fc9aca49480617a65533fc2b4611c518e455"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fb698037c35109d3c2e30f2beb499e5ebae6e4bb8ff2e60c50b9a805a716f79"},
{file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb46b542133999580ffb691baf67410306833ee1e4f58ed06b6a7aaf4e046952"},
{file = "regex-2021.10.23-cp310-cp310-win32.whl", hash = "sha256:5e9c9e0ce92f27cef79e28e877c6b6988c48b16942258f3bc55d39b5f911df4f"},
{file = "regex-2021.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ab7c5684ff3538b67df3f93d66bd3369b749087871ae3786e70ef39e601345b0"},
{file = "regex-2021.10.23-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de557502c3bec8e634246588a94e82f1ee1b9dfcfdc453267c4fb652ff531570"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee684f139c91e69fe09b8e83d18b4d63bf87d9440c1eb2eeb52ee851883b1b29"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5095a411c8479e715784a0c9236568ae72509450ee2226b649083730f3fadfc6"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b568809dca44cb75c8ebb260844ea98252c8c88396f9d203f5094e50a70355f"},
{file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eb672217f7bd640411cfc69756ce721d00ae600814708d35c930930f18e8029f"},
{file = "regex-2021.10.23-cp36-cp36m-win32.whl", hash = "sha256:a7a986c45d1099a5de766a15de7bee3840b1e0e1a344430926af08e5297cf666"},
{file = "regex-2021.10.23-cp36-cp36m-win_amd64.whl", hash = "sha256:6d7722136c6ed75caf84e1788df36397efdc5dbadab95e59c2bba82d4d808a4c"},
{file = "regex-2021.10.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f665677e46c5a4d288ece12fdedf4f4204a422bb28ff05f0e6b08b7447796d1"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:450dc27483548214314640c89a0f275dbc557968ed088da40bde7ef8fb52829e"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:129472cd06062fb13e7b4670a102951a3e655e9b91634432cfbdb7810af9d710"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a940ca7e7189d23da2bfbb38973832813eab6bd83f3bf89a977668c2f813deae"},
{file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:530fc2bbb3dc1ebb17f70f7b234f90a1dd43b1b489ea38cea7be95fb21cdb5c7"},
{file = "regex-2021.10.23-cp37-cp37m-win32.whl", hash = "sha256:ded0c4a3eee56b57fcb2315e40812b173cafe79d2f992d50015f4387445737fa"},
{file = "regex-2021.10.23-cp37-cp37m-win_amd64.whl", hash = "sha256:391703a2abf8013d95bae39145d26b4e21531ab82e22f26cd3a181ee2644c234"},
{file = "regex-2021.10.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be04739a27be55631069b348dda0c81d8ea9822b5da10b8019b789e42d1fe452"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13ec99df95003f56edcd307db44f06fbeb708c4ccdcf940478067dd62353181e"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d1cdcda6bd16268316d5db1038965acf948f2a6f43acc2e0b1641ceab443623"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c186691a7995ef1db61205e00545bf161fb7b59cdb8c1201c89b333141c438a"},
{file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b20f544cbbeffe171911f6ce90388ad36fe3fad26b7c7a35d4762817e9ea69c"},
{file = "regex-2021.10.23-cp38-cp38-win32.whl", hash = "sha256:c0938ddd60cc04e8f1faf7a14a166ac939aac703745bfcd8e8f20322a7373019"},
{file = "regex-2021.10.23-cp38-cp38-win_amd64.whl", hash = "sha256:56f0c81c44638dfd0e2367df1a331b4ddf2e771366c4b9c5d9a473de75e3e1c7"},
{file = "regex-2021.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80bb5d2e92b2258188e7dcae5b188c7bf868eafdf800ea6edd0fbfc029984a88"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1dae12321b31059a1a72aaa0e6ba30156fe7e633355e445451e4021b8e122b6"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f2b59c28afc53973d22e7bc18428721ee8ca6079becf1b36571c42627321c65"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d134757a37d8640f3c0abb41f5e68b7cf66c644f54ef1cb0573b7ea1c63e1509"},
{file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0dcc0e71118be8c69252c207630faf13ca5e1b8583d57012aae191e7d6d28b84"},
{file = "regex-2021.10.23-cp39-cp39-win32.whl", hash = "sha256:a30513828180264294953cecd942202dfda64e85195ae36c265daf4052af0464"},
{file = "regex-2021.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:0f7552429dd39f70057ac5d0e897e5bfe211629652399a21671e53f2a9693a4e"},
{file = "regex-2021.10.23.tar.gz", hash = "sha256:f3f9a91d3cc5e5b0ddf1043c0ae5fa4852f18a1c0050318baf5fc7930ecc1f9c"},
]
rope = [
{file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"},
]
@ -801,52 +757,67 @@ sh = [
{file = "sh-1.14.2.tar.gz", hash = "sha256:9d7bd0334d494b2a4609fe521b2107438cdb21c0e469ffeeb191489883d6fe0d"},
]
simplejson = [
{file = "simplejson-3.17.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:376023f51edaf7290332dacfb055bc00ce864cb013c0338d0dea48731f37e42f"},
{file = "simplejson-3.17.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b2a5688606dffbe95e1347a05b77eb90489fe337edde888e23bbb7fd81b0d93b"},
{file = "simplejson-3.17.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3ba82f8b421886f4a2311c43fb98faaf36c581976192349fef2a89ed0fcdbdef"},
{file = "simplejson-3.17.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7332f7b06d42153255f7bfeb10266141c08d48cc1a022a35473c95238ff2aebc"},
{file = "simplejson-3.17.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c2d5334d935af711f6d6dfeec2d34e071cdf73ec0df8e8bd35ac435b26d8da97"},
{file = "simplejson-3.17.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:417b7e119d66085dc45bdd563dcb2c575ee10a3b1c492dd3502a029448d4be1c"},
{file = "simplejson-3.17.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:42b7c7264229860fe879be961877f7466d9f7173bd6427b3ba98144a031d49fb"},
{file = "simplejson-3.17.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5fe8c6dcb9e6f7066bdc07d3c410a2fca78c0d0b4e0e72510ffd20a60a20eb8e"},
{file = "simplejson-3.17.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:b92fbc2bc549c5045c8233d954f3260ccf99e0f3ec9edfd2372b74b350917752"},
{file = "simplejson-3.17.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f7f53b1edd4b23fb112b89208377480c0bcee45d43a03ffacf30f3290e0ed85"},
{file = "simplejson-3.17.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40ece8fa730d1a947bff792bcc7824bd02d3ce6105432798e9a04a360c8c07b0"},
{file = "simplejson-3.17.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10defa88dd10a0a4763f16c1b5504e96ae6dc68953cfe5fc572b4a8fcaf9409b"},
{file = "simplejson-3.17.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa86cfdeb118795875855589934013e32895715ec2d9e8eb7a59be3e7e07a7e1"},
{file = "simplejson-3.17.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce66f730031b9b3683b2fc6ad4160a18db86557c004c3d490a29bf8d450d7ab9"},
{file = "simplejson-3.17.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:352c11582aa1e49a2f0f7f7d8fd5ec5311da890d1354287e83c63ab6af857cf5"},
{file = "simplejson-3.17.5-cp310-cp310-win32.whl", hash = "sha256:8e595de17178dd3bbeb2c5b8ea97536341c63b7278639cb8ee2681a84c0ef037"},
{file = "simplejson-3.17.5-cp310-cp310-win_amd64.whl", hash = "sha256:cb0afc3bad49eb89a579103616574a54b523856d20fc539a4f7a513a0a8ba4b2"},
{file = "simplejson-3.17.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ade09aa3c284d11f39640aebdcbb748e1996f0c60504f8c4a0c5a9fec821e67a"},
{file = "simplejson-3.17.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87572213965fd8a4fb7a97f837221e01d8fddcfb558363c671b8aa93477fb6a2"},
{file = "simplejson-3.17.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2b59acd09b02da97728d0bae8ff48876d7efcbbb08e569c55e2d0c2e018324f5"},
{file = "simplejson-3.17.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e29b9cea4216ec130df85d8c36efb9985fda1c9039e4706fb30e0fb6a67602ff"},
{file = "simplejson-3.17.5-cp36-cp36m-win32.whl", hash = "sha256:f550730d18edec4ff9d4252784b62adfe885d4542946b6d5a54c8a6521b56afd"},
{file = "simplejson-3.17.5-cp36-cp36m-win_amd64.whl", hash = "sha256:1c2688365743b0f190392e674af5e313ebe9d621813d15f9332e874b7c1f2d04"},
{file = "simplejson-3.17.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f13c48cc4363829bdfecc0c181b6ddf28008931de54908a492dc8ccd0066cd60"},
{file = "simplejson-3.17.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a6943816e10028eeed512ea03be52b54ea83108b408d1049b999f58a760089b"},
{file = "simplejson-3.17.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3d72aa9e73134dacd049a2d6f9bd219f7be9c004d03d52395831611d66cedb71"},
{file = "simplejson-3.17.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b94df70bd34a3b946c0eb272022fb0f8a9eb27cad76e7f313fedbee2ebe4317"},
{file = "simplejson-3.17.5-cp37-cp37m-win32.whl", hash = "sha256:065230b9659ac38c8021fa512802562d122afb0cf8d4b89e257014dcddb5730a"},
{file = "simplejson-3.17.5-cp37-cp37m-win_amd64.whl", hash = "sha256:86fcffc06f1125cb443e2bed812805739d64ceb78597ac3c1b2d439471a09717"},
{file = "simplejson-3.17.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78c6f0ed72b440ebe1892d273c1e5f91e55e6861bea611d3b904e673152a7a4c"},
{file = "simplejson-3.17.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:36b08b886027eac67e7a0e822e3a5bf419429efad7612e69501669d6252a21f2"},
{file = "simplejson-3.17.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fe1c33f78d2060719d52ea9459d97d7ae3a5b707ec02548575c4fbed1d1d345b"},
{file = "simplejson-3.17.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:140eb58809f24d843736edb8080b220417e22c82ac07a3dfa473f57e78216b5f"},
{file = "simplejson-3.17.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c9b30a2524ae6983b708f12741a31fbc2fb8d6fecd0b6c8584a62fd59f59e09"},
{file = "simplejson-3.17.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24e413bd845bd17d4d72063d64e053898543fb7abc81afeae13e5c43cef9c171"},
{file = "simplejson-3.17.5-cp38-cp38-win32.whl", hash = "sha256:5f5051a13e7d53430a990604b532c9124253c5f348857e2d5106d45fc8533860"},
{file = "simplejson-3.17.5-cp38-cp38-win_amd64.whl", hash = "sha256:188f2c78a8ac1eb7a70a4b2b7b9ad11f52181044957bf981fb3e399c719e30ee"},
{file = "simplejson-3.17.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:457d9cfe7ece1571770381edccdad7fc255b12cd7b5b813219441146d4f47595"},
{file = "simplejson-3.17.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa843ee0d34c7193f5a816e79df8142faff851549cab31e84b526f04878ac778"},
{file = "simplejson-3.17.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2cc4b68e59319e3de778325e34fbff487bfdb2225530e89995402989898d681"},
{file = "simplejson-3.17.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90d2e219c3dce1500dda95f5b893c293c4d53c4e330c968afbd4e7a90ff4a5b"},
{file = "simplejson-3.17.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:917f01db71d5e720b731effa3ff4a2c702a1b6dacad9bcdc580d86a018dfc3ca"},
{file = "simplejson-3.17.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:07707ba69324eaf58f0c6f59d289acc3e0ed9ec528dae5b0d4219c0d6da27dc5"},
{file = "simplejson-3.17.5-cp39-cp39-win32.whl", hash = "sha256:2df15814529a4625ea6f7b354a083609b3944c269b954ece0d0e7455872e1b2a"},
{file = "simplejson-3.17.5-cp39-cp39-win_amd64.whl", hash = "sha256:71a54815ec0212b0cba23adc1b2a731bdd2df7b9e4432718b2ed20e8aaf7f01a"},
{file = "simplejson-3.17.5.tar.gz", hash = "sha256:91cfb43fb91ff6d1e4258be04eee84b51a4ef40a28d899679b9ea2556322fb50"},
{file = "simplejson-3.17.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a89acae02b2975b1f8e4974cb8cdf9bf9f6c91162fb8dec50c259ce700f2770a"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:82ff356ff91be0ab2293fc6d8d262451eb6ac4fd999244c4b5f863e049ba219c"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0de783e9c2b87bdd75b57efa2b6260c24b94605b5c9843517577d40ee0c3cc8a"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:d24a9e61df7a7787b338a58abfba975414937b609eb6b18973e25f573bc0eeeb"},
{file = "simplejson-3.17.6-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e8603e691580487f11306ecb066c76f1f4a8b54fb3bdb23fa40643a059509366"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9b01e7b00654115965a206e3015f0166674ec1e575198a62a977355597c0bef5"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:37bc0cf0e5599f36072077e56e248f3336917ded1d33d2688624d8ed3cefd7d2"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cf6e7d5fe2aeb54898df18db1baf479863eae581cce05410f61f6b4188c8ada1"},
{file = "simplejson-3.17.6-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bdfc54b4468ed4cd7415928cbe782f4d782722a81aeb0f81e2ddca9932632211"},
{file = "simplejson-3.17.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd16302d39c4d6f4afde80edd0c97d4db643327d355a312762ccd9bd2ca515ed"},
{file = "simplejson-3.17.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:deac4bdafa19bbb89edfb73b19f7f69a52d0b5bd3bb0c4ad404c1bbfd7b4b7fd"},
{file = "simplejson-3.17.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8bbdb166e2fb816e43ab034c865147edafe28e1b19c72433147789ac83e2dda"},
{file = "simplejson-3.17.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7854326920d41c3b5d468154318fe6ba4390cb2410480976787c640707e0180"},
{file = "simplejson-3.17.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04e31fa6ac8e326480703fb6ded1488bfa6f1d3f760d32e29dbf66d0838982ce"},
{file = "simplejson-3.17.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f63600ec06982cdf480899026f4fda622776f5fabed9a869fdb32d72bc17e99a"},
{file = "simplejson-3.17.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e03c3b8cc7883a54c3f34a6a135c4a17bc9088a33f36796acdb47162791b02f6"},
{file = "simplejson-3.17.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a2d30d6c1652140181dc6861f564449ad71a45e4f165a6868c27d36745b65d40"},
{file = "simplejson-3.17.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1aa6e4cae8e3b8d5321be4f51c5ce77188faf7baa9fe1e78611f93a8eed2882"},
{file = "simplejson-3.17.6-cp310-cp310-win32.whl", hash = "sha256:97202f939c3ff341fc3fa84d15db86156b1edc669424ba20b0a1fcd4a796a045"},
{file = "simplejson-3.17.6-cp310-cp310-win_amd64.whl", hash = "sha256:80d3bc9944be1d73e5b1726c3bbfd2628d3d7fe2880711b1eb90b617b9b8ac70"},
{file = "simplejson-3.17.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9fa621b3c0c05d965882c920347b6593751b7ab20d8fa81e426f1735ca1a9fc7"},
{file = "simplejson-3.17.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2fb11922f58df8528adfca123f6a84748ad17d066007e7ac977720063556bd"},
{file = "simplejson-3.17.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:724c1fe135aa437d5126138d977004d165a3b5e2ee98fc4eb3e7c0ef645e7e27"},
{file = "simplejson-3.17.6-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4ff4ac6ff3aa8f814ac0f50bf218a2e1a434a17aafad4f0400a57a8cc62ef17f"},
{file = "simplejson-3.17.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:67093a526e42981fdd954868062e56c9b67fdd7e712616cc3265ad0c210ecb51"},
{file = "simplejson-3.17.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b4af7ad7e4ac515bc6e602e7b79e2204e25dbd10ab3aa2beef3c5a9cad2c7"},
{file = "simplejson-3.17.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:1c9b1ed7ed282b36571638297525f8ef80f34b3e2d600a56f962c6044f24200d"},
{file = "simplejson-3.17.6-cp36-cp36m-win32.whl", hash = "sha256:632ecbbd2228575e6860c9e49ea3cc5423764d5aa70b92acc4e74096fb434044"},
{file = "simplejson-3.17.6-cp36-cp36m-win_amd64.whl", hash = "sha256:4c09868ddb86bf79b1feb4e3e7e4a35cd6e61ddb3452b54e20cf296313622566"},
{file = "simplejson-3.17.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b6bd8144f15a491c662f06814bd8eaa54b17f26095bb775411f39bacaf66837"},
{file = "simplejson-3.17.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5decdc78849617917c206b01e9fc1d694fd58caa961be816cb37d3150d613d9a"},
{file = "simplejson-3.17.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:521877c7bd060470806eb6335926e27453d740ac1958eaf0d8c00911bc5e1802"},
{file = "simplejson-3.17.6-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:65b998193bd7b0c7ecdfffbc825d808eac66279313cb67d8892bb259c9d91494"},
{file = "simplejson-3.17.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac786f6cb7aa10d44e9641c7a7d16d7f6e095b138795cd43503769d4154e0dc2"},
{file = "simplejson-3.17.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3ff5b3464e1ce86a8de8c88e61d4836927d5595c2162cab22e96ff551b916e81"},
{file = "simplejson-3.17.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:69bd56b1d257a91e763256d63606937ae4eb890b18a789b66951c00062afec33"},
{file = "simplejson-3.17.6-cp37-cp37m-win32.whl", hash = "sha256:b81076552d34c27e5149a40187a8f7e2abb2d3185576a317aaf14aeeedad862a"},
{file = "simplejson-3.17.6-cp37-cp37m-win_amd64.whl", hash = "sha256:07ecaafc1b1501f275bf5acdee34a4ad33c7c24ede287183ea77a02dc071e0c0"},
{file = "simplejson-3.17.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:068670af975247acbb9fc3d5393293368cda17026db467bf7a51548ee8f17ee1"},
{file = "simplejson-3.17.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4d1c135af0c72cb28dd259cf7ba218338f4dc027061262e46fe058b4e6a4c6a3"},
{file = "simplejson-3.17.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23fe704da910ff45e72543cbba152821685a889cf00fc58d5c8ee96a9bad5f94"},
{file = "simplejson-3.17.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f444762fed1bc1fd75187ef14a20ed900c1fbb245d45be9e834b822a0223bc81"},
{file = "simplejson-3.17.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:681eb4d37c9a9a6eb9b3245a5e89d7f7b2b9895590bb08a20aa598c1eb0a1d9d"},
{file = "simplejson-3.17.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e8607d8f6b4f9d46fee11447e334d6ab50e993dd4dbfb22f674616ce20907ab"},
{file = "simplejson-3.17.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b10556817f09d46d420edd982dd0653940b90151d0576f09143a8e773459f6fe"},
{file = "simplejson-3.17.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e1ec8a9ee0987d4524ffd6299e778c16cc35fef6d1a2764e609f90962f0b293a"},
{file = "simplejson-3.17.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b4126cac7d69ac06ff22efd3e0b3328a4a70624fcd6bca4fc1b4e6d9e2e12bf"},
{file = "simplejson-3.17.6-cp38-cp38-win32.whl", hash = "sha256:35a49ebef25f1ebdef54262e54ae80904d8692367a9f208cdfbc38dbf649e00a"},
{file = "simplejson-3.17.6-cp38-cp38-win_amd64.whl", hash = "sha256:743cd768affaa508a21499f4858c5b824ffa2e1394ed94eb85caf47ac0732198"},
{file = "simplejson-3.17.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb62d517a516128bacf08cb6a86ecd39fb06d08e7c4980251f5d5601d29989ba"},
{file = "simplejson-3.17.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:12133863178a8080a3dccbf5cb2edfab0001bc41e5d6d2446af2a1131105adfe"},
{file = "simplejson-3.17.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5540fba2d437edaf4aa4fbb80f43f42a8334206ad1ad3b27aef577fd989f20d9"},
{file = "simplejson-3.17.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d74ee72b5071818a1a5dab47338e87f08a738cb938a3b0653b9e4d959ddd1fd9"},
{file = "simplejson-3.17.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28221620f4dcabdeac310846629b976e599a13f59abb21616356a85231ebd6ad"},
{file = "simplejson-3.17.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b09bc62e5193e31d7f9876220fb429ec13a6a181a24d897b9edfbbdbcd678851"},
{file = "simplejson-3.17.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7255a37ff50593c9b2f1afa8fafd6ef5763213c1ed5a9e2c6f5b9cc925ab979f"},
{file = "simplejson-3.17.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:401d40969cee3df7bda211e57b903a534561b77a7ade0dd622a8d1a31eaa8ba7"},
{file = "simplejson-3.17.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a649d0f66029c7eb67042b15374bd93a26aae202591d9afd71e111dd0006b198"},
{file = "simplejson-3.17.6-cp39-cp39-win32.whl", hash = "sha256:522fad7be85de57430d6d287c4b635813932946ebf41b913fe7e880d154ade2e"},
{file = "simplejson-3.17.6-cp39-cp39-win_amd64.whl", hash = "sha256:3fe87570168b2ae018391e2b43fbf66e8593a86feccb4b0500d134c998983ccc"},
{file = "simplejson-3.17.6.tar.gz", hash = "sha256:cf98038d2abf63a1ada5730e91e84c642ba6c225b0198c3684151b1f80c5f8a6"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@ -861,25 +832,24 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
{file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
{file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
]
typeguard = [
{file = "typeguard-2.13.0-py3-none-any.whl", hash = "sha256:0bc44d1ff865b522eda969627868b0e001c8329296ce50aededbea03febc79ee"},
{file = "typeguard-2.13.0.tar.gz", hash = "sha256:04e38f92eb59410c9375d3be23df65e0a7643f2e8bcbd421423d808d2f9e99df"},
{file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"},
{file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"},
]
types-simplejson = [
{file = "types-simplejson-3.17.1.tar.gz", hash = "sha256:310ef5addfe97c20b9f2cac85079cbab95fd123c2c9a5c6b99856075d6b48211"},
{file = "types_simplejson-3.17.1-py3-none-any.whl", hash = "sha256:7b5ac5549f5269f25e5e932d6f4e96ea8397369d6031300161cd1fda484e2534"},
{file = "types-simplejson-3.17.2.tar.gz", hash = "sha256:37ee5a1e30c69196ab52672664509dc40b9c2fed7dacdb5587701e0b768b6bfb"},
{file = "types_simplejson-3.17.2-py3-none-any.whl", hash = "sha256:a1ea755d518bb87038c7a2aaefc77d3ad43976dee5566dfd6ca5aa5758ec7a0f"},
]
types-toml = [
{file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"},
{file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
]
typing-inspect = [
{file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"},

View File

@ -19,7 +19,7 @@ sortedcontainers = "^2.3.0"
python-constraint = "^1.4.0"
construct = "~=2.10"
construct-typing = "^0.4.2"
marshmallow-dataclass = {extras = ["union", "enum"], version = "^8.4.1"}
marshmallow-dataclass = {extras = ["enum", "union"], version = "^8.5.3"}
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"