gitwebhooks 2.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.
Files changed (40) hide show
  1. gitwebhooks/__init__.py +18 -0
  2. gitwebhooks/__main__.py +10 -0
  3. gitwebhooks/auth/__init__.py +20 -0
  4. gitwebhooks/auth/custom.py +40 -0
  5. gitwebhooks/auth/factory.py +73 -0
  6. gitwebhooks/auth/gitee.py +86 -0
  7. gitwebhooks/auth/github.py +52 -0
  8. gitwebhooks/auth/gitlab.py +29 -0
  9. gitwebhooks/auth/verifier.py +34 -0
  10. gitwebhooks/cli.py +107 -0
  11. gitwebhooks/config/__init__.py +17 -0
  12. gitwebhooks/config/loader.py +170 -0
  13. gitwebhooks/config/models.py +145 -0
  14. gitwebhooks/config/registry.py +104 -0
  15. gitwebhooks/config/server.py +85 -0
  16. gitwebhooks/handlers/__init__.py +22 -0
  17. gitwebhooks/handlers/base.py +107 -0
  18. gitwebhooks/handlers/custom.py +73 -0
  19. gitwebhooks/handlers/factory.py +83 -0
  20. gitwebhooks/handlers/gitee.py +113 -0
  21. gitwebhooks/handlers/github.py +43 -0
  22. gitwebhooks/handlers/gitlab.py +43 -0
  23. gitwebhooks/handlers/request.py +248 -0
  24. gitwebhooks/logging/__init__.py +10 -0
  25. gitwebhooks/logging/setup.py +57 -0
  26. gitwebhooks/models/__init__.py +14 -0
  27. gitwebhooks/models/provider.py +22 -0
  28. gitwebhooks/models/request.py +61 -0
  29. gitwebhooks/models/result.py +40 -0
  30. gitwebhooks/server.py +128 -0
  31. gitwebhooks/utils/__init__.py +14 -0
  32. gitwebhooks/utils/constants.py +42 -0
  33. gitwebhooks/utils/exceptions.py +34 -0
  34. gitwebhooks/utils/executor.py +34 -0
  35. gitwebhooks-2.0.0.dist-info/METADATA +312 -0
  36. gitwebhooks-2.0.0.dist-info/RECORD +40 -0
  37. gitwebhooks-2.0.0.dist-info/WHEEL +5 -0
  38. gitwebhooks-2.0.0.dist-info/entry_points.txt +2 -0
  39. gitwebhooks-2.0.0.dist-info/licenses/LICENSE +21 -0
  40. gitwebhooks-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ """Gitee webhook handler
2
+
3
+ Handles webhook requests from Gitee.
4
+
5
+ Gitee supports two authentication modes:
6
+ 1. Signature mode: X-Gitee-Timestamp + X-Gitee-Token (HMAC-SHA256)
7
+ 2. Password mode: X-Gitee-Token only (plaintext)
8
+
9
+ According to Gitee documentation:
10
+ https://help.gitee.com/webhook/how-to-verify-webhook-keys
11
+
12
+ Even when verify=False, if a password is provided (no timestamp),
13
+ it should still be validated for security.
14
+ """
15
+
16
+ from typing import Optional
17
+
18
+ from gitwebhooks.handlers.base import WebhookHandler
19
+ from gitwebhooks.models.provider import Provider
20
+ from gitwebhooks.models.request import WebhookRequest
21
+ from gitwebhooks.models.result import SignatureVerificationResult
22
+ from gitwebhooks.config.models import ProviderConfig
23
+ from gitwebhooks.auth.factory import VerifierFactory
24
+ from gitwebhooks.utils.exceptions import SignatureValidationError, UnsupportedEventError
25
+
26
+
27
+ class GiteeHandler(WebhookHandler):
28
+ """Gitee webhook handler"""
29
+
30
+ def __init__(self):
31
+ self._verifier = VerifierFactory.create_gitee_verifier()
32
+
33
+ def get_provider(self) -> Provider:
34
+ """Return Provider.GITEE"""
35
+ return Provider.GITEE
36
+
37
+ def verify_signature(self, request: WebhookRequest,
38
+ config: ProviderConfig) -> SignatureVerificationResult:
39
+ """Verify Gitee signature or password
40
+
41
+ Gitee has two modes:
42
+ 1. Signature mode: Has X-Gitee-Timestamp header
43
+ 2. Password mode: Only X-Gitee-Token header
44
+
45
+ Args:
46
+ request: Webhook request
47
+ config: Provider configuration
48
+
49
+ Returns:
50
+ SignatureVerificationResult instance
51
+ """
52
+ signature = request.headers.get('X-Gitee-Token')
53
+ timestamp = request.headers.get('X-Gitee-Timestamp')
54
+
55
+ # Always validate if signature/token is provided
56
+ if signature is None:
57
+ return SignatureVerificationResult.failure('Missing signature or password')
58
+
59
+ return self._verifier.verify(
60
+ request.payload,
61
+ signature,
62
+ config.secret,
63
+ timestamp=timestamp
64
+ )
65
+
66
+ def handle_request(self, request: WebhookRequest,
67
+ config: ProviderConfig) -> Optional[str]:
68
+ """Handle complete webhook request
69
+
70
+ Override base implementation to handle Gitee's special case:
71
+ Even when verify=False, password validation is performed if provided.
72
+
73
+ Args:
74
+ request: Webhook request data
75
+ config: Provider configuration
76
+
77
+ Returns:
78
+ Repository identifier, None if processing failed
79
+
80
+ Raises:
81
+ SignatureValidationError: Signature/password verification failed
82
+ UnsupportedEventError: Event type not allowed
83
+ """
84
+ # Step 1: Check event
85
+ if not self.is_event_allowed(request.event, config):
86
+ raise UnsupportedEventError(f'Event not configured: {request.event}')
87
+
88
+ # Step 2: Verify signature or password
89
+ # For Gitee, we always validate if token is provided
90
+ signature = request.headers.get('X-Gitee-Token')
91
+ if signature is not None:
92
+ result = self.verify_signature(request, config)
93
+ if not result.is_valid:
94
+ raise SignatureValidationError(result.error_message or 'Verification failed')
95
+ elif config.verify:
96
+ # verify=True but no signature provided
97
+ raise SignatureValidationError('Missing signature')
98
+
99
+ # Step 3: Extract repository identifier
100
+ return self.extract_repository(request, config)
101
+
102
+ def extract_repository(self, request: WebhookRequest,
103
+ config: ProviderConfig) -> Optional[str]:
104
+ """Extract Gitee repository full name from request"""
105
+ if request.post_data is None:
106
+ return None
107
+ repo = request.post_data.get('repository', {})
108
+ return repo.get('full_name') if isinstance(repo, dict) else None
109
+
110
+ def is_event_allowed(self, event: Optional[str],
111
+ config: ProviderConfig) -> bool:
112
+ """Check if event is in config.handle_events list"""
113
+ return config.allows_event(event)
@@ -0,0 +1,43 @@
1
+ """GitHub webhook handler
2
+
3
+ Handles webhook requests from GitHub.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ from gitwebhooks.handlers.base import WebhookHandler
9
+ from gitwebhooks.models.provider import Provider
10
+ from gitwebhooks.models.request import WebhookRequest
11
+ from gitwebhooks.models.result import SignatureVerificationResult
12
+ from gitwebhooks.config.models import ProviderConfig
13
+ from gitwebhooks.auth.factory import VerifierFactory
14
+
15
+
16
+ class GithubHandler(WebhookHandler):
17
+ """GitHub webhook handler"""
18
+
19
+ def __init__(self):
20
+ self._verifier = VerifierFactory.create_github_verifier()
21
+
22
+ def get_provider(self) -> Provider:
23
+ """Return Provider.GITHUB"""
24
+ return Provider.GITHUB
25
+
26
+ def verify_signature(self, request: WebhookRequest,
27
+ config: ProviderConfig) -> SignatureVerificationResult:
28
+ """Verify GitHub HMAC-SHA1 signature"""
29
+ signature = request.headers.get('X-Hub-Signature')
30
+ return self._verifier.verify(request.payload, signature, config.secret)
31
+
32
+ def extract_repository(self, request: WebhookRequest,
33
+ config: ProviderConfig) -> Optional[str]:
34
+ """Extract GitHub repository full name from request"""
35
+ if request.post_data is None:
36
+ return None
37
+ repo = request.post_data.get('repository', {})
38
+ return repo.get('full_name') if isinstance(repo, dict) else None
39
+
40
+ def is_event_allowed(self, event: Optional[str],
41
+ config: ProviderConfig) -> bool:
42
+ """Check if event is in config.handle_events list"""
43
+ return config.allows_event(event)
@@ -0,0 +1,43 @@
1
+ """GitLab webhook handler
2
+
3
+ Handles webhook requests from GitLab.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ from gitwebhooks.handlers.base import WebhookHandler
9
+ from gitwebhooks.models.provider import Provider
10
+ from gitwebhooks.models.request import WebhookRequest
11
+ from gitwebhooks.models.result import SignatureVerificationResult
12
+ from gitwebhooks.config.models import ProviderConfig
13
+ from gitwebhooks.auth.factory import VerifierFactory
14
+
15
+
16
+ class GitlabHandler(WebhookHandler):
17
+ """GitLab webhook handler"""
18
+
19
+ def __init__(self):
20
+ self._verifier = VerifierFactory.create_gitlab_verifier()
21
+
22
+ def get_provider(self) -> Provider:
23
+ """Return Provider.GITLAB"""
24
+ return Provider.GITLAB
25
+
26
+ def verify_signature(self, request: WebhookRequest,
27
+ config: ProviderConfig) -> SignatureVerificationResult:
28
+ """Verify GitLab token"""
29
+ signature = request.headers.get('X-Gitlab-Token')
30
+ return self._verifier.verify(request.payload, signature, config.secret)
31
+
32
+ def extract_repository(self, request: WebhookRequest,
33
+ config: ProviderConfig) -> Optional[str]:
34
+ """Extract GitLab project path from request"""
35
+ if request.post_data is None:
36
+ return None
37
+ project = request.post_data.get('project', {})
38
+ return project.get('path_with_namespace') if isinstance(project, dict) else None
39
+
40
+ def is_event_allowed(self, event: Optional[str],
41
+ config: ProviderConfig) -> bool:
42
+ """Check if event is in config.handle_events list"""
43
+ return config.allows_event(event)
@@ -0,0 +1,248 @@
1
+ """HTTP request handler
2
+
3
+ Handles webhook endpoint for HTTP POST requests.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ from http.server import BaseHTTPRequestHandler
9
+ from typing import Dict, Optional
10
+ from urllib.parse import parse_qs
11
+
12
+ from gitwebhooks.models.provider import Provider
13
+ from gitwebhooks.models.request import WebhookRequest
14
+ from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
15
+ from gitwebhooks.handlers.factory import HandlerFactory
16
+ from gitwebhooks.utils.constants import *
17
+ from gitwebhooks.utils.exceptions import *
18
+ from gitwebhooks.utils.executor import execute_deployment
19
+
20
+
21
+ class WebhookRequestHandler(BaseHTTPRequestHandler):
22
+ """Webhook request handler
23
+
24
+ Handles HTTP POST requests, verifies signatures, triggers deployment commands.
25
+ """
26
+
27
+ # Class variables: configuration injection
28
+ _provider_configs: Dict[Provider, ProviderConfig] = {}
29
+ _repository_configs: Dict[str, RepositoryConfig] = {}
30
+
31
+ @classmethod
32
+ def configure(cls, provider_configs: Dict[Provider, ProviderConfig],
33
+ repository_configs: Dict[str, RepositoryConfig]) -> None:
34
+ """Configure handler class (dependency injection)
35
+
36
+ Args:
37
+ provider_configs: Provider configuration dictionary
38
+ repository_configs: Repository configuration dictionary
39
+ """
40
+ cls._provider_configs = provider_configs
41
+ cls._repository_configs = repository_configs
42
+
43
+ def __init__(self, request, client_address, server):
44
+ """Initialize request handler
45
+
46
+ Args:
47
+ request: HTTP request
48
+ client_address: Client address
49
+ server: Server instance
50
+ """
51
+ # Copy class-level config to instance (avoid shared state)
52
+ self._provider_configs = self.__class__._provider_configs.copy()
53
+ self._repository_configs = self.__class__._repository_configs.copy()
54
+ super().__init__(request, client_address, server)
55
+
56
+ def do_GET(self) -> None:
57
+ """Handle GET request
58
+
59
+ All GET requests return 403 Forbidden
60
+ """
61
+ self.send_error(HTTP_FORBIDDEN, MESSAGE_FORBIDDEN)
62
+
63
+ def do_POST(self) -> None:
64
+ """Handle POST request (webhook endpoint)
65
+
66
+ Implementation flow:
67
+ 1. Parse request body
68
+ 2. Identify provider and event
69
+ 3. Create corresponding handler
70
+ 4. Verify signature
71
+ 5. Extract repository identifier
72
+ 6. Execute deployment command
73
+ 7. Send response
74
+
75
+ All errors are caught and appropriate HTTP status codes are returned
76
+ """
77
+ try:
78
+ # Step 1: Parse request body
79
+ try:
80
+ request = self._parse_request()
81
+ except RequestParseError as e:
82
+ logging.warning('Request parse failed: %s', e)
83
+ self._send_error(HTTP_BAD_REQUEST, MESSAGE_BAD_REQUEST)
84
+ return
85
+
86
+ if request.post_data is None:
87
+ logging.warning('Unsupported request format')
88
+ self._send_error(HTTP_BAD_REQUEST, MESSAGE_BAD_REQUEST)
89
+ return
90
+
91
+ # Step 2: Identify provider
92
+ provider = self._identify_provider(request)
93
+ if provider is None:
94
+ logging.warning('Unknown provider')
95
+ self._send_error(HTTP_PRECONDITION_FAILED, MESSAGE_PRECONDITION_FAILED)
96
+ return
97
+
98
+ # Step 3: Create handler
99
+ handler = HandlerFactory.from_handler_type(provider)
100
+
101
+ # Steps 4-6: Process request
102
+ try:
103
+ provider_config = self._provider_configs.get(provider)
104
+ if not provider_config:
105
+ raise UnsupportedProviderError(f'{provider} configuration not found')
106
+
107
+ repo_name = handler.handle_request(request, provider_config)
108
+
109
+ except (SignatureValidationError, UnsupportedEventError,
110
+ UnsupportedProviderError) as e:
111
+ logging.warning('Webhook processing error: %s', e)
112
+ if isinstance(e, SignatureValidationError):
113
+ self._send_error(HTTP_UNAUTHORIZED, MESSAGE_UNAUTHORIZED)
114
+ elif isinstance(e, UnsupportedEventError):
115
+ self._send_error(HTTP_NOT_ACCEPTABLE, MESSAGE_NOT_ACCEPTABLE)
116
+ else:
117
+ self._send_error(HTTP_PRECONDITION_FAILED, MESSAGE_PRECONDITION_FAILED)
118
+ return
119
+
120
+ # Step 7: Execute deployment command
121
+ if repo_name:
122
+ repo_config = self._repository_configs.get(repo_name)
123
+ if not repo_config:
124
+ logging.warning('No repository configuration: %s', repo_name)
125
+ self._send_error(HTTP_NOT_FOUND, MESSAGE_NOT_FOUND)
126
+ return
127
+
128
+ execute_deployment(repo_name, repo_config.cwd, repo_config.cmd)
129
+ self._send_response(HTTP_OK)
130
+ else:
131
+ logging.warning('Repository information missing from payload')
132
+ self._send_error(HTTP_NOT_FOUND, MESSAGE_NOT_FOUND)
133
+
134
+ except Exception as e:
135
+ logging.error('Unexpected error processing webhook: %s', e)
136
+ self._send_error(HTTP_INTERNAL_SERVER_ERROR, MESSAGE_INTERNAL_SERVER_ERROR)
137
+
138
+ def _parse_request(self) -> WebhookRequest:
139
+ """Parse HTTP request
140
+
141
+ Returns:
142
+ WebhookRequest instance
143
+
144
+ Raises:
145
+ RequestParseError: Parsing failed
146
+ """
147
+ # Get Content-Type and Content-Length
148
+ content_type = self.headers.get(HEADER_CONTENT_TYPE, '')
149
+ content_length = int(self.headers.get(HEADER_CONTENT_LENGTH, 0))
150
+
151
+ # Read request body
152
+ if content_length == 0:
153
+ raise RequestParseError('Empty request body')
154
+
155
+ payload = self.rfile.read(content_length)
156
+
157
+ # Parse POST data
158
+ post_data = None
159
+ if CONTENT_TYPE_JSON in content_type:
160
+ try:
161
+ post_data = json.loads(payload.decode('utf-8'))
162
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
163
+ raise RequestParseError(f'Invalid JSON: {e}')
164
+ elif CONTENT_TYPE_FORM_URLENCODED in content_type:
165
+ try:
166
+ post_data = {k: v[0] if v else None for k, v in parse_qs(payload.decode('utf-8')).items()}
167
+ except UnicodeDecodeError as e:
168
+ raise RequestParseError(f'Invalid form data: {e}')
169
+
170
+ # Identify provider and event
171
+ provider, event = self._parse_provider_and_event()
172
+
173
+ return WebhookRequest(
174
+ provider=provider,
175
+ event=event,
176
+ payload=payload,
177
+ headers=dict(self.headers),
178
+ post_data=post_data,
179
+ content_type=content_type,
180
+ content_length=content_length
181
+ )
182
+
183
+ def _parse_provider_and_event(self) -> tuple:
184
+ """Parse provider and event from request headers
185
+
186
+ Returns:
187
+ Tuple of (provider, event)
188
+ """
189
+ headers_dict = dict(self.headers)
190
+
191
+ # GitHub
192
+ if HEADER_GITHUB_EVENT in headers_dict:
193
+ return Provider.GITHUB, headers_dict.get(HEADER_GITHUB_EVENT)
194
+
195
+ # Gitee
196
+ if HEADER_GITEE_EVENT in headers_dict:
197
+ return Provider.GITEE, headers_dict.get(HEADER_GITEE_EVENT)
198
+
199
+ # GitLab
200
+ if HEADER_GITLAB_EVENT in headers_dict:
201
+ return Provider.GITLAB, headers_dict.get(HEADER_GITLAB_EVENT)
202
+
203
+ # Custom
204
+ custom_config = self._provider_configs.get(Provider.CUSTOM)
205
+ if custom_config and custom_config.header_name:
206
+ header_value = headers_dict.get(custom_config.header_name, '')
207
+ if header_value.startswith(custom_config.header_value):
208
+ event = headers_dict.get(custom_config.header_event) if custom_config.header_event else None
209
+ return Provider.CUSTOM, event
210
+
211
+ # Unable to identify
212
+ return None, None
213
+
214
+ def _identify_provider(self, request: WebhookRequest) -> Optional[Provider]:
215
+ """Identify provider from request
216
+
217
+ Args:
218
+ request: Webhook request
219
+
220
+ Returns:
221
+ Provider enum value, None if unable to identify
222
+ """
223
+ return request.provider
224
+
225
+ def _send_response(self, status_code: int, message: bytes = MESSAGE_OK) -> None:
226
+ """Send HTTP response
227
+
228
+ Args:
229
+ status_code: HTTP status code
230
+ message: Response body
231
+ """
232
+ self.send_response(status_code)
233
+ self.send_header('Content-Type', 'text/plain')
234
+ self.end_headers()
235
+ self.wfile.write(message)
236
+
237
+ def _send_error(self, status_code: int, message: str = '') -> None:
238
+ """Send HTTP error response
239
+
240
+ Args:
241
+ status_code: HTTP status code
242
+ message: Error message
243
+ """
244
+ self.send_error(status_code, message)
245
+
246
+ def log_message(self, format: str, *args) -> None:
247
+ """Override to prevent duplicate logging"""
248
+ pass
@@ -0,0 +1,10 @@
1
+ """Logging configuration module
2
+
3
+ Configures application logging system.
4
+ """
5
+
6
+ from .setup import setup_logging
7
+
8
+ __all__ = [
9
+ 'setup_logging',
10
+ ]
@@ -0,0 +1,57 @@
1
+ """日志配置
2
+
3
+ 配置应用程序日志系统。
4
+ """
5
+
6
+ import logging
7
+ import sys
8
+
9
+
10
+ def setup_logging(log_file: str = None):
11
+ """配置日志系统
12
+
13
+ Args:
14
+ log_file: 日志文件路径(空字符串表示不记录到文件)
15
+
16
+ 根据配置设置日志记录:
17
+ - 如果 log_file 为空,只输出到 stdout
18
+ - 如果 log_file 非空,同时输出到文件和 stdout
19
+
20
+ Note:
21
+ Python 3.8+ 使用 force 参数确保重新配置,
22
+ 早期版本手动清除现有 handlers。
23
+ """
24
+ handlers = []
25
+
26
+ # 文件处理器
27
+ if log_file:
28
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
29
+ file_handler.setFormatter(logging.Formatter(
30
+ '%(asctime)s %(message)s',
31
+ datefmt='%Y-%m-%d %H:%M:%S'
32
+ ))
33
+ handlers.append(file_handler)
34
+
35
+ # 控制台处理器
36
+ console_handler = logging.StreamHandler(sys.stdout)
37
+ console_handler.setFormatter(logging.Formatter(
38
+ '%(asctime)s %(message)s',
39
+ datefmt='%Y-%m-%d %H:%M:%S'
40
+ ))
41
+ handlers.append(console_handler)
42
+
43
+ # 配置根日志记录器
44
+ # Python 3.8+ 支持 force 参数
45
+ basic_config_kwargs = {
46
+ 'level': logging.DEBUG,
47
+ 'handlers': handlers,
48
+ }
49
+ if sys.version_info >= (3, 8):
50
+ basic_config_kwargs['force'] = True
51
+ else:
52
+ # 早期版本:手动清除现有 handlers
53
+ root_logger = logging.getLogger()
54
+ for handler in root_logger.handlers[:]:
55
+ root_logger.removeHandler(handler)
56
+
57
+ logging.basicConfig(**basic_config_kwargs)
@@ -0,0 +1,14 @@
1
+ """Data models module
2
+
3
+ Defines core data types and enumerations.
4
+ """
5
+
6
+ from .provider import Provider
7
+ from .request import WebhookRequest
8
+ from .result import SignatureVerificationResult
9
+
10
+ __all__ = [
11
+ 'Provider',
12
+ 'WebhookRequest',
13
+ 'SignatureVerificationResult',
14
+ ]
@@ -0,0 +1,22 @@
1
+ """Git platform provider enumeration
2
+
3
+ Defines supported Git hosting platform types.
4
+ """
5
+
6
+ from enum import Enum, unique
7
+
8
+
9
+ @unique
10
+ class Provider(Enum):
11
+ """Git platform provider type
12
+
13
+ Supported Git hosting platforms:
14
+ - GITHUB: GitHub platform
15
+ - GITEE: Gitee platform (China)
16
+ - GITLAB: GitLab platform
17
+ - CUSTOM: Custom webhook platform
18
+ """
19
+ GITHUB = 'github'
20
+ GITEE = 'gitee'
21
+ GITLAB = 'gitlab'
22
+ CUSTOM = 'custom'
@@ -0,0 +1,61 @@
1
+ """Webhook request data wrapper
2
+
3
+ Represents a parsed HTTP webhook request.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Any, Dict, Optional
8
+
9
+ from gitwebhooks.models.provider import Provider
10
+
11
+
12
+ @dataclass
13
+ class WebhookRequest:
14
+ """Webhook request data wrapper
15
+
16
+ Attributes:
17
+ provider: Git platform provider
18
+ event: Event type (e.g., 'push', 'merge_request')
19
+ payload: Raw request body bytes
20
+ headers: HTTP request headers
21
+ post_data: Parsed JSON/form data
22
+ content_type: Content-Type header value
23
+ content_length: Content-Length header value
24
+ """
25
+ provider: Provider
26
+ event: Optional[str]
27
+ payload: bytes
28
+ headers: Dict[str, str]
29
+ post_data: Optional[Dict[str, Any]]
30
+ content_type: str
31
+ content_length: int
32
+
33
+ @property
34
+ def repo_identifier(self) -> Optional[str]:
35
+ """Extract repository identifier from post_data
36
+
37
+ Returns:
38
+ Repository identifier (e.g., 'owner/repo'), None if not found
39
+
40
+ Extraction rules:
41
+ - GitHub: repository.full_name
42
+ - Gitee: repository.full_name
43
+ - GitLab: project.path_with_namespace
44
+ - Custom: Uses ProviderConfig.identifier_path
45
+ """
46
+ if self.post_data is None:
47
+ return None
48
+
49
+ extractors = {
50
+ Provider.GITHUB: lambda d: d.get('repository', {}).get('full_name'),
51
+ Provider.GITEE: lambda d: d.get('repository', {}).get('full_name'),
52
+ Provider.GITLAB: lambda d: d.get('project', {}).get('path_with_namespace'),
53
+ }
54
+
55
+ extractor = extractors.get(self.provider)
56
+ if extractor:
57
+ result = extractor(self.post_data)
58
+ return result if isinstance(result, str) else None
59
+
60
+ # Custom provider uses identifier_path (handled in handler)
61
+ return None
@@ -0,0 +1,40 @@
1
+ """Signature verification result dataclass
2
+
3
+ Represents the result status of signature verification.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+
10
+ @dataclass
11
+ class SignatureVerificationResult:
12
+ """Signature verification result
13
+
14
+ Attributes:
15
+ is_valid: Whether verification passed
16
+ error_message: Error description when verification fails
17
+ """
18
+ is_valid: bool
19
+ error_message: Optional[str] = None
20
+
21
+ @classmethod
22
+ def success(cls) -> 'SignatureVerificationResult':
23
+ """Create a successful verification result
24
+
25
+ Returns:
26
+ Result object with is_valid=True
27
+ """
28
+ return cls(is_valid=True)
29
+
30
+ @classmethod
31
+ def failure(cls, message: str) -> 'SignatureVerificationResult':
32
+ """Create a failed verification result
33
+
34
+ Args:
35
+ message: Error description
36
+
37
+ Returns:
38
+ Result object with is_valid=False and error message
39
+ """
40
+ return cls(is_valid=False, error_message=message)