git-alternative 0.2.2__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,80 @@
1
+ """SSH key utilities for the Forge platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+
8
+ from git_alternative.exceptions import ForgeInvalidSshKeyError
9
+
10
+
11
+ def compute_key_fingerprint(public_key_str: str) -> str:
12
+ """Compute the SHA256 fingerprint of an SSH public key.
13
+
14
+ Parses the key material (ignoring key type prefix variations and comment
15
+ fields) and returns the fingerprint in OpenSSH format: ``SHA256:<base64>``.
16
+
17
+ Args:
18
+ public_key_str: The full SSH public key string, e.g.
19
+ ``"ssh-ed25519 AAAA... optional-comment"``.
20
+
21
+ Returns:
22
+ The fingerprint string in the format ``"SHA256:<base64>"``.
23
+
24
+ Raises:
25
+ ForgeInvalidSshKeyError: If the key is malformed or contains invalid
26
+ base64 data.
27
+
28
+ Example:
29
+ >>> fp = compute_key_fingerprint("ssh-ed25519 AAAA... my-laptop")
30
+ >>> fp.startswith("SHA256:")
31
+ True
32
+ """
33
+ parts = public_key_str.strip().split()
34
+ if len(parts) < 2:
35
+ raise ForgeInvalidSshKeyError("Malformed public key: expected at least 2 fields")
36
+
37
+ key_type = parts[0]
38
+ valid_key_types = {
39
+ "ssh-rsa",
40
+ "ssh-dss",
41
+ "ssh-ed25519",
42
+ "ecdsa-sha2-nistp256",
43
+ "ecdsa-sha2-nistp384",
44
+ "ecdsa-sha2-nistp521",
45
+ "sk-ssh-ed25519@openssh.com",
46
+ "sk-ecdsa-sha2-nistp256@openssh.com",
47
+ }
48
+ if key_type not in valid_key_types:
49
+ raise ForgeInvalidSshKeyError(f"Unsupported key type: {key_type!r}")
50
+
51
+ try:
52
+ key_bytes = base64.b64decode(parts[1])
53
+ except Exception as exc:
54
+ raise ForgeInvalidSshKeyError(f"Invalid base64 in key: {exc}") from exc
55
+
56
+ digest = hashlib.sha256(key_bytes).digest()
57
+ # OpenSSH omits padding in the fingerprint base64
58
+ b64 = base64.b64encode(digest).decode().rstrip("=")
59
+ return f"SHA256:{b64}"
60
+
61
+
62
+ def normalize_public_key(public_key_str: str) -> str:
63
+ """Return the canonical form of an SSH public key (type + base64 material only).
64
+
65
+ Strips the optional comment field so that keys with different comments but
66
+ the same key material are treated as identical.
67
+
68
+ Args:
69
+ public_key_str: The full SSH public key string.
70
+
71
+ Returns:
72
+ Normalized key string: ``"<type> <base64>"``.
73
+
74
+ Raises:
75
+ ForgeInvalidSshKeyError: If the key is malformed.
76
+ """
77
+ parts = public_key_str.strip().split()
78
+ if len(parts) < 2:
79
+ raise ForgeInvalidSshKeyError("Malformed public key: expected at least 2 fields")
80
+ return f"{parts[0]} {parts[1]}"
@@ -0,0 +1,91 @@
1
+ """Utility functions for the git-alternative SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+
8
+ from git_alternative.exceptions import ForgeSshKeyError
9
+
10
+
11
+ def compute_key_fingerprint(public_key_str: str) -> str:
12
+ """Compute the SHA-256 fingerprint of an SSH public key.
13
+
14
+ Parses the key material from a standard OpenSSH public key string,
15
+ ignoring the key type prefix and optional comment field. The fingerprint
16
+ is returned in OpenSSH format: ``SHA256:<base64>``.
17
+
18
+ Args:
19
+ public_key_str: An OpenSSH public key string, e.g.
20
+ ``ssh-rsa AAAA... optional-comment``
21
+
22
+ Returns:
23
+ The SHA-256 fingerprint in OpenSSH format, e.g.
24
+ ``SHA256:abc123...``
25
+
26
+ Raises:
27
+ ForgeSshKeyError: If the key string is malformed or contains
28
+ invalid base64 data.
29
+
30
+ Example:
31
+ >>> fingerprint = compute_key_fingerprint("ssh-ed25519 AAAA... mykey")
32
+ >>> fingerprint.startswith("SHA256:")
33
+ True
34
+ """
35
+ parts = public_key_str.strip().split()
36
+ if len(parts) < 2:
37
+ raise ForgeSshKeyError("Malformed public key: expected at least 2 space-separated parts")
38
+
39
+ key_b64 = parts[1]
40
+ try:
41
+ key_bytes = base64.b64decode(key_b64)
42
+ except Exception as exc:
43
+ raise ForgeSshKeyError(f"Invalid base64 in SSH key: {exc}") from exc
44
+
45
+ digest = hashlib.sha256(key_bytes).digest()
46
+ encoded = base64.b64encode(digest).decode("ascii")
47
+ return f"SHA256:{encoded}"
48
+
49
+
50
+ def next_retry_delay(attempt: int, base_delay_secs: int = 10, max_delay_secs: int = 86400) -> int:
51
+ """Compute exponential backoff delay for webhook retries.
52
+
53
+ Uses the formula ``base_delay * 10^attempt``, capped at ``max_delay_secs``.
54
+
55
+ Args:
56
+ attempt: Zero-based attempt number (0 = first retry).
57
+ base_delay_secs: Base delay in seconds (default: 10).
58
+ max_delay_secs: Maximum delay in seconds (default: 86400 = 24 hours).
59
+
60
+ Returns:
61
+ Delay in seconds before the next retry attempt.
62
+
63
+ Example:
64
+ >>> next_retry_delay(0) # 10s
65
+ 10
66
+ >>> next_retry_delay(1) # 100s
67
+ 100
68
+ >>> next_retry_delay(5) # capped at 86400s
69
+ 86400
70
+ """
71
+ delay = base_delay_secs * (10**attempt)
72
+ return min(delay, max_delay_secs)
73
+
74
+
75
+ def build_url(host: str, *path_parts: str) -> str:
76
+ """Build a full URL from a host and path parts.
77
+
78
+ Args:
79
+ host: Base host URL (trailing slash is stripped).
80
+ *path_parts: URL path segments to join with ``/``.
81
+
82
+ Returns:
83
+ The full URL string.
84
+
85
+ Example:
86
+ >>> build_url("https://forge.example.com", "api", "v1", "repos")
87
+ 'https://forge.example.com/api/v1/repos'
88
+ """
89
+ base = host.rstrip("/")
90
+ path = "/".join(part.strip("/") for part in path_parts if part)
91
+ return f"{base}/{path}"
@@ -0,0 +1,104 @@
1
+ """Webhook delivery utilities with exponential backoff retry logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta
7
+
8
+ _MAX_RETRIES: int = 5
9
+ _BASE_DELAY_SECS: int = 10
10
+ _MAX_DELAY_SECS: int = 86400 # 24 hours
11
+
12
+
13
+ def next_retry_delay(attempt: int) -> timedelta:
14
+ """Calculate the exponential backoff delay for a webhook retry attempt.
15
+
16
+ Uses the formula: ``BASE_DELAY * 10^attempt``, capped at 24 hours.
17
+
18
+ Args:
19
+ attempt: Zero-based attempt index (0 = first retry, 4 = fifth retry).
20
+
21
+ Returns:
22
+ A :class:`datetime.timedelta` representing how long to wait before the
23
+ next delivery attempt.
24
+
25
+ Example:
26
+ >>> next_retry_delay(0).total_seconds()
27
+ 10.0
28
+ >>> next_retry_delay(1).total_seconds()
29
+ 100.0
30
+ >>> next_retry_delay(4).total_seconds()
31
+ 86400.0
32
+ """
33
+ secs = _BASE_DELAY_SECS * (10**attempt)
34
+ return timedelta(seconds=min(secs, _MAX_DELAY_SECS))
35
+
36
+
37
+ @dataclass
38
+ class WebhookDeliveryConfig:
39
+ """Configuration for webhook delivery retry behaviour.
40
+
41
+ Attributes:
42
+ max_retries: Maximum number of retry attempts before marking the
43
+ delivery as permanently failed.
44
+ base_delay_secs: Base delay in seconds for exponential backoff.
45
+ max_delay_secs: Maximum delay cap in seconds.
46
+ """
47
+
48
+ max_retries: int = _MAX_RETRIES
49
+ base_delay_secs: int = _BASE_DELAY_SECS
50
+ max_delay_secs: int = _MAX_DELAY_SECS
51
+
52
+ def delay_for_attempt(self, attempt: int) -> timedelta:
53
+ """Return the retry delay for the given attempt index.
54
+
55
+ Args:
56
+ attempt: Zero-based attempt index.
57
+
58
+ Returns:
59
+ Backoff delay as a :class:`datetime.timedelta`.
60
+ """
61
+ secs = self.base_delay_secs * (10**attempt)
62
+ return timedelta(seconds=min(secs, self.max_delay_secs))
63
+
64
+ def should_retry(self, attempt: int) -> bool:
65
+ """Return True if another retry should be attempted.
66
+
67
+ Args:
68
+ attempt: The number of attempts already made.
69
+
70
+ Returns:
71
+ ``True`` if ``attempt < max_retries``.
72
+ """
73
+ return attempt < self.max_retries
74
+
75
+
76
+ @dataclass
77
+ class WebhookDelivery:
78
+ """Represents a webhook delivery record.
79
+
80
+ Attributes:
81
+ id: Unique delivery identifier (UUID string).
82
+ webhook_id: ID of the parent webhook configuration.
83
+ event_type: The event type that triggered this delivery.
84
+ payload: The JSON payload to deliver.
85
+ attempt_count: Number of delivery attempts made so far.
86
+ state: Current state: ``"pending"``, ``"success"``, or ``"failed"``.
87
+ next_attempt_at: When the next delivery attempt should be made.
88
+ created_at: When this delivery record was created.
89
+ last_response_code: HTTP status code from the last attempt.
90
+ last_response_body: Response body from the last attempt.
91
+ completed_at: When the delivery reached a terminal state.
92
+ """
93
+
94
+ id: str
95
+ webhook_id: str
96
+ event_type: str
97
+ payload: dict
98
+ attempt_count: int = 0
99
+ state: str = "pending"
100
+ next_attempt_at: datetime | None = None
101
+ created_at: datetime | None = None
102
+ last_response_code: int | None = None
103
+ last_response_body: str | None = None
104
+ completed_at: datetime | None = None
@@ -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,26 @@
1
+ git_alternative/__init__.py,sha256=Wbbo8fP0kHvbbbcQ46Wr4_8ILHpT9KXjZ7c1VGmWUBA,1248
2
+ git_alternative/ci.py,sha256=ORFyCX3peykbYPSqXaMzX7D7p6VXl-k1If1ycl2UeF0,4750
3
+ git_alternative/cli.py,sha256=8cGA0L8YDzjlo2fKMohWaXn9MjUlU3WKECJnXSuPFcE,3629
4
+ git_alternative/client.py,sha256=NHqpDC_HurT_1IEK4FJh6TxUHvpOB60BoTg7i-RJw_A,2442
5
+ git_alternative/exceptions.py,sha256=4Dg4spDAMz4pxlLSWY1keyAI7LZhxnv7DAusRoBapjU,1214
6
+ git_alternative/http.py,sha256=vPTNDDxwl6e3_qSBA2_4mSMyC3qL0sfxD6pBHWkHTNU,4153
7
+ git_alternative/managers/__init__.py,sha256=pOYHc-FjvECzsBlG4XPWe9qtak4H7VdCjGEuDnG_7a0,50
8
+ git_alternative/managers/issues.py,sha256=qPQuSqUu8Eg9aKjoTxeuO2zlDbATjDnBl8dfH5a0nNQ,9074
9
+ git_alternative/managers/repos.py,sha256=Sj2Lo7UZPQCRaeTxWFX5qsew4c7aUzmOUgbNRBUwiZY,6913
10
+ git_alternative/models.py,sha256=goIdtwZM7T8qdUV6KCDK0y4q8lHZUdCx_dh3_JtCeTY,5836
11
+ git_alternative/pagination.py,sha256=Zf9PeQvzuEvlP-SVascX5N9i35vZ7RLRFarqdwXFpAo,1336
12
+ git_alternative/resources/__init__.py,sha256=vts06S5CWAN05oQ7yGkjBU5AnQgLqxwvVnfjHxMvDdk,46
13
+ git_alternative/resources/issues.py,sha256=VmwKFOEWj8iCofCQU-Wh6Ekr4beZHdDXCSo9Lo2c48Y,9338
14
+ git_alternative/resources/labels.py,sha256=NtPHU-o1m2chMkXEZxcx3sonAXVt33Ci14P96fP36OY,2547
15
+ git_alternative/resources/pipelines.py,sha256=LwmqQTtnjdgKwRwlQZLHgxvGYdkAqhgFKizx0iezj4Y,3772
16
+ git_alternative/resources/pulls.py,sha256=dXzldj8OovlwTF-oXXR82XyLSWnrDxeoFTiykQ6gong,6752
17
+ git_alternative/resources/repos.py,sha256=wYTQsC3ArUaPL_XBcwxjZ701_ZeFZfZSCYsYeSTVkPo,5995
18
+ git_alternative/resources/ssh_keys.py,sha256=3BJG39p2x3kI5IXC9Ynj-xhXzWTRPqwWEEGMXS8JpJs,1998
19
+ git_alternative/ssh_keys.py,sha256=sfYTZWBs3dr8sV1okuq0XW4VZxLWZL2h6VXwEnvQy3Q,2523
20
+ git_alternative/utils.py,sha256=1kfjTplxTT1JzJIoLvx_oqEteJb0EfUBqSNSz093dd0,2821
21
+ git_alternative/webhooks.py,sha256=BSWjqLZtTkA8a_WsR6jkmpPTS2BnRYluy2LaeS2z1VA,3324
22
+ git_alternative-0.2.2.dist-info/METADATA,sha256=Y54SW-EIMqANRbBBQJAPqgMMJUg7PHCkzrnDpm8eJS8,1573
23
+ git_alternative-0.2.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
24
+ git_alternative-0.2.2.dist-info/entry_points.txt,sha256=pafcoPXxfuhP6LL3yzDmX3NM5FniKrC71aP6_AiEkY4,50
25
+ git_alternative-0.2.2.dist-info/licenses/LICENSE,sha256=6Y47RSQ-wXvZrjkkdspmKQ7pAlT-Qq6N6eWBjFqOjoo,1066
26
+ git_alternative-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ forge=git_alternative.cli:main
3
+
@@ -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.