1
0
mirror of synced 2024-12-12 06:51:05 +01:00

Merge pull request #13 from Stepland/malody

Malody support
This commit is contained in:
Stepland 2021-05-28 01:15:51 +02:00 committed by GitHub
commit 28208d74b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 555 additions and 59 deletions

View File

@ -1,3 +1,7 @@
# v1.2.0
## Added
- [malody] 🎉 initial malody support !
# v1.1.3
## Fixed
- [jubeat-analyser] All files are read and written in `surrogateescape` error

View File

@ -4,6 +4,7 @@ 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"

View File

@ -12,6 +12,7 @@ from .jubeat_analyser import (
load_mono_column,
)
from .konami import dump_eve, dump_jbsq, load_eve, load_jbsq
from .malody import dump_malody, load_malody
from .memon import (
dump_memon_0_1_0,
dump_memon_0_2_0,
@ -25,6 +26,7 @@ from .typing import Dumper, Loader
LOADERS: Dict[Format, Loader] = {
Format.EVE: load_eve,
Format.JBSQ: load_jbsq,
Format.MALODY: load_malody,
Format.MEMON_LEGACY: load_memon_legacy,
Format.MEMON_0_1_0: load_memon_0_1_0,
Format.MEMON_0_2_0: load_memon_0_2_0,
@ -37,6 +39,7 @@ LOADERS: Dict[Format, Loader] = {
DUMPERS: Dict[Format, Dumper] = {
Format.EVE: dump_eve,
Format.JBSQ: dump_jbsq,
Format.MALODY: dump_malody,
Format.MEMON_LEGACY: dump_memon_legacy,
Format.MEMON_0_1_0: dump_memon_0_1_0,
Format.MEMON_0_2_0: dump_memon_0_2_0,

View File

@ -10,7 +10,7 @@ def guess_format(path: Path) -> Format:
raise ValueError("Can't guess chart format for a folder")
try:
return recognize_memon_version(path)
return recognize_json_formats(path)
except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
pass
@ -28,19 +28,26 @@ def guess_format(path: Path) -> Format:
raise ValueError("Unrecognized file format")
def recognize_memon_version(path: Path) -> Format:
def recognize_json_formats(path: Path) -> Format:
with path.open() as f:
obj = json.load(f)
if not isinstance(obj, dict):
raise ValueError("Top level value is not an object")
if obj.keys() >= {"metadata", "data"}:
return recognize_memon_version(obj)
elif obj.keys() >= {"meta", "time", "note"}:
return Format.MALODY
else:
raise ValueError("Unrecognized file format")
def recognize_memon_version(obj: dict) -> Format:
try:
version = obj["version"]
except KeyError:
return Format.MEMON_LEGACY
except TypeError:
raise ValueError(
"This JSON file is not a correct memon file : the top-level "
"value is not an object"
)
if version == "0.1.0":
return Format.MEMON_0_1_0

View File

@ -8,8 +8,7 @@ from itertools import count
from typing import Iterator, Union
from jubeatools import song
from .timemap import TimeMap
from jubeatools.formats.timemap import TimeMap
AnyNote = Union[song.TapNote, song.LongNote]

View File

@ -6,9 +6,9 @@ from typing import List
from more_itertools import numeric_range
from jubeatools import song
from jubeatools.formats.timemap import TimeMap
from .commons import AnyNote, Command, Event, bpm_to_value, ticks_at_beat
from .timemap import TimeMap
def make_events_from_chart(notes: List[AnyNote], timing: song.Timing) -> List[Event]:

View File

@ -3,7 +3,7 @@ from hypothesis import strategies as st
from jubeatools import song
from jubeatools.formats.konami.commons import EveLong
from jubeatools.formats.konami.timemap import TimeMap
from jubeatools.formats.timemap import TimeMap
from jubeatools.testutils import strategies as jbst

View File

@ -3,7 +3,7 @@ from fractions import Fraction
from hypothesis import given
from jubeatools import song
from jubeatools.formats.konami.timemap import TimeMap
from jubeatools.formats.timemap import TimeMap
from jubeatools.testutils import strategies as jbst
from jubeatools.utils import group_by

View File

@ -5,7 +5,7 @@ from jubeatools.formats import Format
from jubeatools.formats.konami.testutils import eve_compatible_song, open_temp_dir
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
from ..construct import jbsq
from .construct import jbsq
@given(eve_compatible_song())

View File

@ -3,6 +3,7 @@ from typing import Iterable, List
from jubeatools import song
from jubeatools.formats.load_tools import round_beats
from jubeatools.formats.timemap import BPMAtSecond, TimeMap
from jubeatools.utils import group_by
from .commons import (
@ -14,7 +15,6 @@ from .commons import (
ticks_to_seconds,
value_to_truncated_bpm,
)
from .timemap import BPMAtSecond, TimeMap
def make_chart_from_events(events: Iterable[Event], beat_snap: int = 240) -> song.Chart:

View File

@ -1,7 +1,7 @@
from decimal import Decimal
from fractions import Fraction
from pathlib import Path
from typing import Dict, Iterable, Protocol, TypeVar, Union
from typing import Dict, Iterable, Optional, Protocol, TypeVar, Union
from jubeatools import song
@ -14,7 +14,7 @@ class FileLoader(Protocol[T_co]):
contents in whatever form suitable for the current format. Returns None in
case of error"""
def __call__(self, path: Path) -> T_co:
def __call__(self, path: Path) -> Optional[T_co]:
...

View File

@ -0,0 +1,9 @@
"""Malody is a multiplatform rhythm game that mainly lives off content created
by its community, as is common in the rhythm game simulator scene. It support
many different games or "Modes", including jubeat (known as "Pad" Mode)
The file format it uses is not that well documented but is simple enough to
make sense of without docs. It's a json file with some defined schema"""
from .dump import dump_malody
from .load import load_malody

View File

@ -0,0 +1,120 @@
import time
from functools import singledispatch
from pathlib import Path
from typing import List, Tuple, Union
import simplejson as json
from jubeatools import song
from jubeatools.formats.dump_tools import make_dumper_from_chart_file_dumper
from jubeatools.formats.filetypes import ChartFile
from jubeatools.utils import none_or
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():
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")
res.append(ChartFile(chart_bytes, song, dif, chart))
return res
dump_malody = make_dumper_from_chart_file_dumper(
internal_dumper=dump_malody_song, file_name_template=Path("{difficulty:l}.mc")
)
def dump_malody_chart(
metadata: song.Metadata, dif: str, chart: song.Chart, timing: song.Timing
) -> malody.Chart:
meta = dump_metadata(metadata, dif)
time = dump_timing(timing)
notes = dump_notes(chart.notes)
if metadata.audio is not None:
notes += [dump_bgm(metadata.audio, timing)]
return malody.Chart(meta=meta, time=time, note=notes)
def dump_metadata(metadata: song.Metadata, dif: str) -> malody.Metadata:
return malody.Metadata(
cover="",
creator="",
background=none_or(str, metadata.cover),
version=dif,
id=0,
mode=malody.Mode.PAD,
time=int(time.time()),
song=malody.SongInfo(
title=metadata.title,
artist=metadata.artist,
id=0,
),
)
def dump_timing(timing: song.Timing) -> List[malody.BPMEvent]:
sorted_events = sorted(timing.events, key=lambda e: e.time)
return [dump_bpm_change(e) for e in sorted_events]
def dump_bpm_change(b: song.BPMEvent) -> malody.BPMEvent:
return malody.BPMEvent(
beat=beats_to_tuple(b.time),
bpm=b.BPM,
)
def dump_notes(notes: List[Union[song.TapNote, song.LongNote]]) -> List[malody.Event]:
return [dump_note(n) for n in notes]
@singledispatch
def dump_note(
n: Union[song.TapNote, song.LongNote]
) -> Union[malody.TapNote, malody.LongNote]:
raise NotImplementedError(f"Unknown note type : {type(n)}")
@dump_note.register
def dump_tap_note(n: song.TapNote) -> malody.TapNote:
return malody.TapNote(
beat=beats_to_tuple(n.time),
index=n.position.index,
)
@dump_note.register
def dump_long_note(n: song.LongNote) -> malody.LongNote:
return malody.LongNote(
beat=beats_to_tuple(n.time),
index=n.position.index,
endbeat=beats_to_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)),
sound=str(audio),
vol=100,
offset=-int(timing.beat_zero_offset * 1000),
type=malody.SoundType.BACKGROUND_MUSIC,
isBgm=None,
x=None,
)
def beats_to_tuple(b: song.BeatsTime) -> Tuple[int, int, int]:
integer_part = int(b)
remainder = b % 1
return (
integer_part,
remainder.numerator,
remainder.denominator,
)

View File

@ -0,0 +1,121 @@
import warnings
from decimal import Decimal
from fractions import Fraction
from functools import reduce, singledispatch
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union
import simplejson as json
from jubeatools import song
from jubeatools.formats import timemap
from jubeatools.formats.load_tools import make_folder_loader
from jubeatools.utils import none_or
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)
def load_file(path: Path) -> Any:
with path.open() as f:
return json.load(f, use_decimal=True)
load_folder = make_folder_loader("*.mc", load_file)
def load_malody_file(raw_dict: dict) -> song.Song:
file: malody.Chart = malody.CHART_SCHEMA.load(raw_dict)
if file.meta.mode != malody.Mode.PAD:
raise ValueError("This file is not a Malody Pad Chart (Malody's jubeat mode)")
bgm = find_bgm(file.note)
metadata = load_metadata(file.meta, bgm)
time_map = load_timing_info(file.time, bgm)
timing = time_map.convert_to_timing_info()
chart = song.Chart(level=Decimal(0), timing=timing, notes=load_notes(file.note))
dif = file.meta.version or song.Difficulty.EXTREME
return song.Song(metadata=metadata, charts={dif: chart})
def find_bgm(events: List[malody.Event]) -> Optional[malody.Sound]:
sounds = [e for e in events if isinstance(e, malody.Sound)]
bgms = [s for s in sounds if s.type == malody.SoundType.BACKGROUND_MUSIC]
if not bgms:
return None
if len(bgms) > 1:
warnings.warn(
"This file defines more than one background music, the first one "
"will be used"
)
return min(bgms, key=lambda b: tuple_to_beats(b.beat))
def load_metadata(meta: malody.Metadata, bgm: Optional[malody.Sound]) -> song.Metadata:
return song.Metadata(
title=meta.song.title,
artist=meta.song.artist,
audio=none_or(lambda b: Path(b.sound), bgm),
cover=none_or(Path, meta.background),
)
def load_timing_info(
bpm_changes: List[malody.BPMEvent], bgm: Optional[malody.Sound]
) -> timemap.TimeMap:
if bgm is None:
offset = timemap.SecondsAtBeat(seconds=Fraction(0), beats=Fraction(0))
else:
offset = timemap.SecondsAtBeat(
seconds=-Fraction(bgm.offset) / 1000, beats=tuple_to_beats(bgm.beat)
)
return timemap.TimeMap.from_beats(
events=[
timemap.BPMAtBeat(beats=tuple_to_beats(b.beat), BPM=Fraction(b.bpm))
for b in bpm_changes
],
offset=offset,
)
def load_notes(events: List[malody.Event]) -> List[Union[song.TapNote, song.LongNote]]:
# filter out sound events
notes = filter(lambda e: isinstance(e, (malody.TapNote, malody.LongNote)), events)
return [load_note(n) for n in notes]
@singledispatch
def load_note(
n: Union[malody.TapNote, malody.LongNote]
) -> Union[song.TapNote, song.LongNote]:
raise NotImplementedError(f"Unknown note type : {type(n)}")
@load_note.register
def load_tap_note(n: malody.TapNote) -> song.TapNote:
return song.TapNote(
time=tuple_to_beats(n.beat), position=song.NotePosition.from_index(n.index)
)
@load_note.register
def load_long_note(n: malody.LongNote) -> song.LongNote:
start = tuple_to_beats(n.beat)
end = tuple_to_beats(n.endbeat)
return song.LongNote(
time=start,
position=song.NotePosition.from_index(n.index),
duration=end - start,
tail_tip=song.NotePosition.from_index(n.endindex),
)
def tuple_to_beats(b: Tuple[int, int, int]) -> song.BeatsTime:
return b[0] + song.BeatsTime(b[1], b[2])

View File

@ -0,0 +1,102 @@
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import List, Optional, Tuple, Union
from marshmallow.validate import Range
from marshmallow_dataclass import NewType, class_schema
class Ordered:
class Meta:
ordered = True
@dataclass
class SongInfo(Ordered):
title: Optional[str]
artist: Optional[str]
id: Optional[int]
class Mode(int, Enum):
KEY = 0 # Vertical Scrolling Rhythm Game
# 1 : Unused
# 2 : Unused
CATCH = 3 # EZ2CATCH / Catch the Beat
PAD = 4 # Jubeat
TAIKO = 5 # Taiko no Tatsujin
RING = 6 # Reminds me of Beatstream ?
@dataclass
class Metadata(Ordered):
cover: Optional[str] # path to album art ?
creator: Optional[str] # Chart author
background: Optional[str] # path to background image
version: Optional[str] # freeform difficulty name
id: Optional[int]
mode: int
time: Optional[int] # creation timestamp ?
song: SongInfo
PositiveInt = NewType("PositiveInt", int, validate=Range(min=0))
BeatTime = Tuple[PositiveInt, PositiveInt, PositiveInt]
StrictlyPositiveDecimal = NewType(
"StrictlyPositiveDecimal", Decimal, validate=Range(min=0, min_inclusive=False)
)
@dataclass
class BPMEvent(Ordered):
beat: BeatTime
bpm: StrictlyPositiveDecimal
ButtonIndex = NewType("ButtonIndex", int, validate=Range(min=0, max=15))
@dataclass
class TapNote(Ordered):
beat: BeatTime
index: ButtonIndex
@dataclass
class LongNote(Ordered):
beat: BeatTime
index: ButtonIndex
endbeat: BeatTime
endindex: ButtonIndex
@dataclass
class Sound(Ordered):
"""Used both for the background music and keysounds"""
beat: BeatTime
sound: str # audio file path
type: int
offset: int
isBgm: Optional[bool]
vol: Optional[int] # Volume, out of 100
x: Optional[int]
# TODO: find a keysounded chart to discovery the other values
class SoundType(int, Enum):
BACKGROUND_MUSIC = 1
Event = Union[Sound, LongNote, TapNote]
@dataclass
class Chart(Ordered):
meta: Metadata
time: List[BPMEvent] = field(default_factory=list)
note: List[Event] = field(default_factory=list)
CHART_SCHEMA = class_schema(Chart)()

View File

@ -0,0 +1,32 @@
from decimal import Decimal
from hypothesis import given
from hypothesis import strategies as st
from jubeatools import song
from jubeatools.formats import Format
from jubeatools.formats.konami.testutils import open_temp_dir
from jubeatools.testutils import strategies as jbst
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
from jubeatools.testutils.typing import DrawFunc
@st.composite
def malody_compatible_song(draw: DrawFunc) -> song.Song:
"""Malody files only hold one chart and have limited metadata"""
diff = draw(st.sampled_from(list(song.Difficulty))).value
chart = draw(jbst.chart(level_strat=st.just(Decimal(0))))
metadata = draw(jbst.metadata())
metadata.preview = None
metadata.preview_file = None
return song.Song(metadata=metadata, charts={diff: chart})
@given(malody_compatible_song())
def test_that_full_chart_roundtrips(song: song.Song) -> None:
dump_and_load_then_compare(
Format.MALODY,
song,
temp_path=open_temp_dir(),
bytes_decoder=lambda b: b.decode("utf-8"),
)

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from fractions import Fraction
from typing import List, Union
@ -18,6 +18,18 @@ class BPMAtSecond:
BPM: Fraction
@dataclass
class BPMAtBeat:
beats: Fraction
BPM: Fraction
@dataclass
class SecondsAtBeat:
seconds: Fraction
beats: Fraction
@dataclass
class BPMChange:
beats: song.BeatsTime
@ -30,48 +42,69 @@ class TimeMap:
"""Wraps a song.Timing to allow converting symbolic time (in beats)
to clock time (in seconds) and back"""
beat_zero_offset: song.SecondsTime
events_by_beats: SortedKeyList[BPMChange, song.BeatsTime]
events_by_seconds: SortedKeyList[BPMChange, Fraction]
@classmethod
def from_timing(cls, beats: song.Timing) -> TimeMap:
def from_timing(cls, timing: song.Timing) -> TimeMap:
"""Create a time map from a song.Timing object"""
if not beats.events:
return cls.from_beats(
events=[
BPMAtBeat(beats=e.time, BPM=Fraction(e.BPM)) for e in timing.events
],
offset=SecondsAtBeat(
seconds=Fraction(timing.beat_zero_offset), beats=Fraction(0)
),
)
@classmethod
def from_beats(cls, events: List[BPMAtBeat], offset: SecondsAtBeat) -> TimeMap:
"""Create a time map from a list of BPM changes with times given in
beats, the offset parameter is more flexible than a "regular" beat zero
offset as it accepts non-zero beats"""
if not events:
raise ValueError("No BPM defined")
grouped_by_time = group_by(beats.events, key=lambda e: e.time)
for time, events in grouped_by_time.items():
if len(events) > 1:
raise ValueError(f"Multiple BPMs defined on beat {time} : {events}")
grouped_by_time = group_by(events, key=lambda e: e.beats)
for time, events_at_time in grouped_by_time.items():
if len(events_at_time) > 1:
raise ValueError(f"Multiple BPMs defined at beat {time} : {events}")
sorted_events = sorted(beats.events, key=lambda e: e.time)
# First compute everything as if the first BPM change happened at
# zero seconds, then shift according to the offset
sorted_events = sorted(events, key=lambda e: e.beats)
first_event = sorted_events[0]
if first_event.time != song.BeatsTime(0):
raise ValueError("First BPM event is not on beat zero")
# set first BPM change then compute from there
current_second = Fraction(beats.beat_zero_offset)
current_second = Fraction(0)
bpm_changes = [
BPMChange(first_event.time, current_second, Fraction(first_event.BPM))
BPMChange(first_event.beats, current_second, Fraction(first_event.BPM))
]
for previous, current in windowed(sorted_events, 2):
if previous is None or current is None:
continue
beats_since_last_event = current.time - previous.time
beats_since_last_event = current.beats - previous.beats
seconds_since_last_event = (60 * beats_since_last_event) / Fraction(
previous.BPM
)
current_second += seconds_since_last_event
bpm_change = BPMChange(current.time, current_second, Fraction(current.BPM))
bpm_change = BPMChange(current.beats, current_second, Fraction(current.BPM))
bpm_changes.append(bpm_change)
return cls(
beat_zero_offset=beats.beat_zero_offset,
not_shifted = cls(
events_by_beats=SortedKeyList(bpm_changes, key=lambda b: b.beats),
events_by_seconds=SortedKeyList(bpm_changes, key=lambda b: b.seconds),
)
unshifted_seconds_at_offset = not_shifted.fractional_seconds_at(offset.beats)
shift = offset.seconds - unshifted_seconds_at_offset
shifted_bpm_changes = [
replace(b, seconds=b.seconds + shift) for b in bpm_changes
]
return cls(
events_by_beats=SortedKeyList(shifted_bpm_changes, key=lambda b: b.beats),
events_by_seconds=SortedKeyList(
shifted_bpm_changes, key=lambda b: b.seconds
),
)
@classmethod
def from_seconds(cls, events: List[BPMAtSecond]) -> TimeMap:
@ -103,7 +136,6 @@ class TimeMap:
bpm_changes.append(bpm_change)
return cls(
beat_zero_offset=fraction_to_decimal(first_event.seconds),
events_by_beats=SortedKeyList(bpm_changes, key=lambda b: b.beats),
events_by_seconds=SortedKeyList(bpm_changes, key=lambda b: b.seconds),
)
@ -113,31 +145,21 @@ class TimeMap:
return fraction_to_decimal(frac_seconds)
def fractional_seconds_at(self, beat: song.BeatsTime) -> Fraction:
if beat < 0:
raise ValueError("Can't compute seconds at negative beat")
# find previous bpm change
index = self.events_by_beats.bisect_key_right(beat) - 1
bpm_change: BPMChange = self.events_by_beats[index]
# compute seconds since last bpm change
"""Before the first bpm change, compute backwards from the first bpm,
after the first bpm change, compute forwards from the previous bpm
change"""
index = self.events_by_beats.bisect_key_right(beat)
first_or_previous_index = max(0, index - 1)
bpm_change: BPMChange = self.events_by_beats[first_or_previous_index]
beats_since_last_event = beat - bpm_change.beats
seconds_since_last_event = (60 * beats_since_last_event) / bpm_change.BPM
return bpm_change.seconds + seconds_since_last_event
def beats_at(self, seconds: Union[song.SecondsTime, Fraction]) -> song.BeatsTime:
if seconds < self.beat_zero_offset:
raise ValueError(
f"Can't compute beat time at {seconds} seconds, since it predates "
f"beat zero, which happens at {self.beat_zero_offset} seconds"
)
# find previous bpm change
frac_seconds = Fraction(seconds)
index = self.events_by_seconds.bisect_key_right(frac_seconds) - 1
bpm_change: BPMChange = self.events_by_seconds[index]
# compute beats since last bpm change
index = self.events_by_seconds.bisect_key_right(frac_seconds)
first_or_previous_index = max(0, index - 1)
bpm_change: BPMChange = self.events_by_seconds[first_or_previous_index]
seconds_since_last_event = frac_seconds - bpm_change.seconds
beats_since_last_event = (bpm_change.BPM * seconds_since_last_event) / Fraction(
60
@ -153,5 +175,5 @@ class TimeMap:
)
for e in self.events_by_beats
],
beat_zero_offset=self.beat_zero_offset,
beat_zero_offset=self.seconds_at(song.BeatsTime(0)),
)

View File

@ -9,6 +9,7 @@ warn_redundant_casts = True
warn_unused_ignores = True
warn_return_any = True
warn_unreachable = True
plugins = marshmallow_dataclass.mypy
[mypy-constraint]
ignore_missing_imports = True

80
poetry.lock generated
View File

@ -189,6 +189,39 @@ docs = ["sphinx (==3.4.3)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "
lint = ["mypy (==0.812)", "flake8 (==3.9.0)", "flake8-bugbear (==21.3.2)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]]
name = "marshmallow-dataclass"
version = "8.4.1"
description = "Python library to convert dataclasses into marshmallow schemas."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
marshmallow = ">=3.0.0,<4.0"
marshmallow-enum = {version = "*", optional = true, markers = "extra == \"enum\""}
typeguard = {version = "*", optional = true, markers = "extra == \"union\""}
typing-inspect = "*"
[package.extras]
dev = ["marshmallow-enum", "typeguard", "pre-commit (>=1.18,<2.0)", "sphinx", "pytest (>=5.4)", "pytest-mypy-plugins (>=1.2.0)", "typing-extensions (>=3.7.2,<3.8.0)"]
docs = ["sphinx"]
enum = ["marshmallow-enum"]
lint = ["pre-commit (>=1.18,<2.0)"]
tests = ["pytest (>=5.4)", "pytest-mypy-plugins (>=1.2.0)", "typing-extensions (>=3.7.2,<3.8.0)"]
union = ["typeguard"]
[[package]]
name = "marshmallow-enum"
version = "1.5.1"
description = "Enum field for Marshmallow"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
marshmallow = ">=2.0.0"
[[package]]
name = "mccabe"
version = "0.6.1"
@ -233,7 +266,7 @@ dmypy = ["psutil (>=4.0)"]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
category = "main"
optional = false
python-versions = "*"
@ -430,18 +463,42 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typeguard"
version = "2.12.0"
description = "Run-time type checker for Python"
category = "main"
optional = false
python-versions = ">=3.5.3"
[package.extras]
doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["pytest", "typing-extensions", "mypy"]
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "typing-inspect"
version = "0.6.0"
description = "Runtime inspection utilities for typing module."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
mypy-extensions = ">=0.3.0"
typing-extensions = ">=3.7.4"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "dd2989bf15b7c389b4d6d8f28335f4fea3cb064b41723e5fe500f38dd92c39bb"
content-hash = "0e7f4c7f3e3554861ffd76a0e4143dd5c5218904234ee5641ad106a26f17b2bb"
[metadata.files]
appdirs = [
@ -502,6 +559,14 @@ marshmallow = [
{file = "marshmallow-3.11.1-py2.py3-none-any.whl", hash = "sha256:0dd42891a5ef288217ed6410917f3c6048f585f8692075a0052c24f9bfff9dfd"},
{file = "marshmallow-3.11.1.tar.gz", hash = "sha256:16e99cb7f630c0ef4d7d364ed0109ac194268dde123966076ab3dafb9ae3906b"},
]
marshmallow-dataclass = [
{file = "marshmallow_dataclass-8.4.1-py3-none-any.whl", hash = "sha256:035f4aa9f516ca3c14c9ae3905fe8370b14cea6462ec1a9d4451209a6117976e"},
{file = "marshmallow_dataclass-8.4.1.tar.gz", hash = "sha256:26b6ef76646c6cd71df3163c7106ddeaab27d9fac355cad41046627d5c15cda0"},
]
marshmallow-enum = [
{file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"},
{file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
@ -764,8 +829,17 @@ typed-ast = [
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
typeguard = [
{file = "typeguard-2.12.0-py3-none-any.whl", hash = "sha256:7d1cf82b35e9ff3cd083133ebda54ad1d7a40296471397e6c6b229cf07fe5307"},
{file = "typeguard-2.12.0.tar.gz", hash = "sha256:fca77fd4ccba63465b421cdbbab5a1a8e3994e6d6f18b45da2bb475c09f147ef"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
typing-inspect = [
{file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"},
{file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"},
{file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"},
]

View File

@ -19,6 +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"}
[tool.poetry.dev-dependencies]
pytest = "^6.2.3"