1
0
mirror of synced 2024-11-13 18:10:48 +01:00

HOLY FUCK #memo PASSED UNIT TESTS

This commit is contained in:
Stepland 2020-07-18 17:31:01 +02:00
parent 0fc63e08aa
commit 0ec5acc086
9 changed files with 314 additions and 257 deletions

View File

@ -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
return sections

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)):

View File

@ -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

View File

@ -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:

137
poetry.lock generated
View File

@ -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"},