gitwebhooks 2.1.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 (47) 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/__init__.py +107 -0
  11. gitwebhooks/cli/config.py +216 -0
  12. gitwebhooks/cli/init_wizard.py +701 -0
  13. gitwebhooks/cli/prompts.py +275 -0
  14. gitwebhooks/cli/service.py +374 -0
  15. gitwebhooks/config/__init__.py +17 -0
  16. gitwebhooks/config/loader.py +170 -0
  17. gitwebhooks/config/models.py +145 -0
  18. gitwebhooks/config/registry.py +104 -0
  19. gitwebhooks/config/server.py +85 -0
  20. gitwebhooks/handlers/__init__.py +22 -0
  21. gitwebhooks/handlers/base.py +107 -0
  22. gitwebhooks/handlers/custom.py +73 -0
  23. gitwebhooks/handlers/factory.py +83 -0
  24. gitwebhooks/handlers/gitee.py +113 -0
  25. gitwebhooks/handlers/github.py +43 -0
  26. gitwebhooks/handlers/gitlab.py +43 -0
  27. gitwebhooks/handlers/request.py +248 -0
  28. gitwebhooks/logging/__init__.py +10 -0
  29. gitwebhooks/logging/setup.py +57 -0
  30. gitwebhooks/main/__init__.py +117 -0
  31. gitwebhooks/main/__main__.py +12 -0
  32. gitwebhooks/models/__init__.py +14 -0
  33. gitwebhooks/models/provider.py +22 -0
  34. gitwebhooks/models/request.py +61 -0
  35. gitwebhooks/models/result.py +40 -0
  36. gitwebhooks/server.py +128 -0
  37. gitwebhooks/utils/__init__.py +14 -0
  38. gitwebhooks/utils/constants.py +55 -0
  39. gitwebhooks/utils/exceptions.py +34 -0
  40. gitwebhooks/utils/executor.py +34 -0
  41. gitwebhooks/utils/systemd.py +274 -0
  42. gitwebhooks-2.1.0.dist-info/METADATA +494 -0
  43. gitwebhooks-2.1.0.dist-info/RECORD +47 -0
  44. gitwebhooks-2.1.0.dist-info/WHEEL +5 -0
  45. gitwebhooks-2.1.0.dist-info/entry_points.txt +2 -0
  46. gitwebhooks-2.1.0.dist-info/licenses/LICENSE +21 -0
  47. gitwebhooks-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,170 @@
1
+ """Configuration loader
2
+
3
+ Load and parse configuration from INI files.
4
+ """
5
+
6
+ import configparser
7
+ from pathlib import Path
8
+ from typing import Dict, Optional
9
+
10
+ from gitwebhooks.config.models import ProviderConfig, RepositoryConfig
11
+ from gitwebhooks.models.provider import Provider
12
+ from gitwebhooks.utils.exceptions import ConfigurationError
13
+
14
+
15
+ class ConfigLoader:
16
+ """Configuration loader
17
+
18
+ Responsible for loading and validating configuration from INI files
19
+ """
20
+
21
+ def __init__(self, config_path: str):
22
+ """Initialize configuration loader
23
+
24
+ Args:
25
+ config_path: INI configuration file path
26
+
27
+ Raises:
28
+ ConfigurationError: File does not exist or cannot be parsed
29
+ """
30
+ self.config_path = Path(config_path)
31
+ self.parser = configparser.ConfigParser()
32
+ self._load_file()
33
+
34
+ def _load_file(self) -> None:
35
+ """Load configuration file
36
+
37
+ Raises:
38
+ ConfigurationError: File does not exist or parsing failed
39
+ """
40
+ if not self.config_path.exists():
41
+ raise ConfigurationError(f'Config file not found: {self.config_path}')
42
+
43
+ try:
44
+ self.parser.read(str(self.config_path))
45
+ except (configparser.Error, UnicodeDecodeError) as e:
46
+ raise ConfigurationError(f'Failed to parse config: {e}')
47
+
48
+ def load_provider_config(self, provider: Provider) -> ProviderConfig:
49
+ """Load configuration for specified provider
50
+
51
+ Args:
52
+ provider: Provider type
53
+
54
+ Returns:
55
+ ProviderConfig instance
56
+
57
+ Raises:
58
+ ConfigurationError: Configuration section does not exist
59
+ """
60
+ try:
61
+ return ProviderConfig.from_config_parser(self.parser, provider)
62
+ except configparser.NoSectionError:
63
+ # Return default configuration
64
+ return ProviderConfig(
65
+ provider=provider,
66
+ verify=False,
67
+ secret='',
68
+ handle_events=[]
69
+ )
70
+
71
+ def load_all_provider_configs(self) -> Dict[Provider, ProviderConfig]:
72
+ """Load all provider configurations
73
+
74
+ Returns:
75
+ Mapping of provider to configuration
76
+ """
77
+ configs = {}
78
+ for provider in Provider:
79
+ configs[provider] = self.load_provider_config(provider)
80
+ return configs
81
+
82
+ def load_repository_config(self, name: str) -> Optional[RepositoryConfig]:
83
+ """Load configuration for specified repository
84
+
85
+ Args:
86
+ name: Repository name (configuration section name)
87
+
88
+ Returns:
89
+ RepositoryConfig instance, or None if section does not exist
90
+ """
91
+ return RepositoryConfig.from_config_parser(self.parser, name)
92
+
93
+ def load_all_repository_configs(self) -> Dict[str, RepositoryConfig]:
94
+ """Load all repository configurations
95
+
96
+ Returns:
97
+ Mapping of repository name to configuration
98
+
99
+ Note:
100
+ Skips reserved section names: server, ssl, github, gitee, gitlab, custom
101
+ """
102
+ configs = {}
103
+ reserved_sections = self._get_reserved_sections()
104
+
105
+ for section in self.parser.sections():
106
+ if section not in reserved_sections:
107
+ repo_config = self.load_repository_config(section)
108
+ if repo_config is not None:
109
+ configs[section] = repo_config
110
+
111
+ return configs
112
+
113
+ def _get_reserved_sections(self) -> set:
114
+ """Get reserved configuration section names
115
+
116
+ Returns:
117
+ Set of reserved section names
118
+ """
119
+ return {'server', 'ssl', 'github', 'gitee', 'gitlab', 'custom'}
120
+
121
+ def get_server_config(self) -> Dict[str, any]:
122
+ """Get server configuration
123
+
124
+ Returns:
125
+ Dictionary containing server configuration, keys include:
126
+ - address: Listen address
127
+ - port: Listen port
128
+ - log_file: Log file path
129
+ """
130
+ return {
131
+ 'address': self.parser.get('server', 'address', fallback='0.0.0.0'),
132
+ 'port': self.parser.getint('server', 'port', fallback=6789),
133
+ 'log_file': self.parser.get('server', 'log_file', fallback='').strip(),
134
+ }
135
+
136
+ def get_ssl_config(self) -> Optional[Dict[str, str]]:
137
+ """Get SSL configuration
138
+
139
+ Returns:
140
+ Dictionary containing SSL configuration, or None if not enabled
141
+ Keys include:
142
+ - enable: Whether SSL is enabled
143
+ - key_file: SSL key file path
144
+ - cert_file: SSL certificate file path
145
+ """
146
+ try:
147
+ if not self.parser.getboolean('ssl', 'enable'):
148
+ return None
149
+ except (configparser.NoSectionError, ValueError):
150
+ return None
151
+
152
+ return {
153
+ 'enable': True,
154
+ 'key_file': self.parser.get('ssl', 'key_file'),
155
+ 'cert_file': self.parser.get('ssl', 'cert_file'),
156
+ }
157
+
158
+ def validate_repository_configs(self) -> None:
159
+ """Validate all repository configurations
160
+
161
+ Raises:
162
+ ConfigurationError: Configuration is invalid
163
+ """
164
+ configs = self.load_all_repository_configs()
165
+
166
+ for name, config in configs.items():
167
+ try:
168
+ config.validate()
169
+ except ConfigurationError as e:
170
+ raise ConfigurationError(f'Repository {name}: {e}')
@@ -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)