PK!spor/__init__.pyPK!1ݴ ""spor/alignment/__init__.pyfrom .smith_waterman import align PK!Sspor/alignment/matrix.pyimport array class Matrix: """A simple 2D matrix designed to support alignment calculation. """ def __init__(self, rows, cols, type_code='I', initial_value=0): if rows < 0 or cols < 0: raise ValueError('Matrix size must be non-negative') self._rows = rows self._cols = cols self._data = array.array( type_code, (initial_value for _ in range(cols * rows))) @property def rows(self): return self._rows @property def cols(self): return self._cols def _to_index(self, row, col): return row * self._cols + col def __getitem__(self, index): return self._data[self._to_index(*index)] def __setitem__(self, index, val): self._data[self._to_index(*index)] = val def __iter__(self): return ((row, col) for row in range(self.rows) for col in range(self.cols)) def items(self): for idx in self: yield (idx, self[idx]) def values(self): return iter(self._data) def __eq__(self, other): return self._data == other._data def __str__(self): row_tokens = [ ['{:3}'.format(x) for x in self._data[row * self.cols:(row + 1) * self.cols]] for row in range(self.rows)] row_strings = [''.join(tok) for tok in row_tokens] return '\n'.join(row_strings) PK!ݲh spor/alignment/smith_waterman.py"""Implementation of the Smith-Waterman algorithm. Given two sequences of elements, a way to score similarity between any two elements, and a function defining the penalty for gaps in a match, this can tell you how the two sequences align. That is, it find the best "match" between the two sequences. This has applications in genomics (e.g. finding how well two sequences of bases match) and, in our case, in determining where an anchor matches modified source code. """ from collections import namedtuple import enum import itertools from .matrix import Matrix class Direction(enum.Enum): """Possible directions in the traceback matrix. These are bitfields that can be ORd together in the matrix. """ NONE = 0x00 DIAG = 0x01 UP = 0x02 LEFT = 0x04 def _tracebacks(score_matrix, traceback_matrix, idx): """Implementation of traceeback. This version can produce empty tracebacks, which we generally don't want users seeing. So the higher level `tracebacks` filters those out. """ score = score_matrix[idx] if score == 0: yield () return directions = traceback_matrix[idx] assert directions != Direction.NONE, 'Tracebacks with direction NONE should have value 0!' row, col = idx if directions & Direction.UP.value: for tb in _tracebacks(score_matrix, traceback_matrix, (row - 1, col)): yield itertools.chain(tb, ((idx, Direction.UP),)) if directions & Direction.LEFT.value: for tb in _tracebacks(score_matrix, traceback_matrix, (row, col - 1)): yield itertools.chain(tb, ((idx, Direction.LEFT),)) if directions & Direction.DIAG.value: for tb in _tracebacks(score_matrix, traceback_matrix, (row - 1, col - 1)): yield itertools.chain(tb, ((idx, Direction.DIAG),)) def tracebacks(score_matrix, traceback_matrix, idx): """Calculate the tracebacks for `traceback_matrix` starting at index `idx`. Returns: An iterable of tracebacks where each traceback is sequence of (index, direction) tuples. Each `index` is an index into `traceback_matrix`. `direction` indicates the direction into which the traceback "entered" the index. """ return filter(lambda tb: tb != (), _tracebacks(score_matrix, traceback_matrix, idx)) def build_score_matrix(a, b, score_func, gap_penalty): """Calculate the score and traceback matrices for two input sequences and scoring functions. Returns: A tuple of (score-matrix, traceback-matrix). Each entry in the score-matrix is a numeric score. Each entry in the traceback-matrix is a logical ORing of the direction bitfields. """ score_matrix = Matrix(rows=len(a) + 1, cols=len(b) + 1) traceback_matrix = Matrix(rows=len(a) + 1, cols=len(b) + 1, type_code='B') for row in range(1, score_matrix.rows): for col in range(1, score_matrix.cols): match_score = score_func(a[row - 1], b[col - 1]) scores = sorted( ((score_matrix[(row - 1, col - 1)] + match_score, Direction.DIAG), (score_matrix[(row - 1, col)] - gap_penalty(1), Direction.UP), (score_matrix[(row, col - 1)] - gap_penalty(1), Direction.LEFT), (0, Direction.NONE)), key=lambda x: x[0], reverse=True) max_score = scores[0][0] scores = itertools.takewhile( lambda x: x[0] == max_score, scores) score_matrix[row, col] = max_score for _, direction in scores: traceback_matrix[row, col] = traceback_matrix[row, col] | direction.value return score_matrix, traceback_matrix def _traceback_to_alignment(tb, a, b): """Convert a traceback (i.e. as returned by `tracebacks()`) into an alignment (i.e. as returned by `align`). Arguments: tb: A traceback. a: the sequence defining the rows in the traceback matrix. b: the sequence defining the columns in the traceback matrix. Returns: An iterable of (index, index) tupless where ether (but not both) tuples can be `None`. """ # We subtract 1 from the indices here because we're translating from the # alignment matrix space (which has one extra row and column) to the space # of the input sequences. for idx, direction in tb: if direction == Direction.DIAG: yield (idx[0] - 1, idx[1] - 1) elif direction == Direction.UP: yield (idx[0] - 1, None) elif direction == Direction.LEFT: yield (None, idx[1] - 1) def align(a, b, score_func, gap_penalty): """Calculate the best alignments of sequences `a` and `b`. Arguments: a: The first of two sequences to align b: The second of two sequences to align score_func: A 2-ary callable which calculates the "match" score between two elements in the sequences. gap_penalty: A 1-ary callable which calculates the gap penalty for a gap of a given size. Returns: A (score, alignments) tuple. `score` is the score that all of the `alignments` received. `alignments` is an iterable of `((index, index), . . .)` tuples describing the best (i.e. maximal and equally good) alignments. The first index in each pair is an index into `a` and the second is into `b`. Either (but not both) indices in a pair may be `None` indicating a gap in the corresponding sequence. """ score_matrix, tb_matrix = build_score_matrix(a, b, score_func, gap_penalty) max_score = max(score_matrix.values()) max_indices = (index for index, score in score_matrix.items() if score == max_score) alignments = ( tuple(_traceback_to_alignment(tb, a, b)) for idx in max_indices for tb in tracebacks(score_matrix, tb_matrix, idx)) return (max_score, alignments) PK!v(spor/anchor.pyfrom contextlib import contextmanager import pathlib class Context: def __init__(self, offset, topic, before, after, width): if offset < 0: raise ValueError("Context offset {} is less than 0".format(offset)) self._before = before self._offset = offset self._topic = topic self._after = after self._width = width @property def before(self): "The text before the topic." return self._before @property def offset(self): "The offset of the topic in the source." return self._offset @property def topic(self): "The text of the topic." return self._topic @property def after(self): "The text after the topic." return self._after @property def width(self): "The nominal width of the context." return self._width @property def full_text(self): return self.before + self.topic + self.after def __eq__(self, rhs): return all(( self.before == rhs.before, self.offset == rhs.offset, self.topic == rhs.topic, self.after == rhs.after, self.width == rhs.width, )) def __repr__(self): return 'Context(offset={}, topic="{}", before="{}", after="{}", width={})'.format( self.offset, self.topic, self.before, self.after, self.width) class Anchor: def __init__(self, file_path, encoding, context, metadata): if not file_path.is_absolute(): raise ValueError("Anchors file-paths must be absolute.") self.file_path = file_path self.encoding = encoding self.context = context self.metadata = metadata def __eq__(self, rhs): return all(( self.file_path == rhs.file_path, self.encoding == rhs.encoding, self.context == rhs.context, self.metadata == rhs.metadata )) def __repr__(self): return 'Anchor(file_path="{}", context={}, metadata={})'.format( self.file_path, self.context, self.metadata) def _make_context(handle, offset, width, context_width): if context_width < 0: raise ValueError( 'Context width must not be negative') # read topic handle.seek(offset) topic = handle.read(width) if len(topic) < width: raise ValueError( "Unable to read topic of length {} at offset {}".format( width, offset)) # read before before_offset = max(0, offset - context_width) before_width = offset - before_offset handle.seek(before_offset) before = handle.read(before_width) if len(before) < before_width: raise ValueError( "Unable to read before-text of length {} at offset {}".format( before_width, before_offset)) # read after after_offset = offset + width handle.seek(after_offset) after = handle.read(context_width) return Context(offset, topic, before, after, width=context_width) def make_anchor(file_path: pathlib.Path, offset: int, width: int, context_width: int, metadata, encoding: str = 'utf-8', handle=None): """Construct a new `Anchor`. Args: file_path: The absolute path to the target file for the anchor. offset: The offset of the anchored text in codepoints in `file_path`'s contents. width: The width in codepoints of the anchored text. context_width: The width in codepoints of context on either side of the anchor. metadata: The metadata to attach to the anchor. Must be json-serializeable. encoding: The encoding of the contents of `file_path`. handle: If not `None`, this is a file-like object the contents of which are used to calculate the context of the anchor. If `None`, then the file indicated by `file_path` is opened instead. Raises: ValueError: `width` characters can't be read at `offset`. ValueError: `file_path` is not absolute. """ @contextmanager def get_handle(): if handle is None: with file_path.open(mode='rt', encoding=encoding) as fp: yield fp else: yield handle with get_handle() as fp: context = _make_context(fp, offset, width, context_width) return Anchor( file_path=file_path, encoding=encoding, context=context, metadata=metadata) PK! d spor/cli.pyimport json import os import pathlib import signal import subprocess import sys import tempfile import docopt import docopt_subcommands as dsc from exit_codes import ExitCode from .anchor import make_anchor from .repository import initialize_repository, open_repository from .updating import AlignmentError, update from .diff import get_anchor_diff class ExitError(Exception): """Exception indicating that the program should exit with a specific code. """ def __init__(self, code, *args): super().__init__(*args) self.code = code def _open_repo(args, path_key=''): """Open and return the repository containing the specified file. The file is specified by looking up `path_key` in `args`. This value or `None` is passed to `open_repository`. Returns: A `Repository` instance. Raises: ExitError: If there is a problem opening the repo. """ path = pathlib.Path(args[path_key]) if args[path_key] else None try: repo = open_repository(path) except ValueError as exc: raise ExitError(ExitCode.DATA_ERR, str(exc)) return repo def _get_anchor(repo, id_prefix): """Get an anchor by ID, or a prefix of its id. """ result = None for anchor_id, anchor in repo.items(): if anchor_id.startswith(id_prefix): if result is not None: raise ExitError( ExitCode.DATA_ERR, 'Ambiguous ID specification') result = (anchor_id, anchor) if result is None: raise ExitError( ExitCode.DATA_ERR, 'No anchor matching ID specification') return result @dsc.command() def init_handler(args): """usage: {program} init Initialize a new spor repository in the current directory. """ try: initialize_repository(pathlib.Path.cwd()) except ValueError as exc: print(exc, file=sys.stderr) return ExitCode.DATAERR return ExitCode.OK @dsc.command() def list_handler(args): """usage: {program} list List the anchors for a file. """ repo = open_repository(args['']) for anchor_id, anchor in repo.items(): print("{} {}:{} => {}".format(anchor_id, anchor.file_path.relative_to(repo.root), anchor.context.offset, anchor.metadata)) return ExitCode.OK @dsc.command() def add_handler(args): """usage: {program} add Add a new anchor for a file. """ file_path = pathlib.Path(args['']).resolve() try: offset = int(args['']) width = int(args['']) context_width = int(args['']) except ValueError as exc: print(exc, file=sys.stderr) return ExitCode.DATAERR repo = _open_repo(args, '') if sys.stdin.isatty(): text = _launch_editor('# json metadata') else: text = sys.stdin.read() try: metadata = json.loads(text) except json.JSONDecodeError: print( 'Failed to create anchor. Invalid JSON metadata.', file=sys.stderr) return ExitCode.DATAERR # TODO: let user specify encoding with file_path.open(mode='rt') as handle: anchor = make_anchor( file_path, offset, width, context_width, metadata, handle=handle) repo.add(anchor) return ExitCode.OK @dsc.command() def remove_handler(args): """usage: {program} remove [] Remove an existing anchor. """ repo = _open_repo(args) anchor_id, anchor = _get_anchor(repo, args['']) del repo[anchor_id] return ExitCode.OK def _launch_editor(starting_text=''): "Launch editor, let user write text, then return that text." # TODO: What is a reasonable default for windows? Does this approach even # make sense on windows? editor = os.environ.get('EDITOR', 'vim') with tempfile.TemporaryDirectory() as dirname: filename = pathlib.Path(dirname) / 'metadata.yml' with filename.open(mode='wt') as handle: handle.write(starting_text) subprocess.call([editor, filename]) with filename.open(mode='rt') as handle: text = handle.read() return text @dsc.command() def update_handler(args): """usage: {program} update [] Update out of date anchors in the current repository. """ repo = _open_repo(args) for anchor_id, anchor in repo.items(): try: anchor = update(anchor) except AlignmentError as e: print('Unable to update anchor {}. Reason: {}'.format( anchor_id, e)) else: repo[anchor_id] = anchor @dsc.command() def status_handler(args): """usage: {program} status [] Validate the anchors in the current repository. """ repo = _open_repo(args) for anchor_id, anchor in repo.items(): diff_lines = get_anchor_diff(anchor) if diff_lines: print('{} {}:{} out-of-date'.format( anchor_id, anchor.file_path, anchor.context.offset)) return ExitCode.OK @dsc.command() def diff_handler(args): """usage: {program} diff [] Show the difference between an anchor and the current state of the source. """ repo = _open_repo(args) anchor_id, anchor = _get_anchor(repo, args['']) diff_lines = get_anchor_diff(anchor) sys.stdout.writelines(diff_lines) return ExitCode.OK @dsc.command() def details_handler(args): """usage: {program} details [] Get the details of a single anchor. """ repo = _open_repo(args) anchor = _get_anchor(repo, args['']) print("""path: {file_path} encoding: {encoding} context: before: {before} -------------- topic: {topic} -------------- after: {after} -------------- offset: {offset} width: {width}""".format( file_path=anchor.file_path, encoding=anchor.encoding, before=anchor.context.before, topic=anchor.context.topic, after=anchor.context.after, offset=anchor.context.offset, width=anchor.context.width)) return ExitCode.OK # TODO: edit _SIGNAL_EXIT_CODE_BASE = 128 def main(): signal.signal( signal.SIGINT, lambda *args: sys.exit(_SIGNAL_EXIT_CODE_BASE + signal.SIGINT)) try: return dsc.main(program='spor', version='spor v0.0.0') except docopt.DocoptExit as exc: print(exc, file=sys.stderr) return ExitCode.USAGE except FileNotFoundError as exc: print(exc, file=sys.stderr) return ExitCode.NOINPUT except PermissionError as exc: print(exc, file=sys.stderr) return ExitCode.NOPERM except ExitError as exc: print(exc, file=sys.stderr) return exc.code if __name__ == '__main__': sys.exit(main()) PK!4u11 spor/diff.pyimport difflib from .anchor import make_anchor def _split_keep_sep(s, sep): toks = s.split(sep) result = [tok + sep for tok in toks[:-1]] result.append(toks[-1]) return result def _context_diff(file_name, c1, c2): c1_text = _split_keep_sep(c1.full_text, '\n') c2_text = _split_keep_sep(c2.full_text, '\n') return difflib.context_diff( c1_text, c2_text, fromfile='{} [original]'.format(file_name), tofile='{} [current]'.format(file_name)) def get_anchor_diff(anchor): """Get the get_anchor_diff between an anchor and the current state of its source. Returns: A tuple of get_anchor_diff lines. If there is not different, then this returns an empty tuple. """ new_anchor = make_anchor( file_path=anchor.file_path, offset=anchor.context.offset, width=len(anchor.context.topic), context_width=anchor.context.width, metadata=anchor.metadata) assert anchor.file_path == new_anchor.file_path assert anchor.context.offset == new_anchor.context.offset assert len(anchor.context.topic) == len(new_anchor.context.topic) assert anchor.metadata == new_anchor.metadata return tuple( _context_diff( anchor.file_path, anchor.context, new_anchor.context)) PK!; RRspor/repository/__init__.py# spor.repository from .repository import initialize_repository, open_repository PK!6//spor/repository/persistence.pyimport json import spor.anchor def save_anchor(fp, anchor, repo_root): json.dump(anchor, fp, cls=make_encoder(repo_root)) def load_anchor(fp, repo_root): return json.load(fp, cls=make_decoder(repo_root)) def make_encoder(repo_root): class JSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, spor.anchor.Anchor): return { '!spor_anchor': { 'file_path': str(obj.file_path.relative_to(repo_root)), 'encoding': obj.encoding, 'context': obj.context, 'metadata': obj.metadata } } elif isinstance(obj, spor.anchor.Context): return { '!spor_context': { 'before': obj.before, 'after': obj.after, 'topic': obj.topic, 'offset': obj.offset, 'width': obj.width } } return super().default(self, obj) return JSONEncoder def make_decoder(repo_root): class JSONDecoder(json.JSONDecoder): def __init__(self): super().__init__(object_hook=self.anchor_decoder) def anchor_decoder(self, dct): if '!spor_anchor' in dct: data = dct['!spor_anchor'] return spor.anchor.Anchor( file_path=repo_root / data['file_path'], encoding=data['encoding'], context=data['context'], metadata=data['metadata']) elif '!spor_context' in dct: return spor.anchor.Context(**dct['!spor_context']) return dct return JSONDecoder PK!lspor/repository/repository.pyimport os import pathlib import uuid from .persistence import load_anchor, save_anchor def initialize_repository(path, spor_dir='.spor'): """Initialize a spor repository in `path` if one doesn't already exist. Args: path: Path to any file or directory within the repository. spor_dir: The name of the directory containing spor data. Returns: A `Repository` instance. Raises: ValueError: A repository already exists at `path`. """ path = pathlib.Path(path) spor_path = path / spor_dir if spor_path.exists(): raise ValueError('spor directory already exists: {}'.format(spor_path)) spor_path.mkdir() return Repository(path, spor_dir) def open_repository(path, spor_dir='.spor'): """Open an existing repository. Args: path: Path to any file or directory within the repository. spor_dir: The name of the directory containing spor data. Returns: A `Repository` instance. Raises: ValueError: No repository is found. """ root = _find_root_dir(path, spor_dir) return Repository(root, spor_dir) class Repository: """Storage for anchors. """ def __init__(self, root, spor_dir): self._root = pathlib.Path(root).resolve() self._spor_dir = self.root / spor_dir if not self._spor_dir.exists(): raise ValueError("Repository directory does not exist: {}".format( self._spor_dir)) @property def root(self): """The root directory of the repository.""" return self._root def add(self, anchor): """Add a new anchor to the repository. This will create a new ID for the anchor and provision new storage for it. Returns: The storage ID for the Anchor which can be used to retrieve the anchor later. """ anchor_id = uuid.uuid4().hex anchor_path = self._anchor_path(anchor_id) with anchor_path.open(mode='wt') as f: save_anchor(f, anchor, self.root) return anchor_id def __getitem__(self, anchor_id): """Get an Anchor by ID. Args: anchor_id: The ID of the anchor to retrieve. Returns: An anchor instance. Raises: KeyError: The anchor can not be found. """ file_path = self._anchor_path(anchor_id) try: with file_path.open(mode='rt') as handle: return load_anchor(handle, self.root) except OSError: raise KeyError('No anchor with id {}'.format(anchor_id)) def __setitem__(self, anchor_id, anchor): """Update an anchor. This will update an existing anchor if it exists, or it will create new storage if not. Args: anchor_id: The ID of the anchor to update. anchor: The anchor to store. """ with self._anchor_path(anchor_id).open(mode='wt') as f: save_anchor(f, anchor, self.root) def __delitem__(self, anchor_id): """Remove an anchor from storage. Args: anchor_id: The ID of the anchor to remove. Raises: KeyError: There is no anchor with that ID. """ try: self._anchor_path(anchor_id).unlink() except OSError: raise KeyError('No anchor with id {}'.format(anchor_id)) def __iter__(self): """An iterable of all anchor IDs in the repository. """ for spor_file in self._spor_dir.glob('**/*.yml'): yield str(spor_file.name)[:-4] def items(self): """An iterable of all (anchor-id, Anchor) mappings in the repository. """ for anchor_id in self: try: anchor = self[anchor_id] except KeyError: assert False, 'Trying to load from missing file or something' yield (anchor_id, anchor) def _anchor_path(self, anchor_id): "Absolute path to the data file for `anchor_id`." file_name = '{}.yml'.format(anchor_id) file_path = self._spor_dir / file_name return file_path def _find_root_dir(path, spor_dir): """Search for a spor repo containing `path`. This searches for `spor_dir` in directories dominating `path`. If a directory containing `spor_dir` is found, then that directory is returned as a `pathlib.Path`. Returns: The dominating directory containing `spor_dir` as a `pathlib.Path`. Raises: ValueError: No repository is found. """ start_path = pathlib.Path(os.getcwd() if path is None else path) paths = [start_path] + list(start_path.parents) for path in paths: data_dir = path / spor_dir if data_dir.exists() and data_dir.is_dir(): return path raise ValueError('No spor repository found') PK!spor/updating.pyfrom spor.alignment import align from spor.anchor import make_anchor def score(a, b): if a == b: return 3 else: return -3 def gap_penalty(gap): if gap == 1: return 2 else: gap * gap_penalty(1) def _index_in_topic(index, anchor): return (index >= anchor.context.offset and index < anchor.context.offset + len(anchor.context.topic)) class AlignmentError(Exception): pass def update(anchor, handle=None): """Update an anchor based on the current contents of its source file. Args: anchor: The `Anchor` to be updated. handle: File-like object containing contents of the anchor's file. If `None`, then this function will open the file and read it. Returns: A new `Anchor`, possibly identical to the input. Raises: ValueError: No alignments could be found between old anchor and new text. AlignmentError: If no anchor could be created. The message of the exception will say what the problem is. """ if handle is None: with anchor.file_path.open(mode='rt') as fp: source_text = fp.read() else: source_text = handle.read() handle.seek(0) ctxt = anchor.context a_score, alignments = align(ctxt.full_text, source_text, score, gap_penalty) # max_score = len(ctxt.full_text) * 3 try: alignment = next(alignments) except StopIteration: raise AlignmentError('No alignments for anchor: {}'.format(anchor)) anchor_offset = ctxt.offset - len(ctxt.before) source_indices = tuple( s_idx for (a_idx, s_idx) in alignment if a_idx is not None if s_idx is not None if _index_in_topic(a_idx + anchor_offset, anchor)) if not source_indices: raise AlignmentError( "Best alignment does not map topic to updated source.") return make_anchor( file_path=anchor.file_path, offset=source_indices[0], width=len(source_indices), context_width=anchor.context.width, metadata=anchor.metadata, handle=handle) PK!Y8]yjjspor/version.py"""spor version info.""" __version_info__ = (1, 1, 0) __version__ = '.'.join(map(str, __version_info__)) PK!Hd%&%spor-1.1.0.dist-info/entry_points.txtN+I/N.,()*./z9Vy\\PK!hspor-1.1.0.dist-info/LICENSECopyright 2017 Sixty North AS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!H\TTspor-1.1.0.dist-info/WHEEL 1 0 нR \I$ơ7.ZON `h6oi14m,b4>4ɛpK>X;baP>PK!H]j4 spor-1.1.0.dist-info/METADATAXmob+`($D uDrݥe63D' 6ėٙٙgՕ2^NӦyr&RƊ,97MYJ%yUXU1VWk*;}+Rw^J҅r 9LQfr%nJ56n64$3T.m#ipT yu^l*wԚrҕ:(`P%vdV,c(%mUٮ$jiW2)X>%Y~#Shu;fy`*V/Fq8iHf 6?wV+Yo\f^3C7)n̒%qkJMhOHxm**Uĸv֎cFxb[|UKT؅~,Z2԰$Fg[L"T^)DI$gb^t ]i++అ4e4f3!+!s0t iC,jmg+iB(KOU*XŠYc-hї.^MS# !GM՝TRBJJ2"iTɌcolEwg:λb~DP6j\ -Zez"Dd9|cV+r:;#ռ>-G3|iD12s:iECc ݓF"EkyqT Ĭh"ch;$6555B*g quxK¸Ӕ  Aq\6 T!_2;?"iM׃ E0S c8=Z L3XQC1`U498 abtTS" # M4!_#{4#})oo=XrpSH\O_WꌮeK?|e2L{,Nl_E"^_3.iY j wf--1  dT =uMUJ%kRQ**`) X"`4%fLge*&АS9O+RtEv#8d6)P89&irٱ=#zSigh!@ǁy7q/ 8Lq7gOBGWqz}NsZCx}o[4t4Q-()+:PpМpo~ӔqW:,dZQz C { Cxba>B~X?*Wbt6P5\ AL,,&(ydD ?u03Jc(.y>,A9G֘"p^:N0A"M c/ O a[gɒ80xW`o$ F) ƠJt(ҩQjꨙ&(ƳY;T78Kh~GS $g-JPA@i \+[s+tSL(]4rOL7^#8%b3=1#{rq6nK%mYILc/nNf R F珟=}v~~t}/Nzn|/z狃|ݻ|/OqغO=y-OJ|juzzz,WS?c@;u6fh3]|qѢ[4HnTnܢfZ8Ԕ~/®`$a_hZuiqK _;}.NՅpAz qRh ! |S!PK!H H%spor-1.1.0.dist-info/RECORDmɒJ}? V3&2 1R$|[v/st ?/XCr|431+_.- ^\U-[m++a5vt"=E~+uWyRTDymijj1~Ъ Z`J*&N/&#w"24zGqȡ bײ(R ϋ]Iqc*_4S3TvZ[\#_7|k$@~Owbn^QOxZX$bY~ڃrEyym{/yeUOnIY+6SVεV9"NQvwCP _A/q=Θ03t- 8)*Pd${AuЪa6;rr.<(F=狚cGݜ9= 賳 !c ΧCxIԾIcb8tͦj *_֠UhœbYyu_҃U2-^:.Ws5Dq(Lb>bJ>5V(>s>6Vt8<9Imw&?}uVmPk6O. QU8Wޣ0or l%̮^;ZվI\\F9gʷy\x {ݟus-xwR8@WwtPK!spor/__init__.pyPK!1ݴ "".spor/alignment/__init__.pyPK!Sspor/alignment/matrix.pyPK!ݲh espor/alignment/smith_waterman.pyPK!v(hspor/anchor.pyPK! d 0spor/cli.pyPK!4u11 Lspor/diff.pyPK!; RRRspor/repository/__init__.pyPK!6//Rspor/repository/persistence.pyPK!lYspor/repository/repository.pyPK!Jmspor/updating.pyPK!Y8]yjjAvspor/version.pyPK!Hd%&%vspor-1.1.0.dist-info/entry_points.txtPK!h@wspor-1.1.0.dist-info/LICENSEPK!H\TT{spor-1.1.0.dist-info/WHEELPK!H]j4 #|spor-1.1.0.dist-info/METADATAPK!H H%spor-1.1.0.dist-info/RECORDPKS