From 0ec5acc08634ad6b3ca18895bf724dbf1bfc6f59 Mon Sep 17 00:00:00 2001 From: Stepland <16676308+Stepland@users.noreply.github.com> Date: Sat, 18 Jul 2020 17:31:01 +0200 Subject: [PATCH] HOLY FUCK #memo PASSED UNIT TESTS --- .../formats/jubeat_analyser/dump_tools.py | 35 ++--- .../formats/jubeat_analyser/load_tools.py | 112 +++++++++++--- .../formats/jubeat_analyser/memo/dump.py | 75 ++++++---- .../formats/jubeat_analyser/memo/load.py | 102 ++++--------- .../jubeat_analyser/mono_column/dump.py | 22 ++- .../jubeat_analyser/mono_column/load.py | 72 +++------ .../jubeat_analyser/tests/test_memo.py | 13 +- jubeatools/testutils/strategies.py | 3 +- poetry.lock | 137 ++++++++++-------- 9 files changed, 314 insertions(+), 257 deletions(-) diff --git a/jubeatools/formats/jubeat_analyser/dump_tools.py b/jubeatools/formats/jubeat_analyser/dump_tools.py index 48d6295..0a65286 100644 --- a/jubeatools/formats/jubeat_analyser/dump_tools.py +++ b/jubeatools/formats/jubeat_analyser/dump_tools.py @@ -5,12 +5,20 @@ from dataclasses import dataclass, field from decimal import Decimal from fractions import Fraction from itertools import chain -from typing import Dict, Optional, List, Union, Iterator, Callable, Mapping +from typing import Callable, Dict, Iterator, List, Mapping, Optional, Union from more_itertools import collapse, intersperse, mark_ends, windowed from sortedcontainers import SortedDict, SortedKeyList -from jubeatools.song import BeatsTime, TapNote, LongNote, NotePosition, Chart, Timing, Metadata +from jubeatools.song import ( + BeatsTime, + Chart, + LongNote, + Metadata, + NotePosition, + TapNote, + Timing, +) from .command import dump_command from .symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS @@ -28,7 +36,7 @@ COMMAND_ORDER = [ "jacket", "prevpos", "holdbyarrow", - "circlefree" + "circlefree", ] BEATS_TIME_TO_SYMBOL = { @@ -48,6 +56,7 @@ DIRECTION_TO_ARROW = { NotePosition(0, 1): "∧", # U+2227 : LOGICAL AND } +# do NOT use the regular vertical bar, it will clash with the timing portion DIRECTION_TO_LINE = { NotePosition(-1, 0): "―", # U+2015 : HORIZONTAL BAR NotePosition(1, 0): "―", @@ -71,6 +80,7 @@ def fraction_to_decimal(frac: Fraction): "Thanks stackoverflow ! https://stackoverflow.com/a/40468867/10768117" return frac.numerator / Decimal(frac.denominator) + @dataclass(frozen=True) class LongNoteEnd: time: BeatsTime @@ -106,19 +116,6 @@ class JubeatAnalyserDumpedSection(ABC): symbols: Dict[BeatsTime, str] = field(default_factory=dict) notes: List[Union[TapNote, LongNote, LongNoteEnd]] = field(default_factory=list) - def render(self, circle_free: bool = False) -> str: - blocs = [] - commands = list(self._dump_commands()) - if commands: - blocs.append(commands) - symbols = list(self._dump_symbol_definitions()) - if symbols: - blocs.append(symbols) - notes = list(self._dump_notes(circle_free)) - if notes: - blocs.append(notes) - return "\n".join(collapse([intersperse("", blocs), "--"])) - def _dump_commands(self) -> Iterator[str]: keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER)) for key in keys: @@ -144,7 +141,7 @@ def create_sections_from_chart( difficulty: str, timing: Timing, metadata: Metadata, - circle_free: bool + circle_free: bool, ) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]: sections = SortedDefaultDict(section_factory) @@ -210,5 +207,5 @@ def create_sections_from_chart( sections[key].notes = list( notes.irange_key(min_key=key, max_key=next_key, inclusive=(True, False)) ) - - return sections \ No newline at end of file + + return sections diff --git a/jubeatools/formats/jubeat_analyser/load_tools.py b/jubeatools/formats/jubeat_analyser/load_tools.py index 3b094fb..7029b4c 100644 --- a/jubeatools/formats/jubeat_analyser/load_tools.py +++ b/jubeatools/formats/jubeat_analyser/load_tools.py @@ -1,10 +1,14 @@ """Collection of parsing tools that are common to all the jubeat analyser formats""" import re +import warnings from collections import Counter from copy import deepcopy from dataclasses import dataclass from decimal import Decimal -from typing import Dict, List, Tuple +from itertools import product +from typing import Dict, Iterator, List, Set, Tuple + +import constraint from jubeatools.song import BeatsTime, BPMEvent, LongNote, NotePosition @@ -65,24 +69,6 @@ def decimal_to_beats(decimal_time: Decimal) -> BeatsTime: return BeatsTime(decimal_time).limit_denominator(240) -def note_distance(a: NotePosition, b: NotePosition) -> float: - return abs(complex(*a.as_tuple()) - complex(*b.as_tuple())) - - -def long_note_solution_heuristic( - solution: Dict[NotePosition, NotePosition] -) -> Tuple[int, int, int]: - c = Counter(int(note_distance(k, v)) for k, v in solution.items()) - return (c[3], c[2], c[1]) - - -def is_simple_solution(solution, domains) -> bool: - return all( - solution[v] == min(domains[v], key=lambda e: note_distance(e, v)) - for v in solution.keys() - ) - - @dataclass(frozen=True) class UnfinishedLongNote: time: BeatsTime @@ -102,6 +88,94 @@ class UnfinishedLongNote: ) +def find_long_note_candidates( + bloc: List[List[str]], note_symbols: Set[str], should_skip: Set[NotePosition] +) -> Dict[NotePosition, Set[NotePosition]]: + "Return a dict of arrow position to landing note candidates" + arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} + for y, x in product(range(4), range(4)): + pos = NotePosition(x, y) + if pos in should_skip: + continue + symbol = bloc[y][x] + if symbol not in LONG_ARROWS: + continue + + # at this point we are sure we have a long arrow + # we need to check in its direction for note candidates + note_candidates: Set[Tuple[int, int]] = set() + 𝛿pos = LONG_DIRECTION[symbol] + candidate = NotePosition(x, y) + 𝛿pos + while 0 <= candidate.x < 4 and 0 <= candidate.y < 4: + if candidate not in should_skip: + new_symbol = bloc[candidate.y][candidate.x] + if new_symbol in note_symbols: + note_candidates.add(candidate) + candidate += 𝛿pos + + # if no notes have been crossed, we just ignore the arrow + if note_candidates: + arrow_to_note_candidates[pos] = note_candidates + + return arrow_to_note_candidates + + +def pick_correct_long_note_candidates( + arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]], + bloc: List[List[str]], + should_skip: Set[NotePosition], + currently_defined_symbols: Dict[str, Decimal], + section_starting_beat: Decimal, +) -> Iterator[UnfinishedLongNote]: + """Believe it or not, assigning each arrow to a valid note candidate + involves whipping out a CSP solver""" + problem = constraint.Problem() + for arrow_pos, note_candidates in arrow_to_note_candidates.items(): + problem.addVariable(arrow_pos, list(note_candidates)) + problem.addConstraint(constraint.AllDifferentConstraint()) + solutions = problem.getSolutions() + if not solutions: + raise SyntaxError( + "Invalid long note arrow pattern in bloc :\n" + + "\n".join("".join(line) for line in bloc) + ) + solution = min(solutions, key=long_note_solution_heuristic) + if len(solutions) > 1 and not is_simple_solution( + solution, arrow_to_note_candidates + ): + warnings.warn( + "Ambiguous arrow pattern in bloc :\n" + + "\n".join("".join(line) for line in bloc) + + "\n" + "The resulting long notes might not be what you expect" + ) + for arrow_pos, note_pos in solution.items(): + should_skip.add(arrow_pos) + should_skip.add(note_pos) + symbol = bloc[note_pos.y][note_pos.x] + symbol_time = currently_defined_symbols[symbol] + note_time = decimal_to_beats(section_starting_beat + symbol_time) + yield UnfinishedLongNote(time=note_time, position=note_pos, tail_tip=arrow_pos) + + +def note_distance(a: NotePosition, b: NotePosition) -> float: + return abs(complex(*a.as_tuple()) - complex(*b.as_tuple())) + + +def long_note_solution_heuristic( + solution: Dict[NotePosition, NotePosition] +) -> Tuple[int, int, int]: + c = Counter(int(note_distance(k, v)) for k, v in solution.items()) + return (c[3], c[2], c[1]) + + +def is_simple_solution(solution, domains) -> bool: + return all( + solution[v] == min(domains[v], key=lambda e: note_distance(e, v)) + for v in solution.keys() + ) + + class JubeatAnalyserParser: def __init__(self): self.music = None diff --git a/jubeatools/formats/jubeat_analyser/memo/dump.py b/jubeatools/formats/jubeat_analyser/memo/dump.py index fa7bfe5..4a59b13 100644 --- a/jubeatools/formats/jubeat_analyser/memo/dump.py +++ b/jubeatools/formats/jubeat_analyser/memo/dump.py @@ -7,7 +7,7 @@ from functools import partial from io import StringIO from itertools import chain, zip_longest from math import ceil -from typing import Dict, Iterator, List, Optional, Tuple, Union, Set +from typing import Dict, Iterator, List, Optional, Set, Tuple, Union from more_itertools import chunked, collapse, intersperse, mark_ends, windowed from path import Path @@ -36,19 +36,19 @@ from ..dump_tools import ( DIRECTION_TO_LINE, NOTE_TO_CIRCLE_FREE_SYMBOL, JubeatAnalyserDumpedSection, - create_sections_from_chart, LongNoteEnd, SortedDefaultDict, + create_sections_from_chart, fraction_to_decimal, ) from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS - AnyNote = Union[TapNote, LongNote, LongNoteEnd] EMPTY_BEAT_SYMBOL = "-" # U+0FF0D : FULLWIDTH HYPHEN-MINUS EMPTY_POSITION_SYMBOL = "□" # U+025A1 : WHITE SQUARE + @dataclass class Frame: positions: Dict[NotePosition, str] = field(default_factory=dict) @@ -57,21 +57,23 @@ class Frame: def dump(self, length: Decimal) -> Iterator[str]: # Check that bars are contiguous for a, b in windowed(sorted(self.bars), 2): - if b is not None and b-a != 1: + if b is not None and b - a != 1: raise ValueError("Frame has discontinuous bars") # Check all bars are in the same 4-bar group - if self.bars.keys() != set(bar%4 for bar in self.bars): + if self.bars.keys() != set(bar % 4 for bar in self.bars): raise ValueError("Frame contains bars from different 4-bar groups") - + for pos, bar in zip_longest(self.dump_positions(), self.dump_bars(length)): if bar is None: bar = "" yield f"{pos} {bar}" - def dump_positions(self) -> Iterator[str]: for y in range(4): - yield "".join(self.positions.get(NotePosition(x, y), EMPTY_POSITION_SYMBOL) for x in range(4)) + yield "".join( + self.positions.get(NotePosition(x, y), EMPTY_POSITION_SYMBOL) + for x in range(4) + ) def dump_bars(self, length: Decimal) -> Iterator[str]: all_bars = [] @@ -80,7 +82,7 @@ class Frame: time_index = i % 4 symbol = self.bars.get(bar_index, {}).get(time_index, EMPTY_BEAT_SYMBOL) all_bars.append(symbol) - + for i, bar in enumerate(chunked(all_bars, 4)): if i in self.bars: yield f"|{''.join(bar)}|" @@ -89,6 +91,19 @@ class Frame: class MemoDumpedSection(JubeatAnalyserDumpedSection): + def render(self, circle_free: bool = False) -> str: + blocs = [] + commands = list(self._dump_commands()) + if commands: + blocs.append(commands) + symbols = list(self._dump_symbol_definitions()) + if symbols: + blocs.append(symbols) + notes = list(self._dump_notes(circle_free)) + if notes: + blocs.append(notes) + return "\n".join(collapse(intersperse("", blocs))) + def _dump_notes(self, circle_free: bool = False) -> Iterator[str]: notes_by_bar: Dict[int, List[AnyNote]] = defaultdict(list) bars: Dict[int, Dict[int, str]] = defaultdict(dict) @@ -99,14 +114,17 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection): bar_index = int(time_in_section) notes_by_bar[bar_index].append(note) if time_in_section % Fraction(1, 4) == 0: - time_index = int(time_in_section * 4) + time_in_bar = time_in_section % Fraction(1) + time_index = int(time_in_bar * 4) if time_index not in bars[bar_index]: symbol = next(symbols_iterator) chosen_symbols[time_in_section] = symbol bars[bar_index][time_index] = symbol elif time_in_section not in self.symbols: - raise ValueError(f"No symbol defined for time in section : {time_in_section}") - + raise ValueError( + f"No symbol defined for time in section : {time_in_section}" + ) + # Create frame by bar section_symbols = ChainMap(chosen_symbols, self.symbols) frames_by_bar: Dict[int, List[Frame]] = defaultdict(list) @@ -157,11 +175,14 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection): # - The previous and current bars are all in the same 4-bar group # - The note positions in the previous frame do not clash with the current frame if ( - len(frames) == 1 and - final_frames and - final_frames[-1].bars and - max(final_frames[-1].bars.keys()) // 4 == min(frames[0].bars.keys()) // 4 and - (not (final_frames[-1].positions.keys() & frames[0].positions.keys())) + len(frames) == 1 + and final_frames + and final_frames[-1].bars + and max(final_frames[-1].bars.keys()) // 4 + == min(frames[0].bars.keys()) // 4 + and ( + not (final_frames[-1].positions.keys() & frames[0].positions.keys()) + ) ): final_frames[-1].bars.update(frames[0].bars) final_frames[-1].positions.update(frames[0].positions) @@ -169,8 +190,7 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection): final_frames.extend(frames) dumped_frames = map(lambda f: f.dump(self.length), final_frames) - yield from collapse(intersperse("", dumped_frames)) - + yield from collapse(intersperse("", dumped_frames)) def _raise_if_unfit_for_memo(chart: Chart, timing: Timing, circle_free: bool = False): @@ -201,9 +221,11 @@ def _dump_memo_chart( ) -> StringIO: _raise_if_unfit_for_memo(chart, timing, circle_free) - - sections = create_sections_from_chart(MemoDumpedSection, chart, difficulty, timing, metadata, circle_free) - + + sections = create_sections_from_chart( + MemoDumpedSection, chart, difficulty, timing, metadata, circle_free + ) + # Jubeat Analyser format command sections[0].commands["memo"] = None @@ -216,11 +238,14 @@ def _dump_memo_chart( section.symbols = existing_symbols for note in section.notes: time_in_section = note.time - section_start - if time_in_section % Fraction(1, 4) != 0 and time_in_section not in existing_symbols: + if ( + time_in_section % Fraction(1, 4) != 0 + and time_in_section not in existing_symbols + ): new_symbol = next(extra_symbols) section.symbol_definitions[time_in_section] = new_symbol existing_symbols[time_in_section] = new_symbol - + # Actual output to file file = StringIO() file.write(f"// Converted using jubeatools {__version__}\n") @@ -232,7 +257,7 @@ def _dump_memo_chart( def _dump_memo_internal(song: Song, circle_free: bool = False) -> List[JubeatFile]: - files = [] + files: List[JubeatFile] = [] for difficulty, chart in song.charts.items(): contents = _dump_memo_chart( difficulty, diff --git a/jubeatools/formats/jubeat_analyser/memo/load.py b/jubeatools/formats/jubeat_analyser/memo/load.py index b6b6539..24d7318 100644 --- a/jubeatools/formats/jubeat_analyser/memo/load.py +++ b/jubeatools/formats/jubeat_analyser/memo/load.py @@ -8,7 +8,7 @@ from itertools import chain, product, zip_longest from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union import constraint -from more_itertools import mark_ends, collapse +from more_itertools import collapse, mark_ends from parsimonious import Grammar, NodeVisitor, ParseError from path import Path @@ -33,9 +33,11 @@ from ..load_tools import ( JubeatAnalyserParser, UnfinishedLongNote, decimal_to_beats, + find_long_note_candidates, is_empty_line, is_simple_solution, long_note_solution_heuristic, + pick_correct_long_note_candidates, split_double_byte_line, ) from ..symbol_definition import is_symbol_definition, parse_symbol_definition @@ -44,9 +46,8 @@ from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS memo_chart_line_grammar = Grammar( r""" line = ws position_part ws (timing_part ws)? comment? - position_part = ~r"[^*#:||/\s]{4,8}" - timing_part = bar ~r"[^*#:||/\s]+" bar - bar = "|" / "|" + position_part = ~r"[^*#:|/\s]{4,8}" + timing_part = "|" ~r"[^*#:|/\s]+" "|" ws = ~r"[\t ]*" comment = ~r"//.*" """ @@ -265,32 +266,29 @@ class MemoParser(JubeatAnalyserParser): def _iter_frames( self, - ) -> Iterator[ - Tuple[Mapping[str, Decimal], Decimal, MemoFrame, Decimal, MemoLoadedSection] - ]: + ) -> Iterator[Tuple[Mapping[str, Decimal], MemoFrame, Decimal, MemoLoadedSection]]: """iterate over tuples of currently_defined_symbols, frame_starting_beat, frame, section_starting_beat, section""" local_symbols: Dict[str, Decimal] = {} section_starting_beat = Decimal(0) for section in self.sections: frame_starting_beat = Decimal(0) - for frame in section.frames: + for i, frame in enumerate(section.frames): if frame.timing_part: + frame_starting_beat = sum(f.duration for f in section.frames[:i]) local_symbols = { - symbol: Decimal("0.25") * i + symbol: Decimal("0.25") * i + frame_starting_beat for i, symbol in enumerate(collapse(frame.timing_part)) if symbol not in EMPTY_BEAT_SYMBOLS } currently_defined_symbols = ChainMap(local_symbols, section.symbols) - yield currently_defined_symbols, frame_starting_beat, frame, section_starting_beat, section - frame_starting_beat += frame.duration + yield currently_defined_symbols, frame, section_starting_beat, section section_starting_beat += section.length def _iter_notes(self) -> Iterator[Union[TapNote, LongNote]]: unfinished_longs: Dict[NotePosition, UnfinishedLongNote] = {} for ( currently_defined_symbols, - frame_starting_beat, frame, section_starting_beat, section, @@ -319,9 +317,7 @@ class MemoParser(JubeatAnalyserParser): continue should_skip.add(pos) - note_time = decimal_to_beats( - section_starting_beat + frame_starting_beat + symbol_time - ) + note_time = decimal_to_beats(section_starting_beat + symbol_time) yield unfinished_long.ends_at(note_time) unfinished_longs = { @@ -329,59 +325,22 @@ class MemoParser(JubeatAnalyserParser): } # 2/3 : look for new long notes starting on this bloc - arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} - for y, x in product(range(4), range(4)): - pos = NotePosition(x, y) - if pos in should_skip: - continue - symbol = frame.position_part[y][x] - if symbol not in LONG_ARROWS: - continue - # at this point we are sure we have a long arrow - # we need to check in its direction for note candidates - note_candidates: Set[Tuple[int, int]] = set() - 𝛿pos = LONG_DIRECTION[symbol] - candidate = NotePosition(x, y) + 𝛿pos - while 0 <= candidate.x < 4 and 0 <= candidate.y < 4: - if candidate in should_skip: - continue - new_symbol = frame.position_part[candidate.y][candidate.x] - if new_symbol in currently_defined_symbols: - note_candidates.add(candidate) - candidate += 𝛿pos - # if no notes have been crossed, we just ignore the arrow - if note_candidates: - arrow_to_note_candidates[pos] = note_candidates - - # Believe it or not, assigning each arrow to a valid note candidate - # involves whipping out a CSP solver + arrow_to_note_candidates = find_long_note_candidates( + frame.position_part, currently_defined_symbols.keys(), should_skip + ) if arrow_to_note_candidates: - problem = constraint.Problem() - for arrow_pos, note_candidates in arrow_to_note_candidates.items(): - problem.addVariable(arrow_pos, list(note_candidates)) - problem.addConstraint(constraint.AllDifferentConstraint()) - solutions = problem.getSolutions() - if not solutions: - raise SyntaxError( - "Invalid long note arrow pattern in section :\n" + str(section) - ) - solution = min(solutions, key=long_note_solution_heuristic) - if len(solutions) > 1 and not is_simple_solution( - solution, arrow_to_note_candidates - ): - warnings.warn( - "Ambiguous arrow pattern in section :\n" + str(section) + "\n" - "The chosen long notes might not be what you expect" - ) - for arrow_pos, note_pos in solution.items(): - should_skip.add(arrow_pos) - should_skip.add(note_pos) - symbol = frame.position_part[note_pos.y][note_pos.x] - symbol_time = section.symbols[symbol] - note_time = decimal_to_beats(section_starting_beat + symbol_time) - unfinished_longs[note_pos] = UnfinishedLongNote( - time=note_time, position=note_pos, tail_tip=arrow_pos, - ) + unfinished_longs.update( + { + note.position: note + for note in pick_correct_long_note_candidates( + arrow_to_note_candidates, + frame.position_part, + should_skip, + currently_defined_symbols, + section_starting_beat, + ) + } + ) # 3/3 : find regular notes for y, x in product(range(4), range(4)): @@ -393,15 +352,12 @@ class MemoParser(JubeatAnalyserParser): symbol_time = currently_defined_symbols[symbol] except KeyError: continue - note_time = decimal_to_beats( - section_starting_beat + frame_starting_beat + symbol_time - ) + note_time = decimal_to_beats(section_starting_beat + symbol_time) yield TapNote(note_time, position) def _iter_notes_without_longs(self) -> Iterator[TapNote]: for ( currently_defined_symbols, - frame_starting_beat, frame, section_starting_beat, _, @@ -413,9 +369,7 @@ class MemoParser(JubeatAnalyserParser): symbol_time = currently_defined_symbols[symbol] except KeyError: continue - note_time = decimal_to_beats( - section_starting_beat + frame_starting_beat + symbol_time - ) + note_time = decimal_to_beats(section_starting_beat + symbol_time) position = NotePosition(x, y) yield TapNote(note_time, position) diff --git a/jubeatools/formats/jubeat_analyser/mono_column/dump.py b/jubeatools/formats/jubeat_analyser/mono_column/dump.py index 016e590..9dd685c 100644 --- a/jubeatools/formats/jubeat_analyser/mono_column/dump.py +++ b/jubeatools/formats/jubeat_analyser/mono_column/dump.py @@ -33,15 +33,28 @@ from ..dump_tools import ( DIRECTION_TO_ARROW, DIRECTION_TO_LINE, JubeatAnalyserDumpedSection, - create_sections_from_chart, LongNoteEnd, SortedDefaultDict, + create_sections_from_chart, fraction_to_decimal, ) from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS class MonoColumnDumpedSection(JubeatAnalyserDumpedSection): + def render(self, circle_free: bool = False) -> str: + blocs = [] + commands = list(self._dump_commands()) + if commands: + blocs.append(commands) + symbols = list(self._dump_symbol_definitions()) + if symbols: + blocs.append(symbols) + notes = list(self._dump_notes(circle_free)) + if notes: + blocs.append(notes) + return "\n".join(collapse([intersperse("", blocs), "--"])) + def _dump_notes(self, circle_free: bool = False,) -> Iterator[str]: frames: List[Dict[NotePosition, str]] = [] frame: Dict[NotePosition, str] = {} @@ -122,6 +135,7 @@ def _raise_if_unfit_for_mono_column( " representable in #circlefree mode" ) + def _dump_mono_column_chart( difficulty: str, chart: Chart, @@ -132,8 +146,10 @@ def _dump_mono_column_chart( _raise_if_unfit_for_mono_column(chart, timing, circle_free) - sections = create_sections_from_chart(MonoColumnDumpedSection, chart, difficulty, timing, metadata, circle_free) - + sections = create_sections_from_chart( + MonoColumnDumpedSection, chart, difficulty, timing, metadata, circle_free + ) + # Define extra symbols existing_symbols = deepcopy(BEATS_TIME_TO_SYMBOL) extra_symbols = iter(DEFAULT_EXTRA_SYMBOLS) diff --git a/jubeatools/formats/jubeat_analyser/mono_column/load.py b/jubeatools/formats/jubeat_analyser/mono_column/load.py index 6817960..1307835 100644 --- a/jubeatools/formats/jubeat_analyser/mono_column/load.py +++ b/jubeatools/formats/jubeat_analyser/mono_column/load.py @@ -36,9 +36,11 @@ from ..load_tools import ( JubeatAnalyserParser, UnfinishedLongNote, decimal_to_beats, + find_long_note_candidates, is_empty_line, is_simple_solution, long_note_solution_heuristic, + pick_correct_long_note_candidates, split_double_byte_line, ) from ..symbol_definition import is_symbol_definition, parse_symbol_definition @@ -227,62 +229,22 @@ class MonoColumnParser(JubeatAnalyserParser): } # 2/3 : look for new long notes starting on this bloc - arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} - for y, x in product(range(4), range(4)): - pos = NotePosition(x, y) - if pos in should_skip: - continue - symbol = bloc[y][x] - if symbol not in LONG_ARROWS: - continue - # at this point we are sure we have a long arrow - # we need to check in its direction for note candidates - note_candidates: Set[Tuple[int, int]] = set() - 𝛿pos = LONG_DIRECTION[symbol] - candidate = NotePosition(x, y) + 𝛿pos - while 0 <= candidate.x < 4 and 0 <= candidate.y < 4: - if candidate in should_skip: - continue - new_symbol = bloc[candidate.y][candidate.x] - if new_symbol in section.symbols: - note_candidates.add(candidate) - candidate += 𝛿pos - # if no notes have been crossed, we just ignore the arrow - if note_candidates: - arrow_to_note_candidates[pos] = note_candidates - - # Believe it or not, assigning each arrow to a valid note candidate - # involves whipping out a CSP solver + arrow_to_note_candidates = find_long_note_candidates( + bloc, section.symbols.keys(), should_skip + ) if arrow_to_note_candidates: - problem = constraint.Problem() - for arrow_pos, note_candidates in arrow_to_note_candidates.items(): - problem.addVariable(arrow_pos, list(note_candidates)) - problem.addConstraint(constraint.AllDifferentConstraint()) - solutions = problem.getSolutions() - if not solutions: - raise SyntaxError( - "Invalid long note arrow pattern in bloc :\n" - + "\n".join("".join(line) for line in bloc) - ) - solution = min(solutions, key=long_note_solution_heuristic) - if len(solutions) > 1 and not is_simple_solution( - solution, arrow_to_note_candidates - ): - warnings.warn( - "Ambiguous arrow pattern in bloc :\n" - + "\n".join("".join(line) for line in bloc) - + "\n" - "The resulting long notes might not be what you expect" - ) - for arrow_pos, note_pos in solution.items(): - should_skip.add(arrow_pos) - should_skip.add(note_pos) - symbol = bloc[note_pos.y][note_pos.x] - symbol_time = section.symbols[symbol] - note_time = decimal_to_beats(section_starting_beat + symbol_time) - unfinished_longs[note_pos] = UnfinishedLongNote( - time=note_time, position=note_pos, tail_tip=arrow_pos, - ) + unfinished_longs.update( + { + note.position: note + for note in pick_correct_long_note_candidates( + arrow_to_note_candidates, + bloc, + should_skip, + section.symbols, + section_starting_beat, + ) + } + ) # 3/3 : find regular notes for y, x in product(range(4), range(4)): diff --git a/jubeatools/formats/jubeat_analyser/tests/test_memo.py b/jubeatools/formats/jubeat_analyser/tests/test_memo.py index 0d7b003..72292cd 100644 --- a/jubeatools/formats/jubeat_analyser/tests/test_memo.py +++ b/jubeatools/formats/jubeat_analyser/tests/test_memo.py @@ -1,8 +1,19 @@ from decimal import Decimal +from fractions import Fraction from hypothesis import given -from jubeatools.song import BeatsTime, BPMEvent, Chart, Metadata, SecondsTime, Timing +from jubeatools.song import ( + BeatsTime, + BPMEvent, + Chart, + LongNote, + Metadata, + NotePosition, + SecondsTime, + TapNote, + Timing, +) from jubeatools.testutils.strategies import NoteOption from jubeatools.testutils.strategies import notes as notes_strat diff --git a/jubeatools/testutils/strategies.py b/jubeatools/testutils/strategies.py index 4f6fe62..3d98caf 100644 --- a/jubeatools/testutils/strategies.py +++ b/jubeatools/testutils/strategies.py @@ -3,6 +3,7 @@ Hypothesis strategies to generate notes and charts """ from enum import Enum, Flag, auto from itertools import product +from typing import Set, Union import hypothesis.strategies as st from multidict import MultiDict @@ -111,7 +112,7 @@ def notes(draw, options: NoteOption): return raw_notes else: last_notes = {NotePosition(x, y): None for y, x in product(range(4), range(4))} - notes = set() + notes: Set[Union[TapNote, LongNote]] = set() for note in sorted(raw_notes, key=lambda n: (n.time, n.position)): last_note_time = last_notes[note.position] if last_note_time is None: diff --git a/poetry.lock b/poetry.lock index 286976b..da1e73a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -72,7 +72,7 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.19.0" +version = "5.20.1" [package.dependencies] attrs = ">=19.2.0" @@ -109,12 +109,12 @@ description = "A lightweight library for converting complex datatypes to and fro name = "marshmallow" optional = false python-versions = ">=3.5" -version = "3.6.1" +version = "3.7.0" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (0.770)", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (3.0.4)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"] -lint = ["mypy (0.770)", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] +dev = ["pytest", "pytz", "simplejson", "mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (3.1.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"] +lint = ["mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -267,7 +267,7 @@ description = "Alternative regular expression module, to replace re." name = "regex" optional = false python-versions = "*" -version = "2020.6.8" +version = "2020.7.14" [[package]] category = "dev" @@ -286,7 +286,7 @@ description = "Simple, fast, extensible JSON encoder/decoder for Python" name = "simplejson" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" -version = "3.17.0" +version = "3.17.2" [[package]] category = "main" @@ -366,16 +366,16 @@ colorama = [ {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] hypothesis = [ - {file = "hypothesis-5.19.0-py3-none-any.whl", hash = "sha256:dd21b1be951fefc9022047824c262f4e88d95dd24141b837b92e235c63baabb7"}, - {file = "hypothesis-5.19.0.tar.gz", hash = "sha256:ba7c92006716aaee4684f7876c116adedcfb88b19fcb55d21c47b28f03f933bf"}, + {file = "hypothesis-5.20.1-py3-none-any.whl", hash = "sha256:22a3d4388046a02e132fa6889be2a25db70567a218cb73091689a4788c7c9acf"}, + {file = "hypothesis-5.20.1.tar.gz", hash = "sha256:ee9eac5dd988cb438aa1aeb03b62ee5374160ee4e2b24a7d4a141cc188361979"}, ] isort = [ {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, ] marshmallow = [ - {file = "marshmallow-3.6.1-py2.py3-none-any.whl", hash = "sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f"}, - {file = "marshmallow-3.6.1.tar.gz", hash = "sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5"}, + {file = "marshmallow-3.7.0-py2.py3-none-any.whl", hash = "sha256:0f3a630f6a2fd124929f1bdcb5df65bd14cc8f49f52a18d0bdcfa0c42414e4a7"}, + {file = "marshmallow-3.7.0.tar.gz", hash = "sha256:ba949379cb6ef73655f72075e82b31cf57012a5557ede642fc8614ab0354f869"}, ] more-itertools = [ {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, @@ -455,60 +455,77 @@ python-constraint = [ {file = "python-constraint-1.4.0.tar.bz2", hash = "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e"}, ] regex = [ - {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, - {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, - {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, - {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, - {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, - {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, - {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, - {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, - {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, ] rope = [ {file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"}, ] simplejson = [ - {file = "simplejson-3.17.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:87d349517b572964350cc1adc5a31b493bbcee284505e81637d0174b2758ba17"}, - {file = "simplejson-3.17.0-cp27-cp27m-win32.whl", hash = "sha256:1d1e929cdd15151f3c0b2efe953b3281b2fd5ad5f234f77aca725f28486466f6"}, - {file = "simplejson-3.17.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1ea59f570b9d4916ae5540a9181f9c978e16863383738b69a70363bc5e63c4cb"}, - {file = "simplejson-3.17.0-cp33-cp33m-win32.whl", hash = "sha256:8027bd5f1e633eb61b8239994e6fc3aba0346e76294beac22a892eb8faa92ba1"}, - {file = "simplejson-3.17.0-cp33-cp33m-win_amd64.whl", hash = "sha256:22a7acb81968a7c64eba7526af2cf566e7e2ded1cb5c83f0906b17ff1540f866"}, - {file = "simplejson-3.17.0-cp34-cp34m-win32.whl", hash = "sha256:17163e643dbf125bb552de17c826b0161c68c970335d270e174363d19e7ea882"}, - {file = "simplejson-3.17.0-cp34-cp34m-win_amd64.whl", hash = "sha256:0fe3994207485efb63d8f10a833ff31236ed27e3b23dadd0bf51c9900313f8f2"}, - {file = "simplejson-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:4cf91aab51b02b3327c9d51897960c554f00891f9b31abd8a2f50fd4a0071ce8"}, - {file = "simplejson-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fc9051d249dd5512e541f20330a74592f7a65b2d62e18122ca89bf71f94db748"}, - {file = "simplejson-3.17.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86afc5b5cbd42d706efd33f280fec7bd7e2772ef54e3f34cf6b30777cd19a614"}, - {file = "simplejson-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:926bcbef9eb60e798eabda9cd0bbcb0fca70d2779aa0aa56845749d973eb7ad5"}, - {file = "simplejson-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:daaf4d11db982791be74b23ff4729af2c7da79316de0bebf880fa2d60bcc8c5a"}, - {file = "simplejson-3.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a126c3a91df5b1403e965ba63b304a50b53d8efc908a8c71545ed72535374a3"}, - {file = "simplejson-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:fc046afda0ed8f5295212068266c92991ab1f4a50c6a7144b69364bdee4a0159"}, - {file = "simplejson-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cce4bac7e0d66f3a080b80212c2238e063211fe327f98d764c6acbc214497fc"}, - {file = "simplejson-3.17.0.tar.gz", hash = "sha256:2b4b2b738b3b99819a17feaf118265d0753d5536049ea570b3c43b51c4701e81"}, - {file = "simplejson-3.17.0.win-amd64-py2.7.exe", hash = "sha256:1d346c2c1d7dd79c118f0cc7ec5a1c4127e0c8ffc83e7b13fc5709ff78c9bb84"}, - {file = "simplejson-3.17.0.win-amd64-py3.3.exe", hash = "sha256:5cfd495527f8b85ce21db806567de52d98f5078a8e9427b18e251c68bd573a26"}, - {file = "simplejson-3.17.0.win-amd64-py3.4.exe", hash = "sha256:8de378d589eccbc75941e480b4d5b4db66f22e4232f87543b136b1f093fff342"}, - {file = "simplejson-3.17.0.win-amd64-py3.5.exe", hash = "sha256:f4b64a1031acf33e281fd9052336d6dad4d35eee3404c95431c8c6bc7a9c0588"}, - {file = "simplejson-3.17.0.win-amd64-py3.6.exe", hash = "sha256:ad8dd3454d0c65c0f92945ac86f7b9efb67fa2040ba1b0189540e984df904378"}, - {file = "simplejson-3.17.0.win-amd64-py3.7.exe", hash = "sha256:229edb079d5dd81bf12da952d4d825bd68d1241381b37d3acf961b384c9934de"}, - {file = "simplejson-3.17.0.win32-py2.7.exe", hash = "sha256:4fd5f79590694ebff8dc980708e1c182d41ce1fda599a12189f0ca96bf41ad70"}, - {file = "simplejson-3.17.0.win32-py3.3.exe", hash = "sha256:d140e9376e7f73c1f9e0a8e3836caf5eec57bbafd99259d56979da05a6356388"}, - {file = "simplejson-3.17.0.win32-py3.4.exe", hash = "sha256:da00675e5e483ead345429d4f1374ab8b949fba4429d60e71ee9d030ced64037"}, - {file = "simplejson-3.17.0.win32-py3.5.exe", hash = "sha256:7739940d68b200877a15a5ff5149e1599737d6dd55e302625650629350466418"}, - {file = "simplejson-3.17.0.win32-py3.6.exe", hash = "sha256:60aad424e47c5803276e332b2a861ed7a0d46560e8af53790c4c4fb3420c26c2"}, - {file = "simplejson-3.17.0.win32-py3.7.exe", hash = "sha256:1fbba86098bbfc1f85c5b69dc9a6d009055104354e0d9880bb00b692e30e0078"}, + {file = "simplejson-3.17.2-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8"}, + {file = "simplejson-3.17.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6"}, + {file = "simplejson-3.17.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d"}, + {file = "simplejson-3.17.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043"}, + {file = "simplejson-3.17.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4"}, + {file = "simplejson-3.17.2-cp27-cp27m-win32.whl", hash = "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9"}, + {file = "simplejson-3.17.2-cp27-cp27m-win_amd64.whl", hash = "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667"}, + {file = "simplejson-3.17.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06"}, + {file = "simplejson-3.17.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f"}, + {file = "simplejson-3.17.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b"}, + {file = "simplejson-3.17.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f"}, + {file = "simplejson-3.17.2-cp33-cp33m-win32.whl", hash = "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746"}, + {file = "simplejson-3.17.2-cp33-cp33m-win_amd64.whl", hash = "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748"}, + {file = "simplejson-3.17.2-cp34-cp34m-win32.whl", hash = "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3"}, + {file = "simplejson-3.17.2-cp34-cp34m-win_amd64.whl", hash = "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b"}, + {file = "simplejson-3.17.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d"}, + {file = "simplejson-3.17.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956"}, + {file = "simplejson-3.17.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d"}, + {file = "simplejson-3.17.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971"}, + {file = "simplejson-3.17.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a"}, + {file = "simplejson-3.17.2-cp35-cp35m-win32.whl", hash = "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396"}, + {file = "simplejson-3.17.2-cp35-cp35m-win_amd64.whl", hash = "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a"}, + {file = "simplejson-3.17.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb"}, + {file = "simplejson-3.17.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb"}, + {file = "simplejson-3.17.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f"}, + {file = "simplejson-3.17.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45"}, + {file = "simplejson-3.17.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139"}, + {file = "simplejson-3.17.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46"}, + {file = "simplejson-3.17.2-cp36-cp36m-win32.whl", hash = "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b"}, + {file = "simplejson-3.17.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625"}, + {file = "simplejson-3.17.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25"}, + {file = "simplejson-3.17.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf"}, + {file = "simplejson-3.17.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0"}, + {file = "simplejson-3.17.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f"}, + {file = "simplejson-3.17.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04"}, + {file = "simplejson-3.17.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278"}, + {file = "simplejson-3.17.2-cp37-cp37m-win32.whl", hash = "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34"}, + {file = "simplejson-3.17.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0"}, + {file = "simplejson-3.17.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da"}, + {file = "simplejson-3.17.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a"}, + {file = "simplejson-3.17.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995"}, + {file = "simplejson-3.17.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc"}, + {file = "simplejson-3.17.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94"}, + {file = "simplejson-3.17.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8"}, + {file = "simplejson-3.17.2.tar.gz", hash = "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},