optforge 0.2.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.
Files changed (45) hide show
  1. optforge/__init__.py +17 -0
  2. optforge/__main__.py +6 -0
  3. optforge/cli.py +56 -0
  4. optforge/core/__init__.py +24 -0
  5. optforge/core/cli.py +56 -0
  6. optforge/core/console.py +83 -0
  7. optforge/core/errors.py +28 -0
  8. optforge/core/formatting.py +26 -0
  9. optforge/core/net.py +87 -0
  10. optforge/core/processes.py +38 -0
  11. optforge/core/redaction.py +41 -0
  12. optforge/core/version.py +15 -0
  13. optforge/diskcheck/__init__.py +6 -0
  14. optforge/diskcheck/cli.py +113 -0
  15. optforge/diskcheck/models.py +31 -0
  16. optforge/diskcheck/service.py +56 -0
  17. optforge/envcheck/__init__.py +6 -0
  18. optforge/envcheck/cli.py +102 -0
  19. optforge/envcheck/models.py +33 -0
  20. optforge/envcheck/service.py +66 -0
  21. optforge/killNotif/cli.py +110 -0
  22. optforge/killNotif/core.py +24 -0
  23. optforge/killNotif/job.ps1 +52 -0
  24. optforge/portcheck/__init__.py +14 -0
  25. optforge/portcheck/binder.py +72 -0
  26. optforge/portcheck/cli.py +248 -0
  27. optforge/portcheck/connector.py +60 -0
  28. optforge/portcheck/models.py +117 -0
  29. optforge/portcheck/scanner.py +76 -0
  30. optforge/portcheck/service.py +109 -0
  31. optforge/portcheck/waiter.py +54 -0
  32. optforge/pycacheClean/__init__.py +6 -0
  33. optforge/pycacheClean/cleaner.py +43 -0
  34. optforge/pycacheClean/cli.py +127 -0
  35. optforge/pycacheClean/models.py +55 -0
  36. optforge/pycacheClean/scanner.py +122 -0
  37. optforge/pycacheClean/service.py +79 -0
  38. optforge/volume/__init__.py +7 -0
  39. optforge/volume/cli.py +224 -0
  40. optforge/volume/core.py +280 -0
  41. optforge-0.2.0.dist-info/METADATA +322 -0
  42. optforge-0.2.0.dist-info/RECORD +45 -0
  43. optforge-0.2.0.dist-info/WHEEL +4 -0
  44. optforge-0.2.0.dist-info/entry_points.txt +9 -0
  45. optforge-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,56 @@
1
+ """Disk usage collection for a path or every mounted volume."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from collections.abc import Callable, Iterable
8
+ from typing import Any
9
+
10
+ import psutil
11
+
12
+ from optforge.core.errors import OperationFailed, OptForgeError
13
+ from optforge.diskcheck.models import DiskInfo
14
+
15
+ UsageReader = Callable[[str], tuple[int, int, int]]
16
+ PartitionReader = Callable[..., Iterable[Any]]
17
+
18
+
19
+ class DiskService:
20
+ """Read usage without crashing on one unreadable mount."""
21
+
22
+ def __init__(
23
+ self,
24
+ usage: UsageReader = shutil.disk_usage,
25
+ partitions: PartitionReader = psutil.disk_partitions,
26
+ ) -> None:
27
+ self._usage = usage
28
+ self._partitions = partitions
29
+
30
+ def usage(self, path: str = ".") -> DiskInfo:
31
+ """Return usage for ``path``."""
32
+ try:
33
+ total, used, free = self._usage(path)
34
+ except OSError as exc:
35
+ raise OperationFailed(f"Could not read disk usage for {path!r}: {exc}") from exc
36
+ percent_used = (used / total * 100.0) if total else 0.0
37
+ return DiskInfo(
38
+ path=os.path.abspath(path),
39
+ total=int(total),
40
+ used=int(used),
41
+ free=int(free),
42
+ percent_used=float(percent_used),
43
+ )
44
+
45
+ def volumes(self) -> list[DiskInfo]:
46
+ """Return usage for every mounted partition that can be read."""
47
+ results: list[DiskInfo] = []
48
+ for partition in self._partitions(all=True):
49
+ mountpoint = getattr(partition, "mountpoint", None)
50
+ if not mountpoint:
51
+ continue
52
+ try:
53
+ results.append(self.usage(mountpoint))
54
+ except OptForgeError:
55
+ continue
56
+ return results
@@ -0,0 +1,6 @@
1
+ """Inspect environment variables without leaking secrets."""
2
+
3
+ from optforge.envcheck.models import EnvEntry, OutputFormat
4
+ from optforge.envcheck.service import EnvironmentService
5
+
6
+ __all__ = ["EnvEntry", "EnvironmentService", "OutputFormat"]
@@ -0,0 +1,102 @@
1
+ """``envcheck``: inspect environment variables without leaking secrets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.text import Text
9
+
10
+ from optforge.core.cli import ErrorBoundary
11
+ from optforge.core.console import ConsoleIO
12
+ from optforge.core.version import get_version
13
+ from optforge.envcheck.models import EnvEntry, OutputFormat
14
+ from optforge.envcheck.service import EnvironmentService
15
+
16
+ app = typer.Typer(
17
+ name="envcheck",
18
+ help="Inspect environment variables and runtime configuration. Secrets are redacted by default.",
19
+ no_args_is_help=False,
20
+ rich_markup_mode="rich",
21
+ context_settings={"help_option_names": ["-h", "--help"]},
22
+ )
23
+ console = ConsoleIO()
24
+
25
+
26
+ def _service() -> EnvironmentService:
27
+ return EnvironmentService()
28
+
29
+
30
+ def _version_callback(value: bool) -> None:
31
+ if value:
32
+ typer.echo(f"envcheck (optforge {get_version()})")
33
+ raise typer.Exit()
34
+
35
+
36
+ def _render_table(entries: list[EnvEntry]) -> None:
37
+ table = console.table(
38
+ "Environment variables",
39
+ [("Key", {"style": "cyan", "no_wrap": True}), ("Value", {})],
40
+ )
41
+ for entry in entries:
42
+ value = Text(entry.value, style="yellow" if entry.redacted else "")
43
+ table.add_row(console.cell(entry.key), value)
44
+ console.print(table)
45
+ if any(entry.redacted for entry in entries):
46
+ console.hint("Sensitive values are masked. Pass --show-secrets to reveal them.")
47
+
48
+
49
+ @app.callback(invoke_without_command=True)
50
+ def main(
51
+ prefix: Annotated[
52
+ str | None,
53
+ typer.Option("--prefix", "-p", help="Only show variables starting with this prefix."),
54
+ ] = None,
55
+ include_hidden: Annotated[
56
+ bool,
57
+ typer.Option(
58
+ "--include-hidden/--no-include-hidden",
59
+ help="Include variables whose names begin with an underscore.",
60
+ ),
61
+ ] = True,
62
+ show_secrets: Annotated[
63
+ bool,
64
+ typer.Option("--show-secrets", help="Reveal values that look sensitive."),
65
+ ] = False,
66
+ output: Annotated[
67
+ OutputFormat,
68
+ typer.Option("--format", "-f", case_sensitive=False, help="Output format."),
69
+ ] = OutputFormat.table,
70
+ as_json: Annotated[
71
+ bool, typer.Option("--json", help="Alias for --format json.")
72
+ ] = False,
73
+ version: Annotated[
74
+ bool | None,
75
+ typer.Option(
76
+ "--version",
77
+ "-V",
78
+ callback=_version_callback,
79
+ is_eager=True,
80
+ help="Show the version and exit.",
81
+ ),
82
+ ] = None,
83
+ ) -> None:
84
+ """List environment variables, redacting secrets by default."""
85
+ with ErrorBoundary(console):
86
+ entries = _service().entries(
87
+ prefix=prefix,
88
+ include_hidden=include_hidden,
89
+ redact=not show_secrets,
90
+ )
91
+
92
+ chosen = OutputFormat.json if as_json else output
93
+ if chosen is OutputFormat.json:
94
+ console.json([entry.to_dict() for entry in entries])
95
+ elif chosen is OutputFormat.dotenv:
96
+ console.text(_service().to_dotenv(entries))
97
+ else:
98
+ _render_table(entries)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ app()
@@ -0,0 +1,33 @@
1
+ """Value objects for environment inspection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+
10
+ class OutputFormat(str, Enum):
11
+ """Supported ``envcheck`` output formats."""
12
+
13
+ table = "table"
14
+ json = "json"
15
+ dotenv = "dotenv"
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class EnvEntry:
20
+ """One environment variable, optionally redacted."""
21
+
22
+ key: str
23
+ value: str
24
+ sensitive: bool = False
25
+ redacted: bool = False
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ return {
29
+ "key": self.key,
30
+ "value": self.value,
31
+ "sensitive": self.sensitive,
32
+ "redacted": self.redacted,
33
+ }
@@ -0,0 +1,66 @@
1
+ """Environment variable collection and redaction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Mapping
7
+
8
+ from optforge.core.redaction import SecretRedactor
9
+ from optforge.envcheck.models import EnvEntry
10
+
11
+ _QUOTE_TRIGGERS = frozenset(' \t\n#"\'')
12
+
13
+
14
+ class EnvironmentService:
15
+ """Read ``os.environ`` and decide what is safe to display."""
16
+
17
+ def __init__(
18
+ self,
19
+ environ: Mapping[str, str] | None = None,
20
+ redactor: SecretRedactor | None = None,
21
+ ) -> None:
22
+ self._environ = environ if environ is not None else os.environ
23
+ self._redactor = redactor or SecretRedactor()
24
+
25
+ def entries(
26
+ self,
27
+ *,
28
+ prefix: str | None = None,
29
+ include_hidden: bool = True,
30
+ redact: bool = True,
31
+ ) -> list[EnvEntry]:
32
+ """Return sorted entries, masking sensitive values by default."""
33
+ needle = prefix.lower() if prefix else None
34
+ entries: list[EnvEntry] = []
35
+ for key in sorted(self._environ):
36
+ if needle is not None and not key.lower().startswith(needle):
37
+ continue
38
+ if not include_hidden and key.startswith("_"):
39
+ continue
40
+ raw = self._environ[key]
41
+ sensitive = self._redactor.is_sensitive(key)
42
+ value = raw
43
+ redacted = False
44
+ if redact and sensitive:
45
+ value, redacted = self._redactor.redact(key, raw)
46
+ entries.append(
47
+ EnvEntry(key=key, value=value, sensitive=sensitive, redacted=redacted)
48
+ )
49
+ return entries
50
+
51
+ @staticmethod
52
+ def to_dotenv(entries: list[EnvEntry]) -> str:
53
+ """Render entries as a ``.env`` fragment."""
54
+ return "\n".join(_dotenv_line(entry) for entry in entries)
55
+
56
+
57
+ def _dotenv_line(entry: EnvEntry) -> str:
58
+ if not entry.value or not (set(entry.value) & _QUOTE_TRIGGERS):
59
+ return f"{entry.key}={entry.value}"
60
+ escaped = (
61
+ entry.value.replace("\\", "\\\\")
62
+ .replace('"', '\\"')
63
+ .replace("\n", "\\n")
64
+ .replace("\t", "\\t")
65
+ )
66
+ return f'{entry.key}="{escaped}"'
@@ -0,0 +1,110 @@
1
+ """``killNotif``: clear Windows toast notifications.
2
+
3
+ Exit codes
4
+ 0 the command ran successfully (even when nothing needed clearing)
5
+ 2 invalid usage (raised by Typer)
6
+ 3 the operation could not run (wrong OS, missing helper, PowerShell error)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from typing import Annotated
13
+
14
+ import typer
15
+ from rich.text import Text
16
+
17
+ from optforge.core.cli import ErrorBoundary
18
+ from optforge.core.console import ConsoleIO
19
+ from optforge.core.errors import OperationFailed
20
+ from optforge.core.version import get_version
21
+
22
+ app = typer.Typer(
23
+ name="killNotif",
24
+ help="Clear Windows toast notifications, optionally for a single app.",
25
+ epilog="Exit codes: 0 ok | 2 bad usage | 3 could not run",
26
+ no_args_is_help=False,
27
+ rich_markup_mode="rich",
28
+ context_settings={"help_option_names": ["-h", "--help"]},
29
+ )
30
+ console = ConsoleIO()
31
+
32
+ AumidOpt = Annotated[
33
+ str | None,
34
+ typer.Option(
35
+ "--aumid",
36
+ "-a",
37
+ metavar="AUMID",
38
+ help="Only clear notifications from this AppUserModelID.",
39
+ ),
40
+ ]
41
+ QuietOpt = Annotated[
42
+ bool, typer.Option("--quiet", "-q", help="Print nothing; use the exit code only.")
43
+ ]
44
+ JsonOpt = Annotated[bool, typer.Option("--json", help="Print machine-readable JSON.")]
45
+
46
+
47
+ def _version_callback(value: bool) -> None:
48
+ if value:
49
+ typer.echo(f"killNotif (optforge {get_version()})")
50
+ raise typer.Exit()
51
+
52
+
53
+ def _clear(aumid: str | None) -> int:
54
+ """Import lazily so ``--help`` and ``--version`` stay fast."""
55
+ from optforge.killNotif.core import clear_notifications
56
+
57
+ return clear_notifications(aumid)
58
+
59
+
60
+ def _run(aumid: str | None) -> int:
61
+ if sys.platform != "win32":
62
+ raise OperationFailed("killNotif requires Windows 10/11 (WinRT toast history).")
63
+
64
+ try:
65
+ return _clear(aumid)
66
+ except FileNotFoundError as exc:
67
+ raise OperationFailed(f"{exc} Ensure PowerShell 5.1+ is installed.") from exc
68
+ except RuntimeError as exc:
69
+ raise OperationFailed(str(exc)) from exc
70
+ except ValueError as exc:
71
+ raise OperationFailed(f"unexpected helper output: {exc}") from exc
72
+
73
+
74
+ @app.command()
75
+ def main(
76
+ aumid: AumidOpt = None,
77
+ json_output: JsonOpt = False,
78
+ quiet: QuietOpt = False,
79
+ version: Annotated[
80
+ bool | None,
81
+ typer.Option(
82
+ "--version",
83
+ "-V",
84
+ callback=_version_callback,
85
+ is_eager=True,
86
+ help="Show the version and exit.",
87
+ ),
88
+ ] = None,
89
+ ) -> None:
90
+ """Clear toast notifications from the Windows Action Center."""
91
+ target = aumid.strip() if aumid else None
92
+
93
+ with ErrorBoundary(console):
94
+ cleared = _run(target)
95
+
96
+ if quiet:
97
+ return
98
+ if json_output:
99
+ console.json({"aumid": target, "cleared": cleared})
100
+ return
101
+ if cleared == 0:
102
+ console.print(Text("No notifications to clear.", style="dim"))
103
+ elif target:
104
+ console.print(Text(f"Cleared {cleared} sender(s) matching {target}.", style="green"))
105
+ else:
106
+ console.print(Text(f"Cleared {cleared} sender(s).", style="green"))
107
+
108
+
109
+ if __name__ == "__main__":
110
+ app()
@@ -0,0 +1,24 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ JOB = Path(__file__).with_name("job.ps1")
5
+
6
+
7
+ def clear_notifications(aumid: str | None = None) -> int:
8
+ """Clear all notifications (or one sender's). Returns senders cleared."""
9
+ if not JOB.exists():
10
+ raise FileNotFoundError(f"helper script missing: {JOB}")
11
+
12
+ cmd = [
13
+ "powershell", "-NoProfile", "-NonInteractive",
14
+ "-ExecutionPolicy", "Bypass", "-File", str(JOB),
15
+ ]
16
+ if aumid:
17
+ cmd += ["-Aumid", aumid]
18
+
19
+ res = subprocess.run(cmd, capture_output=True, text=True)
20
+ if res.returncode != 0:
21
+ raise RuntimeError((res.stderr or res.stdout).strip() or "job.ps1 failed")
22
+
23
+ out = res.stdout.strip().splitlines()[-1] if res.stdout.strip() else "0"
24
+ return int(out)
@@ -0,0 +1,52 @@
1
+ #Requires -Version 5.1
2
+ [CmdletBinding()]
3
+ param(
4
+ [string]$Aumid # optional: clear only this sender; empty = all
5
+ )
6
+
7
+ $ErrorActionPreference = 'Stop'
8
+
9
+ try {
10
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null
11
+ } catch {
12
+ Write-Error "WinRT notification API unavailable (not Windows 10/11?)"
13
+ exit 2
14
+ }
15
+
16
+ $history = [Windows.UI.Notifications.ToastNotificationManager]::History
17
+
18
+ if ($Aumid) {
19
+ $targets = @($Aumid)
20
+ } else {
21
+ $key = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings'
22
+ if (-not (Test-Path $key)) {
23
+ Write-Output 0
24
+ exit 0
25
+ }
26
+
27
+ $targets = @(
28
+ Get-ChildItem -Path $key -ErrorAction Stop |
29
+ ForEach-Object { $_.PSChildName }
30
+ )
31
+ }
32
+
33
+ $cleared = 0
34
+ foreach ($target in $targets) {
35
+ if ([string]::IsNullOrWhiteSpace($target)) {
36
+ continue
37
+ }
38
+
39
+ try {
40
+ $items = $history.GetHistory($target)
41
+ if (@($items).Count -eq 0) {
42
+ continue
43
+ }
44
+
45
+ $history.Clear($target)
46
+ $cleared++
47
+ } catch {
48
+ Write-Warning "Could not clear notifications for '$target': $($_.Exception.Message)"
49
+ }
50
+ }
51
+
52
+ Write-Output $cleared
@@ -0,0 +1,14 @@
1
+ """Inspect listening TCP ports and test connectivity."""
2
+
3
+ from optforge.portcheck.models import CheckResult, ConnectResult, Listener, PortState, SortKey, WaitResult
4
+ from optforge.portcheck.service import PortService
5
+
6
+ __all__ = [
7
+ "CheckResult",
8
+ "ConnectResult",
9
+ "Listener",
10
+ "PortService",
11
+ "PortState",
12
+ "SortKey",
13
+ "WaitResult",
14
+ ]
@@ -0,0 +1,72 @@
1
+ """Bind probes that answer "can this host:port be bound right now?"."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import socket
7
+
8
+ from optforge.core.errors import OperationFailed
9
+ from optforge.core.net import Endpoint, resolve_endpoints
10
+
11
+ _WILDCARD_HOSTS = frozenset({"", "*", "any", "all"})
12
+
13
+
14
+ class PortBinder:
15
+ """Test bindability without leaving a socket behind.
16
+
17
+ Notes:
18
+ * POSIX sets ``SO_REUSEADDR`` so a socket in ``TIME_WAIT`` does not make
19
+ a genuinely free port look busy.
20
+ * Windows sets ``SO_EXCLUSIVEADDRUSE`` instead; ``SO_REUSEADDR`` on
21
+ Windows lets two processes bind the same port, which is exactly the
22
+ bug this tool exists to detect.
23
+ * A successful probe is inherently racy: another process may take the
24
+ port after the probe closes. Callers must treat the answer as a
25
+ snapshot.
26
+ """
27
+
28
+ def __init__(self, default_host: str = "0.0.0.0") -> None:
29
+ self._default_host = default_host
30
+
31
+ def is_free(self, port: int, host: str | None = None) -> bool:
32
+ """Return ``True`` only if every resolved address for the host binds."""
33
+ endpoints = self._endpoints(port, host or self._default_host)
34
+ if not endpoints:
35
+ return True
36
+ return all(self._can_bind(endpoint) for endpoint in endpoints)
37
+
38
+ def is_free_on_all_interfaces(self, port: int) -> bool:
39
+ """Return ``True`` if both the IPv4 and IPv6 wildcard addresses bind."""
40
+ return self.is_free(port, "0.0.0.0") and self.is_free(port, "::")
41
+
42
+ def _endpoints(self, port: int, host: str) -> list[Endpoint]:
43
+ target = None if host.lower() in _WILDCARD_HOSTS else host
44
+ try:
45
+ return resolve_endpoints(target or "", port, passive=True)
46
+ except OperationFailed:
47
+ # No address of this family exists on the machine (for example IPv6
48
+ # is disabled). There is nothing to bind, so it cannot be occupied.
49
+ return []
50
+
51
+ def _can_bind(self, endpoint: Endpoint) -> bool:
52
+ try:
53
+ sock = socket.socket(endpoint.family, socket.SOCK_STREAM)
54
+ except OSError:
55
+ return False
56
+ try:
57
+ self._configure(sock)
58
+ sock.bind(endpoint.sockaddr)
59
+ return True
60
+ except OSError:
61
+ return False
62
+ finally:
63
+ sock.close()
64
+
65
+ @staticmethod
66
+ def _configure(sock: socket.socket) -> None:
67
+ if os.name == "nt":
68
+ exclusive = getattr(socket, "SO_EXCLUSIVEADDRUSE", None)
69
+ if exclusive is not None:
70
+ sock.setsockopt(socket.SOL_SOCKET, exclusive, 1)
71
+ return
72
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)