HOLY FUCK #memo PASSED UNIT TESTS
This commit is contained in:
parent
0fc63e08aa
commit
0ec5acc086
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,10 +57,10 @@ 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)):
|
||||
@ -68,10 +68,12 @@ class Frame:
|
||||
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 = []
|
||||
@ -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,13 +114,16 @@ 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)
|
||||
@ -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)
|
||||
@ -172,7 +193,6 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection):
|
||||
yield from collapse(intersperse("", dumped_frames))
|
||||
|
||||
|
||||
|
||||
def _raise_if_unfit_for_memo(chart: Chart, timing: Timing, circle_free: bool = False):
|
||||
if len(timing.events) < 1:
|
||||
raise ValueError("No BPM found in file") from None
|
||||
@ -202,7 +222,9 @@ def _dump_memo_chart(
|
||||
|
||||
_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,7 +238,10 @@ 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
|
||||
@ -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,
|
||||
|
@ -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,58 +325,21 @@ 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)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
# 3/3 : find regular notes
|
||||
@ -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)
|
||||
|
||||
|
@ -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,7 +146,9 @@ 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)
|
||||
|
@ -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,61 +229,21 @@ 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)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
# 3/3 : find regular notes
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
137
poetry.lock
generated
@ -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"},
|
||||
|
Loading…
Reference in New Issue
Block a user