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,275 @@
1
+ """Interactive prompt utilities
2
+
3
+ Provides functions for interactive user input and validation.
4
+ """
5
+
6
+ import sys
7
+ from typing import Any, Callable, Optional, Dict, List
8
+
9
+
10
+ # Question template for configuration initialization
11
+ QUESTIONS = [
12
+ # Server configuration
13
+ {
14
+ 'key': 'server_address',
15
+ 'prompt': 'Server listen address [default: 0.0.0.0]: ',
16
+ 'default': '0.0.0.0',
17
+ 'validator': None,
18
+ 'required': True
19
+ },
20
+ {
21
+ 'key': 'server_port',
22
+ 'prompt': 'Server port [default: 6789]: ',
23
+ 'default': '6789',
24
+ 'validator': 'validate_port',
25
+ 'required': True
26
+ },
27
+ {
28
+ 'key': 'log_file',
29
+ 'prompt': 'Log file path [default: ~/.gitwebhook.log]: ',
30
+ 'default': '~/.gitwebhook.log',
31
+ 'validator': None,
32
+ 'required': False
33
+ },
34
+ ]
35
+
36
+
37
+ # Platform-specific questions
38
+ PLATFORM_QUESTIONS = {
39
+ 'github': [
40
+ {
41
+ 'section': 'github',
42
+ 'key': 'verify',
43
+ 'prompt': ' Enable signature verification? (Y/n) [default: Y]: ',
44
+ 'default': 'true',
45
+ 'validator': None,
46
+ 'required': False
47
+ },
48
+ {
49
+ 'section': 'github',
50
+ 'key': 'secret',
51
+ 'prompt': ' Webhook secret (leave empty to skip): ',
52
+ 'default': '',
53
+ 'validator': None,
54
+ 'required': False
55
+ },
56
+ ],
57
+ 'gitee': [
58
+ {
59
+ 'section': 'gitee',
60
+ 'key': 'verify',
61
+ 'prompt': ' Enable signature verification? (Y/n) [default: Y]: ',
62
+ 'default': 'true',
63
+ 'validator': None,
64
+ 'required': False
65
+ },
66
+ {
67
+ 'section': 'gitee',
68
+ 'key': 'secret',
69
+ 'prompt': ' Webhook secret (leave empty to skip): ',
70
+ 'default': '',
71
+ 'validator': None,
72
+ 'required': False
73
+ },
74
+ ],
75
+ 'gitlab': [
76
+ {
77
+ 'section': 'gitlab',
78
+ 'key': 'verify',
79
+ 'prompt': ' Enable token verification? (Y/n) [default: Y]: ',
80
+ 'default': 'true',
81
+ 'validator': None,
82
+ 'required': False
83
+ },
84
+ {
85
+ 'section': 'gitlab',
86
+ 'key': 'secret',
87
+ 'prompt': ' Webhook token (leave empty to skip): ',
88
+ 'default': '',
89
+ 'validator': None,
90
+ 'required': False
91
+ },
92
+ ],
93
+ }
94
+
95
+
96
+ def ask_question(prompt: str,
97
+ validator: Optional[Callable[[str], Any]] = None,
98
+ default: Optional[Any] = None,
99
+ required: bool = True) -> Any:
100
+ """Ask a question with optional validation and default value
101
+
102
+ Args:
103
+ prompt: Question prompt to display
104
+ validator: Optional validation function that raises ValueError on invalid input
105
+ default: Optional default value if user provides empty input
106
+ required: Whether a value is required (defaults to True)
107
+
108
+ Returns:
109
+ Validated user input or default value
110
+
111
+ Raises:
112
+ KeyboardInterrupt: If user confirms exit on Ctrl+C
113
+ """
114
+ while True:
115
+ try:
116
+ response = input(prompt).strip()
117
+
118
+ # Use default if empty input provided
119
+ if not response and default is not None:
120
+ return default
121
+
122
+ # Validate if validator provided
123
+ if validator:
124
+ if isinstance(validator, str):
125
+ # Validator is a string name, look up function
126
+ validator_func = globals().get(validator)
127
+ if validator_func:
128
+ return validator_func(response)
129
+ else:
130
+ return validator(response)
131
+
132
+ return response
133
+
134
+ except KeyboardInterrupt:
135
+ # Ask for confirmation before exiting
136
+ if ask_yes_no('Confirm exit? Unsaved changes will be lost (y/N) ', default=False):
137
+ print('Configuration cancelled')
138
+ sys.exit(0)
139
+ except ValueError as e:
140
+ print(f'Invalid input: {e}')
141
+ print('Please try again')
142
+
143
+
144
+ def ask_yes_no(prompt: str, default: bool = False) -> bool:
145
+ """Ask a yes/no question
146
+
147
+ Args:
148
+ prompt: Question prompt
149
+ default: Default answer if user just presses Enter
150
+
151
+ Returns:
152
+ True for yes, False for no
153
+ """
154
+ # Set default indicator
155
+ default_indicator = 'Y' if default else 'N'
156
+ if not default:
157
+ prompt = prompt.replace('(y/N)', f'[{default_indicator}]')
158
+ else:
159
+ prompt = prompt.replace('(Y/n)', f'[{default_indicator}]')
160
+
161
+ while True:
162
+ try:
163
+ response = input(prompt).strip().lower()
164
+
165
+ # Use default if empty input
166
+ if not response:
167
+ return default
168
+
169
+ # Parse response
170
+ if response in ('y', 'yes'):
171
+ return True
172
+ elif response in ('n', 'no'):
173
+ return False
174
+ else:
175
+ print('Please enter y or n')
176
+
177
+ except KeyboardInterrupt:
178
+ if ask_yes_no('Confirm exit? Unsaved changes will be lost (y/N) ', default=False):
179
+ print('Configuration cancelled')
180
+ sys.exit(0)
181
+
182
+
183
+ def validate_port(value: str) -> int:
184
+ """Validate port number
185
+
186
+ Args:
187
+ value: String input to validate
188
+
189
+ Returns:
190
+ Integer port number
191
+
192
+ Raises:
193
+ ValueError: If port is invalid
194
+ """
195
+ try:
196
+ port = int(value)
197
+ if not 1 <= port <= 65535:
198
+ raise ValueError('Port must be between 1 and 65535')
199
+ return port
200
+ except ValueError as e:
201
+ if 'must be between' in str(e):
202
+ raise
203
+ raise ValueError('Port must be a number')
204
+
205
+
206
+ def validate_address(value: str) -> str:
207
+ """Validate server address
208
+
209
+ Args:
210
+ value: String input to validate
211
+
212
+ Returns:
213
+ Validated address string
214
+
215
+ Raises:
216
+ ValueError: If address is empty
217
+ """
218
+ if not value:
219
+ raise ValueError('Server address cannot be empty')
220
+ return value
221
+
222
+
223
+ def validate_path(value: str) -> str:
224
+ """Validate file path
225
+
226
+ Args:
227
+ value: String input to validate
228
+
229
+ Returns:
230
+ Validated path string
231
+
232
+ Raises:
233
+ ValueError: If path is empty
234
+ """
235
+ if not value:
236
+ raise ValueError('Path cannot be empty')
237
+ return value
238
+
239
+
240
+ def validate_bool(value: str) -> bool:
241
+ """Validate boolean input
242
+
243
+ Args:
244
+ value: String input to validate
245
+
246
+ Returns:
247
+ True or False
248
+
249
+ Raises:
250
+ ValueError: If input is not a boolean value
251
+ """
252
+ value_lower = value.lower()
253
+ if value_lower in ('y', 'yes', 'true', '1'):
254
+ return True
255
+ elif value_lower in ('n', 'no', 'false', '0'):
256
+ return False
257
+ else:
258
+ raise ValueError('Please enter yes/no or y/n')
259
+
260
+
261
+ def validate_text(value: str) -> str:
262
+ """Validate text input (non-empty)
263
+
264
+ Args:
265
+ value: String input to validate
266
+
267
+ Returns:
268
+ Validated text string
269
+
270
+ Raises:
271
+ ValueError: If text is empty
272
+ """
273
+ if not value:
274
+ raise ValueError('Input cannot be empty')
275
+ return value
@@ -0,0 +1,374 @@
1
+ """Service installation CLI commands
2
+
3
+ Provides systemd service installation and removal functionality.
4
+ """
5
+
6
+ import argparse
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from gitwebhooks.utils.systemd import (
12
+ check_systemd,
13
+ check_root_permission,
14
+ generate_service_file,
15
+ get_service_path,
16
+ detect_installation_type,
17
+ InstallationType,
18
+ )
19
+ from gitwebhooks.cli.prompts import ask_yes_no
20
+
21
+
22
+ # Error codes
23
+ E_PERM = 1
24
+ E_SYSTEMD = 1
25
+ E_EXISTS = 1
26
+
27
+
28
+ def cmd_install(args: argparse.Namespace) -> int:
29
+ """Install systemd service
30
+
31
+ Args:
32
+ args: Parsed command-line arguments
33
+ - force: Force overwrite existing service
34
+ - verbose: Verbosity level (0=normal, 1=verbose, 2=extra verbose)
35
+ - dry_run: Preview mode without actual installation
36
+
37
+ Returns:
38
+ Exit code (0 = success, 1 = error)
39
+ """
40
+ # Get verbose and dry_run from args
41
+ verbose = getattr(args, 'verbose', 0)
42
+ dry_run = getattr(args, 'dry_run', False)
43
+
44
+ # For dry-run mode, skip root check
45
+ if not dry_run:
46
+ # Check root permission
47
+ if not check_root_permission():
48
+ print('Error: This operation requires root privileges', file=sys.stderr)
49
+ print('Please use: sudo gitwebhooks-cli service install', file=sys.stderr)
50
+ return E_PERM
51
+
52
+ # Check systemd availability
53
+ if not check_systemd():
54
+ print('Error: systemd is not supported on this system', file=sys.stderr)
55
+ return E_SYSTEMD
56
+
57
+ # Check if service already exists
58
+ service_path = get_service_path()
59
+ if service_path.exists():
60
+ if not args.force:
61
+ print(f'Error: Service already installed', file=sys.stderr)
62
+ print(f'Service file: {service_path}', file=sys.stderr)
63
+ print('Use --force to force overwrite', file=sys.stderr)
64
+ return E_EXISTS
65
+
66
+ # Confirm overwrite
67
+ if not confirm_overwrite():
68
+ print('Installation cancelled')
69
+ return 0
70
+ else:
71
+ # In dry-run mode, warn if service exists but continue
72
+ service_path = get_service_path()
73
+ if service_path.exists() and not args.force:
74
+ print(f'Notice: Service already exists at {service_path}', file=sys.stderr)
75
+ print('Previewing service file content (use --force to suppress this notice)...', file=sys.stderr)
76
+ print()
77
+
78
+ # Perform installation
79
+ return install_service(force=args.force, verbose=verbose, dry_run=dry_run)
80
+
81
+
82
+ def install_service(force: bool = False, verbose: int = 0, dry_run: bool = False) -> int:
83
+ """Execute service installation logic
84
+
85
+ Args:
86
+ force: Force overwrite existing service
87
+ verbose: Verbosity level (0=normal, 1=verbose, 2=extra verbose)
88
+ dry_run: Preview mode without actual installation
89
+
90
+ Returns:
91
+ Exit code (0 = success, 1 = error)
92
+ """
93
+ # Detect installation environment
94
+ env = detect_installation_type()
95
+
96
+ # Verbose output
97
+ if verbose >= 1:
98
+ print('Detecting Python environment...')
99
+ print(f' Installation type: {env.type.value}')
100
+ print(f' Python interpreter: {env.python_path}')
101
+ print(f' CLI path: {env.cli_path}')
102
+ if verbose >= 2 and env.detection_methods:
103
+ print(f' Detection methods: {", ".join(env.detection_methods)}')
104
+ if env.venv_root:
105
+ print(f' Virtual environment root: {env.venv_root}')
106
+ print()
107
+
108
+ # Check if environment is supported
109
+ if not env.is_supported:
110
+ print(f'Error: {env.error_message}', file=sys.stderr)
111
+ print()
112
+ print('For migration from conda:', file=sys.stderr)
113
+ print(' 1. Deactivate your conda environment: conda deactivate', file=sys.stderr)
114
+ print(' 2. Install using pipx: pipx install git-webhooks-server', file=sys.stderr)
115
+ print(' OR create a venv: python3 -m venv ~/venv/gitwebhooks', file=sys.stderr)
116
+ return 1
117
+
118
+ # Warnings for non-recommended installation types
119
+ if env.type == InstallationType.VIRTUALENV:
120
+ print('Warning: virtualenv detected. venv is recommended.', file=sys.stderr)
121
+ print()
122
+ elif env.type == InstallationType.SYSTEM_PIP:
123
+ print('Warning: System pip installation is not recommended.', file=sys.stderr)
124
+ print('Consider using pipx or venv for better isolation.', file=sys.stderr)
125
+ print()
126
+
127
+ # Get paths
128
+ service_path = get_service_path()
129
+ config_path = '~/.gitwebhook.ini'
130
+
131
+ # Generate service file based on installation type
132
+ # For pipx: use cli_path directly
133
+ # For others: use python -m gitwebhooks.cli
134
+ use_python_module = env.type != InstallationType.PIPX
135
+
136
+ service_content = generate_service_file(
137
+ cli_path=env.cli_path,
138
+ config_path=config_path,
139
+ python_path=env.python_path if use_python_module else None,
140
+ use_python_module=use_python_module
141
+ )
142
+
143
+ # Dry-run mode
144
+ if dry_run:
145
+ print('Dry-run mode: Preview of service file:')
146
+ print('---')
147
+ print(service_content)
148
+ print('---')
149
+ print()
150
+ print(f'Service file would be written to: {service_path}')
151
+ print('Dry-run complete: No changes made.')
152
+ return 0
153
+
154
+ # Actual installation
155
+ print('Installing systemd service...')
156
+
157
+ # Write service file
158
+ try:
159
+ service_path.write_text(service_content)
160
+ print(f'Service file: {service_path}')
161
+ except (IOError, OSError) as e:
162
+ print(f'Error: Cannot write service file: {e}', file=sys.stderr)
163
+ return 1
164
+
165
+ # Reload systemd daemon
166
+ print('Reloading systemd daemon...')
167
+ try:
168
+ subprocess.run(
169
+ ['systemctl', 'daemon-reload'],
170
+ check=True,
171
+ capture_output=True
172
+ )
173
+ except subprocess.CalledProcessError as e:
174
+ print(f'Warning: systemctl daemon-reload failed: {e}', file=sys.stderr)
175
+ # Continue anyway
176
+
177
+ # Enable and start service
178
+ return enable_and_start_service()
179
+
180
+
181
+ def check_service_exists() -> bool:
182
+ """Check if service is already installed
183
+
184
+ Returns:
185
+ True if service file exists, False otherwise
186
+ """
187
+ return get_service_path().exists()
188
+
189
+
190
+ def confirm_overwrite() -> bool:
191
+ """Ask user to confirm overwrite
192
+
193
+ Returns:
194
+ True if user confirms, False otherwise
195
+ """
196
+ return ask_yes_no('Service already exists, overwrite? (y/N) ', default=False)
197
+
198
+
199
+ def enable_and_start_service() -> int:
200
+ """Enable and start the service
201
+
202
+ Returns:
203
+ Exit code (0 = success, 1 = error)
204
+ """
205
+ # Ask if user wants to enable and start
206
+ if not ask_yes_no('Enable and start service? (Y/n) ', default=True):
207
+ print('Service installed but not started')
208
+ print('Start manually: sudo systemctl start git-webhooks-server')
209
+ return 0
210
+
211
+ # Enable service
212
+ print('Enabling service...')
213
+ try:
214
+ subprocess.run(
215
+ ['systemctl', 'enable', 'git-webhooks-server'],
216
+ check=True,
217
+ capture_output=True
218
+ )
219
+ except subprocess.CalledProcessError as e:
220
+ print(f'Warning: systemctl enable failed: {e}', file=sys.stderr)
221
+ # Continue anyway
222
+
223
+ # Start service
224
+ print('Starting service...')
225
+ try:
226
+ subprocess.run(
227
+ ['systemctl', 'start', 'git-webhooks-server'],
228
+ check=True,
229
+ capture_output=True
230
+ )
231
+ except subprocess.CalledProcessError as e:
232
+ print(f'Warning: systemctl start failed: {e}', file=sys.stderr)
233
+ # Continue anyway
234
+
235
+ print()
236
+ print('Installation complete!')
237
+ print('Service status: systemctl status git-webhooks-server')
238
+
239
+ return 0
240
+
241
+
242
+ def cmd_uninstall(args: argparse.Namespace) -> int:
243
+ """Uninstall systemd service
244
+
245
+ Args:
246
+ args: Parsed command-line arguments
247
+ - purge: Also remove configuration file
248
+
249
+ Returns:
250
+ Exit code (0 = success, 1 = error)
251
+ """
252
+ # Check root permission
253
+ if not check_root_permission():
254
+ print('Error: This operation requires root privileges', file=sys.stderr)
255
+ print('Please use: sudo gitwebhooks-cli service uninstall', file=sys.stderr)
256
+ return E_PERM
257
+
258
+ # Check if service is installed
259
+ if not check_service_exists():
260
+ print('Error: Service not installed', file=sys.stderr)
261
+ return 1
262
+
263
+ # Perform uninstallation
264
+ return uninstall_service(args.purge)
265
+
266
+
267
+ def uninstall_service(purge: bool = False) -> int:
268
+ """Execute service uninstallation logic
269
+
270
+ Args:
271
+ purge: Also remove configuration file
272
+
273
+ Returns:
274
+ Exit code (0 = success, 1 = error)
275
+ """
276
+ print('Uninstalling systemd service...')
277
+
278
+ # Stop and disable service
279
+ stop_and_disable_service()
280
+
281
+ # Remove service file
282
+ remove_service_file()
283
+
284
+ # Handle configuration cleanup
285
+ handle_config_cleanup(purge)
286
+
287
+ print()
288
+ print('Uninstallation complete!')
289
+
290
+ return 0
291
+
292
+
293
+ def stop_and_disable_service() -> None:
294
+ """Stop and disable the service
295
+
296
+ Returns:
297
+ None
298
+ """
299
+ service_path = get_service_path()
300
+
301
+ # Stop service if running
302
+ print('Stopping service...')
303
+ try:
304
+ subprocess.run(
305
+ ['systemctl', 'stop', 'git-webhooks-server'],
306
+ check=False,
307
+ capture_output=True
308
+ )
309
+ except OSError as e:
310
+ print(f'Warning: systemctl stop failed: {e}', file=sys.stderr)
311
+ # Continue anyway
312
+
313
+ # Disable service
314
+ print('Disabling service...')
315
+ try:
316
+ subprocess.run(
317
+ ['systemctl', 'disable', 'git-webhooks-server'],
318
+ check=False,
319
+ capture_output=True
320
+ )
321
+ except OSError as e:
322
+ print(f'Warning: systemctl disable failed: {e}', file=sys.stderr)
323
+ # Continue anyway
324
+
325
+
326
+ def remove_service_file() -> None:
327
+ """Remove the service file
328
+
329
+ Returns:
330
+ None
331
+ """
332
+ service_path = get_service_path()
333
+
334
+ print('Removing service file...')
335
+ try:
336
+ service_path.unlink()
337
+ except OSError as e:
338
+ print(f'Warning: Failed to remove service file: {e}', file=sys.stderr)
339
+ # Continue anyway
340
+
341
+ # Reload systemd daemon
342
+ print('Reloading systemd daemon...')
343
+ try:
344
+ subprocess.run(
345
+ ['systemctl', 'daemon-reload'],
346
+ check=False,
347
+ capture_output=True
348
+ )
349
+ except OSError as e:
350
+ print(f'Warning: systemctl daemon-reload failed: {e}', file=sys.stderr)
351
+ # Continue anyway
352
+
353
+
354
+ def handle_config_cleanup(purge: bool) -> None:
355
+ """Handle configuration file cleanup
356
+
357
+ Args:
358
+ purge: If True, remove configuration file
359
+
360
+ Returns:
361
+ None
362
+ """
363
+ config_path = Path('~/.gitwebhook.ini').expanduser()
364
+
365
+ if not purge:
366
+ return
367
+
368
+ if config_path.exists():
369
+ print(f'Removing configuration file: {config_path}')
370
+ try:
371
+ config_path.unlink()
372
+ except OSError as e:
373
+ print(f'Warning: Failed to remove configuration file: {e}', file=sys.stderr)
374
+ # Continue anyway
@@ -0,0 +1,17 @@
1
+ """Configuration management module
2
+
3
+ Responsible for loading, parsing, and managing INI format configuration files.
4
+ """
5
+
6
+ from .loader import ConfigLoader
7
+ from .models import ProviderConfig, RepositoryConfig
8
+ from .registry import ConfigurationRegistry
9
+ from .server import ServerConfig
10
+
11
+ __all__ = [
12
+ 'ConfigLoader',
13
+ 'ProviderConfig',
14
+ 'RepositoryConfig',
15
+ 'ServerConfig',
16
+ 'ConfigurationRegistry',
17
+ ]