code-puppy 0.0.154__py3-none-any.whl → 0.0.156__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.
Files changed (48) hide show
  1. code_puppy/agent.py +26 -5
  2. code_puppy/agents/agent_creator_agent.py +65 -13
  3. code_puppy/agents/json_agent.py +8 -0
  4. code_puppy/agents/runtime_manager.py +12 -4
  5. code_puppy/command_line/command_handler.py +83 -0
  6. code_puppy/command_line/mcp/install_command.py +50 -1
  7. code_puppy/command_line/mcp/wizard_utils.py +88 -17
  8. code_puppy/command_line/prompt_toolkit_completion.py +18 -2
  9. code_puppy/config.py +8 -2
  10. code_puppy/main.py +17 -4
  11. code_puppy/mcp/__init__.py +2 -2
  12. code_puppy/mcp/config_wizard.py +1 -1
  13. code_puppy/messaging/spinner/console_spinner.py +1 -1
  14. code_puppy/model_factory.py +13 -12
  15. code_puppy/models.json +26 -0
  16. code_puppy/round_robin_model.py +35 -18
  17. code_puppy/summarization_agent.py +1 -3
  18. code_puppy/tools/agent_tools.py +41 -138
  19. code_puppy/tools/file_operations.py +116 -96
  20. code_puppy/tui/app.py +1 -1
  21. {code_puppy-0.0.154.data → code_puppy-0.0.156.data}/data/code_puppy/models.json +26 -0
  22. {code_puppy-0.0.154.dist-info → code_puppy-0.0.156.dist-info}/METADATA +4 -3
  23. {code_puppy-0.0.154.dist-info → code_puppy-0.0.156.dist-info}/RECORD +26 -48
  24. code_puppy/token_utils.py +0 -67
  25. code_puppy/tools/token_check.py +0 -32
  26. code_puppy/tui/tests/__init__.py +0 -1
  27. code_puppy/tui/tests/test_agent_command.py +0 -79
  28. code_puppy/tui/tests/test_chat_message.py +0 -28
  29. code_puppy/tui/tests/test_chat_view.py +0 -88
  30. code_puppy/tui/tests/test_command_history.py +0 -89
  31. code_puppy/tui/tests/test_copy_button.py +0 -191
  32. code_puppy/tui/tests/test_custom_widgets.py +0 -27
  33. code_puppy/tui/tests/test_disclaimer.py +0 -27
  34. code_puppy/tui/tests/test_enums.py +0 -15
  35. code_puppy/tui/tests/test_file_browser.py +0 -60
  36. code_puppy/tui/tests/test_help.py +0 -38
  37. code_puppy/tui/tests/test_history_file_reader.py +0 -107
  38. code_puppy/tui/tests/test_input_area.py +0 -33
  39. code_puppy/tui/tests/test_settings.py +0 -44
  40. code_puppy/tui/tests/test_sidebar.py +0 -33
  41. code_puppy/tui/tests/test_sidebar_history.py +0 -153
  42. code_puppy/tui/tests/test_sidebar_history_navigation.py +0 -132
  43. code_puppy/tui/tests/test_status_bar.py +0 -54
  44. code_puppy/tui/tests/test_timestamped_history.py +0 -52
  45. code_puppy/tui/tests/test_tools.py +0 -82
  46. {code_puppy-0.0.154.dist-info → code_puppy-0.0.156.dist-info}/WHEEL +0 -0
  47. {code_puppy-0.0.154.dist-info → code_puppy-0.0.156.dist-info}/entry_points.txt +0 -0
  48. {code_puppy-0.0.154.dist-info → code_puppy-0.0.156.dist-info}/licenses/LICENSE +0 -0
@@ -18,23 +18,7 @@ from code_puppy.messaging import (
18
18
  emit_system_message,
19
19
  emit_warning,
20
20
  )
21
- from code_puppy.tools.common import generate_group_id, should_ignore_path
22
-
23
- # Add token checking functionality
24
- try:
25
- from code_puppy.token_utils import get_tokenizer
26
- from code_puppy.tools.token_check import token_guard
27
- except ImportError:
28
- # Fallback for when token checking modules aren't available
29
- def get_tokenizer():
30
- # Simple token estimation - no longer using tiktoken
31
- return None
32
-
33
- def token_guard(num_tokens):
34
- if num_tokens > 10000:
35
- raise ValueError(
36
- f"Token count {num_tokens} exceeds safety limit of 10,000 tokens"
37
- )
21
+ from code_puppy.tools.common import generate_group_id
38
22
 
39
23
 
40
24
  # Pydantic models for tool return types
@@ -127,7 +111,6 @@ def _list_files(
127
111
  context: RunContext, directory: str = ".", recursive: bool = True
128
112
  ) -> ListFileOutput:
129
113
  import subprocess
130
- import tempfile
131
114
  import shutil
132
115
  import sys
133
116
 
@@ -136,26 +119,30 @@ def _list_files(
136
119
 
137
120
  # Build string representation
138
121
  output_lines = []
139
-
140
- directory_listing_header = "\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]"
122
+
123
+ directory_listing_header = (
124
+ "\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]"
125
+ )
141
126
  output_lines.append(directory_listing_header)
142
-
127
+
143
128
  directory_info = f"\U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]\n"
144
129
  output_lines.append(directory_info)
145
-
130
+
146
131
  divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
147
132
  output_lines.append(divider)
148
-
133
+
149
134
  if not os.path.exists(directory):
150
- error_msg = f"[red bold]Error:[/red bold] Directory '{directory}' does not exist"
135
+ error_msg = (
136
+ f"[red bold]Error:[/red bold] Directory '{directory}' does not exist"
137
+ )
151
138
  output_lines.append(error_msg)
152
-
139
+
153
140
  output_lines.append(divider)
154
141
  return ListFileOutput(content="\n".join(output_lines))
155
142
  if not os.path.isdir(directory):
156
143
  error_msg = f"[red bold]Error:[/red bold] '{directory}' is not a directory"
157
144
  output_lines.append(error_msg)
158
-
145
+
159
146
  output_lines.append(divider)
160
147
  return ListFileOutput(content="\n".join(output_lines))
161
148
 
@@ -165,11 +152,11 @@ def _list_files(
165
152
  if not is_project_directory(directory):
166
153
  warning_msg = "[yellow bold]Warning:[/yellow bold] 🏠 Detected home directory - limiting to non-recursive listing for performance"
167
154
  output_lines.append(warning_msg)
168
-
155
+
169
156
  info_msg = f"[dim]💡 To force recursive listing in home directory, use list_files('{directory}', recursive=True) explicitly[/dim]"
170
157
  output_lines.append(info_msg)
171
158
  recursive = False
172
-
159
+
173
160
  # Create a temporary ignore file with our ignore patterns
174
161
  ignore_file = None
175
162
  try:
@@ -190,50 +177,51 @@ def _list_files(
190
177
  if os.path.exists(venv_rg_exe_path):
191
178
  rg_path = venv_rg_exe_path
192
179
  break
193
-
180
+
194
181
  if not rg_path:
195
- error_msg = f"[red bold]Error:[/red bold] ripgrep (rg) not found. Please install ripgrep to use this tool."
182
+ error_msg = "[red bold]Error:[/red bold] ripgrep (rg) not found. Please install ripgrep to use this tool."
196
183
  output_lines.append(error_msg)
197
184
  return ListFileOutput(content="\n".join(output_lines))
198
-
185
+
199
186
  # Build command for ripgrep --files
200
187
  cmd = [rg_path, "--files"]
201
-
188
+
202
189
  # For non-recursive mode, we'll limit depth after getting results
203
190
  if not recursive:
204
191
  cmd.extend(["--max-depth", "1"])
205
-
192
+
206
193
  # Add ignore patterns to the command via a temporary file
207
194
  from code_puppy.tools.common import IGNORE_PATTERNS
208
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.ignore') as f:
195
+
196
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".ignore") as f:
209
197
  ignore_file = f.name
210
198
  for pattern in IGNORE_PATTERNS:
211
199
  f.write(f"{pattern}\n")
212
-
200
+
213
201
  cmd.extend(["--ignore-file", ignore_file])
214
202
  cmd.append(directory)
215
-
203
+
216
204
  # Run ripgrep to get file listing
217
205
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
218
-
206
+
219
207
  # Process the output lines
220
- files = result.stdout.strip().split('\n') if result.stdout.strip() else []
221
-
208
+ files = result.stdout.strip().split("\n") if result.stdout.strip() else []
209
+
222
210
  # Create ListedFile objects with metadata
223
211
  for file_path in files:
224
212
  if not file_path: # Skip empty lines
225
213
  continue
226
-
214
+
227
215
  full_path = os.path.join(directory, file_path)
228
-
216
+
229
217
  # Skip if file doesn't exist (though it should)
230
218
  if not os.path.exists(full_path):
231
219
  continue
232
-
220
+
233
221
  # For non-recursive mode, skip files in subdirectories
234
222
  if not recursive and os.sep in file_path:
235
223
  continue
236
-
224
+
237
225
  # Check if path is a file or directory
238
226
  if os.path.isfile(full_path):
239
227
  entry_type = "file"
@@ -244,19 +232,19 @@ def _list_files(
244
232
  else:
245
233
  # Skip if it's neither a file nor directory
246
234
  continue
247
-
235
+
248
236
  try:
249
237
  # Get stats for the entry
250
238
  stat_info = os.stat(full_path)
251
239
  actual_size = stat_info.st_size
252
-
240
+
253
241
  # For files, we use the actual size; for directories, we keep size=0
254
242
  if entry_type == "file":
255
243
  size = actual_size
256
-
244
+
257
245
  # Calculate depth
258
246
  depth = file_path.count(os.sep)
259
-
247
+
260
248
  # Add directory entries if needed for files
261
249
  if entry_type == "file":
262
250
  dir_path = os.path.dirname(file_path)
@@ -264,9 +252,12 @@ def _list_files(
264
252
  # Add directory path components if they don't exist
265
253
  path_parts = dir_path.split(os.sep)
266
254
  for i in range(len(path_parts)):
267
- partial_path = os.sep.join(path_parts[:i+1])
255
+ partial_path = os.sep.join(path_parts[: i + 1])
268
256
  # Check if we already added this directory
269
- if not any(f.path == partial_path and f.type == "directory" for f in results):
257
+ if not any(
258
+ f.path == partial_path and f.type == "directory"
259
+ for f in results
260
+ ):
270
261
  results.append(
271
262
  ListedFile(
272
263
  path=partial_path,
@@ -276,7 +267,7 @@ def _list_files(
276
267
  depth=partial_path.count(os.sep),
277
268
  )
278
269
  )
279
-
270
+
280
271
  # Add the entry (file or directory)
281
272
  results.append(
282
273
  ListedFile(
@@ -291,11 +282,15 @@ def _list_files(
291
282
  # Skip files we can't access
292
283
  continue
293
284
  except subprocess.TimeoutExpired:
294
- error_msg = f"[red bold]Error:[/red bold] List files command timed out after 30 seconds"
285
+ error_msg = (
286
+ "[red bold]Error:[/red bold] List files command timed out after 30 seconds"
287
+ )
295
288
  output_lines.append(error_msg)
296
289
  return ListFileOutput(content="\n".join(output_lines))
297
290
  except Exception as e:
298
- error_msg = f"[red bold]Error:[/red bold] Error during list files operation: {e}"
291
+ error_msg = (
292
+ f"[red bold]Error:[/red bold] Error during list files operation: {e}"
293
+ )
299
294
  output_lines.append(error_msg)
300
295
  return ListFileOutput(content="\n".join(output_lines))
301
296
  finally:
@@ -345,27 +340,27 @@ def _list_files(
345
340
  dir_count = sum(1 for item in results if item.type == "directory")
346
341
  file_count = sum(1 for item in results if item.type == "file")
347
342
  total_size = sum(item.size for item in results if item.type == "file")
348
-
343
+
349
344
  # Build the directory header section
350
345
  dir_name = os.path.basename(directory) or directory
351
346
  dir_header = f"\U0001f4c1 [bold blue]{dir_name}[/bold blue]"
352
347
  output_lines.append(dir_header)
353
-
348
+
354
349
  # Sort all items by path for consistent display
355
350
  all_items = sorted(results, key=lambda x: x.path)
356
-
351
+
357
352
  # Build file and directory tree representation
358
353
  parent_dirs_with_content = set()
359
354
  for item in all_items:
360
355
  # Skip root directory entries with no path
361
356
  if item.type == "directory" and not item.path:
362
357
  continue
363
-
358
+
364
359
  # Track parent directories that contain files/dirs
365
360
  if os.sep in item.path:
366
361
  parent_path = os.path.dirname(item.path)
367
362
  parent_dirs_with_content.add(parent_path)
368
-
363
+
369
364
  # Calculate indentation depth based on path separators
370
365
  depth = item.path.count(os.sep) + 1 if item.path else 0
371
366
  prefix = ""
@@ -374,10 +369,10 @@ def _list_files(
374
369
  prefix += "\u2514\u2500\u2500 "
375
370
  else:
376
371
  prefix += " "
377
-
372
+
378
373
  # Get the display name (basename) of the item
379
374
  name = os.path.basename(item.path) or item.path
380
-
375
+
381
376
  # Add directory or file line with appropriate formatting
382
377
  if item.type == "directory":
383
378
  dir_line = f"{prefix}\U0001f4c1 [bold blue]{name}/[/bold blue]"
@@ -387,17 +382,17 @@ def _list_files(
387
382
  size_str = format_size(item.size)
388
383
  file_line = f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]"
389
384
  output_lines.append(file_line)
390
-
385
+
391
386
  # Add summary information
392
387
  summary_header = "\n[bold cyan]Summary:[/bold cyan]"
393
388
  output_lines.append(summary_header)
394
-
389
+
395
390
  summary_line = f"\U0001f4c1 [blue]{dir_count} directories[/blue], \U0001f4c4 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
396
391
  output_lines.append(summary_line)
397
-
392
+
398
393
  final_divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
399
394
  output_lines.append(final_divider)
400
-
395
+
401
396
  # Return both the content string and the list of ListedFile objects
402
397
  return ListFileOutput(content="\n".join(output_lines), files=results)
403
398
 
@@ -463,11 +458,10 @@ def _read_file(
463
458
  def _grep(context: RunContext, search_string: str, directory: str = ".") -> GrepOutput:
464
459
  import subprocess
465
460
  import json
466
- import tempfile
467
461
  import os
468
462
  import shutil
469
463
  import sys
470
-
464
+
471
465
  directory = os.path.abspath(directory)
472
466
  matches: List[MatchInfo] = []
473
467
 
@@ -490,7 +484,7 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
490
484
  # --max-filesize 5M to avoid huge files (increased from 1M)
491
485
  # --type=all to search across all recognized text file types
492
486
  # --ignore-file to obey our ignore list
493
-
487
+
494
488
  # Find ripgrep executable - first check system PATH, then virtual environment
495
489
  rg_path = shutil.which("rg")
496
490
  if not rg_path:
@@ -508,43 +502,62 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
508
502
  if os.path.exists(venv_rg_exe_path):
509
503
  rg_path = venv_rg_exe_path
510
504
  break
511
-
505
+
512
506
  if not rg_path:
513
- emit_error(f"ripgrep (rg) not found. Please install ripgrep to use this tool.", message_group=group_id)
507
+ emit_error(
508
+ "ripgrep (rg) not found. Please install ripgrep to use this tool.",
509
+ message_group=group_id,
510
+ )
514
511
  return GrepOutput(matches=[])
515
-
516
- cmd = [rg_path, "--json", "--max-count", "50", "--max-filesize", "5M", "--type=all"]
517
-
512
+
513
+ cmd = [
514
+ rg_path,
515
+ "--json",
516
+ "--max-count",
517
+ "50",
518
+ "--max-filesize",
519
+ "5M",
520
+ "--type=all",
521
+ ]
522
+
518
523
  # Add ignore patterns to the command via a temporary file
519
524
  from code_puppy.tools.common import IGNORE_PATTERNS
520
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.ignore') as f:
525
+
526
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".ignore") as f:
521
527
  ignore_file = f.name
522
528
  for pattern in IGNORE_PATTERNS:
523
529
  f.write(f"{pattern}\n")
524
-
530
+
525
531
  cmd.extend(["--ignore-file", ignore_file])
526
532
  cmd.extend([search_string, directory])
527
533
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
528
-
534
+
529
535
  # Parse the JSON output from ripgrep
530
- for line in result.stdout.strip().split('\n'):
536
+ for line in result.stdout.strip().split("\n"):
531
537
  if not line:
532
538
  continue
533
539
  try:
534
540
  match_data = json.loads(line)
535
541
  # Only process match events, not context or summary
536
- if match_data.get('type') == 'match':
537
- data = match_data.get('data', {})
538
- path_data = data.get('path', {})
539
- file_path = path_data.get('text', '') if path_data.get('text') else ''
540
- line_number = data.get('line_number', None)
541
- line_content = data.get('lines', {}).get('text', '') if data.get('lines', {}).get('text') else ''
542
-
542
+ if match_data.get("type") == "match":
543
+ data = match_data.get("data", {})
544
+ path_data = data.get("path", {})
545
+ file_path = (
546
+ path_data.get("text", "") if path_data.get("text") else ""
547
+ )
548
+ line_number = data.get("line_number", None)
549
+ line_content = (
550
+ data.get("lines", {}).get("text", "")
551
+ if data.get("lines", {}).get("text")
552
+ else ""
553
+ )
554
+ if len(line_content.strip()) > 512:
555
+ line_content = line_content.strip()[0:512]
543
556
  if file_path and line_number:
544
557
  match_info = MatchInfo(
545
558
  file_path=file_path,
546
559
  line_number=line_number,
547
- line_content=line_content.strip()
560
+ line_content=line_content.strip(),
548
561
  )
549
562
  matches.append(match_info)
550
563
  # Limit to 50 matches total, same as original implementation
@@ -557,7 +570,7 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
557
570
  except json.JSONDecodeError:
558
571
  # Skip lines that aren't valid JSON
559
572
  continue
560
-
573
+
561
574
  if not matches:
562
575
  emit_warning(
563
576
  f"No matches found for '{search_string}' in {directory}",
@@ -568,18 +581,21 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
568
581
  f"Found {len(matches)} match(es) for '{search_string}' in {directory}",
569
582
  message_group=group_id,
570
583
  )
571
-
584
+
572
585
  except subprocess.TimeoutExpired:
573
- emit_error(f"Grep command timed out after 30 seconds", message_group=group_id)
586
+ emit_error("Grep command timed out after 30 seconds", message_group=group_id)
574
587
  except FileNotFoundError:
575
- emit_error(f"ripgrep (rg) not found. Please install ripgrep to use this tool.", message_group=group_id)
588
+ emit_error(
589
+ "ripgrep (rg) not found. Please install ripgrep to use this tool.",
590
+ message_group=group_id,
591
+ )
576
592
  except Exception as e:
577
593
  emit_error(f"Error during grep operation: {e}", message_group=group_id)
578
594
  finally:
579
595
  # Clean up the temporary ignore file
580
596
  if ignore_file and os.path.exists(ignore_file):
581
597
  os.unlink(ignore_file)
582
-
598
+
583
599
  return GrepOutput(matches=matches)
584
600
 
585
601
 
@@ -592,8 +608,8 @@ def register_list_files(agent):
592
608
  context: RunContext, directory: str = ".", recursive: bool = True
593
609
  ) -> ListFileOutput:
594
610
  """List files and directories with intelligent filtering and safety features.
595
-
596
- This function will only allow recursive listing when the allow_recursion
611
+
612
+ This function will only allow recursive listing when the allow_recursion
597
613
  configuration is set to true via the /set allow_recursion=true command.
598
614
 
599
615
  This tool provides comprehensive directory listing with smart home directory
@@ -635,17 +651,21 @@ def register_list_files(agent):
635
651
  - Check for errors in the response
636
652
  - Combine with grep to find specific file patterns
637
653
  """
638
- warning=None
654
+ warning = None
639
655
  if recursive and not get_allow_recursion():
640
656
  warning = "Recursion disabled globally for list_files - returning non-recursive results"
641
657
  recursive = False
642
658
  result = _list_files(context, directory, recursive)
643
-
659
+
644
660
  # Emit the content directly to ensure it's displayed to the user
645
- emit_info(result.content, message_group=generate_group_id("list_files", directory))
646
-
661
+ emit_info(
662
+ result.content, message_group=generate_group_id("list_files", directory)
663
+ )
647
664
  if warning:
648
665
  result.error = warning
666
+ if (len(result.content)) > 200000:
667
+ result.content = result.content[0:200000]
668
+ result.error = "Results truncated. This is a massive directory tree, recommend non-recursive calls to list_files"
649
669
  return result
650
670
 
651
671
 
@@ -716,9 +736,9 @@ def register_grep(agent):
716
736
  ) -> GrepOutput:
717
737
  """Recursively search for text patterns across files using ripgrep (rg).
718
738
 
719
- This tool leverages the high-performance ripgrep utility for fast text
739
+ This tool leverages the high-performance ripgrep utility for fast text
720
740
  searching across directory trees. It searches across all recognized text file
721
- types (Python, JavaScript, HTML, CSS, Markdown, etc.) while automatically
741
+ types (Python, JavaScript, HTML, CSS, Markdown, etc.) while automatically
722
742
  filtering binary files and limiting results for performance.
723
743
 
724
744
  The search_string parameter supports ripgrep's full flag syntax, allowing
code_puppy/tui/app.py CHANGED
@@ -431,7 +431,7 @@ class CodePuppyTUI(App):
431
431
  # Only cancel the agent task if NO processes were killed
432
432
  self._current_worker.cancel()
433
433
  state_management._message_history = prune_interrupted_tool_calls(
434
- state_management._message_history
434
+ state_management.get_message_history()
435
435
  )
436
436
  self.add_system_message("⚠️ Processing cancelled by user")
437
437
  # Stop spinner and clear state only when agent is actually cancelled
@@ -1,4 +1,30 @@
1
1
  {
2
+ "openrouter-sonoma-dusk-alpha": {
3
+ "type": "custom_openai",
4
+ "name": "openrouter/sonoma-dusk-alpha",
5
+ "custom_endpoint": {
6
+ "url": "https://openrouter.ai/api/v1",
7
+ "api_key": "$OPENROUTER_API_KEY",
8
+ "headers": {
9
+ "HTTP-Referer": "https://github.com/mpfaffenberger/code_puppy",
10
+ "X-Title": "Code Puppy"
11
+ }
12
+ },
13
+ "context_length": 2000000
14
+ },
15
+ "openrouter-sonoma-sky-alpha": {
16
+ "type": "custom_openai",
17
+ "name": "openrouter/sonoma-sky-alpha",
18
+ "custom_endpoint": {
19
+ "url": "https://openrouter.ai/api/v1",
20
+ "api_key": "$OPENROUTER_API_KEY",
21
+ "headers": {
22
+ "HTTP-Referer": "https://github.com/mpfaffenberger/code_puppy",
23
+ "X-Title": "Code Puppy"
24
+ }
25
+ },
26
+ "context_length": 2000000
27
+ },
2
28
  "gpt-5": {
3
29
  "type": "openai",
4
30
  "name": "gpt-5",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.154
3
+ Version: 0.0.156
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -10,10 +10,11 @@ License-File: LICENSE
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
15
16
  Classifier: Topic :: Software Development :: Code Generators
16
- Requires-Python: >=3.10
17
+ Requires-Python: >=3.11
17
18
  Requires-Dist: bs4>=0.0.2
18
19
  Requires-Dist: fastapi>=0.110.0
19
20
  Requires-Dist: httpx-limiter>=0.3.0