Add support for memon 0.3.0
This commit is contained in:
parent
93ada9fd31
commit
2e4b5978bd
4
.flake8
4
.flake8
@ -32,6 +32,6 @@ per-file-ignores =
|
|||||||
example*.py: F405, F403
|
example*.py: F405, F403
|
||||||
# Silence weird false positive on inline comments ...
|
# Silence weird false positive on inline comments ...
|
||||||
jubeatools/formats/jubeat_analyser/symbols.py: E262
|
jubeatools/formats/jubeat_analyser/symbols.py: E262
|
||||||
# there's a field named "l" in a marshmallow schema
|
# there's a field named "l" in a marshmallow schema and I don't want te rename it
|
||||||
jubeatools/formats/memon/memon.py: E741
|
jubeatools/formats/memon/v0.py: E741
|
||||||
max-line-length = 120
|
max-line-length = 120
|
@ -1,3 +1,7 @@
|
|||||||
|
# v1.3.0
|
||||||
|
## Added
|
||||||
|
- [memon] 🎉 v0.3.0 support
|
||||||
|
|
||||||
# v1.2.3
|
# v1.2.3
|
||||||
## Fixed
|
## Fixed
|
||||||
- Loaders and Dumpers would recieve options with unwanted default values when
|
- Loaders and Dumpers would recieve options with unwanted default values when
|
||||||
|
@ -16,7 +16,8 @@ jubeatools ${source} ${destination} -f ${output format} (... format specific opt
|
|||||||
## Which formats are supported
|
## Which formats are supported
|
||||||
| | | input | output |
|
| | | input | output |
|
||||||
|-----------------|----------------------|:-----:|:------:|
|
|-----------------|----------------------|:-----:|:------:|
|
||||||
| memon | v0.2.0 | ✔️ | ✔️ |
|
| memon | v0.3.0 | ✔️ | ✔️ |
|
||||||
|
| | v0.2.0 | ✔️ | ✔️ |
|
||||||
| | v0.1.0 | ✔️ | ✔️ |
|
| | v0.1.0 | ✔️ | ✔️ |
|
||||||
| | legacy | ✔️ | ✔️ |
|
| | legacy | ✔️ | ✔️ |
|
||||||
| jubeat analyser | #memo2 | ✔️ | ✔️ |
|
| jubeat analyser | #memo2 | ✔️ | ✔️ |
|
||||||
|
@ -18,7 +18,8 @@ Sanity checks before anything serious happens, from the repo's root :
|
|||||||
Now that this is done you can move on to actually making a new version,
|
Now that this is done you can move on to actually making a new version,
|
||||||
while still being in the repo's root :
|
while still being in the repo's root :
|
||||||
1. Update `CHANGELOG.md`
|
1. Update `CHANGELOG.md`
|
||||||
1. Commit everything you want in the new release, including the changelog
|
1. Update `README.md` if you've just added support for a new format
|
||||||
|
1. Commit everything you want in the new release
|
||||||
1. Run the script <br> `$ poetry run python utils/bump_version.py {rule}`
|
1. Run the script <br> `$ poetry run python utils/bump_version.py {rule}`
|
||||||
|
|
||||||
`{rule}` will usually be one of `patch`, `minor` or `major`. But it can be anything `poetry version` handles.
|
`{rule}` will usually be one of `patch`, `minor` or `major`. But it can be anything `poetry version` handles.
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import string
|
import string
|
||||||
|
from functools import singledispatch
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AbstractSet, Any, Dict, Iterator, TypedDict
|
from typing import AbstractSet, Any, Dict, Iterator, Optional, TypedDict
|
||||||
|
|
||||||
from jubeatools.formats.filetypes import ChartFile
|
from jubeatools.formats.filetypes import ChartFile, JubeatFile, SongFile
|
||||||
from jubeatools.formats.typing import ChartFileDumper, Dumper
|
from jubeatools.formats.typing import ChartFileDumper, Dumper
|
||||||
from jubeatools.song import Difficulty, Song
|
from jubeatools.song import Difficulty, Song
|
||||||
from jubeatools.utils import none_or
|
from jubeatools.utils import none_or
|
||||||
@ -31,17 +32,12 @@ def make_dumper_from_chart_file_dumper(
|
|||||||
|
|
||||||
def dumper(song: Song, path: Path, **kwargs: Any) -> Dict[Path, bytes]:
|
def dumper(song: Song, path: Path, **kwargs: Any) -> Dict[Path, bytes]:
|
||||||
res: Dict[Path, bytes] = {}
|
res: Dict[Path, bytes] = {}
|
||||||
if path.is_dir():
|
name_format = FileNameFormat(file_name_template, suggestion=path)
|
||||||
file_path = file_name_template
|
|
||||||
parent = path
|
|
||||||
else:
|
|
||||||
file_path = path
|
|
||||||
parent = path.parent
|
|
||||||
|
|
||||||
name_format = f"{file_path.stem}{{dedup}}{file_path.suffix}"
|
|
||||||
files = internal_dumper(song, **kwargs)
|
files = internal_dumper(song, **kwargs)
|
||||||
for chartfile in files:
|
for chartfile in files:
|
||||||
filepath = choose_file_path(chartfile, name_format, parent, res.keys())
|
filepath = name_format.available_filename_for(
|
||||||
|
chartfile, already_chosen=res.keys()
|
||||||
|
)
|
||||||
res[filepath] = chartfile.contents
|
res[filepath] = chartfile.contents
|
||||||
|
|
||||||
return res
|
return res
|
||||||
@ -49,28 +45,6 @@ def make_dumper_from_chart_file_dumper(
|
|||||||
return dumper
|
return dumper
|
||||||
|
|
||||||
|
|
||||||
def choose_file_path(
|
|
||||||
chart_file: ChartFile,
|
|
||||||
name_format: str,
|
|
||||||
parent: Path,
|
|
||||||
already_chosen: AbstractSet[Path],
|
|
||||||
) -> Path:
|
|
||||||
all_paths = iter_possible_paths(chart_file, name_format, parent)
|
|
||||||
not_on_filesystem = filter(lambda p: not p.exists(), all_paths)
|
|
||||||
not_already_chosen = filter(lambda p: p not in already_chosen, not_on_filesystem)
|
|
||||||
return next(not_already_chosen)
|
|
||||||
|
|
||||||
|
|
||||||
def iter_possible_paths(
|
|
||||||
chart_file: ChartFile, name_format: str, parent: Path
|
|
||||||
) -> Iterator[Path]:
|
|
||||||
for dedup_index in count(start=0):
|
|
||||||
params = extract_format_params(chart_file, dedup_index)
|
|
||||||
formatter = BetterStringFormatter()
|
|
||||||
filename = formatter.format(name_format, **params).strip()
|
|
||||||
yield parent / filename
|
|
||||||
|
|
||||||
|
|
||||||
class FormatParameters(TypedDict, total=False):
|
class FormatParameters(TypedDict, total=False):
|
||||||
title: str
|
title: str
|
||||||
# uppercase BSC ADV EXT
|
# uppercase BSC ADV EXT
|
||||||
@ -82,13 +56,64 @@ class FormatParameters(TypedDict, total=False):
|
|||||||
dedup: str
|
dedup: str
|
||||||
|
|
||||||
|
|
||||||
def extract_format_params(chartfile: ChartFile, dedup_index: int) -> FormatParameters:
|
class FileNameFormat:
|
||||||
|
def __init__(self, file_name_template: Path, suggestion: Path):
|
||||||
|
if suggestion.is_dir():
|
||||||
|
file_path = file_name_template
|
||||||
|
self.parent = suggestion
|
||||||
|
else:
|
||||||
|
file_path = suggestion
|
||||||
|
self.parent = suggestion.parent
|
||||||
|
|
||||||
|
self.name_format = f"{file_path.stem}{{dedup}}{file_path.suffix}"
|
||||||
|
|
||||||
|
def available_filename_for(
|
||||||
|
self, file: JubeatFile, already_chosen: Optional[AbstractSet[Path]] = None
|
||||||
|
) -> Path:
|
||||||
|
fixed_params = extract_format_params(file)
|
||||||
|
return next(self.iter_possible_paths(fixed_params, already_chosen))
|
||||||
|
|
||||||
|
def iter_possible_paths(
|
||||||
|
self,
|
||||||
|
fixed_params: FormatParameters,
|
||||||
|
already_chosen: Optional[AbstractSet[Path]] = None,
|
||||||
|
) -> Iterator[Path]:
|
||||||
|
all_paths = self.iter_deduped_paths(fixed_params)
|
||||||
|
not_on_filesystem = (p for p in all_paths if not p.exists())
|
||||||
|
if already_chosen is not None:
|
||||||
|
yield from (p for p in not_on_filesystem if p not in already_chosen)
|
||||||
|
else:
|
||||||
|
yield from not_on_filesystem
|
||||||
|
|
||||||
|
def iter_deduped_paths(self, params: FormatParameters) -> Iterator[Path]:
|
||||||
|
for dedup_index in count(start=0):
|
||||||
|
# TODO Remove the type ignore once this issue is fixed
|
||||||
|
# https://github.com/python/mypy/issues/6019
|
||||||
|
params.update( # type: ignore[call-arg]
|
||||||
|
dedup="" if dedup_index == 0 else f"-{dedup_index}"
|
||||||
|
)
|
||||||
|
formatter = BetterStringFormatter()
|
||||||
|
filename = formatter.format(self.name_format, **params).strip()
|
||||||
|
yield self.parent / filename
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def extract_format_params(file: JubeatFile) -> FormatParameters:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@extract_format_params.register
|
||||||
|
def extract_song_format_params(songfile: SongFile) -> FormatParameters:
|
||||||
|
return FormatParameters(title=none_or(slugify, songfile.song.metadata.title) or "")
|
||||||
|
|
||||||
|
|
||||||
|
@extract_format_params.register
|
||||||
|
def extract_chart_format_params(chartfile: ChartFile) -> FormatParameters:
|
||||||
return FormatParameters(
|
return FormatParameters(
|
||||||
title=none_or(slugify, chartfile.song.metadata.title) or "",
|
title=none_or(slugify, chartfile.song.metadata.title) or "",
|
||||||
difficulty=slugify(chartfile.difficulty),
|
difficulty=slugify(chartfile.difficulty),
|
||||||
difficulty_index=str(DIFFICULTY_INDEX.get(chartfile.difficulty, 2)),
|
difficulty_index=str(DIFFICULTY_INDEX.get(chartfile.difficulty, 2)),
|
||||||
difficulty_number=str(DIFFICULTY_NUMBER.get(chartfile.difficulty, 3)),
|
difficulty_number=str(DIFFICULTY_NUMBER.get(chartfile.difficulty, 3)),
|
||||||
dedup="" if dedup_index == 0 else f"-{dedup_index}",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ class Format(str, Enum):
|
|||||||
MEMON_LEGACY = "memon:legacy"
|
MEMON_LEGACY = "memon:legacy"
|
||||||
MEMON_0_1_0 = "memon:v0.1.0"
|
MEMON_0_1_0 = "memon:v0.1.0"
|
||||||
MEMON_0_2_0 = "memon:v0.2.0"
|
MEMON_0_2_0 = "memon:v0.2.0"
|
||||||
|
MEMON_0_3_0 = "memon:v0.3.0"
|
||||||
MONO_COLUMN = "mono-column"
|
MONO_COLUMN = "mono-column"
|
||||||
MEMO = "memo"
|
MEMO = "memo"
|
||||||
MEMO_1 = "memo1"
|
MEMO_1 = "memo1"
|
||||||
|
@ -1,50 +1,33 @@
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
from . import jubeat_analyser, konami, malody, memon
|
||||||
from .enum import Format
|
from .enum import Format
|
||||||
from .jubeat_analyser import (
|
|
||||||
dump_memo,
|
|
||||||
dump_memo1,
|
|
||||||
dump_memo2,
|
|
||||||
dump_mono_column,
|
|
||||||
load_memo,
|
|
||||||
load_memo1,
|
|
||||||
load_memo2,
|
|
||||||
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,
|
|
||||||
dump_memon_legacy,
|
|
||||||
load_memon_0_1_0,
|
|
||||||
load_memon_0_2_0,
|
|
||||||
load_memon_legacy,
|
|
||||||
)
|
|
||||||
from .typing import Dumper, Loader
|
from .typing import Dumper, Loader
|
||||||
|
|
||||||
LOADERS: Dict[Format, Loader] = {
|
LOADERS: Dict[Format, Loader] = {
|
||||||
Format.EVE: load_eve,
|
Format.EVE: konami.load_eve,
|
||||||
Format.JBSQ: load_jbsq,
|
Format.JBSQ: konami.load_jbsq,
|
||||||
Format.MALODY: load_malody,
|
Format.MALODY: malody.load_malody,
|
||||||
Format.MEMON_LEGACY: load_memon_legacy,
|
Format.MEMON_LEGACY: memon.load_memon_legacy,
|
||||||
Format.MEMON_0_1_0: load_memon_0_1_0,
|
Format.MEMON_0_1_0: memon.load_memon_0_1_0,
|
||||||
Format.MEMON_0_2_0: load_memon_0_2_0,
|
Format.MEMON_0_2_0: memon.load_memon_0_2_0,
|
||||||
Format.MONO_COLUMN: load_mono_column,
|
Format.MEMON_0_3_0: memon.load_memon_0_3_0,
|
||||||
Format.MEMO: load_memo,
|
Format.MONO_COLUMN: jubeat_analyser.load_mono_column,
|
||||||
Format.MEMO_1: load_memo1,
|
Format.MEMO: jubeat_analyser.load_memo,
|
||||||
Format.MEMO_2: load_memo2,
|
Format.MEMO_1: jubeat_analyser.load_memo1,
|
||||||
|
Format.MEMO_2: jubeat_analyser.load_memo2,
|
||||||
}
|
}
|
||||||
|
|
||||||
DUMPERS: Dict[Format, Dumper] = {
|
DUMPERS: Dict[Format, Dumper] = {
|
||||||
Format.EVE: dump_eve,
|
Format.EVE: konami.dump_eve,
|
||||||
Format.JBSQ: dump_jbsq,
|
Format.JBSQ: konami.dump_jbsq,
|
||||||
Format.MALODY: dump_malody,
|
Format.MALODY: malody.dump_malody,
|
||||||
Format.MEMON_LEGACY: dump_memon_legacy,
|
Format.MEMON_LEGACY: memon.dump_memon_legacy,
|
||||||
Format.MEMON_0_1_0: dump_memon_0_1_0,
|
Format.MEMON_0_1_0: memon.dump_memon_0_1_0,
|
||||||
Format.MEMON_0_2_0: dump_memon_0_2_0,
|
Format.MEMON_0_2_0: memon.dump_memon_0_2_0,
|
||||||
Format.MONO_COLUMN: dump_mono_column,
|
Format.MEMON_0_3_0: memon.dump_memon_0_3_0,
|
||||||
Format.MEMO: dump_memo,
|
Format.MONO_COLUMN: jubeat_analyser.dump_mono_column,
|
||||||
Format.MEMO_1: dump_memo1,
|
Format.MEMO: jubeat_analyser.dump_memo,
|
||||||
Format.MEMO_2: dump_memo2,
|
Format.MEMO_1: jubeat_analyser.dump_memo1,
|
||||||
|
Format.MEMO_2: jubeat_analyser.dump_memo2,
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ def recognize_memon_version(obj: dict) -> Format:
|
|||||||
return Format.MEMON_0_1_0
|
return Format.MEMON_0_1_0
|
||||||
elif version == "0.2.0":
|
elif version == "0.2.0":
|
||||||
return Format.MEMON_0_2_0
|
return Format.MEMON_0_2_0
|
||||||
|
elif version == "0.3.0":
|
||||||
|
return Format.MEMON_0_3_0
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported memon version : {version}")
|
raise ValueError(f"Unsupported memon version : {version}")
|
||||||
|
|
||||||
|
@ -210,7 +210,8 @@ def create_sections_from_chart(
|
|||||||
|
|
||||||
header = sections[BeatsTime(0)].commands
|
header = sections[BeatsTime(0)].commands
|
||||||
header["o"] = int(timing.beat_zero_offset * 1000)
|
header["o"] = int(timing.beat_zero_offset * 1000)
|
||||||
header["lev"] = Decimal(chart.level)
|
if chart.level is not None:
|
||||||
|
header["lev"] = Decimal(chart.level)
|
||||||
header["dif"] = DIFFICULTY_NUMBER.get(difficulty, 3)
|
header["dif"] = DIFFICULTY_NUMBER.get(difficulty, 3)
|
||||||
if metadata.audio is not None:
|
if metadata.audio is not None:
|
||||||
header["m"] = metadata.audio
|
header["m"] = metadata.audio
|
||||||
|
@ -299,7 +299,8 @@ def _dump_memo2_chart(
|
|||||||
file.write(f"// https://github.com/Stepland/jubeatools\n\n")
|
file.write(f"// https://github.com/Stepland/jubeatools\n\n")
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
file.write(dump_command("lev", Decimal(chart.level)) + "\n")
|
if chart.level is not None:
|
||||||
|
file.write(dump_command("lev", Decimal(chart.level)) + "\n")
|
||||||
file.write(dump_command("dif", DIFFICULTY_NUMBER.get(difficulty, 1)) + "\n")
|
file.write(dump_command("dif", DIFFICULTY_NUMBER.get(difficulty, 1)) + "\n")
|
||||||
if metadata.audio is not None:
|
if metadata.audio is not None:
|
||||||
file.write(dump_command("m", metadata.audio) + "\n")
|
file.write(dump_command("m", metadata.audio) + "\n")
|
||||||
|
@ -27,7 +27,9 @@ class FolderLoader(Protocol[T]):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def make_folder_loader(glob_pattern: str, file_loader: FileLoader) -> FolderLoader:
|
def make_folder_loader(
|
||||||
|
glob_pattern: str, file_loader: FileLoader[T]
|
||||||
|
) -> FolderLoader[T]:
|
||||||
def folder_loader(path: Path) -> Dict[Path, T]:
|
def folder_loader(path: Path) -> Dict[Path, T]:
|
||||||
files: Dict[Path, T] = {}
|
files: Dict[Path, T] = {}
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
|
@ -9,7 +9,7 @@ import simplejson as json
|
|||||||
|
|
||||||
from jubeatools import song
|
from jubeatools import song
|
||||||
from jubeatools.formats import timemap
|
from jubeatools.formats import timemap
|
||||||
from jubeatools.formats.load_tools import make_folder_loader
|
from jubeatools.formats.load_tools import FolderLoader, make_folder_loader
|
||||||
from jubeatools.utils import none_or
|
from jubeatools.utils import none_or
|
||||||
|
|
||||||
from . import schema as malody
|
from . import schema as malody
|
||||||
@ -26,7 +26,7 @@ def load_file(path: Path) -> Any:
|
|||||||
return json.load(f, use_decimal=True)
|
return json.load(f, use_decimal=True)
|
||||||
|
|
||||||
|
|
||||||
load_folder = make_folder_loader("*.mc", load_file)
|
load_folder: FolderLoader[Any] = make_folder_loader("*.mc", load_file)
|
||||||
|
|
||||||
|
|
||||||
def load_malody_file(raw_dict: dict) -> song.Song:
|
def load_malody_file(raw_dict: dict) -> song.Song:
|
||||||
|
@ -8,11 +8,13 @@ parse than existing "memo-like" formats (memo, youbeat, etc ...).
|
|||||||
https://github.com/Stepland/memon
|
https://github.com/Stepland/memon
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .memon import (
|
from .v0 import (
|
||||||
dump_memon_0_1_0,
|
dump_memon_0_1_0,
|
||||||
dump_memon_0_2_0,
|
dump_memon_0_2_0,
|
||||||
|
dump_memon_0_3_0,
|
||||||
dump_memon_legacy,
|
dump_memon_legacy,
|
||||||
load_memon_0_1_0,
|
load_memon_0_1_0,
|
||||||
load_memon_0_2_0,
|
load_memon_0_2_0,
|
||||||
|
load_memon_0_3_0,
|
||||||
load_memon_legacy,
|
load_memon_legacy,
|
||||||
)
|
)
|
||||||
|
@ -1,36 +1,12 @@
|
|||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
import hypothesis.strategies as st
|
import hypothesis.strategies as st
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
|
|
||||||
from jubeatools import song
|
from jubeatools import song
|
||||||
from jubeatools.formats.typing import Dumper, Loader
|
from jubeatools.formats.enum import Format
|
||||||
from jubeatools.testutils import strategies as jbst
|
from jubeatools.testutils import strategies as jbst
|
||||||
|
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
|
||||||
from . import (
|
|
||||||
dump_memon_0_1_0,
|
|
||||||
dump_memon_0_2_0,
|
|
||||||
dump_memon_legacy,
|
|
||||||
load_memon_0_1_0,
|
|
||||||
load_memon_0_2_0,
|
|
||||||
load_memon_legacy,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def dump_and_load(
|
|
||||||
expected_song: song.Song, dump_function: Dumper, load_function: Loader
|
|
||||||
) -> None:
|
|
||||||
with tempfile.NamedTemporaryFile(mode="wb") as file:
|
|
||||||
files = dump_function(expected_song, Path(file.name))
|
|
||||||
assert len(files) == 1
|
|
||||||
filename, contents = list(files.items())[0]
|
|
||||||
file.write(contents)
|
|
||||||
file.seek(0)
|
|
||||||
actual_song = load_function(Path(file.name))
|
|
||||||
|
|
||||||
assert actual_song == expected_song
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
@st.composite
|
||||||
@ -56,6 +32,11 @@ def memon_legacy_compatible_song(draw: st.DrawFn) -> song.Song:
|
|||||||
diffs_strat=memon_diffs(),
|
diffs_strat=memon_diffs(),
|
||||||
chart_strat=jbst.chart(timing_strat=st.none()),
|
chart_strat=jbst.chart(timing_strat=st.none()),
|
||||||
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
|
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
|
random_song.metadata.preview = None
|
||||||
@ -65,7 +46,7 @@ def memon_legacy_compatible_song(draw: st.DrawFn) -> song.Song:
|
|||||||
|
|
||||||
@given(memon_legacy_compatible_song())
|
@given(memon_legacy_compatible_song())
|
||||||
def test_memon_legacy(song: song.Song) -> None:
|
def test_memon_legacy(song: song.Song) -> None:
|
||||||
dump_and_load(song, dump_memon_legacy, load_memon_legacy)
|
dump_and_load_then_compare(Format.MEMON_LEGACY, song)
|
||||||
|
|
||||||
|
|
||||||
memon_0_1_0_compatible_song = memon_legacy_compatible_song
|
memon_0_1_0_compatible_song = memon_legacy_compatible_song
|
||||||
@ -73,7 +54,7 @@ memon_0_1_0_compatible_song = memon_legacy_compatible_song
|
|||||||
|
|
||||||
@given(memon_0_1_0_compatible_song())
|
@given(memon_0_1_0_compatible_song())
|
||||||
def test_memon_0_1_0(song: song.Song) -> None:
|
def test_memon_0_1_0(song: song.Song) -> None:
|
||||||
dump_and_load(song, dump_memon_0_1_0, load_memon_0_1_0)
|
dump_and_load_then_compare(Format.MEMON_0_1_0, song)
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
@st.composite
|
||||||
@ -84,6 +65,11 @@ def memon_0_2_0_compatible_song(draw: st.DrawFn) -> song.Song:
|
|||||||
diffs_strat=memon_diffs(),
|
diffs_strat=memon_diffs(),
|
||||||
chart_strat=jbst.chart(timing_strat=st.none()),
|
chart_strat=jbst.chart(timing_strat=st.none()),
|
||||||
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
|
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
|
random_song.metadata.preview_file = None
|
||||||
@ -92,4 +78,25 @@ def memon_0_2_0_compatible_song(draw: st.DrawFn) -> song.Song:
|
|||||||
|
|
||||||
@given(memon_0_2_0_compatible_song())
|
@given(memon_0_2_0_compatible_song())
|
||||||
def test_memon_0_2_0(song: song.Song) -> None:
|
def test_memon_0_2_0(song: song.Song) -> None:
|
||||||
dump_and_load(song, dump_memon_0_2_0, load_memon_0_2_0)
|
dump_and_load_then_compare(Format.MEMON_0_2_0, song)
|
||||||
|
|
||||||
|
|
||||||
|
@st.composite
|
||||||
|
def memon_0_3_0_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||||
|
return draw(
|
||||||
|
jbst.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")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@given(memon_0_3_0_compatible_song())
|
||||||
|
def test_memon_0_3_0(song: song.Song) -> None:
|
||||||
|
dump_and_load_then_compare(Format.MEMON_0_3_0, song)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
from functools import reduce
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Callable, Dict, List, Union
|
||||||
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from marshmallow import (
|
from marshmallow import (
|
||||||
@ -15,7 +16,11 @@ from marshmallow import (
|
|||||||
from multidict import MultiDict
|
from multidict import MultiDict
|
||||||
|
|
||||||
from jubeatools import song as jbt
|
from jubeatools import song as jbt
|
||||||
from jubeatools.utils import lcm
|
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 :
|
# v0.x.x long note value :
|
||||||
#
|
#
|
||||||
@ -102,6 +107,12 @@ class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
|
|||||||
preview = fields.Nested(MemonPreview)
|
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):
|
class Memon_legacy(StrictSchema):
|
||||||
metadata = fields.Nested(MemonMetadata_legacy, required=True)
|
metadata = fields.Nested(MemonMetadata_legacy, required=True)
|
||||||
data = fields.Nested(MemonChart_legacy, required=True, many=True)
|
data = fields.Nested(MemonChart_legacy, required=True, many=True)
|
||||||
@ -123,15 +134,39 @@ class Memon_0_2_0(StrictSchema):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_raw_memon(file: Path) -> Dict[str, Any]:
|
class Memon_0_3_0(StrictSchema):
|
||||||
with open(file) as f:
|
version = fields.String(required=True, validate=validate.OneOf(["0.3.0"]))
|
||||||
res = json.load(f, use_decimal=True)
|
metadata = fields.Nested(MemonMetadata_0_3_0, required=True)
|
||||||
if not isinstance(res, dict):
|
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(
|
raise ValueError(
|
||||||
"JSON file does not represent a valid memon file : "
|
"Multiple .memon files were found in the given folder, "
|
||||||
"The top level of a memon file should be a JSON Object"
|
"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"
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
|
charts = [memon_loader(d) for d in files.values()]
|
||||||
|
return reduce(jbt.Song.merge, charts)
|
||||||
|
|
||||||
|
return load
|
||||||
|
|
||||||
|
|
||||||
def _load_memon_note_v0(
|
def _load_memon_note_v0(
|
||||||
@ -149,8 +184,7 @@ def _load_memon_note_v0(
|
|||||||
return jbt.TapNote(time, position)
|
return jbt.TapNote(time, position)
|
||||||
|
|
||||||
|
|
||||||
def load_memon_legacy(path: Path, **kwargs: Any) -> jbt.Song:
|
def _load_memon_legacy(raw_memon: Any) -> jbt.Song:
|
||||||
raw_memon = _load_raw_memon(path)
|
|
||||||
schema = Memon_legacy()
|
schema = Memon_legacy()
|
||||||
memon = schema.load(raw_memon)
|
memon = schema.load(raw_memon)
|
||||||
metadata = jbt.Metadata(
|
metadata = jbt.Metadata(
|
||||||
@ -179,8 +213,10 @@ def load_memon_legacy(path: Path, **kwargs: Any) -> jbt.Song:
|
|||||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
||||||
|
|
||||||
|
|
||||||
def load_memon_0_1_0(path: Path, **kwargs: Any) -> jbt.Song:
|
load_memon_legacy = make_folder_loader_with_optional_merge(_load_memon_legacy)
|
||||||
raw_memon = _load_raw_memon(path)
|
|
||||||
|
|
||||||
|
def _load_memon_0_1_0(raw_memon: Any) -> jbt.Song:
|
||||||
schema = Memon_0_1_0()
|
schema = Memon_0_1_0()
|
||||||
memon = schema.load(raw_memon)
|
memon = schema.load(raw_memon)
|
||||||
metadata = jbt.Metadata(
|
metadata = jbt.Metadata(
|
||||||
@ -209,8 +245,10 @@ def load_memon_0_1_0(path: Path, **kwargs: Any) -> jbt.Song:
|
|||||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
||||||
|
|
||||||
|
|
||||||
def load_memon_0_2_0(path: Path, **kwargs: Any) -> jbt.Song:
|
load_memon_0_1_0 = make_folder_loader_with_optional_merge(_load_memon_0_1_0)
|
||||||
raw_memon = _load_raw_memon(path)
|
|
||||||
|
|
||||||
|
def _load_memon_0_2_0(raw_memon: Any) -> jbt.Song:
|
||||||
schema = Memon_0_2_0()
|
schema = Memon_0_2_0()
|
||||||
memon = schema.load(raw_memon)
|
memon = schema.load(raw_memon)
|
||||||
preview = None
|
preview = None
|
||||||
@ -246,6 +284,49 @@ def load_memon_0_2_0(path: Path, **kwargs: Any) -> jbt.Song:
|
|||||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
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:
|
def _long_note_tail_value_v0(note: jbt.LongNote) -> int:
|
||||||
dx = note.tail_tip.x - note.position.x
|
dx = note.tail_tip.x - note.position.x
|
||||||
dy = note.tail_tip.y - note.position.y
|
dy = note.tail_tip.y - note.position.y
|
||||||
@ -267,7 +348,6 @@ def _get_timing(song: jbt.Song) -> jbt.Timing:
|
|||||||
|
|
||||||
|
|
||||||
def _raise_if_unfit_for_v0(song: jbt.Song, version: str) -> 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
|
"""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)"""
|
that cannot be represented in a memon v0.x.y file (includes legacy)"""
|
||||||
|
|
||||||
@ -347,8 +427,7 @@ def _dump_memon_note_v0(
|
|||||||
return memon_note
|
return memon_note
|
||||||
|
|
||||||
|
|
||||||
def dump_memon_legacy(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
def _dump_memon_legacy(song: jbt.Song) -> SongFile:
|
||||||
|
|
||||||
_raise_if_unfit_for_v0(song, "legacy")
|
_raise_if_unfit_for_v0(song, "legacy")
|
||||||
timing = _get_timing(song)
|
timing = _get_timing(song)
|
||||||
|
|
||||||
@ -379,16 +458,23 @@ def dump_memon_legacy(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path,
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if path.is_dir():
|
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||||
filepath = path / f"{song.metadata.title}.memon"
|
|
||||||
else:
|
|
||||||
filepath = path
|
|
||||||
|
|
||||||
return {filepath: _dump_to_json(memon)}
|
|
||||||
|
|
||||||
|
|
||||||
def dump_memon_0_1_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
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")
|
_raise_if_unfit_for_v0(song, "v0.1.0")
|
||||||
timing = _get_timing(song)
|
timing = _get_timing(song)
|
||||||
|
|
||||||
@ -415,15 +501,13 @@ def dump_memon_0_1_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, b
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.is_dir():
|
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||||
filepath = path / f"{song.metadata.title}.memon"
|
|
||||||
else:
|
|
||||||
filepath = path
|
|
||||||
|
|
||||||
return {filepath: _dump_to_json(memon)}
|
|
||||||
|
|
||||||
|
|
||||||
def dump_memon_0_2_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
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")
|
_raise_if_unfit_for_v0(song, "v0.2.0")
|
||||||
timing = _get_timing(song)
|
timing = _get_timing(song)
|
||||||
@ -458,9 +542,55 @@ def dump_memon_0_2_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, b
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.is_dir():
|
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||||
filepath = path / f"{song.metadata.title}.memon"
|
|
||||||
else:
|
|
||||||
filepath = path
|
|
||||||
|
|
||||||
return {filepath: _dump_to_json(memon)}
|
|
||||||
|
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)
|
@ -1,17 +1,15 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..load_tools import round_beats
|
from ..load_tools import round_beats
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("numerator", range(240))
|
def test_fraction_recovery_after_rounding_to_three_decimals() -> None:
|
||||||
def test_fraction_recovery_after_rounding_to_three_decimals(numerator: int) -> None:
|
for numerator in range(240):
|
||||||
fraction = Fraction(numerator, 240)
|
fraction = Fraction(numerator, 240)
|
||||||
decimal = numerator / Decimal(240)
|
decimal = numerator / Decimal(240)
|
||||||
rounded = round(decimal, 3)
|
rounded = round(decimal, 3)
|
||||||
text_form = str(rounded)
|
text_form = str(rounded)
|
||||||
re_parsed_decimal = Decimal(text_form)
|
re_parsed_decimal = Decimal(text_form)
|
||||||
result = round_beats(re_parsed_decimal)
|
result = round_beats(re_parsed_decimal)
|
||||||
assert fraction == result
|
assert fraction == result
|
||||||
|
@ -180,7 +180,7 @@ class Timing:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Chart:
|
class Chart:
|
||||||
level: Decimal
|
level: Optional[Decimal]
|
||||||
timing: Optional[Timing] = None
|
timing: Optional[Timing] = None
|
||||||
notes: List[Union[TapNote, LongNote]] = field(default_factory=list)
|
notes: List[Union[TapNote, LongNote]] = field(default_factory=list)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ def open_temp_dir() -> Iterator[Path]:
|
|||||||
def dump_and_load_then_compare(
|
def dump_and_load_then_compare(
|
||||||
format_: Format,
|
format_: Format,
|
||||||
song: song.Song,
|
song: song.Song,
|
||||||
bytes_decoder: Callable[[bytes], str],
|
bytes_decoder: Callable[[bytes], str] = lambda b: b.decode("utf-8"),
|
||||||
temp_path: Callable[[], ContextManager[Path]] = open_temp_dir,
|
temp_path: Callable[[], ContextManager[Path]] = open_temp_dir,
|
||||||
load_options: Optional[dict] = None,
|
load_options: Optional[dict] = None,
|
||||||
dump_options: Optional[dict] = None,
|
dump_options: Optional[dict] = None,
|
||||||
|
14
poetry.lock
generated
14
poetry.lock
generated
@ -498,6 +498,14 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-toml"
|
||||||
|
version = "0.10.1"
|
||||||
|
description = "Typing stubs for toml"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "3.10.0.2"
|
version = "3.10.0.2"
|
||||||
@ -521,7 +529,7 @@ typing-extensions = ">=3.7.4"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "667e5000f1e328f2d07a86946ad1423912c875c0abcc7efb97a72aec38fdb916"
|
content-hash = "025ff93ec3b033c89bdaaeab7905dc10ff450ed70695bcbd863a10e6af6fa9ea"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
atomicwrites = [
|
atomicwrites = [
|
||||||
@ -864,6 +872,10 @@ types-simplejson = [
|
|||||||
{file = "types-simplejson-3.17.1.tar.gz", hash = "sha256:310ef5addfe97c20b9f2cac85079cbab95fd123c2c9a5c6b99856075d6b48211"},
|
{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.1-py3-none-any.whl", hash = "sha256:7b5ac5549f5269f25e5e932d6f4e96ea8397369d6031300161cd1fda484e2534"},
|
||||||
]
|
]
|
||||||
|
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 = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
{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-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
|
||||||
|
@ -32,6 +32,7 @@ toml = "^0.10.2"
|
|||||||
flake8 = "^3.9.1"
|
flake8 = "^3.9.1"
|
||||||
autoimport = "^0.7.0"
|
autoimport = "^0.7.0"
|
||||||
types-simplejson = "^3.17.1"
|
types-simplejson = "^3.17.1"
|
||||||
|
types-toml = "^0.10.1"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
jubeatools = 'jubeatools.cli.cli:convert'
|
jubeatools = 'jubeatools.cli.cli:convert'
|
||||||
|
Loading…
Reference in New Issue
Block a user