From 82b03688aa8ed66bec07ff600a5210c3999a97f5 Mon Sep 17 00:00:00 2001 From: Stepland <16676308+Stepland@users.noreply.github.com> Date: Mon, 27 Dec 2021 02:05:27 +0100 Subject: [PATCH] Add support for memon v1.0.0 --- .flake8 | 2 - CHANGELOG.md | 8 + jubeatools/cli/tests/test_cli.py | 2 + jubeatools/formats/__init__.py | 2 +- jubeatools/formats/enum.py | 1 + jubeatools/formats/format_names.py | 16 + jubeatools/formats/guess.py | 4 +- .../formats/jubeat_analyser/dump_tools.py | 2 +- .../formats/jubeat_analyser/memo/load.py | 3 +- .../formats/jubeat_analyser/memo1/load.py | 3 +- .../formats/jubeat_analyser/memo2/load.py | 3 +- .../jubeat_analyser/mono_column/load.py | 3 +- jubeatools/formats/konami/eve/dump.py | 2 +- jubeatools/formats/konami/eve/load.py | 3 +- jubeatools/formats/konami/jbsq/dump.py | 2 +- jubeatools/formats/konami/jbsq/load.py | 3 +- .../{formats.py => loaders_and_dumpers.py} | 2 + jubeatools/formats/malody/dump.py | 14 +- jubeatools/formats/malody/load.py | 4 +- .../formats/malody/tests/test_malody.py | 2 +- jubeatools/formats/memon/__init__.py | 6 +- jubeatools/formats/memon/tools.py | 46 ++ jubeatools/formats/memon/v0.py | 596 ------------------ jubeatools/formats/memon/v0/__init__.py | 0 jubeatools/formats/memon/v0/dump.py | 269 ++++++++ jubeatools/formats/memon/v0/load.py | 168 +++++ jubeatools/formats/memon/v0/schema.py | 131 ++++ .../memon/{test_memon.py => v0/test_v0.py} | 15 - jubeatools/formats/memon/v1/__init__.py | 0 jubeatools/formats/memon/v1/dump.py | 185 ++++++ jubeatools/formats/memon/v1/load.py | 157 +++++ jubeatools/formats/memon/v1/schema.py | 186 ++++++ jubeatools/formats/memon/v1/tests/__init__.py | 0 jubeatools/formats/memon/v1/tests/test_v1.py | 53 ++ jubeatools/formats/typing.py | 10 +- jubeatools/song.py | 153 ++++- jubeatools/testutils/strategies.py | 21 +- jubeatools/testutils/test_patterns.py | 4 + poetry.lock | 252 ++++---- pyproject.toml | 2 +- 40 files changed, 1526 insertions(+), 809 deletions(-) create mode 100644 jubeatools/formats/format_names.py rename jubeatools/formats/{formats.py => loaders_and_dumpers.py} (92%) create mode 100644 jubeatools/formats/memon/tools.py delete mode 100644 jubeatools/formats/memon/v0.py create mode 100644 jubeatools/formats/memon/v0/__init__.py create mode 100644 jubeatools/formats/memon/v0/dump.py create mode 100644 jubeatools/formats/memon/v0/load.py create mode 100644 jubeatools/formats/memon/v0/schema.py rename jubeatools/formats/memon/{test_memon.py => v0/test_v0.py} (82%) create mode 100644 jubeatools/formats/memon/v1/__init__.py create mode 100644 jubeatools/formats/memon/v1/dump.py create mode 100644 jubeatools/formats/memon/v1/load.py create mode 100644 jubeatools/formats/memon/v1/schema.py create mode 100644 jubeatools/formats/memon/v1/tests/__init__.py create mode 100644 jubeatools/formats/memon/v1/tests/test_v1.py diff --git a/.flake8 b/.flake8 index 4295e73..d26e936 100644 --- a/.flake8 +++ b/.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 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1595cfb..88c948d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/jubeatools/cli/tests/test_cli.py b/jubeatools/cli/tests/test_cli.py index a11cbba..7ad93e1 100644 --- a/jubeatools/cli/tests/test_cli.py +++ b/jubeatools/cli/tests/test_cli.py @@ -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 diff --git a/jubeatools/formats/__init__.py b/jubeatools/formats/__init__.py index 1494926..56e6d1c 100644 --- a/jubeatools/formats/__init__.py +++ b/jubeatools/formats/__init__.py @@ -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 diff --git a/jubeatools/formats/enum.py b/jubeatools/formats/enum.py index 96fb76a..b4b198a 100644 --- a/jubeatools/formats/enum.py +++ b/jubeatools/formats/enum.py @@ -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" diff --git a/jubeatools/formats/format_names.py b/jubeatools/formats/format_names.py new file mode 100644 index 0000000..b4b198a --- /dev/null +++ b/jubeatools/formats/format_names.py @@ -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" diff --git a/jubeatools/formats/guess.py b/jubeatools/formats/guess.py index 689400c..fc4821c 100644 --- a/jubeatools/formats/guess.py +++ b/jubeatools/formats/guess.py @@ -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}") diff --git a/jubeatools/formats/jubeat_analyser/dump_tools.py b/jubeatools/formats/jubeat_analyser/dump_tools.py index cdfcc16..a0a649d 100644 --- a/jubeatools/formats/jubeat_analyser/dump_tools.py +++ b/jubeatools/formats/jubeat_analyser/dump_tools.py @@ -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, diff --git a/jubeatools/formats/jubeat_analyser/memo/load.py b/jubeatools/formats/jubeat_analyser/memo/load.py index a83d2df..498e5c7 100644 --- a/jubeatools/formats/jubeat_analyser/memo/load.py +++ b/jubeatools/formats/jubeat_analyser/memo/load.py @@ -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) diff --git a/jubeatools/formats/jubeat_analyser/memo1/load.py b/jubeatools/formats/jubeat_analyser/memo1/load.py index c83bb05..2667c26 100644 --- a/jubeatools/formats/jubeat_analyser/memo1/load.py +++ b/jubeatools/formats/jubeat_analyser/memo1/load.py @@ -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) diff --git a/jubeatools/formats/jubeat_analyser/memo2/load.py b/jubeatools/formats/jubeat_analyser/memo2/load.py index 1c0d25c..ec48ffe 100644 --- a/jubeatools/formats/jubeat_analyser/memo2/load.py +++ b/jubeatools/formats/jubeat_analyser/memo2/load.py @@ -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) diff --git a/jubeatools/formats/jubeat_analyser/mono_column/load.py b/jubeatools/formats/jubeat_analyser/mono_column/load.py index fa42a6f..98f63ae 100644 --- a/jubeatools/formats/jubeat_analyser/mono_column/load.py +++ b/jubeatools/formats/jubeat_analyser/mono_column/load.py @@ -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: diff --git a/jubeatools/formats/konami/eve/dump.py b/jubeatools/formats/konami/eve/dump.py index c2a99c7..7f7aca3 100644 --- a/jubeatools/formats/konami/eve/dump.py +++ b/jubeatools/formats/konami/eve/dump.py @@ -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") diff --git a/jubeatools/formats/konami/eve/load.py b/jubeatools/formats/konami/eve/load.py index 536588a..d42e738 100644 --- a/jubeatools/formats/konami/eve/load.py +++ b/jubeatools/formats/konami/eve/load.py @@ -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]: diff --git a/jubeatools/formats/konami/jbsq/dump.py b/jubeatools/formats/konami/jbsq/dump.py index 4638490..c83a678 100644 --- a/jubeatools/formats/konami/jbsq/dump.py +++ b/jubeatools/formats/konami/jbsq/dump.py @@ -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) diff --git a/jubeatools/formats/konami/jbsq/load.py b/jubeatools/formats/konami/jbsq/load.py index 8617055..e7ad02c 100644 --- a/jubeatools/formats/konami/jbsq/load.py +++ b/jubeatools/formats/konami/jbsq/load.py @@ -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: diff --git a/jubeatools/formats/formats.py b/jubeatools/formats/loaders_and_dumpers.py similarity index 92% rename from jubeatools/formats/formats.py rename to jubeatools/formats/loaders_and_dumpers.py index b43a487..7aa613c 100644 --- a/jubeatools/formats/formats.py +++ b/jubeatools/formats/loaders_and_dumpers.py @@ -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, diff --git a/jubeatools/formats/malody/dump.py b/jubeatools/formats/malody/dump.py index cffa553..bc1fefa 100644 --- a/jubeatools/formats/malody/dump.py +++ b/jubeatools/formats/malody/dump.py @@ -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 ( diff --git a/jubeatools/formats/malody/load.py b/jubeatools/formats/malody/load.py index 2ddf8d3..dadc4ff 100644 --- a/jubeatools/formats/malody/load.py +++ b/jubeatools/formats/malody/load.py @@ -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: diff --git a/jubeatools/formats/malody/tests/test_malody.py b/jubeatools/formats/malody/tests/test_malody.py index c1d714a..7bafd5e 100644 --- a/jubeatools/formats/malody/tests/test_malody.py +++ b/jubeatools/formats/malody/tests/test_malody.py @@ -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) diff --git a/jubeatools/formats/memon/__init__.py b/jubeatools/formats/memon/__init__.py index 2b4782f..9ec9408 100644 --- a/jubeatools/formats/memon/__init__.py +++ b/jubeatools/formats/memon/__init__.py @@ -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 diff --git a/jubeatools/formats/memon/tools.py b/jubeatools/formats/memon/tools.py new file mode 100644 index 0000000..6ea2aaf --- /dev/null +++ b/jubeatools/formats/memon/tools.py @@ -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 diff --git a/jubeatools/formats/memon/v0.py b/jubeatools/formats/memon/v0.py deleted file mode 100644 index c050acd..0000000 --- a/jubeatools/formats/memon/v0.py +++ /dev/null @@ -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) diff --git a/jubeatools/formats/memon/v0/__init__.py b/jubeatools/formats/memon/v0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jubeatools/formats/memon/v0/dump.py b/jubeatools/formats/memon/v0/dump.py new file mode 100644 index 0000000..1182576 --- /dev/null +++ b/jubeatools/formats/memon/v0/dump.py @@ -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) diff --git a/jubeatools/formats/memon/v0/load.py b/jubeatools/formats/memon/v0/load.py new file mode 100644 index 0000000..180de09 --- /dev/null +++ b/jubeatools/formats/memon/v0/load.py @@ -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) diff --git a/jubeatools/formats/memon/v0/schema.py b/jubeatools/formats/memon/v0/schema.py new file mode 100644 index 0000000..16c02ee --- /dev/null +++ b/jubeatools/formats/memon/v0/schema.py @@ -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 + ) diff --git a/jubeatools/formats/memon/test_memon.py b/jubeatools/formats/memon/v0/test_v0.py similarity index 82% rename from jubeatools/formats/memon/test_memon.py rename to jubeatools/formats/memon/v0/test_v0.py index 6a9b1b9..54fe1d7 100644 --- a/jubeatools/formats/memon/test_memon.py +++ b/jubeatools/formats/memon/v0/test_v0.py @@ -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")), - ), - ), ) ) diff --git a/jubeatools/formats/memon/v1/__init__.py b/jubeatools/formats/memon/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jubeatools/formats/memon/v1/dump.py b/jubeatools/formats/memon/v1/dump.py new file mode 100644 index 0000000..1778586 --- /dev/null +++ b/jubeatools/formats/memon/v1/dump.py @@ -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) diff --git a/jubeatools/formats/memon/v1/load.py b/jubeatools/formats/memon/v1/load.py new file mode 100644 index 0000000..7514e7e --- /dev/null +++ b/jubeatools/formats/memon/v1/load.py @@ -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) diff --git a/jubeatools/formats/memon/v1/schema.py b/jubeatools/formats/memon/v1/schema.py new file mode 100644 index 0000000..18fd88b --- /dev/null +++ b/jubeatools/formats/memon/v1/schema.py @@ -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)() diff --git a/jubeatools/formats/memon/v1/tests/__init__.py b/jubeatools/formats/memon/v1/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jubeatools/formats/memon/v1/tests/test_v1.py b/jubeatools/formats/memon/v1/tests/test_v1.py new file mode 100644 index 0000000..1dfe907 --- /dev/null +++ b/jubeatools/formats/memon/v1/tests/test_v1.py @@ -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) diff --git a/jubeatools/formats/typing.py b/jubeatools/formats/typing.py index 53249c4..a1b2eff 100644 --- a/jubeatools/formats/typing.py +++ b/jubeatools/formats/typing.py @@ -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. diff --git a/jubeatools/song.py b/jubeatools/song.py index fdfcb1e..fbc905b 100644 --- a/jubeatools/song.py +++ b/jubeatools/song.py @@ -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: diff --git a/jubeatools/testutils/strategies.py b/jubeatools/testutils/strategies.py index 2c40d4c..eab8f8b 100644 --- a/jubeatools/testutils/strategies.py +++ b/jubeatools/testutils/strategies.py @@ -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), ) diff --git a/jubeatools/testutils/test_patterns.py b/jubeatools/testutils/test_patterns.py index 4c73d7c..2a43686 100644 --- a/jubeatools/testutils/test_patterns.py +++ b/jubeatools/testutils/test_patterns.py @@ -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 diff --git a/poetry.lock b/poetry.lock index 04c7311..23b30bd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index fa58427..7eb4d7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"