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,145 @@
1
+ """Configuration data models
2
+
3
+ Defines configuration-related dataclasses.
4
+ """
5
+
6
+ import configparser
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import List
10
+
11
+ from gitwebhooks.models.provider import Provider
12
+ from gitwebhooks.utils.exceptions import ConfigurationError
13
+
14
+
15
+ @dataclass
16
+ class ProviderConfig:
17
+ """Git platform provider configuration
18
+
19
+ Attributes:
20
+ provider: Provider type
21
+ verify: Whether to verify signature/token
22
+ secret: Secret key for verification
23
+ handle_events: List of events to handle (empty list = handle all events)
24
+ header_name: Custom identification header (CUSTOM only)
25
+ header_value: Custom identification value (CUSTOM only)
26
+ header_event: Custom event header (CUSTOM only)
27
+ header_token: Custom token header (CUSTOM only)
28
+ identifier_path: Repository identifier JSON path (CUSTOM only)
29
+ """
30
+ provider: Provider
31
+ verify: bool
32
+ secret: str
33
+ handle_events: List[str]
34
+
35
+ # Custom provider only
36
+ header_name: str = ''
37
+ header_value: str = ''
38
+ header_event: str = ''
39
+ header_token: str = ''
40
+ identifier_path: str = ''
41
+
42
+ @classmethod
43
+ def from_config_parser(cls, parser: configparser.ConfigParser,
44
+ provider: Provider) -> 'ProviderConfig':
45
+ """Load provider configuration from ConfigParser
46
+
47
+ Args:
48
+ parser: ConfigParser instance
49
+ provider: Provider type
50
+
51
+ Returns:
52
+ ProviderConfig instance
53
+
54
+ Raises:
55
+ configparser.NoSectionError: Configuration section does not exist
56
+ """
57
+ section = provider.value
58
+ handle_events_str = parser.get(section, 'handle_events',
59
+ fallback='').strip()
60
+ handle_events = [e.strip() for e in handle_events_str.split(',')
61
+ if e.strip()] if handle_events_str else []
62
+
63
+ config = cls(
64
+ provider=provider,
65
+ verify=parser.getboolean(section, 'verify', fallback=False),
66
+ secret=parser.get(section, 'secret', fallback=''),
67
+ handle_events=handle_events
68
+ )
69
+
70
+ # Custom provider additional fields
71
+ if provider == Provider.CUSTOM:
72
+ config.header_name = parser.get(section, 'header_name',
73
+ fallback='X-Custom-Webhook')
74
+ config.header_value = parser.get(section, 'header_value',
75
+ fallback='custom')
76
+ config.header_event = parser.get(section, 'header_event',
77
+ fallback='X-Custom-Event')
78
+ config.header_token = parser.get(section, 'header_token',
79
+ fallback='X-Custom-Token')
80
+ config.identifier_path = parser.get(section, 'identifier_path',
81
+ fallback='')
82
+
83
+ return config
84
+
85
+ def allows_event(self, event: str) -> bool:
86
+ """Check if an event is allowed to be handled
87
+
88
+ Args:
89
+ event: Event type (None means all events)
90
+
91
+ Returns:
92
+ True if event is allowed, False otherwise
93
+ """
94
+ if not self.handle_events:
95
+ return True # Empty list = handle all events
96
+ return event in self.handle_events
97
+
98
+
99
+ @dataclass
100
+ class RepositoryConfig:
101
+ """Repository deployment configuration
102
+
103
+ Attributes:
104
+ name: Repository name (format: 'owner/repo' or 'group/project/repo')
105
+ cwd: Working directory for deployment command execution
106
+ cmd: Deployment command (shell string)
107
+ """
108
+ name: str
109
+ cwd: str
110
+ cmd: str
111
+
112
+ @classmethod
113
+ def from_config_parser(cls, parser: configparser.ConfigParser,
114
+ name: str):
115
+ """Load repository configuration from ConfigParser
116
+
117
+ Args:
118
+ parser: ConfigParser instance
119
+ name: Configuration section name (repository name)
120
+
121
+ Returns:
122
+ RepositoryConfig instance, or None if section does not exist
123
+
124
+ Raises:
125
+ configparser.NoOptionError: Section exists but missing required options
126
+ """
127
+ if not parser.has_section(name):
128
+ return None
129
+
130
+ return cls(
131
+ name=name,
132
+ cwd=parser.get(name, 'cwd'),
133
+ cmd=parser.get(name, 'cmd')
134
+ )
135
+
136
+ def validate(self) -> None:
137
+ """Validate configuration
138
+
139
+ Raises:
140
+ ConfigurationError: Configuration is invalid
141
+ """
142
+ if not self.cwd:
143
+ raise ConfigurationError(f'Repository {self.name}: cwd is required')
144
+ if not self.cmd:
145
+ raise ConfigurationError(f'Repository {self.name}: cmd is required')
@@ -0,0 +1,104 @@
1
+ """Configuration registry
2
+
3
+ Holds all configuration and provides access interface.
4
+ """
5
+
6
+ from typing import Dict
7
+
8
+ from gitwebhooks.config.loader import ConfigLoader
9
+ from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
10
+ from gitwebhooks.config.server import ServerConfig
11
+ from gitwebhooks.models.provider import Provider
12
+ from gitwebhooks.utils.exceptions import ConfigurationError
13
+
14
+
15
+ class ConfigurationRegistry:
16
+ """Configuration registry
17
+
18
+ Holds all configuration and provides access interface
19
+ """
20
+
21
+ def __init__(self, loader: ConfigLoader):
22
+ """Initialize configuration registry
23
+
24
+ Args:
25
+ loader: Configuration loader instance
26
+ """
27
+ self.loader = loader
28
+ self._server_config = None
29
+ self._provider_configs = None
30
+ self._repository_configs = None
31
+ self._load_all_configs()
32
+
33
+ def _load_all_configs(self) -> None:
34
+ """Load all configurations
35
+
36
+ Raises:
37
+ ConfigurationError: Configuration loading or validation failed
38
+ """
39
+ self._server_config = ServerConfig.from_loader(self.loader)
40
+ self._server_config.validate()
41
+
42
+ self._provider_configs = self.loader.load_all_provider_configs()
43
+ self._repository_configs = self.loader.load_all_repository_configs()
44
+
45
+ self.loader.validate_repository_configs()
46
+
47
+ @property
48
+ def server_config(self) -> ServerConfig:
49
+ """Get server configuration"""
50
+ if self._server_config is None:
51
+ raise ConfigurationError('Server config not loaded')
52
+ return self._server_config
53
+
54
+ @property
55
+ def provider_configs(self) -> Dict[Provider, ProviderConfig]:
56
+ """Get provider configuration dictionary"""
57
+ if self._provider_configs is None:
58
+ raise ConfigurationError('Provider configs not loaded')
59
+ return self._provider_configs
60
+
61
+ @property
62
+ def repository_configs(self) -> Dict[str, RepositoryConfig]:
63
+ """Get repository configuration dictionary"""
64
+ if self._repository_configs is None:
65
+ raise ConfigurationError('Repository configs not loaded')
66
+ return self._repository_configs
67
+
68
+ def get_provider_config(self, provider: Provider) -> ProviderConfig:
69
+ """Get configuration for specified provider
70
+
71
+ Args:
72
+ provider: Provider type
73
+
74
+ Returns:
75
+ ProviderConfig instance
76
+ """
77
+ return self.provider_configs.get(provider, ProviderConfig(
78
+ provider=provider,
79
+ verify=False,
80
+ secret='',
81
+ handle_events=[]
82
+ ))
83
+
84
+ def get_repository_config(self, name: str) -> RepositoryConfig:
85
+ """Get configuration for specified repository
86
+
87
+ Args:
88
+ name: Repository name
89
+
90
+ Returns:
91
+ RepositoryConfig instance, or None if not found
92
+ """
93
+ return self.repository_configs.get(name)
94
+
95
+ def has_repository(self, name: str) -> bool:
96
+ """Check if repository exists
97
+
98
+ Args:
99
+ name: Repository name
100
+
101
+ Returns:
102
+ True if exists, False otherwise
103
+ """
104
+ return name in self.repository_configs
@@ -0,0 +1,85 @@
1
+ """Server configuration
2
+
3
+ Defines server-related configuration dataclasses.
4
+ """
5
+
6
+ import os
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Dict, Optional
10
+
11
+ from gitwebhooks.utils.exceptions import ConfigurationError
12
+
13
+
14
+ @dataclass
15
+ class ServerConfig:
16
+ """Server configuration
17
+
18
+ Attributes:
19
+ address: Listen address
20
+ port: Listen port
21
+ log_file: Log file path (empty string means no file logging)
22
+ ssl_enabled: Whether SSL is enabled
23
+ ssl_key_file: SSL key file path
24
+ ssl_cert_file: SSL certificate file path
25
+ """
26
+ address: str
27
+ port: int
28
+ log_file: str
29
+ ssl_enabled: bool = False
30
+ ssl_key_file: Optional[str] = None
31
+ ssl_cert_file: Optional[str] = None
32
+
33
+ @classmethod
34
+ def from_loader(cls, loader: 'ConfigLoader') -> 'ServerConfig':
35
+ """Create server configuration from ConfigLoader
36
+
37
+ Args:
38
+ loader: Configuration loader instance
39
+
40
+ Returns:
41
+ ServerConfig instance
42
+ """
43
+ server_cfg = loader.get_server_config()
44
+ ssl_cfg = loader.get_ssl_config()
45
+
46
+ return cls(
47
+ address=server_cfg['address'],
48
+ port=server_cfg['port'],
49
+ log_file=server_cfg['log_file'],
50
+ ssl_enabled=ssl_cfg is not None,
51
+ ssl_key_file=ssl_cfg['key_file'] if ssl_cfg else None,
52
+ ssl_cert_file=ssl_cfg['cert_file'] if ssl_cfg else None,
53
+ )
54
+
55
+ def validate(self) -> None:
56
+ """Validate server configuration
57
+
58
+ Raises:
59
+ ConfigurationError: Configuration is invalid
60
+ """
61
+ if self.ssl_enabled:
62
+ if not self.ssl_key_file:
63
+ raise ConfigurationError('SSL enabled but key_file not specified')
64
+ if not self.ssl_cert_file:
65
+ raise ConfigurationError('SSL enabled but cert_file not specified')
66
+
67
+ # Verify files exist
68
+ key_path = Path(self.ssl_key_file)
69
+ cert_path = Path(self.ssl_cert_file)
70
+
71
+ if not key_path.exists():
72
+ raise ConfigurationError(f'SSL key file not found: {self.ssl_key_file}')
73
+ if not cert_path.exists():
74
+ raise ConfigurationError(f'SSL cert file not found: {self.ssl_cert_file}')
75
+
76
+ # Validate port range
77
+ if not (1 <= self.port <= 65535):
78
+ raise ConfigurationError(f'Invalid port number: {self.port}')
79
+
80
+ # Validate log file writable (if specified)
81
+ if self.log_file:
82
+ log_path = Path(self.log_file)
83
+ parent_dir = log_path.parent
84
+ if parent_dir.exists() and not os.access(parent_dir, os.W_OK):
85
+ raise ConfigurationError(f'Log directory not writable: {parent_dir}')
@@ -0,0 +1,22 @@
1
+ """Webhook handlers module
2
+
3
+ Handles webhook requests from different Git platforms.
4
+ """
5
+
6
+ from .base import WebhookHandler
7
+ from .github import GithubHandler
8
+ from .gitee import GiteeHandler
9
+ from .gitlab import GitlabHandler
10
+ from .custom import CustomHandler
11
+ from .factory import HandlerFactory
12
+ from .request import WebhookRequestHandler
13
+
14
+ __all__ = [
15
+ 'WebhookHandler',
16
+ 'GithubHandler',
17
+ 'GiteeHandler',
18
+ 'GitlabHandler',
19
+ 'CustomHandler',
20
+ 'HandlerFactory',
21
+ 'WebhookRequestHandler',
22
+ ]
@@ -0,0 +1,107 @@
1
+ """Handler base class
2
+
3
+ Defines the abstract interface for webhook handlers.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Optional
8
+
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.utils.exceptions import SignatureValidationError, UnsupportedEventError
14
+
15
+
16
+ class WebhookHandler(ABC):
17
+ """Webhook handler base class
18
+
19
+ All platform-specific handlers must inherit from this class and implement all abstract methods.
20
+ """
21
+
22
+ @abstractmethod
23
+ def get_provider(self) -> Provider:
24
+ """Return the provider type supported by this handler
25
+
26
+ Returns:
27
+ Provider enum value
28
+ """
29
+ pass
30
+
31
+ @abstractmethod
32
+ def verify_signature(self, request: WebhookRequest,
33
+ config: ProviderConfig) -> SignatureVerificationResult:
34
+ """Verify webhook signature
35
+
36
+ Args:
37
+ request: Webhook request data
38
+ config: Provider configuration (includes secret)
39
+
40
+ Returns:
41
+ Signature verification result
42
+
43
+ Raises:
44
+ SignatureValidationError: When verification fails
45
+ """
46
+ pass
47
+
48
+ @abstractmethod
49
+ def extract_repository(self, request: WebhookRequest,
50
+ config: ProviderConfig) -> Optional[str]:
51
+ """Extract repository identifier from request
52
+
53
+ Args:
54
+ request: Webhook request data
55
+ config: Provider configuration (may include identifier_path)
56
+
57
+ Returns:
58
+ Repository identifier (e.g., 'owner/repo'), None if not found
59
+ """
60
+ pass
61
+
62
+ @abstractmethod
63
+ def is_event_allowed(self, event: Optional[str],
64
+ config: ProviderConfig) -> bool:
65
+ """Check if event type is allowed to be handled
66
+
67
+ Args:
68
+ event: Event type (e.g., 'push', 'merge_request')
69
+ config: Provider configuration (includes handle_events list)
70
+
71
+ Returns:
72
+ True if event is allowed, False otherwise
73
+ """
74
+ pass
75
+
76
+ def handle_request(self, request: WebhookRequest,
77
+ config: ProviderConfig) -> Optional[str]:
78
+ """Handle complete webhook request
79
+
80
+ This is a template method that defines the fixed flow of request processing:
81
+ 1. Check if event is allowed
82
+ 2. Verify signature
83
+ 3. Extract repository identifier
84
+
85
+ Args:
86
+ request: Webhook request data
87
+ config: Provider configuration
88
+
89
+ Returns:
90
+ Repository identifier, None if processing failed
91
+
92
+ Raises:
93
+ SignatureValidationError: Signature verification failed
94
+ UnsupportedEventError: Event type not allowed
95
+ """
96
+ # Step 1: Check event
97
+ if not self.is_event_allowed(request.event, config):
98
+ raise UnsupportedEventError(f'Event not configured: {request.event}')
99
+
100
+ # Step 2: Verify signature (if required)
101
+ if config.verify:
102
+ result = self.verify_signature(request, config)
103
+ if not result.is_valid:
104
+ raise SignatureValidationError(result.error_message or 'Verification failed')
105
+
106
+ # Step 3: Extract repository identifier
107
+ return self.extract_repository(request, config)
@@ -0,0 +1,73 @@
1
+ """Custom webhook handler
2
+
3
+ Handles webhook requests from custom webhook sources.
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 CustomHandler(WebhookHandler):
17
+ """Custom webhook handler"""
18
+
19
+ def __init__(self):
20
+ self._verifier = None # Created dynamically based on configuration
21
+
22
+ def get_provider(self) -> Provider:
23
+ """Return Provider.CUSTOM"""
24
+ return Provider.CUSTOM
25
+
26
+ def verify_signature(self, request: WebhookRequest,
27
+ config: ProviderConfig) -> SignatureVerificationResult:
28
+ """Verify custom token"""
29
+ # Create verifier (verification enabled depends on whether header_token is configured)
30
+ verify_enabled = bool(config.header_token)
31
+ if not self._verifier:
32
+ self._verifier = VerifierFactory.create_custom_verifier(verify_enabled)
33
+
34
+ signature = request.headers.get(config.header_token) if config.header_token else ''
35
+ return self._verifier.verify(request.payload, signature, config.secret)
36
+
37
+ def extract_repository(self, request: WebhookRequest,
38
+ config: ProviderConfig) -> Optional[str]:
39
+ """Extract repository identifier using JSON path"""
40
+ if request.post_data is None or not config.identifier_path:
41
+ return None
42
+
43
+ # Split path by '.'
44
+ path_parts = config.identifier_path.split('.')
45
+ current = request.post_data
46
+
47
+ # Traverse level by level
48
+ for part in path_parts:
49
+ if isinstance(current, dict):
50
+ current = current.get(part)
51
+ else:
52
+ return None
53
+
54
+ # Return final value (must be string)
55
+ return current if isinstance(current, str) else None
56
+
57
+ def is_event_allowed(self, event: Optional[str],
58
+ config: ProviderConfig) -> bool:
59
+ """Check if event is allowed
60
+
61
+ Args:
62
+ event: Event type (already parsed from headers by caller)
63
+ config: Provider configuration
64
+
65
+ Returns:
66
+ True if event is allowed, False otherwise
67
+ """
68
+ # If no event header configured, allow all events
69
+ if not config.header_event:
70
+ return config.allows_event(event)
71
+
72
+ # Use the passed event parameter (already parsed from headers by caller)
73
+ return config.allows_event(event)
@@ -0,0 +1,83 @@
1
+ """Handler factory class
2
+
3
+ Creates corresponding handler instances based on request headers.
4
+ """
5
+
6
+ from typing import Dict
7
+
8
+ from gitwebhooks.handlers.base import WebhookHandler
9
+ from gitwebhooks.handlers.github import GithubHandler
10
+ from gitwebhooks.handlers.gitee import GiteeHandler
11
+ from gitwebhooks.handlers.gitlab import GitlabHandler
12
+ from gitwebhooks.handlers.custom import CustomHandler
13
+ from gitwebhooks.models.provider import Provider
14
+ from gitwebhooks.config.models import ProviderConfig
15
+ from gitwebhooks.utils.exceptions import UnsupportedProviderError
16
+
17
+
18
+ class HandlerFactory:
19
+ """Handler factory class
20
+
21
+ Creates corresponding handler instances based on request headers
22
+ """
23
+
24
+ @staticmethod
25
+ def from_headers(headers: Dict[str, str],
26
+ configs: Dict[Provider, ProviderConfig]) -> WebhookHandler:
27
+ """Create handler based on request headers
28
+
29
+ Args:
30
+ headers: HTTP request header dictionary
31
+ configs: Provider configuration dictionary
32
+
33
+ Returns:
34
+ Corresponding WebhookHandler instance
35
+
36
+ Raises:
37
+ UnsupportedProviderError: Unrecognized platform
38
+ """
39
+ # Check X-GitHub-Event
40
+ if 'X-GitHub-Event' in headers:
41
+ return GithubHandler()
42
+
43
+ # Check X-Gitee-Event
44
+ if 'X-Gitee-Event' in headers:
45
+ return GiteeHandler()
46
+
47
+ # Check X-Gitlab-Event
48
+ if 'X-Gitlab-Event' in headers:
49
+ return GitlabHandler()
50
+
51
+ # Check custom header
52
+ custom_config = configs.get(Provider.CUSTOM)
53
+ if custom_config and custom_config.header_name:
54
+ if headers.get(custom_config.header_name) == custom_config.header_value:
55
+ return CustomHandler()
56
+
57
+ raise UnsupportedProviderError('Unable to identify provider from headers')
58
+
59
+ @staticmethod
60
+ def from_handler_type(provider: Provider) -> WebhookHandler:
61
+ """Create handler based on provider type
62
+
63
+ Args:
64
+ provider: Provider type
65
+
66
+ Returns:
67
+ Corresponding WebhookHandler instance
68
+
69
+ Raises:
70
+ UnsupportedProviderError: Unsupported provider
71
+ """
72
+ handlers = {
73
+ Provider.GITHUB: GithubHandler,
74
+ Provider.GITEE: GiteeHandler,
75
+ Provider.GITLAB: GitlabHandler,
76
+ Provider.CUSTOM: CustomHandler,
77
+ }
78
+
79
+ handler_class = handlers.get(provider)
80
+ if handler_class is None:
81
+ raise UnsupportedProviderError(f'No handler for provider: {provider}')
82
+
83
+ return handler_class()