skim-python 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skim_py/block.py CHANGED
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from typing import List, Optional
8
8
 
9
9
  from .types import OutlineItem, ParsedFile
10
- from .parser import parse_source_file
10
+ from .parser import parse_source_file, split_source_lines
11
11
 
12
12
 
13
13
  def collect_all_items(items: List[OutlineItem]) -> List[OutlineItem]:
@@ -38,6 +38,8 @@ def run_block(
38
38
  return 1
39
39
 
40
40
  parsed = parse_source_file(file_path, content, include_tests=include_tests)
41
+ if parsed.masking_warning:
42
+ sys.stderr.write(f"{file_path}: {parsed.masking_warning}\n")
41
43
  all_items = collect_all_items(parsed.items)
42
44
 
43
45
  matches: List[OutlineItem] = []
@@ -123,7 +125,7 @@ def run_block(
123
125
  return 3
124
126
 
125
127
  chosen = matches[0]
126
- src_lines = content.splitlines()
128
+ src_lines = split_source_lines(content, keepends=False)
127
129
  block_lines = src_lines[chosen.start_line - 1 : chosen.end_line]
128
130
 
129
131
  # Check for syntax errors within block range
skim_py/glance.py CHANGED
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  from typing import List, Optional
9
9
 
10
10
  from .types import OutlineItem, ParsedFile
11
- from .parser import parse_source_file, is_test_file
11
+ from .parser import parse_source_file, is_test_file, split_source_lines
12
12
 
13
13
 
14
14
  IGNORED_DIRS = {
@@ -121,7 +121,7 @@ def run_glance(
121
121
  sys.stderr.write(f"Error reading {file_path}: {err}\n")
122
122
  continue
123
123
 
124
- src_lines = content.splitlines()
124
+ src_lines = split_source_lines(content, keepends=False)
125
125
  parsed = parse_source_file(file_path, content, include_tests=include_tests)
126
126
 
127
127
  if filters:
@@ -155,6 +155,8 @@ def run_glance(
155
155
  rel_path = file_path
156
156
 
157
157
  print(f"=== {rel_path} ({parsed.total_lines} lines){test_info}{error_tag} ===")
158
+ if parsed.masking_warning:
159
+ print(parsed.masking_warning)
158
160
 
159
161
  for err in parsed.syntax_errors:
160
162
  print(f" Syntax Notice: L{err.line}: {err.message}")
skim_py/parser.py CHANGED
@@ -14,6 +14,43 @@ TEST_DIR_NAMES = {"tests", "test", "testing", "__tests__"}
14
14
  TEST_FILE_PATTERN = re.compile(r"^(test_.*|.*_test|conftest)\.py$", re.IGNORECASE)
15
15
  TEST_CLASS_PATTERN = re.compile(r"^Test[A-Z0-9_].*")
16
16
  TEST_FUNC_PATTERN = re.compile(r"^test(_|[A-Z0-9_]).*")
17
+ HEADER_DEF_PATTERN = re.compile(r"^\s*(async\s+def|def|class)\s+([a-zA-Z_][a-zA-Z0-9_]*)")
18
+
19
+
20
+ def get_indent(line: str) -> int:
21
+ """Return indentation level expanding tabs to 8 spaces (CPython rule)."""
22
+ expanded = line.expandtabs(8)
23
+ return len(expanded) - len(expanded.lstrip(" "))
24
+
25
+
26
+ def get_block_end(lines: List[str], start: int) -> int:
27
+ """Find the last indented code line, ignoring blank lines and comments."""
28
+ indent = get_indent(lines[start])
29
+ end = start
30
+ for idx in range(start + 1, len(lines)):
31
+ stripped = lines[idx].strip()
32
+ if not stripped or stripped.startswith("#"):
33
+ continue
34
+ if get_indent(lines[idx]) <= indent:
35
+ break
36
+ end = idx
37
+ return end
38
+
39
+
40
+ def split_source_lines(content: str, keepends: bool = False) -> List[str]:
41
+ """Normalize newlines (\r\n, \r -> \n) and split into lines."""
42
+ norm = content.replace("\r\n", "\n").replace("\r", "\n")
43
+ if not norm:
44
+ return []
45
+ raw = norm.split("\n")
46
+ if norm.endswith("\n"):
47
+ raw = raw[:-1]
48
+ if keepends:
49
+ return [l + "\n" for l in raw]
50
+ return raw
51
+ if keepends:
52
+ return [l + "\n" for l in raw[:-1]] + [raw[-1]]
53
+ return raw
17
54
 
18
55
 
19
56
  def is_test_file(file_path: str) -> bool:
@@ -79,7 +116,7 @@ def is_test_func(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
79
116
 
80
117
 
81
118
  def parse_source_file(file_path: str, content: str, include_tests: bool = False) -> ParsedFile:
82
- src_lines = content.splitlines()
119
+ src_lines = split_source_lines(content, keepends=False)
83
120
  total_lines = len(src_lines)
84
121
  is_test = is_test_file(file_path)
85
122
 
@@ -90,14 +127,21 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
90
127
  )
91
128
 
92
129
  tree: Optional[ast.AST] = None
130
+ salvaged_items: List[OutlineItem] = []
131
+ masked_block_ends = {}
132
+ stub_lines = set()
133
+ masked_lines = set()
134
+
93
135
  try:
94
136
  tree = ast.parse(content, filename=file_path, type_comments=True)
95
137
  except SyntaxError as first_err:
96
- parsed.syntax_errors.append(
97
- SyntaxNotice(line=first_err.lineno or 1, message=first_err.msg or "syntax error")
98
- )
138
+ first_line = first_err.lineno or 1
139
+ first_msg = first_err.msg or "syntax error"
140
+ notice = SyntaxNotice(line=first_line, message=first_msg)
141
+ parsed.syntax_errors.append(notice)
142
+
99
143
  # Fallback: Iterative line masking with up to 100 attempts
100
- working_lines = content.splitlines(keepends=True)
144
+ working_lines = split_source_lines(content, keepends=True)
101
145
  seen_states = set()
102
146
  max_attempts = 100
103
147
  current_err: Optional[SyntaxError] = first_err
@@ -111,7 +155,8 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
111
155
 
112
156
  line_idx = lineno - 1
113
157
  line = working_lines[line_idx]
114
- indent = len(line) - len(line.lstrip())
158
+ indent = get_indent(line)
159
+ leading_ws = line[: len(line) - len(line.lstrip())]
115
160
 
116
161
  state_key = (line_idx, line)
117
162
  if state_key in seen_states:
@@ -119,10 +164,135 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
119
164
  break
120
165
  seen_states.add(state_key)
121
166
 
122
- if "# syntax error masked" not in line:
123
- working_lines[line_idx] = " " * indent + "# syntax error masked\n"
167
+ # Check if this line is a block header followed by an indented block.
168
+ # E.g. broken def/class/if header whose masking would cause IndentationError cascades.
169
+ end_idx = get_block_end(working_lines, line_idx)
170
+
171
+ # Check if header matches def/async def/class
172
+ m_hdr = HEADER_DEF_PATTERN.match(line)
173
+
174
+ if end_idx > line_idx or m_hdr:
175
+ # Update notice message with masked line range if not already suffixed
176
+ mask_suffix = f"(L{line_idx + 1}-L{end_idx + 1} masked)"
177
+ for n in parsed.syntax_errors:
178
+ if n.line == lineno and "masked)" not in n.message:
179
+ n.message = f"{n.message} {mask_suffix}"
180
+
181
+ # Salvage header definition if it matches def/async def/class
182
+ if m_hdr:
183
+ k_raw, name = m_hdr.group(1), m_hdr.group(2)
184
+ start_l = line_idx + 1
185
+ end_l = end_idx + 1
186
+
187
+ # Scan upward for leading decorators
188
+ decos = []
189
+ for d_idx in range(line_idx - 1, -1, -1):
190
+ prev_d = working_lines[d_idx]
191
+ stripped_d = prev_d.strip()
192
+ if stripped_d.startswith("@"):
193
+ start_l = d_idx + 1
194
+ decos.insert(0, stripped_d)
195
+ elif not stripped_d or stripped_d.startswith("#"):
196
+ continue
197
+ else:
198
+ break
199
+
200
+ deco_prefix = (" ".join(decos) + " ") if decos else ""
201
+
202
+ if k_raw == "class":
203
+ cls_is_test = bool(TEST_CLASS_PATTERN.match(name))
204
+ class_item = OutlineItem(
205
+ name=name,
206
+ kind="test" if cls_is_test else "class",
207
+ start_line=start_l,
208
+ end_line=end_l,
209
+ raw_header=f"{deco_prefix}class {name} [syntax error]",
210
+ )
211
+ # Extract child methods inside the unparsable class
212
+ k = line_idx + 1
213
+ while k <= end_idx:
214
+ m_ch = HEADER_DEF_PATTERN.match(working_lines[k])
215
+ if m_ch and m_ch.group(1) in ("def", "async def"):
216
+ ch_k_raw, ch_name = m_ch.group(1), m_ch.group(2)
217
+ ch_start = k + 1
218
+ # Scan upward for method decorators inside the class
219
+ ch_decos = []
220
+ for cj in range(k - 1, line_idx, -1):
221
+ prev_cj = working_lines[cj]
222
+ str_cj = prev_cj.strip()
223
+ if str_cj.startswith("@"):
224
+ ch_start = cj + 1
225
+ ch_decos.insert(0, str_cj)
226
+ elif not str_cj or str_cj.startswith("#"):
227
+ continue
228
+ else:
229
+ break
230
+
231
+ ch_deco_prefix = (" ".join(ch_decos) + " ") if ch_decos else ""
232
+
233
+ ch_end_idx = min(get_block_end(working_lines, k), end_idx)
234
+ ch_end = ch_end_idx + 1
235
+ ch_is_test = bool(TEST_FUNC_PATTERN.match(ch_name))
236
+ ch_kind = "test" if ch_is_test else "method"
237
+ class_item.children.append(
238
+ OutlineItem(
239
+ name=ch_name,
240
+ kind=ch_kind,
241
+ start_line=ch_start,
242
+ end_line=ch_end,
243
+ raw_header=f"{ch_deco_prefix}{ch_k_raw} {ch_name}()",
244
+ parent_name=name,
245
+ )
246
+ )
247
+ k = ch_end_idx + 1
248
+ else:
249
+ k += 1
250
+
251
+ salvaged_items.append(class_item)
252
+ else:
253
+ fn_is_test = bool(TEST_FUNC_PATTERN.match(name))
254
+ fn_kind = "test" if fn_is_test else ("async def" if "async" in k_raw else "function")
255
+ # Check if method inside an outer class, skipping comments and blank lines
256
+ parent_cls = None
257
+ for p_idx in range(line_idx - 1, -1, -1):
258
+ prev_l = working_lines[p_idx]
259
+ stripped_p = prev_l.strip()
260
+ if not stripped_p or stripped_p.startswith("#"):
261
+ continue
262
+ if get_indent(prev_l) < indent:
263
+ m_p = re.match(r"^\s*class\s+([a-zA-Z_][a-zA-Z0-9_]*)", prev_l)
264
+ if m_p:
265
+ parent_cls = m_p.group(1)
266
+ break
267
+
268
+ salvaged_items.append(
269
+ OutlineItem(
270
+ name=name,
271
+ kind="test" if fn_is_test else ("method" if parent_cls else fn_kind),
272
+ start_line=start_l,
273
+ end_line=end_l,
274
+ raw_header=f"{deco_prefix}{k_raw} {name} [syntax error]",
275
+ parent_name=parent_cls,
276
+ )
277
+ )
278
+
279
+ # Keep decorators attached to a harmless definition stub. Otherwise
280
+ # use pass so masking the only statement does not empty its suite.
281
+ if m_hdr:
282
+ stub = (
283
+ "class __skim_masked__: pass\n"
284
+ if m_hdr.group(1) == "class"
285
+ else "def __skim_masked__(): pass\n"
286
+ )
287
+ working_lines[line_idx] = leading_ws + stub
288
+ stub_lines.add(lineno)
124
289
  else:
125
- working_lines[line_idx] = " " * indent + "pass\n"
290
+ working_lines[line_idx] = leading_ws + "pass\n"
291
+ for k in range(line_idx + 1, end_idx + 1):
292
+ working_lines[k] = "# syntax error masked\n"
293
+
294
+ masked_lines.update(range(line_idx + 1, end_idx + 2))
295
+ masked_block_ends[lineno] = end_idx + 1
126
296
 
127
297
  try:
128
298
  tree = ast.parse("".join(working_lines), filename=file_path, type_comments=True)
@@ -142,7 +312,63 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
142
312
  )
143
313
  return parsed
144
314
 
315
+ parsed.masked_lines = sorted(masked_lines)
316
+
317
+ def recovered_end(node: ast.AST) -> int:
318
+ end = get_end_line(node)
319
+ return max([end] + [
320
+ masked_end for start, masked_end in masked_block_ends.items()
321
+ if node.lineno <= start <= end
322
+ ])
323
+
324
+ def merge_salvaged() -> None:
325
+ if not salvaged_items:
326
+ return
327
+ for s_item in salvaged_items:
328
+ # The AST still contains excluded test classes, but parsed.items
329
+ # does not. Do not promote their recovered methods to top level.
330
+ if not include_tests and any(
331
+ start <= s_item.start_line <= end
332
+ for start, end in parsed.test_ranges
333
+ ):
334
+ continue
335
+ # Check test filtering for salvaged items
336
+ if s_item.kind == "test":
337
+ parsed.test_ranges.append((s_item.start_line, s_item.end_line))
338
+ if not include_tests:
339
+ continue
340
+
341
+ if s_item.parent_name:
342
+ parent = next(
343
+ (it for it in parsed.items
344
+ if it.name == s_item.parent_name and it.kind in ("class", "test")
345
+ and it.start_line <= s_item.start_line <= it.end_line),
346
+ None,
347
+ )
348
+ if parent:
349
+ parent.children.append(s_item)
350
+ parent.children.sort(key=lambda ch: ch.start_line)
351
+ parent.end_line = max(parent.end_line, s_item.end_line)
352
+ else:
353
+ parsed.items.append(s_item)
354
+ else:
355
+ # If s_item is a class, filter its child test methods if include_tests is False
356
+ if s_item.kind in ("class", "test"):
357
+ filtered_children = []
358
+ for ch in s_item.children:
359
+ if ch.kind == "test":
360
+ parsed.test_ranges.append((ch.start_line, ch.end_line))
361
+ if not include_tests:
362
+ continue
363
+ filtered_children.append(ch)
364
+ s_item.children = filtered_children
365
+
366
+ parsed.items.append(s_item)
367
+
368
+ parsed.items.sort(key=lambda it: it.start_line)
369
+
145
370
  if tree is None:
371
+ merge_salvaged()
146
372
  return parsed
147
373
 
148
374
  # 1. Collect imports
@@ -155,11 +381,15 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
155
381
 
156
382
  # 2. Extract outline items
157
383
  for node in tree.body:
384
+ # Skip internal mask stubs
385
+ if node.lineno in stub_lines:
386
+ continue
387
+
158
388
  if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
159
389
  is_async = isinstance(node, ast.AsyncFunctionDef)
160
390
  is_test_fn = is_test_func(node)
161
391
  start_l = get_start_line(node)
162
- end_l = get_end_line(node)
392
+ end_l = recovered_end(node)
163
393
 
164
394
  if is_test_fn:
165
395
  parsed.test_ranges.append((start_l, end_l))
@@ -186,7 +416,7 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
186
416
  elif isinstance(node, ast.ClassDef):
187
417
  is_test_cls = is_test_class(node)
188
418
  start_l = get_start_line(node)
189
- end_l = get_end_line(node)
419
+ end_l = recovered_end(node)
190
420
 
191
421
  if is_test_cls:
192
422
  parsed.test_ranges.append((start_l, end_l))
@@ -208,11 +438,14 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
208
438
 
209
439
  # Process class body
210
440
  for child in node.body:
441
+ if child.lineno in stub_lines:
442
+ continue
443
+
211
444
  if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
212
445
  child_async = isinstance(child, ast.AsyncFunctionDef)
213
446
  child_test = is_test_func(child)
214
447
  ch_start = get_start_line(child)
215
- ch_end = get_end_line(child)
448
+ ch_end = recovered_end(child)
216
449
 
217
450
  if child_test:
218
451
  parsed.test_ranges.append((ch_start, ch_end))
@@ -297,4 +530,5 @@ def parse_source_file(file_path: str, content: str, include_tests: bool = False)
297
530
  )
298
531
  )
299
532
 
533
+ merge_salvaged()
300
534
  return parsed
skim_py/types.py CHANGED
@@ -38,3 +38,13 @@ class ParsedFile:
38
38
  syntax_errors: List[SyntaxNotice] = field(default_factory=list)
39
39
  is_test_file: bool = False
40
40
  test_ranges: List[tuple[int, int]] = field(default_factory=list)
41
+ masked_lines: List[int] = field(default_factory=list)
42
+
43
+ @property
44
+ def masking_warning(self) -> str:
45
+ if not self.masked_lines:
46
+ return ""
47
+ locations = ", ".join(f"L{line}" for line in self.masked_lines)
48
+ count = len(self.masked_lines)
49
+ noun = "line" if count == 1 else "lines"
50
+ return f"# parsed with {count} {noun} masked ({locations})"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: skim-python
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Language-aware, AST-resilient outline and verbatim code extractor for Python tailored for AI agents
5
5
  Project-URL: Homepage, https://github.com/benedictjohannes/programming-helper-utils
6
6
  Project-URL: Repository, https://github.com/benedictjohannes/programming-helper-utils
@@ -335,6 +335,7 @@ skim-py glance tests/fixtures/syntax_error.py -s
335
335
  ```
336
336
  ```text
337
337
  === tests/fixtures/syntax_error.py (12 lines) [! 1 syntax error] ===
338
+ # parsed with 1 line masked (L7)
338
339
  Syntax Notice: L7: invalid syntax
339
340
 
340
341
  Outline:
@@ -343,12 +344,13 @@ Outline:
343
344
  L11-L12 def valid_after()
344
345
  ```
345
346
 
346
- Surrounding valid functions remain fully extractable via `block`:
347
+ Surrounding valid functions remain fully extractable via `block` (masking warnings are surfaced via stderr):
347
348
 
348
349
  ```bash
349
350
  skim-py block tests/fixtures/syntax_error.py valid_after
350
351
  ```
351
352
  ```text
353
+ tests/fixtures/syntax_error.py: # parsed with 1 line masked (L7)
352
354
  === tests/fixtures/syntax_error.py (L11-L12) ===
353
355
  11 | def valid_after():
354
356
  12 | return 2
@@ -360,6 +362,7 @@ And if an extracted block contains a syntax error within its line span, the warn
360
362
  skim-py block tests/fixtures/syntax_error.py broken_function
361
363
  ```
362
364
  ```text
365
+ tests/fixtures/syntax_error.py: # parsed with 1 line masked (L7)
363
366
  === tests/fixtures/syntax_error.py (L5-L8) [! 1 syntax error] ===
364
367
  Syntax Notice: L7: invalid syntax
365
368
  5 | def broken_function():
@@ -368,6 +371,34 @@ skim-py block tests/fixtures/syntax_error.py broken_function
368
371
  8 | return x
369
372
  ```
370
373
 
374
+ For cascade-prone mid-edit states (such as an unclosed `def foo(x,` header or unclosed bracket structure), `skim-py` masks the entire broken block in one step, marks the masked line range in the notice, and salvages the broken declaration in the outline so it remains targetable by `block`:
375
+
376
+ ```bash
377
+ skim-py glance tests/fixtures/syntax_error_cascade.py -s
378
+ ```
379
+ ```text
380
+ === tests/fixtures/syntax_error_cascade.py (11 lines) [! 1 syntax error] ===
381
+ # parsed with 3 lines masked (L5, L6, L7)
382
+ Syntax Notice: L5: '(' was never closed (L5-L7 masked)
383
+
384
+ Outline:
385
+ L1-L2 def valid_before()
386
+ L5-L7 def broken_header [syntax error]
387
+ L10-L11 def valid_after()
388
+ ```
389
+
390
+ ```bash
391
+ skim-py block tests/fixtures/syntax_error_cascade.py broken_header
392
+ ```
393
+ ```text
394
+ tests/fixtures/syntax_error_cascade.py: # parsed with 3 lines masked (L5, L6, L7)
395
+ === tests/fixtures/syntax_error_cascade.py (L5-L7) [! 1 syntax error] ===
396
+ Syntax Notice: L5: '(' was never closed (L5-L7 masked)
397
+ 5 | def broken_header(x,
398
+ 6 | y = 1
399
+ 7 | return y
400
+ ```
401
+
371
402
  ---
372
403
 
373
404
  ### 2. `block` Subcommand
@@ -0,0 +1,11 @@
1
+ skim_py/__init__.py,sha256=GCEQwh8hAZr6Vd8gbG-xpnqnulWXEAXhG-nA7sWtmoA,71
2
+ skim_py/block.py,sha256=1up79BSBdFxPspp9CFcJNRGMiQmAz_frCbwp8LrqvG4,4961
3
+ skim_py/cli.py,sha256=gcPPkHI9GCFjmhvWyGc4srC3xeA0I14e8MgAQYY643g,4977
4
+ skim_py/glance.py,sha256=zMP2_VJMfdF2ZTOWBRTuC-zcF6vNc7i5R5nHO63da0c,6931
5
+ skim_py/parser.py,sha256=3-at3xgB5y4ICSekduAjUQpFmmAkO_tmjEZzdVXwlCY,21784
6
+ skim_py/types.py,sha256=k_e9kEx-jhGtJIrh9eNqe8R87JlYvMFQvGGlbzwpXm8,1478
7
+ skim_python-0.0.2.dist-info/METADATA,sha256=3SljznEc0UcoT21BUxfiR5-pi-XXZtw69wlBZ9MwSGY,22791
8
+ skim_python-0.0.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
9
+ skim_python-0.0.2.dist-info/entry_points.txt,sha256=nwzHJF7CgNobxT923pwxljnEuDdJe9a2k81Q91bPY8U,45
10
+ skim_python-0.0.2.dist-info/licenses/LICENSE,sha256=at6MgKqAJV-KpSzR2j1hUc2hTZfymTMkbvDwmypUA9I,1074
11
+ skim_python-0.0.2.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- skim_py/__init__.py,sha256=GCEQwh8hAZr6Vd8gbG-xpnqnulWXEAXhG-nA7sWtmoA,71
2
- skim_py/block.py,sha256=R6KWLB8-WtsKxFMPKJces_H30vk7ht_E-ig5dEnDwns,4818
3
- skim_py/cli.py,sha256=gcPPkHI9GCFjmhvWyGc4srC3xeA0I14e8MgAQYY643g,4977
4
- skim_py/glance.py,sha256=4tsJT1cxlWUCJhi3fiLI1t_57UnP9cA6nrstzSOZrqM,6811
5
- skim_py/parser.py,sha256=d9GiiG34iBl5WhdjUU1hFyQ1_3l8ynBsLeDsoz0Wufw,11255
6
- skim_py/types.py,sha256=5Eeif5OKjqD9hWc5JFccMttSLrwIB1KnoC4cgzG4SBk,1083
7
- skim_python-0.0.1.dist-info/METADATA,sha256=qMFLXCH1kZQot_guaA2soAS-w2WwK6QHOjgMN2gYUfI,21537
8
- skim_python-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
9
- skim_python-0.0.1.dist-info/entry_points.txt,sha256=nwzHJF7CgNobxT923pwxljnEuDdJe9a2k81Q91bPY8U,45
10
- skim_python-0.0.1.dist-info/licenses/LICENSE,sha256=at6MgKqAJV-KpSzR2j1hUc2hTZfymTMkbvDwmypUA9I,1074
11
- skim_python-0.0.1.dist-info/RECORD,,