switchforge 1.0.0__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.
@@ -0,0 +1,89 @@
1
+ """Rich-based structured logging with progress bars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
8
+ from rich.table import Table
9
+
10
+ console = Console()
11
+
12
+
13
+ def phase_start(phase: str, title: str) -> None:
14
+ """Log the start of a pipeline phase."""
15
+ console.print(f"\n[bold cyan][{phase}][/bold cyan] {title}...")
16
+
17
+
18
+ def phase_done(phase: str, summary: str) -> None:
19
+ """Log the completion of a pipeline phase."""
20
+ console.print(f" [green]✓[/green] {summary}")
21
+
22
+
23
+ def phase_skip(phase: str, reason: str) -> None:
24
+ """Log a skipped phase."""
25
+ console.print(f" [yellow]⊘[/yellow] Skipped: {reason}")
26
+
27
+
28
+ def phase_error(phase: str, error: str) -> None:
29
+ """Log a phase error."""
30
+ console.print(f" [red]✗[/red] Error: {error}")
31
+
32
+
33
+ def info(msg: str) -> None:
34
+ console.print(f" [dim]{msg}[/dim]")
35
+
36
+
37
+ def warn(msg: str) -> None:
38
+ console.print(f" [yellow]⚠[/yellow] {msg}")
39
+
40
+
41
+ def error(msg: str) -> None:
42
+ console.print(f" [red]✗[/red] {msg}")
43
+
44
+
45
+ def success(msg: str) -> None:
46
+ console.print(f" [green]✓[/green] {msg}")
47
+
48
+
49
+ def banner(title: str, subtitle: str = "") -> None:
50
+ """Display the Forge Core banner."""
51
+ text = f"[bold blue]{title}[/bold blue]"
52
+ if subtitle:
53
+ text += f"\n[dim]{subtitle}[/dim]"
54
+ console.print(Panel(text, border_style="blue", padding=(1, 4)))
55
+
56
+
57
+ def coverage_table(before: float, after: float, tests_before: int, tests_after: int) -> None:
58
+ """Display a before/after coverage comparison table."""
59
+ table = Table(title="Coverage Results", border_style="blue")
60
+ table.add_column("Metric", style="cyan")
61
+ table.add_column("Before", style="red")
62
+ table.add_column("After", style="green")
63
+ table.add_column("Delta", style="yellow")
64
+
65
+ table.add_row(
66
+ "Line Coverage",
67
+ f"{before:.1f}%",
68
+ f"{after:.1f}%",
69
+ f"+{after - before:.1f}%",
70
+ )
71
+ table.add_row(
72
+ "Total Tests",
73
+ str(tests_before),
74
+ str(tests_after),
75
+ f"+{tests_after - tests_before}",
76
+ )
77
+ console.print(table)
78
+
79
+
80
+ def create_progress() -> Progress:
81
+ """Create a Rich progress bar for phase tracking."""
82
+ return Progress(
83
+ SpinnerColumn(),
84
+ TextColumn("[progress.description]{task.description}"),
85
+ BarColumn(),
86
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
87
+ TimeElapsedColumn(),
88
+ console=console,
89
+ )
@@ -0,0 +1,70 @@
1
+ """Upload run reports to the SaaS API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from forge_core.models.config import ForgeConfig
10
+ from forge_core.utils import logger
11
+
12
+
13
+ async def upload_report(config: ForgeConfig, report: dict[str, Any]) -> bool:
14
+ """Upload a run report to the SaaS dashboard.
15
+
16
+ Returns True if upload succeeded, False otherwise.
17
+ Reports are optional — engine works fully offline.
18
+ """
19
+ if not config.auth_token:
20
+ logger.info("No auth token — skipping report upload (offline mode)")
21
+ return False
22
+
23
+ url = f"{config.saas_api_url}/api/v1/runs"
24
+ headers = {
25
+ "Authorization": f"Bearer {config.auth_token}",
26
+ "Content-Type": "application/json",
27
+ }
28
+ payload = {
29
+ "org_id": config.tenant.org_id,
30
+ "user_id": config.tenant.user_id,
31
+ "project_id": config.tenant.project_id,
32
+ "report": report,
33
+ }
34
+
35
+ try:
36
+ async with httpx.AsyncClient(timeout=30) as client:
37
+ resp = await client.post(url, json=payload, headers=headers)
38
+ if resp.status_code == 200:
39
+ data = resp.json()
40
+ report_url = data.get("url", "")
41
+ logger.success(f"Report uploaded: {report_url}")
42
+ return True
43
+ else:
44
+ logger.warn(f"Report upload failed ({resp.status_code}): {resp.text[:200]}")
45
+ return False
46
+ except Exception as e:
47
+ logger.warn(f"Report upload failed (network): {e}")
48
+ return False
49
+
50
+
51
+ async def check_license(config: ForgeConfig) -> dict[str, Any]:
52
+ """Check license and plan limits with the SaaS API.
53
+
54
+ Returns plan info dict or empty dict if offline/failed.
55
+ """
56
+ if not config.auth_token:
57
+ return {}
58
+
59
+ url = f"{config.saas_api_url}/api/v1/license"
60
+ headers = {"Authorization": f"Bearer {config.auth_token}"}
61
+
62
+ try:
63
+ async with httpx.AsyncClient(timeout=10) as client:
64
+ resp = await client.get(url, headers=headers)
65
+ if resp.status_code == 200:
66
+ return resp.json()
67
+ except Exception:
68
+ pass
69
+
70
+ return {}
@@ -0,0 +1,93 @@
1
+ """Subprocess wrapper for running build/test commands safely."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from forge_core.utils import logger
10
+
11
+
12
+ @dataclass
13
+ class ShellResult:
14
+ """Result of a shell command execution."""
15
+
16
+ command: str
17
+ returncode: int
18
+ stdout: str
19
+ stderr: str
20
+ duration_seconds: float = 0.0
21
+
22
+ @property
23
+ def success(self) -> bool:
24
+ return self.returncode == 0
25
+
26
+ @property
27
+ def output(self) -> str:
28
+ return self.stdout + self.stderr
29
+
30
+
31
+ def run(
32
+ command: str,
33
+ cwd: Path | str | None = None,
34
+ timeout: int = 600,
35
+ capture: bool = True,
36
+ env: dict[str, str] | None = None,
37
+ ) -> ShellResult:
38
+ """Run a shell command and return structured result.
39
+
40
+ Args:
41
+ command: Shell command to execute.
42
+ cwd: Working directory.
43
+ timeout: Max seconds before kill (default 10 min).
44
+ capture: Whether to capture stdout/stderr.
45
+ env: Extra environment variables (merged with current env).
46
+ """
47
+ import os
48
+ import time
49
+
50
+ full_env = {**os.environ, **(env or {})}
51
+
52
+ logger.info(f"$ {command}")
53
+
54
+ start = time.time()
55
+ try:
56
+ result = subprocess.run(
57
+ command,
58
+ shell=True,
59
+ cwd=str(cwd) if cwd else None,
60
+ capture_output=capture,
61
+ text=True,
62
+ timeout=timeout,
63
+ env=full_env,
64
+ )
65
+ duration = time.time() - start
66
+
67
+ return ShellResult(
68
+ command=command,
69
+ returncode=result.returncode,
70
+ stdout=result.stdout or "",
71
+ stderr=result.stderr or "",
72
+ duration_seconds=duration,
73
+ )
74
+ except subprocess.TimeoutExpired:
75
+ duration = time.time() - start
76
+ logger.warn(f"Command timed out after {timeout}s: {command}")
77
+ return ShellResult(
78
+ command=command,
79
+ returncode=-1,
80
+ stdout="",
81
+ stderr=f"TIMEOUT after {timeout}s",
82
+ duration_seconds=duration,
83
+ )
84
+ except Exception as e:
85
+ duration = time.time() - start
86
+ logger.error(f"Command failed: {e}")
87
+ return ShellResult(
88
+ command=command,
89
+ returncode=-1,
90
+ stdout="",
91
+ stderr=str(e),
92
+ duration_seconds=duration,
93
+ )
@@ -0,0 +1,67 @@
1
+ """Token counting and prompt size management using tiktoken."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tiktoken
6
+
7
+
8
+ def count_tokens(text: str, model: str = "gpt-4o") -> int:
9
+ """Count tokens in text for a given model."""
10
+ try:
11
+ enc = tiktoken.encoding_for_model(model)
12
+ except KeyError:
13
+ enc = tiktoken.get_encoding("cl100k_base")
14
+ return len(enc.encode(text))
15
+
16
+
17
+ def truncate_to_tokens(text: str, max_tokens: int, model: str = "gpt-4o") -> str:
18
+ """Truncate text to fit within max_tokens."""
19
+ try:
20
+ enc = tiktoken.encoding_for_model(model)
21
+ except KeyError:
22
+ enc = tiktoken.get_encoding("cl100k_base")
23
+
24
+ tokens = enc.encode(text)
25
+ if len(tokens) <= max_tokens:
26
+ return text
27
+ return enc.decode(tokens[:max_tokens])
28
+
29
+
30
+ def fits_in_context(
31
+ system_prompt: str,
32
+ user_content: str,
33
+ model: str = "gpt-4o",
34
+ max_context: int = 128_000,
35
+ reserve_for_response: int = 4096,
36
+ ) -> bool:
37
+ """Check if system + user content fits in the model's context window."""
38
+ total = count_tokens(system_prompt, model) + count_tokens(user_content, model)
39
+ return total <= (max_context - reserve_for_response)
40
+
41
+
42
+ def split_for_context(
43
+ files: dict[str, str],
44
+ max_tokens: int,
45
+ model: str = "gpt-4o",
46
+ ) -> list[dict[str, str]]:
47
+ """Split a dict of {path: content} into batches that fit in context.
48
+
49
+ Returns list of batches, each a dict of {path: content}.
50
+ """
51
+ batches: list[dict[str, str]] = []
52
+ current_batch: dict[str, str] = {}
53
+ current_tokens = 0
54
+
55
+ for path, content in files.items():
56
+ file_tokens = count_tokens(f"--- {path} ---\n{content}\n", model)
57
+ if current_tokens + file_tokens > max_tokens and current_batch:
58
+ batches.append(current_batch)
59
+ current_batch = {}
60
+ current_tokens = 0
61
+ current_batch[path] = content
62
+ current_tokens += file_tokens
63
+
64
+ if current_batch:
65
+ batches.append(current_batch)
66
+
67
+ return batches
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: switchforge
3
+ Version: 1.0.0
4
+ Summary: AI-powered backend test generation engine
5
+ Project-URL: Homepage, https://theswitchcompany.online/products/forge/core
6
+ Project-URL: Repository, https://github.com/switchcompany/forge-core
7
+ Project-URL: Documentation, https://theswitchcompany.online/docs/forge-core
8
+ Author-email: TheSwitchCompany <hello@theswitchcompany.online>
9
+ License-Expression: MIT
10
+ Keywords: ai,backend,code-generation,testing,unit-tests
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: instructor>=1.3.0
22
+ Requires-Dist: litellm>=1.40.0
23
+ Requires-Dist: pydantic>=2.7.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: rich>=13.7.0
26
+ Requires-Dist: tiktoken>=0.7.0
27
+ Requires-Dist: typer>=0.12.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pyinstaller>=6.0; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.5.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Switchforge (Forge Core CLI)
36
+
37
+ AI-powered backend test generation engine by [TheSwitchCompany](https://theswitchcompany.online).
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install switchforge
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ switchforge login --token YOUR_API_TOKEN
49
+ switchforge init
50
+ switchforge run . --target 90
51
+ ```
52
+
53
+ ### Windows Users
54
+
55
+ If `switchforge` is not recognized, use `python -m forge_core`:
56
+
57
+ ```powershell
58
+ python -m forge_core login --token YOUR_API_TOKEN
59
+ python -m forge_core init
60
+ python -m forge_core run . --target 90
61
+ ```
62
+
63
+ ## Documentation
64
+
65
+ [theswitchcompany.online/docs/forge-core](https://theswitchcompany.online/docs/forge-core)
@@ -0,0 +1,39 @@
1
+ forge_core/__init__.py,sha256=M73aKzgrfrqVz1KEj2PCvF2he8e-pPA-CisRqUiqrTc,87
2
+ forge_core/__main__.py,sha256=Bgjh1sswksrd2aWTaOCPlGUGKh2M49VQ_QEB28ysRfw,116
3
+ forge_core/auth.py,sha256=HarMTW4dwSr-7MnJ2Fa-wYPaY2VlXMP_7Q_G6LW3syA,2391
4
+ forge_core/cli.py,sha256=Rqx52EKLpqaN2t4PRa7x-1-Idm9-aq7OU4nSu3-Dcxc,6405
5
+ forge_core/config.py,sha256=SjnHrXiuPoF-2Zl4lqpdym-6jC-53iRzUEY3GE7LyoQ,3637
6
+ forge_core/orchestrator.py,sha256=SlXuzoAXzllxu9BM9cWfk3HglIpAe2bLXG7M-dOXSTI,9048
7
+ forge_core/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ forge_core/ai/prompts.py,sha256=1_MCoN_xDlULDEz_7uUasxaYbRSTb1pi2alk469KNQw,2733
9
+ forge_core/ai/provider.py,sha256=XQSCFdF9Md5p6r69vzyI7hCY4BtlfXKKf3wIPDhPoIM,3705
10
+ forge_core/ai/structured.py,sha256=EaS1Kig4XTofSWjQrm52HZRUzWtKbrbr8_npGk4GKOQ,1930
11
+ forge_core/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ forge_core/core/agent_manager.py,sha256=gMk8NqgYfSBk1b2-ok_nsdr1Rznlgh_pUXUJwCHGz_A,3718
13
+ forge_core/core/coverage.py,sha256=zmQ4x-QSEQVEpaiMFMoWWs4iuZaEwxSxPcxrdj2WCPw,8488
14
+ forge_core/core/file_manager.py,sha256=2JUpLYkgonkCa1pYtI0ZASxtEZ0tnQxpMN8eQuqpAC8,3820
15
+ forge_core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ forge_core/models/config.py,sha256=JZ4oGzkHfdB91XaGe_fb0DQhx2iU3Y5gIcWkXDLTtzE,2832
17
+ forge_core/models/dto.py,sha256=ooVsfeNtsxsfyjl8pg6xShChE9oDvp0rzc1FNXyLEuo,1808
18
+ forge_core/models/project.py,sha256=w1TkrqHkaM_XGtlQVcu2A0WDRna9dod3QPohAPgIAr0,1895
19
+ forge_core/models/test_result.py,sha256=YOYW9iKIqFz1HQWa084KDOJcwsipvl4MXoRsHrsagU4,2381
20
+ forge_core/phases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ forge_core/phases/analyze_project.py,sha256=GAVWGdpHsrh4RViZ67sSaQ7N2-zCwgIssTVRg-2EwUU,5412
22
+ forge_core/phases/audit_tests.py,sha256=zzWV7x9Q3S1iZ6szju94BhZCnZXAWiUASowYZYnPbaQ,1082
23
+ forge_core/phases/compile_fix.py,sha256=DQgy-yhUybddLJhut6G5FLicrrWyD8qzTXve8E0-isQ,2499
24
+ forge_core/phases/coverage_report.py,sha256=KveXSwoVQb-KokDnJS1EGxNidwOmyPNWv9i75b2ZJUw,648
25
+ forge_core/phases/detect_stack.py,sha256=vbJqxwCw1sT7pryRpP0iKd9bpPH8pQcU-2rLceTPa8E,2128
26
+ forge_core/phases/exclusion_scan.py,sha256=Z3vlwbaXpmlsWKJ9FS4d_d-WcuPM5eaAE05eplCQJSc,2636
27
+ forge_core/phases/fix_broken.py,sha256=NcovciAHgRnkV9RLcO7V2uS1CPK43liTlJkKJbFywlQ,2676
28
+ forge_core/phases/generate_tests.py,sha256=lWA0rozpU6nbeMV-ag2coEO-Nu7LPJDmPK75CGcYww8,7782
29
+ forge_core/phases/journey_mapping.py,sha256=BmBA3yYRaWUNgOsqdMvQtZ41W_Te0FKTgX9G5TebqcU,4299
30
+ forge_core/phases/self_learn.py,sha256=HK_CagBIRjn9eeWpr5bUCvfN29HRD5E7CjblUyXVXZk,3317
31
+ forge_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ forge_core/utils/logger.py,sha256=b_PaZgxdERw0lAaWLFsnrnfffrCSUhn4UpESj97crAs,2533
33
+ forge_core/utils/reporter.py,sha256=yhzaMoByj9tM0RAHefcv8sPcEWbTESiev98usm9zgzk,2145
34
+ forge_core/utils/shell.py,sha256=mRn2tNMRtdOxXkst6DWasVBd0pRVGJmaqKukU-QHd_c,2367
35
+ forge_core/utils/tokens.py,sha256=YQZ4AOkuOwUYghFZicGC6isnLdyro-3j3tDNEkI3lM0,1997
36
+ switchforge-1.0.0.dist-info/METADATA,sha256=WvYw-iz4eBiQ_Kh4ZlXQgdb1bK18fjTOP_Rf4NXpQSE,1995
37
+ switchforge-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
+ switchforge-1.0.0.dist-info/entry_points.txt,sha256=PwgdQo8NrcBuvyW3DlkfyF2Wa0XXNkHyir20DsD86dY,51
39
+ switchforge-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ switchforge = forge_core.cli:app