Merge pull request #20 from Stepland/memon-1-support
Add support for memon v1.0.0
This commit is contained in:
commit
1ce5c75969
2
.flake8
2
.flake8
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
16
jubeatools/formats/format_names.py
Normal file
16
jubeatools/formats/format_names.py
Normal 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"
|
@ -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}")
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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]:
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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,
|
@ -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 (
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
46
jubeatools/formats/memon/tools.py
Normal file
46
jubeatools/formats/memon/tools.py
Normal 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
|
@ -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)
|
0
jubeatools/formats/memon/v0/__init__.py
Normal file
0
jubeatools/formats/memon/v0/__init__.py
Normal file
269
jubeatools/formats/memon/v0/dump.py
Normal file
269
jubeatools/formats/memon/v0/dump.py
Normal 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)
|
168
jubeatools/formats/memon/v0/load.py
Normal file
168
jubeatools/formats/memon/v0/load.py
Normal 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)
|
131
jubeatools/formats/memon/v0/schema.py
Normal file
131
jubeatools/formats/memon/v0/schema.py
Normal 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
|
||||
)
|
@ -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")),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
0
jubeatools/formats/memon/v1/__init__.py
Normal file
0
jubeatools/formats/memon/v1/__init__.py
Normal file
185
jubeatools/formats/memon/v1/dump.py
Normal file
185
jubeatools/formats/memon/v1/dump.py
Normal 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)
|
157
jubeatools/formats/memon/v1/load.py
Normal file
157
jubeatools/formats/memon/v1/load.py
Normal 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)
|
186
jubeatools/formats/memon/v1/schema.py
Normal file
186
jubeatools/formats/memon/v1/schema.py
Normal 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)()
|
0
jubeatools/formats/memon/v1/tests/__init__.py
Normal file
0
jubeatools/formats/memon/v1/tests/__init__.py
Normal file
53
jubeatools/formats/memon/v1/tests/test_v1.py
Normal file
53
jubeatools/formats/memon/v1/tests/test_v1.py
Normal 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)
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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),
|
||||
)
|
||||
|
@ -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
252
poetry.lock
generated
@ -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"},
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user