crackerjack 0.21.5__py3-none-any.whl → 0.21.7__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.
crackerjack/.gitignore CHANGED
@@ -13,5 +13,8 @@
13
13
  /scratch/
14
14
  /.zencoder/
15
15
  /.benchmarks/
16
-
17
16
  **/.claude/settings.local.json
17
+ /complexipy.json
18
+ /coverage.json
19
+ /test-results.xml
20
+ /.coverage
crackerjack/__init__.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
-
3
2
  from .crackerjack import Crackerjack, create_crackerjack_runner
4
3
  from .errors import (
5
4
  CleaningError,
crackerjack/__main__.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from enum import Enum
2
-
3
2
  import typer
4
3
  from pydantic import BaseModel, field_validator
5
4
  from rich.console import Console
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from subprocess import CompletedProcess
7
7
  from subprocess import run as execute
8
8
  from tomllib import loads
9
-
10
9
  from pydantic import BaseModel
11
10
  from rich.console import Console
12
11
  from tomli_w import dumps
@@ -86,28 +85,93 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
86
85
  except Exception as e:
87
86
  print(f"Error cleaning {file_path}: {e}")
88
87
 
88
+ def _initialize_docstring_state(self) -> dict[str, t.Any]:
89
+ return {
90
+ "in_docstring": False,
91
+ "delimiter": None,
92
+ "waiting": False,
93
+ "function_indent": 0,
94
+ "removed_docstring": False,
95
+ "in_multiline_def": False,
96
+ }
97
+
98
+ def _handle_function_definition(
99
+ self, line: str, stripped: str, state: dict[str, t.Any]
100
+ ) -> bool:
101
+ if self._is_function_or_class_definition(stripped):
102
+ state["waiting"] = True
103
+ state["function_indent"] = len(line) - len(line.lstrip())
104
+ state["removed_docstring"] = False
105
+ state["in_multiline_def"] = not stripped.endswith(":")
106
+ return True
107
+ return False
108
+
109
+ def _handle_multiline_definition(
110
+ self, line: str, stripped: str, state: dict[str, t.Any]
111
+ ) -> bool:
112
+ if state["in_multiline_def"]:
113
+ if stripped.endswith(":"):
114
+ state["in_multiline_def"] = False
115
+ return True
116
+ return False
117
+
118
+ def _handle_waiting_docstring(
119
+ self, lines: list[str], i: int, stripped: str, state: dict[str, t.Any]
120
+ ) -> tuple[bool, str | None]:
121
+ if state["waiting"] and stripped:
122
+ if self._handle_docstring_start(stripped, state):
123
+ pass_line = None
124
+ if not state["in_docstring"]:
125
+ function_indent: int = state["function_indent"]
126
+ if self._needs_pass_statement(lines, i + 1, function_indent):
127
+ pass_line = " " * (function_indent + 4) + "pass"
128
+ state["removed_docstring"] = True
129
+ return True, pass_line
130
+ else:
131
+ state["waiting"] = False
132
+ return False, None
133
+
134
+ def _handle_docstring_content(
135
+ self, lines: list[str], i: int, stripped: str, state: dict[str, t.Any]
136
+ ) -> tuple[bool, str | None]:
137
+ if state["in_docstring"]:
138
+ if self._handle_docstring_end(stripped, state):
139
+ pass_line = None
140
+ function_indent: int = state["function_indent"]
141
+ if self._needs_pass_statement(lines, i + 1, function_indent):
142
+ pass_line = " " * (function_indent + 4) + "pass"
143
+ state["removed_docstring"] = False
144
+ return True, pass_line
145
+ else:
146
+ return True, None
147
+ return False, None
148
+
149
+ def _process_line(
150
+ self, lines: list[str], i: int, line: str, state: dict[str, t.Any]
151
+ ) -> tuple[bool, str | None]:
152
+ stripped = line.strip()
153
+ if self._handle_function_definition(line, stripped, state):
154
+ return True, line
155
+ if self._handle_multiline_definition(line, stripped, state):
156
+ return True, line
157
+ handled, pass_line = self._handle_waiting_docstring(lines, i, stripped, state)
158
+ if handled:
159
+ return True, pass_line
160
+ handled, pass_line = self._handle_docstring_content(lines, i, stripped, state)
161
+ if handled:
162
+ return True, pass_line
163
+ if state["removed_docstring"] and stripped:
164
+ state["removed_docstring"] = False
165
+ return False, line
166
+
89
167
  def remove_docstrings(self, code: str) -> str:
90
168
  lines = code.split("\n")
91
169
  cleaned_lines = []
92
- docstring_state = {"in_docstring": False, "delimiter": None, "waiting": False}
93
- for line in lines:
94
- stripped = line.strip()
95
- if self._is_function_or_class_definition(stripped):
96
- docstring_state["waiting"] = True
97
- cleaned_lines.append(line)
98
- continue
99
- if docstring_state["waiting"] and stripped:
100
- if self._handle_docstring_start(stripped, docstring_state):
101
- continue
102
- else:
103
- docstring_state["waiting"] = False
104
- if docstring_state["in_docstring"]:
105
- if self._handle_docstring_end(stripped, docstring_state):
106
- continue
107
- else:
108
- continue
109
- cleaned_lines.append(line)
110
-
170
+ docstring_state = self._initialize_docstring_state()
171
+ for i, line in enumerate(lines):
172
+ _, result_line = self._process_line(lines, i, line, docstring_state)
173
+ if result_line:
174
+ cleaned_lines.append(result_line)
111
175
  return "\n".join(cleaned_lines)
112
176
 
113
177
  def _is_function_or_class_definition(self, stripped_line: str) -> bool:
@@ -137,6 +201,21 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
137
201
  state["in_docstring"] = False
138
202
  state["delimiter"] = None
139
203
  return True
204
+ return False
205
+
206
+ def _needs_pass_statement(
207
+ self, lines: list[str], start_index: int, function_indent: int
208
+ ) -> bool:
209
+ for i in range(start_index, len(lines)):
210
+ line = lines[i]
211
+ stripped = line.strip()
212
+ if not stripped:
213
+ continue
214
+ line_indent = len(line) - len(line.lstrip())
215
+ if line_indent <= function_indent:
216
+ return True
217
+ if line_indent > function_indent:
218
+ return False
140
219
  return True
141
220
 
142
221
  def remove_line_comments(self, code: str) -> str:
@@ -149,7 +228,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
149
228
  cleaned_line = self._process_line_for_comments(line)
150
229
  if cleaned_line or not line.strip():
151
230
  cleaned_lines.append(cleaned_line or line)
152
-
153
231
  return "\n".join(cleaned_lines)
154
232
 
155
233
  def _process_line_for_comments(self, line: str) -> str:
@@ -162,7 +240,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
162
240
  break
163
241
  else:
164
242
  result.append(char)
165
-
166
243
  return "".join(result).rstrip()
167
244
 
168
245
  def _handle_string_character(
@@ -173,18 +250,14 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
173
250
  string_state: dict[str, t.Any],
174
251
  result: list[str],
175
252
  ) -> bool:
176
- """Handle string quote characters. Returns True if character was handled."""
177
253
  if char not in ("'", '"'):
178
254
  return False
179
-
180
255
  if index > 0 and line[index - 1] == "\\":
181
256
  return False
182
-
183
257
  if string_state["in_string"] is None:
184
258
  string_state["in_string"] = char
185
259
  elif string_state["in_string"] == char:
186
260
  string_state["in_string"] = None
187
-
188
261
  result.append(char)
189
262
  return True
190
263
 
@@ -196,14 +269,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
196
269
  string_state: dict[str, t.Any],
197
270
  result: list[str],
198
271
  ) -> bool:
199
- """Handle comment character. Returns True if comment was found."""
200
272
  if char != "#" or string_state["in_string"] is not None:
201
273
  return False
202
-
203
274
  comment = line[index:].strip()
204
275
  if self._is_special_comment_line(comment):
205
276
  result.append(line[index:])
206
-
207
277
  return True
208
278
 
209
279
  def _is_special_comment_line(self, comment: str) -> bool:
@@ -227,13 +297,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
227
297
  ):
228
298
  continue
229
299
  cleaned_lines.append(line)
230
-
231
300
  return "\n".join(self._remove_trailing_empty_lines(cleaned_lines))
232
301
 
233
302
  def _update_function_state(
234
303
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
235
304
  ) -> None:
236
- """Update function tracking state based on current line."""
237
305
  if stripped_line.startswith(("def ", "async def ")):
238
306
  function_tracker["in_function"] = True
239
307
  function_tracker["function_indent"] = len(line) - len(stripped_line)
@@ -244,7 +312,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
244
312
  def _is_function_end(
245
313
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
246
314
  ) -> bool:
247
- """Check if current line marks the end of a function."""
248
315
  return (
249
316
  function_tracker["in_function"]
250
317
  and bool(line)
@@ -259,13 +326,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
259
326
  cleaned_lines: list[str],
260
327
  function_tracker: dict[str, t.Any],
261
328
  ) -> bool:
262
- """Determine if an empty line should be skipped."""
263
329
  if line_idx > 0 and cleaned_lines and (not cleaned_lines[-1]):
264
330
  return True
265
-
266
331
  if function_tracker["in_function"]:
267
332
  return self._should_skip_function_empty_line(line_idx, lines)
268
-
269
333
  return False
270
334
 
271
335
  def _should_skip_function_empty_line(self, line_idx: int, lines: list[str]) -> bool:
@@ -280,7 +344,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
280
344
  return True
281
345
  if next_line in ("pass", "break", "continue", "raise"):
282
346
  return True
283
-
284
347
  return self._is_special_comment(next_line)
285
348
 
286
349
  def _is_special_comment(self, line: str) -> bool:
@@ -420,15 +483,12 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
420
483
  for tool, settings in our_toml_config.get("tool", {}).items():
421
484
  if tool not in pkg_toml_config["tool"]:
422
485
  pkg_toml_config["tool"][tool] = {}
423
-
424
486
  pkg_tool_config = pkg_toml_config["tool"][tool]
425
-
426
487
  self._merge_tool_config(settings, pkg_tool_config, tool)
427
488
 
428
489
  def _merge_tool_config(
429
490
  self, our_config: dict[str, t.Any], pkg_config: dict[str, t.Any], tool: str
430
491
  ) -> None:
431
- """Recursively merge tool configuration, preserving existing project settings."""
432
492
  for setting, value in our_config.items():
433
493
  if isinstance(value, dict):
434
494
  self._merge_nested_config(setting, value, pkg_config)
@@ -438,21 +498,17 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
438
498
  def _merge_nested_config(
439
499
  self, setting: str, value: dict[str, t.Any], pkg_config: dict[str, t.Any]
440
500
  ) -> None:
441
- """Handle nested configuration merging."""
442
501
  if setting not in pkg_config:
443
502
  pkg_config[setting] = {}
444
503
  elif not isinstance(pkg_config[setting], dict):
445
504
  pkg_config[setting] = {}
446
-
447
505
  self._merge_tool_config(value, pkg_config[setting], "")
448
-
449
506
  for k, v in value.items():
450
507
  self._merge_nested_value(k, v, pkg_config[setting])
451
508
 
452
509
  def _merge_nested_value(
453
510
  self, key: str, value: t.Any, nested_config: dict[str, t.Any]
454
511
  ) -> None:
455
- """Merge individual nested values."""
456
512
  if isinstance(value, str | list) and "crackerjack" in str(value):
457
513
  nested_config[key] = self.swap_package_name(value)
458
514
  elif self._is_mergeable_list(key, value):
@@ -467,7 +523,6 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
467
523
  def _merge_direct_config(
468
524
  self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
469
525
  ) -> None:
470
- """Handle direct configuration merging."""
471
526
  if isinstance(value, str | list) and "crackerjack" in str(value):
472
527
  pkg_config[setting] = self.swap_package_name(value)
473
528
  elif self._is_mergeable_list(setting, value):
@@ -633,7 +688,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
633
688
  )
634
689
  if result.returncode == 0:
635
690
  self.console.print("PDM installed: ✅\n")
636
- self.execute_command(["pdm", "sync"])
691
+ self.execute_command(["pdm", "lock"])
637
692
  self.console.print("Lock file updated: ✅\n")
638
693
  else:
639
694
  self.console.print(
@@ -730,7 +785,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
730
785
  self._add_benchmark_flags(test, options)
731
786
  else:
732
787
  self._add_worker_flags(test, options, project_size)
733
-
734
788
  return test
735
789
 
736
790
  def _detect_project_size(self) -> str:
crackerjack/errors.py CHANGED
@@ -2,7 +2,6 @@ import sys
2
2
  import typing as t
3
3
  from enum import Enum
4
4
  from pathlib import Path
5
-
6
5
  from rich.console import Console
7
6
  from rich.panel import Panel
8
7
 
@@ -2,7 +2,6 @@ import time
2
2
  import typing as t
3
3
  from enum import Enum, auto
4
4
  from pathlib import Path
5
-
6
5
  from rich.box import ROUNDED
7
6
  from rich.console import Console
8
7
  from rich.layout import Layout
@@ -19,7 +18,6 @@ from rich.prompt import Confirm, Prompt
19
18
  from rich.table import Table
20
19
  from rich.text import Text
21
20
  from rich.tree import Tree
22
-
23
21
  from .errors import CrackerjackError, ErrorCode, handle_error
24
22
 
25
23
 
@@ -289,7 +287,6 @@ class InteractiveCLI:
289
287
  )
290
288
  total_tasks = len(self.workflow.tasks)
291
289
  progress_task = progress.add_task("Running workflow", total=total_tasks)
292
-
293
290
  return {
294
291
  "progress": progress,
295
292
  "progress_task": progress_task,
@@ -299,14 +296,11 @@ class InteractiveCLI:
299
296
  def _execute_workflow_loop(
300
297
  self, layout: Layout, progress_tracker: dict[str, t.Any], live: Live
301
298
  ) -> None:
302
- """Execute the main workflow loop."""
303
299
  while not self.workflow.all_tasks_completed():
304
300
  layout["tasks"].update(self.show_task_table())
305
301
  next_task = self.workflow.get_next_task()
306
-
307
302
  if not next_task:
308
303
  break
309
-
310
304
  if self._should_execute_task(layout, next_task, live):
311
305
  self._execute_task(layout, next_task, progress_tracker)
312
306
  else:
@@ -322,20 +316,16 @@ class InteractiveCLI:
322
316
  def _execute_task(
323
317
  self, layout: Layout, task: Task, progress_tracker: dict[str, t.Any]
324
318
  ) -> None:
325
- """Execute a single task and update progress."""
326
319
  task.start()
327
320
  layout["details"].update(self.show_task_status(task))
328
321
  time.sleep(1)
329
-
330
322
  success = self._simulate_task_execution()
331
-
332
323
  if success:
333
324
  task.complete()
334
325
  progress_tracker["completed_tasks"] += 1
335
326
  else:
336
327
  error = self._create_task_error(task.name)
337
328
  task.fail(error)
338
-
339
329
  progress_tracker["progress"].update(
340
330
  progress_tracker["progress_task"],
341
331
  completed=progress_tracker["completed_tasks"],
@@ -4,7 +4,7 @@ requires = [ "pdm-backend" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.21.4"
7
+ version = "0.21.6"
8
8
  description = "Crackerjack: code quality toolkit"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crackerjack
3
- Version: 0.21.5
3
+ Version: 0.21.7
4
4
  Summary: Crackerjack: code quality toolkit
5
5
  Keywords: bandit,black,creosote,mypy,pyright,pytest,refurb,ruff
6
6
  Author-Email: lesleslie <les@wedgwoodwebworks.com>
@@ -102,7 +102,7 @@ If you're new to Crackerjack, follow these steps:
102
102
 
103
103
  Or use the interactive Rich UI:
104
104
  ```
105
- python -m crackerjack --rich-ui
105
+ python -m crackerjack --interactive
106
106
  ```
107
107
 
108
108
  ---
@@ -269,7 +269,7 @@ python -m crackerjack -t --benchmark-regression --benchmark-regression-threshold
269
269
 
270
270
  Or with the interactive Rich UI:
271
271
  ```
272
- python -m crackerjack --rich-ui
272
+ python -m crackerjack --interactive
273
273
  ```
274
274
 
275
275
  ## Usage
@@ -361,7 +361,7 @@ runner.process(MyOptions())
361
361
  - `--benchmark-regression`: Fail tests if benchmarks regress beyond threshold.
362
362
  - `--benchmark-regression-threshold`: Set threshold percentage for benchmark regression (default 5.0%).
363
363
  - `-a`, `--all`: Run with `-x -t -p <micro|minor|major> -c` development options.
364
- - `--rich-ui`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
364
+ - `--interactive`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
365
365
  - `--ai-agent`: Enable AI agent mode with structured output (see [AI Agent Integration](#ai-agent-integration)).
366
366
  - `--help`: Display help.
367
367
 
@@ -461,7 +461,7 @@ runner.process(MyOptions())
461
461
 
462
462
  - **Rich Interactive Mode** - Run with the interactive Rich UI:
463
463
  ```bash
464
- python -m crackerjack --rich-ui
464
+ python -m crackerjack --interactive
465
465
  ```
466
466
 
467
467
  - **AI Integration** - Run with structured output for AI tools:
@@ -500,10 +500,10 @@ Crackerjack now offers an enhanced interactive experience through its Rich UI:
500
500
  - **Error Visualization:** Errors are presented in a structured, easy-to-understand format with recovery suggestions
501
501
  - **File Selection:** Interactive file browser for operations that require selecting files
502
502
 
503
- To use the Rich UI, run Crackerjack with the `--rich-ui` flag:
503
+ To use the Rich UI, run Crackerjack with the `--interactive` flag:
504
504
 
505
505
  ```bash
506
- python -m crackerjack --rich-ui
506
+ python -m crackerjack --interactive
507
507
  ```
508
508
 
509
509
  This launches an interactive terminal interface where you can:
@@ -542,7 +542,7 @@ python -m crackerjack -v
542
542
  For the most comprehensive error details with visual formatting, combine verbose mode with the Rich UI:
543
543
 
544
544
  ```bash
545
- python -m crackerjack --rich-ui -v
545
+ python -m crackerjack --interactive -v
546
546
  ```
547
547
 
548
548
  ## Python 3.13+ Features
@@ -1,8 +1,8 @@
1
- crackerjack-0.21.5.dist-info/METADATA,sha256=rHhOecmqcNGFvoh0eIB1WvvYBpcPDfJN8qTMiMF8qSs,26414
2
- crackerjack-0.21.5.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- crackerjack-0.21.5.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- crackerjack-0.21.5.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
5
- crackerjack/.gitignore,sha256=FOLrDV-tuHpwof310aOi6cWOkJXeVI_gvWvg_paDzs4,199
1
+ crackerjack-0.21.7.dist-info/METADATA,sha256=suf-qK6MQQTYL6v8ZXcTUT70x6ADX6u1Dd8YvhQSV60,26442
2
+ crackerjack-0.21.7.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ crackerjack-0.21.7.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ crackerjack-0.21.7.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
5
+ crackerjack/.gitignore,sha256=n8cD6U16L3XZn__PvhYm_F7-YeFHFucHCyxWj2NZCGs,259
6
6
  crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
7
7
  crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
8
8
  crackerjack/.pre-commit-config-ai.yaml,sha256=K8xXKMJcdhfXOk24L4XpK7H8YlvnZfOh4NVA6qvOz8I,3319
@@ -34,7 +34,7 @@ crackerjack/.ruff_cache/0.11.7/10386934055395314831,sha256=lBNwN5zAgM4OzbkXIOzCc
34
34
  crackerjack/.ruff_cache/0.11.7/3557596832929915217,sha256=fKlwUbsvT3YIKV6UR-aA_i64lLignWeVfVu-MMmVbU0,207
35
35
  crackerjack/.ruff_cache/0.11.8/530407680854991027,sha256=xAMAL3Vu_HR6M-h5ojCTaak0By5ii8u-14pXULLgLqw,224
36
36
  crackerjack/.ruff_cache/0.12.0/5056746222905752453,sha256=MqrIT5qymJcgAOBZyn-TvYoGCFfDFCgN9IwSULq8n14,256
37
- crackerjack/.ruff_cache/0.12.1/5056746222905752453,sha256=ksDcvUi9t_w2SUbEhH8Yk-ArZbLaWKIkwnKqJ0Q9ceA,256
37
+ crackerjack/.ruff_cache/0.12.1/5056746222905752453,sha256=CHy2IV23GK0EauutmCmddO7Ck-fHMzbvpvZ1ZO9Cp8Y,256
38
38
  crackerjack/.ruff_cache/0.2.0/10047773857155985907,sha256=j9LNa_RQ4Plor7go1uTYgz17cEENKvZQ-dP6b9MX0ik,248
39
39
  crackerjack/.ruff_cache/0.2.1/8522267973936635051,sha256=u_aPBMibtAp_iYvLwR88GMAECMcIgHezxMyuapmU2P4,248
40
40
  crackerjack/.ruff_cache/0.2.2/18053836298936336950,sha256=Xb_ebP0pVuUfSqPEZKlhQ70so_vqkEfMYpuHQ06iR5U,248
@@ -62,11 +62,11 @@ crackerjack/.ruff_cache/0.9.3/13948373885254993391,sha256=kGhtIkzPUtKAgvlKs3D8j4
62
62
  crackerjack/.ruff_cache/0.9.9/12813592349865671909,sha256=tmr8_vhRD2OxsVuMfbJPdT9fDFX-d5tfC5U9jgziyho,224
63
63
  crackerjack/.ruff_cache/0.9.9/8843823720003377982,sha256=e4ymkXfQsUg5e_mtO34xTsaTvs1uA3_fI216Qq9qCAM,136
64
64
  crackerjack/.ruff_cache/CACHEDIR.TAG,sha256=WVMVbX4MVkpCclExbq8m-IcOZIOuIZf5FrYw5Pk-Ma4,43
65
- crackerjack/__init__.py,sha256=8tBSPAru_YDuPpjz05cL7pNbZjYFoRT_agGd_FWa3gY,839
66
- crackerjack/__main__.py,sha256=AknITUlFjq3YUK9s2xeL62dM0GN82JBQyDkPzQ_hCUg,6561
67
- crackerjack/crackerjack.py,sha256=H88eTvf51jOmFK6oXxtCpyDLtOTJqrUgQ-AKax9vnUY,33310
68
- crackerjack/errors.py,sha256=QEPtVuMtKtQHuawgr1ToMaN1KbUg5h9-4mS33YB5Znk,4062
69
- crackerjack/interactive.py,sha256=5KXKSvWKttLvHcI1L4VEDc3Rb-ZpHBOl_Qr7lhD-O4Q,16262
65
+ crackerjack/__init__.py,sha256=LyIPEfjBcqWp7UVuJ-b7cc8oiFxrQqyL8RJ7g6c17Qs,838
66
+ crackerjack/__main__.py,sha256=GP5GBbWK5lDTQanCHKVEyS3hDWvshwlrhWagzQlRuD4,6560
67
+ crackerjack/crackerjack.py,sha256=jlepiHgMkvTyl83O8HUqC4mCsQbZIejerOHNQD15JcU,35854
68
+ crackerjack/errors.py,sha256=xvXO3bk0VrxQz0lysIuVOju6SxxDgf04Wne1BedLFB8,4061
69
+ crackerjack/interactive.py,sha256=ierA2jeUsigTJhKOcJJsjhBZm2WwsQpvow3Y5ppF7IE,16151
70
70
  crackerjack/py313.py,sha256=buYE7LO11Q64ffowEhTZRFQoAGj_8sg3DTlZuv8M9eo,5890
71
- crackerjack/pyproject.toml,sha256=jCrCGfS3TmqKQRsSCd_ZEq8d0F-vY7v2u1xnKeQIkpM,4988
72
- crackerjack-0.21.5.dist-info/RECORD,,
71
+ crackerjack/pyproject.toml,sha256=OIoNmIktC1abFPgePb95Zg9BDTGfRxXftK-KccWgalc,4988
72
+ crackerjack-0.21.7.dist-info/RECORD,,