git-alternative 0.2.2__tar.gz

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 (25) hide show
  1. git_alternative-0.2.2/LICENSE +21 -0
  2. git_alternative-0.2.2/PKG-INFO +36 -0
  3. git_alternative-0.2.2/README.md +6 -0
  4. git_alternative-0.2.2/pyproject.toml +61 -0
  5. git_alternative-0.2.2/src/git_alternative/__init__.py +63 -0
  6. git_alternative-0.2.2/src/git_alternative/ci.py +162 -0
  7. git_alternative-0.2.2/src/git_alternative/cli.py +117 -0
  8. git_alternative-0.2.2/src/git_alternative/client.py +72 -0
  9. git_alternative-0.2.2/src/git_alternative/exceptions.py +43 -0
  10. git_alternative-0.2.2/src/git_alternative/http.py +137 -0
  11. git_alternative-0.2.2/src/git_alternative/managers/__init__.py +1 -0
  12. git_alternative-0.2.2/src/git_alternative/managers/issues.py +292 -0
  13. git_alternative-0.2.2/src/git_alternative/managers/repos.py +218 -0
  14. git_alternative-0.2.2/src/git_alternative/models.py +291 -0
  15. git_alternative-0.2.2/src/git_alternative/pagination.py +45 -0
  16. git_alternative-0.2.2/src/git_alternative/resources/__init__.py +1 -0
  17. git_alternative-0.2.2/src/git_alternative/resources/issues.py +291 -0
  18. git_alternative-0.2.2/src/git_alternative/resources/labels.py +91 -0
  19. git_alternative-0.2.2/src/git_alternative/resources/pipelines.py +118 -0
  20. git_alternative-0.2.2/src/git_alternative/resources/pulls.py +207 -0
  21. git_alternative-0.2.2/src/git_alternative/resources/repos.py +184 -0
  22. git_alternative-0.2.2/src/git_alternative/resources/ssh_keys.py +64 -0
  23. git_alternative-0.2.2/src/git_alternative/ssh_keys.py +80 -0
  24. git_alternative-0.2.2/src/git_alternative/utils.py +91 -0
  25. git_alternative-0.2.2/src/git_alternative/webhooks.py +104 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 AgentSoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-alternative
3
+ Version: 0.2.2
4
+ Summary: A Python SDK and CLI toolkit for Forge — a keyboard-driven, self-hosted Git collaboration platform
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: git,forge,collaboration,devtools,cli
8
+ Author: AgentSoft
9
+ Author-email: agentsoft@example.com
10
+ Requires-Python: >=3.9,<4.0
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.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Software Development :: Version Control
23
+ Requires-Dist: pyyaml (>=6.0,<7.0)
24
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
25
+ Requires-Dist: tomli (>=2.0,<3.0) ; python_version < "3.11"
26
+ Project-URL: Homepage, https://github.com/agentsoft/git-alternative
27
+ Project-URL: Repository, https://github.com/agentsoft/git-alternative
28
+ Description-Content-Type: text/markdown
29
+
30
+ # git-alternative
31
+
32
+ A Python SDK and CLI toolkit for interacting with **Forge** — a keyboard-driven, self-hosted Git collaboration platform. This package provides a clean Python API for managing repositories, issues, pull requests, CI pipelines, and more via the Forge REST API.
33
+
34
+ ## Installation
35
+
36
+
@@ -0,0 +1,6 @@
1
+ # git-alternative
2
+
3
+ A Python SDK and CLI toolkit for interacting with **Forge** — a keyboard-driven, self-hosted Git collaboration platform. This package provides a clean Python API for managing repositories, issues, pull requests, CI pipelines, and more via the Forge REST API.
4
+
5
+ ## Installation
6
+
@@ -0,0 +1,61 @@
1
+ [tool.poetry]
2
+ name = "git-alternative"
3
+ version = "0.2.2"
4
+ description = "A Python SDK and CLI toolkit for Forge — a keyboard-driven, self-hosted Git collaboration platform"
5
+ authors = ["AgentSoft <agentsoft@example.com>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ homepage = "https://github.com/agentsoft/git-alternative"
9
+ repository = "https://github.com/agentsoft/git-alternative"
10
+ keywords = ["git", "forge", "collaboration", "devtools", "cli"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: Software Development :: Version Control",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+ packages = [{include = "git_alternative", from = "src"}]
24
+
25
+ [tool.poetry.dependencies]
26
+ python = "^3.9"
27
+ requests = "^2.31.0"
28
+ pyyaml = "^6.0"
29
+ tomli = {version = "^2.0", python = "<3.11"}
30
+
31
+ [tool.poetry.group.dev.dependencies]
32
+ pytest = "^7.4.0"
33
+ pytest-cov = "^4.1.0"
34
+ ruff = "^0.1.0"
35
+ responses = "^0.24.0"
36
+
37
+ [tool.poetry.scripts]
38
+ forge = "git_alternative.cli:main"
39
+
40
+ [build-system]
41
+ requires = ["poetry-core"]
42
+ build-backend = "poetry.core.masonry.api"
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ addopts = "--cov=git_alternative --cov-report=term-missing --cov-fail-under=80"
47
+
48
+ [tool.coverage.run]
49
+ source = ["src/git_alternative"]
50
+ omit = ["tests/*"]
51
+
52
+ [tool.coverage.report]
53
+ show_missing = true
54
+
55
+ [tool.ruff]
56
+ line-length = 100
57
+ target-version = "py39"
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "I", "N", "W"]
61
+ ignore = ["E501"]
@@ -0,0 +1,63 @@
1
+ """
2
+ git-alternative: Python SDK for the Forge Git collaboration platform.
3
+
4
+ Forge is a keyboard-driven, self-hosted Git forge with full REST API support.
5
+ This package provides a clean Python interface for all Forge operations.
6
+ """
7
+
8
+ from git_alternative.client import ForgeClient
9
+ from git_alternative.exceptions import (
10
+ ForgeAuthError,
11
+ ForgeConflictError,
12
+ ForgeError,
13
+ ForgeNotFoundError,
14
+ ForgeValidationError,
15
+ )
16
+ from git_alternative.models import (
17
+ Branch,
18
+ CIConfig,
19
+ Comment,
20
+ CommitSummary,
21
+ CreateRepoRequest,
22
+ Issue,
23
+ IssueState,
24
+ Label,
25
+ MergeStrategy,
26
+ PaginatedResponse,
27
+ Pipeline,
28
+ PipelineState,
29
+ PipelineTrigger,
30
+ PrState,
31
+ PullRequest,
32
+ Repository,
33
+ SshKey,
34
+ User,
35
+ )
36
+
37
+ __version__ = "0.2.2"
38
+ __all__ = [
39
+ "ForgeClient",
40
+ "ForgeError",
41
+ "ForgeAuthError",
42
+ "ForgeNotFoundError",
43
+ "ForgeConflictError",
44
+ "ForgeValidationError",
45
+ "Repository",
46
+ "CreateRepoRequest",
47
+ "Issue",
48
+ "IssueState",
49
+ "PullRequest",
50
+ "PrState",
51
+ "MergeStrategy",
52
+ "Pipeline",
53
+ "PipelineState",
54
+ "PipelineTrigger",
55
+ "Branch",
56
+ "CommitSummary",
57
+ "PaginatedResponse",
58
+ "Comment",
59
+ "Label",
60
+ "SshKey",
61
+ "User",
62
+ "CIConfig",
63
+ ]
@@ -0,0 +1,162 @@
1
+ """CI pipeline configuration parser for .forge/ci.yml files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+ from git_alternative.exceptions import ForgeValidationError
10
+ from git_alternative.models import CIConfig, CIJobDefinition, CIJobStep, CITrigger
11
+
12
+
13
+ def parse_ci_config(yaml_str: str) -> CIConfig:
14
+ """Parse a ``.forge/ci.yml`` pipeline configuration string.
15
+
16
+ Args:
17
+ yaml_str: YAML content of the CI configuration file.
18
+
19
+ Returns:
20
+ A :class:`~git_alternative.models.CIConfig` instance representing the
21
+ parsed pipeline definition.
22
+
23
+ Raises:
24
+ ForgeValidationError: If the YAML is invalid or required fields are
25
+ missing.
26
+
27
+ Example:
28
+ >>> config = parse_ci_config('''
29
+ ... name: Build and Test
30
+ ... on:
31
+ ... push:
32
+ ... branches: [main]
33
+ ... jobs:
34
+ ... test:
35
+ ... name: Run Tests
36
+ ... runs-on: ubuntu-22.04
37
+ ... steps:
38
+ ... - name: Run tests
39
+ ... run: cargo test
40
+ ... ''')
41
+ >>> config.name
42
+ 'Build and Test'
43
+ >>> "test" in config.jobs
44
+ True
45
+ """
46
+ try:
47
+ data = yaml.safe_load(yaml_str)
48
+ except yaml.YAMLError as exc:
49
+ raise ForgeValidationError(f"Invalid YAML in CI config: {exc}") from exc
50
+
51
+ if not isinstance(data, dict):
52
+ raise ForgeValidationError("CI config must be a YAML mapping")
53
+
54
+ name = data.get("name")
55
+ if not name:
56
+ raise ForgeValidationError("CI config missing required field: 'name'")
57
+
58
+ trigger = _parse_trigger(data.get("on", {}))
59
+ jobs = _parse_jobs(data.get("jobs", {}))
60
+
61
+ return CIConfig(name=str(name), trigger=trigger, jobs=jobs)
62
+
63
+
64
+ def _parse_trigger(on_data: Any) -> CITrigger:
65
+ """Parse the ``on:`` trigger section of a CI config.
66
+
67
+ Args:
68
+ on_data: The value of the ``on`` key in the YAML document.
69
+
70
+ Returns:
71
+ A :class:`~git_alternative.models.CITrigger` instance.
72
+ """
73
+ push_branches: list[str] = []
74
+ pr_branches: list[str] = []
75
+
76
+ if isinstance(on_data, dict):
77
+ push = on_data.get("push", {})
78
+ if isinstance(push, dict):
79
+ push_branches = push.get("branches", [])
80
+
81
+ pr = on_data.get("pull_request", {})
82
+ if isinstance(pr, dict):
83
+ pr_branches = pr.get("branches", [])
84
+
85
+ return CITrigger(push_branches=push_branches, pr_branches=pr_branches)
86
+
87
+
88
+ def _parse_jobs(jobs_data: Any) -> dict[str, CIJobDefinition]:
89
+ """Parse the ``jobs:`` section of a CI config.
90
+
91
+ Args:
92
+ jobs_data: The value of the ``jobs`` key in the YAML document.
93
+
94
+ Returns:
95
+ A mapping of job ID to :class:`~git_alternative.models.CIJobDefinition`.
96
+
97
+ Raises:
98
+ ForgeValidationError: If a job definition is missing required fields.
99
+ """
100
+ if not isinstance(jobs_data, dict):
101
+ return {}
102
+
103
+ result: dict[str, CIJobDefinition] = {}
104
+ for job_id, job_data in jobs_data.items():
105
+ if not isinstance(job_data, dict):
106
+ raise ForgeValidationError(f"Job '{job_id}' must be a mapping")
107
+
108
+ runs_on = job_data.get("runs-on")
109
+ if not runs_on:
110
+ raise ForgeValidationError(f"Job '{job_id}' missing required field: 'runs-on'")
111
+
112
+ job_name = job_data.get("name", job_id)
113
+ needs = job_data.get("needs", [])
114
+ if isinstance(needs, str):
115
+ needs = [needs]
116
+
117
+ steps = _parse_steps(job_id, job_data.get("steps", []))
118
+
119
+ result[job_id] = CIJobDefinition(
120
+ name=str(job_name),
121
+ runs_on=str(runs_on),
122
+ steps=steps,
123
+ needs=list(needs),
124
+ )
125
+
126
+ return result
127
+
128
+
129
+ def _parse_steps(job_id: str, steps_data: Any) -> list[CIJobStep]:
130
+ """Parse the ``steps:`` list within a job definition.
131
+
132
+ Args:
133
+ job_id: The job identifier (used in error messages).
134
+ steps_data: The value of the ``steps`` key for this job.
135
+
136
+ Returns:
137
+ A list of :class:`~git_alternative.models.CIJobStep` instances.
138
+
139
+ Raises:
140
+ ForgeValidationError: If a step is missing a ``name`` field.
141
+ """
142
+ if not isinstance(steps_data, list):
143
+ return []
144
+
145
+ steps: list[CIJobStep] = []
146
+ for i, step in enumerate(steps_data):
147
+ if not isinstance(step, dict):
148
+ raise ForgeValidationError(
149
+ f"Job '{job_id}' step {i} must be a mapping"
150
+ )
151
+ step_name = step.get("name", f"step-{i}")
152
+ with_params = step.get("with", {}) or {}
153
+ steps.append(
154
+ CIJobStep(
155
+ name=str(step_name),
156
+ run=step.get("run"),
157
+ uses=step.get("uses"),
158
+ with_params=dict(with_params),
159
+ )
160
+ )
161
+
162
+ return steps
@@ -0,0 +1,117 @@
1
+ """Minimal CLI entry point for the git-alternative package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+
10
+ def _build_parser() -> argparse.ArgumentParser:
11
+ parser = argparse.ArgumentParser(
12
+ prog="forge",
13
+ description="forge — keyboard-driven Git collaboration platform CLI",
14
+ )
15
+ parser.add_argument("--version", action="version", version="git-alternative 0.2.1")
16
+ parser.add_argument("--host", default=None, help="Forge instance URL")
17
+ parser.add_argument("--token", default=None, help="Personal access token")
18
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
19
+
20
+ sub = parser.add_subparsers(dest="command")
21
+
22
+ # fingerprint sub-command
23
+ fp_parser = sub.add_parser("fingerprint", help="Compute SSH key fingerprint")
24
+ fp_parser.add_argument("key", help="SSH public key string or path to key file")
25
+
26
+ # ci validate sub-command
27
+ ci_parser = sub.add_parser("ci", help="CI configuration utilities")
28
+ ci_sub = ci_parser.add_subparsers(dest="ci_command")
29
+ validate_parser = ci_sub.add_parser("validate", help="Validate a .forge/ci.yml file")
30
+ validate_parser.add_argument("file", help="Path to ci.yml file")
31
+
32
+ return parser
33
+
34
+
35
+ def main(argv: list[str] | None = None) -> int:
36
+ """Entry point for the ``forge`` CLI.
37
+
38
+ Args:
39
+ argv: Argument list (defaults to ``sys.argv[1:]``).
40
+
41
+ Returns:
42
+ Exit code (0 = success, non-zero = error).
43
+ """
44
+ parser = _build_parser()
45
+ args = parser.parse_args(argv)
46
+
47
+ if args.command == "fingerprint":
48
+ return _cmd_fingerprint(args)
49
+ if args.command == "ci":
50
+ return _cmd_ci(args)
51
+
52
+ parser.print_help()
53
+ return 0
54
+
55
+
56
+ def _cmd_fingerprint(args: argparse.Namespace) -> int:
57
+ """Handle the ``forge fingerprint`` sub-command."""
58
+ from git_alternative.utils import compute_key_fingerprint
59
+ from git_alternative.exceptions import ForgeValidationError
60
+
61
+ key_input = args.key
62
+ # If it looks like a file path, try to read it
63
+ if not key_input.startswith("ssh-") and not key_input.startswith("ecdsa-"):
64
+ try:
65
+ with open(key_input) as fh:
66
+ key_input = fh.read().strip()
67
+ except OSError:
68
+ pass # treat as literal key string
69
+
70
+ try:
71
+ fp = compute_key_fingerprint(key_input)
72
+ except ForgeValidationError as exc:
73
+ print(f"Error: {exc}", file=sys.stderr)
74
+ return 1
75
+
76
+ if args.json:
77
+ print(json.dumps({"fingerprint": fp}))
78
+ else:
79
+ print(fp)
80
+ return 0
81
+
82
+
83
+ def _cmd_ci(args: argparse.Namespace) -> int:
84
+ """Handle the ``forge ci`` sub-commands."""
85
+ if args.ci_command == "validate":
86
+ return _cmd_ci_validate(args)
87
+ # No sub-command given
88
+ print("Usage: forge ci <validate>", file=sys.stderr)
89
+ return 1
90
+
91
+
92
+ def _cmd_ci_validate(args: argparse.Namespace) -> int:
93
+ """Handle the ``forge ci validate`` sub-command."""
94
+ from git_alternative.ci import CiConfig
95
+ from git_alternative.exceptions import ForgeValidationError
96
+
97
+ try:
98
+ with open(args.file) as fh:
99
+ content = fh.read()
100
+ except OSError as exc:
101
+ print(f"Error reading file: {exc}", file=sys.stderr)
102
+ return 1
103
+
104
+ try:
105
+ config = CiConfig.from_yaml(content)
106
+ except ForgeValidationError as exc:
107
+ print(f"Invalid CI config: {exc}", file=sys.stderr)
108
+ return 1
109
+
110
+ errors = config.validate()
111
+ if errors:
112
+ for err in errors:
113
+ print(f" - {err}", file=sys.stderr)
114
+ return 1
115
+
116
+ print(f"✓ CI config '{config.name}' is valid ({len(config.jobs)} job(s))")
117
+ return 0
@@ -0,0 +1,72 @@
1
+ """Main ForgeClient entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from git_alternative.http import HttpClient
6
+ from git_alternative.resources.issues import IssuesResource
7
+ from git_alternative.resources.pipelines import PipelinesResource
8
+ from git_alternative.resources.pulls import PullsResource
9
+ from git_alternative.resources.repos import ReposResource
10
+ from git_alternative.resources.users import UsersResource
11
+ from git_alternative.resources.webhooks import WebhooksResource
12
+
13
+
14
+ class ForgeClient:
15
+ """Python SDK client for the Forge Git collaboration platform.
16
+
17
+ Provides access to all Forge API resources through sub-clients.
18
+
19
+ Args:
20
+ host: Base URL of the Forge instance (e.g., ``https://forge.example.com``).
21
+ token: Personal Access Token for authentication. If omitted, only
22
+ public endpoints are accessible.
23
+ timeout: HTTP request timeout in seconds (default: 30).
24
+
25
+ Example:
26
+ >>> client = ForgeClient(
27
+ ... host="https://forge.example.com",
28
+ ... token="pat_your_token",
29
+ ... )
30
+ >>> repos = client.repos.list(owner="alice")
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ host: str,
36
+ token: str | None = None,
37
+ timeout: int = 30,
38
+ ) -> None:
39
+ self._http = HttpClient(host=host, token=token, timeout=timeout)
40
+ self.repos = ReposResource(self._http)
41
+ self.issues = IssuesResource(self._http)
42
+ self.pulls = PullsResource(self._http)
43
+ self.pipelines = PipelinesResource(self._http)
44
+ self.users = UsersResource(self._http)
45
+ self.webhooks = WebhooksResource(self._http)
46
+
47
+ @property
48
+ def host(self) -> str:
49
+ """The base URL of the connected Forge instance."""
50
+ return self._http.host
51
+
52
+ def authenticate(self, username: str, password: str) -> str:
53
+ """Authenticate with username and password, returning a session token.
54
+
55
+ Args:
56
+ username: Forge username.
57
+ password: Account password.
58
+
59
+ Returns:
60
+ A session token string.
61
+
62
+ Raises:
63
+ ForgeAuthError: If credentials are invalid.
64
+ """
65
+ data = self._http.post(
66
+ "/api/v1/auth/login",
67
+ json={"username": username, "password": password},
68
+ )
69
+ token = data.get("token", "")
70
+ self._http.token = token
71
+ self._http._session.headers["Authorization"] = f"Bearer {token}"
72
+ return token
@@ -0,0 +1,43 @@
1
+ """Custom exceptions for the git-alternative SDK."""
2
+
3
+
4
+ class ForgeError(Exception):
5
+ """Base exception for all Forge API errors."""
6
+
7
+ def __init__(self, message: str, status_code: int | None = None) -> None:
8
+ super().__init__(message)
9
+ self.message = message
10
+ self.status_code = status_code
11
+
12
+ def __str__(self) -> str:
13
+ if self.status_code:
14
+ return f"[HTTP {self.status_code}] {self.message}"
15
+ return self.message
16
+
17
+
18
+ class ForgeAuthError(ForgeError):
19
+ """Raised when authentication fails (401 Unauthorized)."""
20
+
21
+
22
+ class ForgeNotFoundError(ForgeError):
23
+ """Raised when a resource is not found (404 Not Found)."""
24
+
25
+
26
+ class ForgeConflictError(ForgeError):
27
+ """Raised when a resource conflict occurs (409 Conflict)."""
28
+
29
+
30
+ class ForgeValidationError(ForgeError):
31
+ """Raised when request validation fails (422 Unprocessable Entity)."""
32
+
33
+
34
+ class ForgeRateLimitError(ForgeError):
35
+ """Raised when the API rate limit is exceeded (429 Too Many Requests)."""
36
+
37
+
38
+ class ForgeServerError(ForgeError):
39
+ """Raised when the server returns a 5xx error."""
40
+
41
+
42
+ class ForgeInvalidSshKeyError(ForgeError):
43
+ """Raised when an SSH public key is malformed or invalid."""