testbench-defect-service 0.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 (48) hide show
  1. testbench_defect_service/__init__.py +8 -0
  2. testbench_defect_service/__main__.py +8 -0
  3. testbench_defect_service/app.py +73 -0
  4. testbench_defect_service/cli.py +282 -0
  5. testbench_defect_service/clients/__init__.py +13 -0
  6. testbench_defect_service/clients/abstract_client.py +244 -0
  7. testbench_defect_service/clients/jira/__init__.py +0 -0
  8. testbench_defect_service/clients/jira/client.py +928 -0
  9. testbench_defect_service/clients/jira/config.py +333 -0
  10. testbench_defect_service/clients/jira/defect_mapping_service.py +207 -0
  11. testbench_defect_service/clients/jira/jira_client.py +639 -0
  12. testbench_defect_service/clients/jira/utils.py +218 -0
  13. testbench_defect_service/clients/jsonl/__init__.py +0 -0
  14. testbench_defect_service/clients/jsonl/client.py +500 -0
  15. testbench_defect_service/clients/jsonl/config.py +85 -0
  16. testbench_defect_service/clients/jsonl/utils.py +356 -0
  17. testbench_defect_service/clients/utils.py +178 -0
  18. testbench_defect_service/config.py +151 -0
  19. testbench_defect_service/exceptions.py +86 -0
  20. testbench_defect_service/log.py +294 -0
  21. testbench_defect_service/middleware.py +166 -0
  22. testbench_defect_service/models/__init__.py +0 -0
  23. testbench_defect_service/models/config.py +110 -0
  24. testbench_defect_service/models/defects.py +242 -0
  25. testbench_defect_service/models/logging.py +21 -0
  26. testbench_defect_service/openapi.yaml +716 -0
  27. testbench_defect_service/routes.py +209 -0
  28. testbench_defect_service/static/swagger-ui/favicon-16x16.png +0 -0
  29. testbench_defect_service/static/swagger-ui/favicon-32x32.png +0 -0
  30. testbench_defect_service/static/swagger-ui/index.css +16 -0
  31. testbench_defect_service/static/swagger-ui/index.html +19 -0
  32. testbench_defect_service/static/swagger-ui/swagger-initializer.js +21 -0
  33. testbench_defect_service/static/swagger-ui/swagger-ui-bundle.min.js +17 -0
  34. testbench_defect_service/static/swagger-ui/swagger-ui-standalone-preset.min.js +1 -0
  35. testbench_defect_service/static/swagger-ui/swagger-ui.min.css +1 -0
  36. testbench_defect_service/utils/__init__.py +0 -0
  37. testbench_defect_service/utils/auth.py +120 -0
  38. testbench_defect_service/utils/config.py +344 -0
  39. testbench_defect_service/utils/config_wizard.py +631 -0
  40. testbench_defect_service/utils/dependencies.py +121 -0
  41. testbench_defect_service/utils/import_utils.py +209 -0
  42. testbench_defect_service/utils/validation.py +42 -0
  43. testbench_defect_service/utils/wizard.py +1148 -0
  44. testbench_defect_service-0.1.0.dist-info/METADATA +127 -0
  45. testbench_defect_service-0.1.0.dist-info/RECORD +48 -0
  46. testbench_defect_service-0.1.0.dist-info/WHEEL +4 -0
  47. testbench_defect_service-0.1.0.dist-info/entry_points.txt +3 -0
  48. testbench_defect_service-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,8 @@
1
+ """
2
+ # TestBench Defect Service
3
+
4
+ Service to serve defects for TestBench DefectServiceWrapper interface.
5
+ """
6
+
7
+ __title__ = "TestBenchDefectService"
8
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ import multiprocessing
2
+
3
+ if __name__ == "__main__":
4
+ multiprocessing.freeze_support()
5
+
6
+ from testbench_defect_service.cli import cli
7
+
8
+ cli()
@@ -0,0 +1,73 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import ValidationError
4
+ from sanic import Sanic
5
+
6
+ from testbench_defect_service.config import AppConfig
7
+ from testbench_defect_service.exceptions import (
8
+ AppErrorHandler,
9
+ handle_jira_error,
10
+ handle_validation_error,
11
+ )
12
+ from testbench_defect_service.log import get_logging_dict
13
+ from testbench_defect_service.middleware import check_request_auth, log_request, log_response
14
+ from testbench_defect_service.routes import router
15
+ from testbench_defect_service.utils.dependencies import (
16
+ check_excel_dependencies,
17
+ check_jira_dependencies,
18
+ )
19
+
20
+
21
+ def register_middlewares(app: Sanic) -> None:
22
+ """Register application middlewares."""
23
+ app.register_middleware(log_request, "request")
24
+ app.register_middleware(check_request_auth, "request")
25
+ app.register_middleware(log_response, "response") # type: ignore
26
+
27
+
28
+ def register_exception_handlers(app: Sanic) -> None:
29
+ """Register application exception handlers."""
30
+ app.exception(ValidationError)(handle_validation_error)
31
+ try:
32
+ from jira import JIRAError # noqa: PLC0415
33
+
34
+ app.exception(JIRAError)(handle_jira_error)
35
+ except ImportError:
36
+ pass
37
+
38
+
39
+ def check_dependencies(app: Sanic) -> None:
40
+ """Check and validate optional dependencies based on client type."""
41
+ if "ExcelDefectClient" in app.config.CLIENT_CLASS:
42
+ check_excel_dependencies(raise_on_missing=True)
43
+
44
+ if "JiraDefectClient" in app.config.CLIENT_CLASS:
45
+ check_jira_dependencies(raise_on_missing=True)
46
+
47
+
48
+ def create_app(name: str, config: AppConfig | None = None) -> Sanic:
49
+ """Create and configure the Sanic application."""
50
+ if not config:
51
+ config = AppConfig()
52
+
53
+ log_config = get_logging_dict(config.LOG_CONFIG, debug=config.DEBUG)
54
+
55
+ # Create Sanic app
56
+ app = Sanic(name, log_config=log_config, error_handler=AppErrorHandler())
57
+
58
+ # Apply configuration after Sanic initialization
59
+ app.update_config(config)
60
+
61
+ # Validate dependencies
62
+ check_dependencies(app)
63
+
64
+ # Setup application
65
+ register_middlewares(app)
66
+ register_exception_handlers(app)
67
+ app.blueprint(router)
68
+
69
+ # Serve static assets
70
+ static_path = (Path(__file__).parent / "static/swagger-ui").resolve().as_posix()
71
+ app.static("/static/swagger-ui", static_path)
72
+
73
+ return app
@@ -0,0 +1,282 @@
1
+ import sys
2
+ from functools import partial
3
+ from pathlib import Path
4
+ from ssl import SSLContext
5
+
6
+ import click
7
+ from dotenv import load_dotenv
8
+ from sanic import Sanic
9
+ from sanic.worker.loader import AppLoader
10
+
11
+ from testbench_defect_service import __title__, __version__
12
+ from testbench_defect_service.app import AppConfig, create_app
13
+ from testbench_defect_service.log import logger
14
+ from testbench_defect_service.utils.config_wizard import (
15
+ configure_client_only,
16
+ configure_credentials_only,
17
+ configure_service_only,
18
+ run_full_wizard,
19
+ show_main_menu,
20
+ view_current_config,
21
+ )
22
+
23
+
24
+ def print_service_banner():
25
+ click.echo(rf""" ______ __ ____ __ ____ __ ___ _____ _
26
+ /_ __/__ _____/ /_/ __ )___ ____ _____/ /_ / __ \/ |/ / / ___/___ ______ __(_)_______
27
+ / / / _ \/ ___/ __/ __ / _ \/ __ \/ ___/ __ \ / / / / /|_/ / \__ \/ _ \/ ___/ | / / / ___/ _ \
28
+ / / / __(__ ) /_/ /_/ / __/ / / / /__/ / / / / /_/ / / / / ___/ / __/ / | |/ / / /__/ __/ version:
29
+ /_/ \___/____/\__/_____/\___/_/ /_/\___/_/ /_/ /_____/_/ /_/ /____/\___/_/ |___/_/\___/\___/ {__version__}
30
+ """) # noqa: W291, E501
31
+
32
+
33
+ def print_wizard_banner():
34
+ click.echo("╔════════════════════════════════════════════════════════╗")
35
+ click.echo("║ TestBench Defect Service - Configuration Wizard ║")
36
+ click.echo("╚════════════════════════════════════════════════════════╝\n")
37
+
38
+
39
+ @click.group()
40
+ @click.version_option(version=__version__, prog_name=__title__, message="%(prog)s %(version)s")
41
+ @click.pass_context
42
+ def cli(ctx):
43
+ ctx.max_content_width = 120
44
+ load_dotenv()
45
+
46
+
47
+ @click.command()
48
+ @click.option(
49
+ "--path",
50
+ "config_path",
51
+ type=click.Path(dir_okay=False, writable=True, path_type=Path),
52
+ metavar="PATH",
53
+ default="config.toml",
54
+ help="Path to the configuration file.",
55
+ )
56
+ def init(config_path: Path):
57
+ """Initialize service configuration interactively."""
58
+ print_wizard_banner()
59
+ run_full_wizard(config_path)
60
+
61
+
62
+ @click.command()
63
+ @click.option(
64
+ "--path",
65
+ "config_path",
66
+ type=click.Path(dir_okay=False, writable=True, path_type=Path),
67
+ default="config.toml",
68
+ help="Path to the app configuration file",
69
+ )
70
+ @click.option("--full", is_flag=True, help="Run full configuration wizard (skip menu)")
71
+ @click.option("--service-only", is_flag=True, help="Configure service settings only")
72
+ @click.option("--credentials-only", is_flag=True, help="Configure service credentials only")
73
+ @click.option("--client-only", is_flag=True, help="Configure client settings only")
74
+ @click.option("--view", is_flag=True, help="View current configuration")
75
+ def configure( # noqa: PLR0911, PLR0913, C901
76
+ config_path: Path,
77
+ full: bool,
78
+ service_only: bool,
79
+ credentials_only: bool,
80
+ client_only: bool,
81
+ view: bool,
82
+ ):
83
+ """Create or update configuration files interactively."""
84
+ print_wizard_banner()
85
+
86
+ # Handle command flags (direct modes)
87
+ if service_only:
88
+ configure_service_only(config_path)
89
+ return
90
+
91
+ if credentials_only:
92
+ configure_credentials_only(config_path)
93
+ return
94
+
95
+ if client_only:
96
+ configure_client_only(config_path)
97
+ return
98
+
99
+ if view:
100
+ view_current_config(config_path)
101
+ return
102
+
103
+ # Show menu if no flags specified
104
+ if not full:
105
+ mode = show_main_menu(config_path)
106
+ if mode is None or mode == "quit":
107
+ click.echo("\nConfiguration cancelled.")
108
+ return
109
+
110
+ if mode == "service":
111
+ configure_service_only(config_path)
112
+ return
113
+ if mode == "credentials":
114
+ configure_credentials_only(config_path)
115
+ return
116
+ if mode == "client":
117
+ configure_client_only(config_path)
118
+ return
119
+ if mode == "view":
120
+ view_current_config(config_path)
121
+ return
122
+
123
+ run_full_wizard(config_path)
124
+
125
+
126
+ @click.command()
127
+ @click.option(
128
+ "--config",
129
+ "config_path",
130
+ type=click.Path(dir_okay=False, path_type=Path),
131
+ metavar="PATH",
132
+ help=("Path to the app config file [default: config.toml]"),
133
+ )
134
+ @click.option(
135
+ "--client-class",
136
+ type=str,
137
+ metavar="PATH",
138
+ help="""Path or module string to the client class \b
139
+ [default: testbench_defect_service.clients.JsonlDefectClient]""",
140
+ )
141
+ @click.option(
142
+ "--client-config",
143
+ type=click.Path(dir_okay=False, path_type=Path),
144
+ metavar="PATH",
145
+ help="Path to the client config file [default: client_config.toml]",
146
+ )
147
+ @click.option(
148
+ "--host", type=str, metavar="HOST", help="Host to run the service on [default: 127.0.0.1]"
149
+ )
150
+ @click.option(
151
+ "--port", type=int, metavar="PORT", help="Port to run the service on [default: 8000]"
152
+ )
153
+ @click.option(
154
+ "--dev",
155
+ is_flag=True,
156
+ default=False,
157
+ show_default=True,
158
+ help="Run the service in dev mode (debug + auto reload)",
159
+ )
160
+ @click.option(
161
+ "--ssl-cert",
162
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
163
+ metavar="PATH",
164
+ help="Path to SSL certificate file for HTTPS",
165
+ )
166
+ @click.option(
167
+ "--ssl-key",
168
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
169
+ metavar="PATH",
170
+ help="Path to SSL private key file for HTTPS",
171
+ )
172
+ @click.option(
173
+ "--ssl-ca-cert",
174
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
175
+ metavar="PATH",
176
+ help="Path to CA certificate file for client verification (optional)",
177
+ )
178
+ def start( # noqa: PLR0913
179
+ config_path: Path | None = None,
180
+ client_class: str | None = None,
181
+ client_config: Path | None = None,
182
+ host: str | None = None,
183
+ port: int | None = None,
184
+ dev: bool = False,
185
+ ssl_cert: Path | None = None,
186
+ ssl_key: Path | None = None,
187
+ ssl_ca_cert: Path | None = None,
188
+ ):
189
+ """Start the TestBench Defect Service."""
190
+ app_name = __title__
191
+ app_config = AppConfig(
192
+ config_path=config_path,
193
+ client_class=client_class,
194
+ client_config_path=client_config,
195
+ host=host,
196
+ port=port,
197
+ debug=dev,
198
+ ssl_cert=ssl_cert,
199
+ ssl_key=ssl_key,
200
+ ssl_ca_cert=ssl_ca_cert,
201
+ )
202
+
203
+ print_service_banner()
204
+
205
+ factory = partial(create_app, app_name, app_config)
206
+ loader = AppLoader(factory=factory)
207
+ try:
208
+ app = loader.load()
209
+ except ImportError as e:
210
+ raise click.ClickException(str(e)) from e
211
+
212
+ logger.info("Starting %s v%s", app_name, __version__)
213
+
214
+ if not host:
215
+ host = getattr(app.config, "HOST", None)
216
+ if not port:
217
+ port = getattr(app.config, "PORT", None)
218
+
219
+ server_config = app_config.SERVER_CONFIG
220
+ ssl_context = app_config.get_ssl_context()
221
+
222
+ use_single_process = server_config.single_process
223
+ if dev and use_single_process:
224
+ logger.info(
225
+ "Dev mode enabled: switching to multi-process mode to support auto-reload. "
226
+ "Set 'server.single_process = false' explicitly to suppress this message."
227
+ )
228
+ use_single_process = False
229
+ if getattr(sys, "frozen", False) and not use_single_process:
230
+ logger.warning(
231
+ "Auto-reload is not supported in the executable. Falling back to single-process mode."
232
+ )
233
+ use_single_process = True
234
+ if isinstance(ssl_context, SSLContext) and not use_single_process:
235
+ logger.warning(
236
+ "mTLS (ssl_ca_cert) requires single-process mode. "
237
+ "Ignoring 'single_process = false' and running in single-process mode."
238
+ )
239
+ use_single_process = True
240
+
241
+ common_kwargs: dict = {
242
+ "host": host,
243
+ "port": port,
244
+ "debug": app_config.DEBUG,
245
+ "access_log": True,
246
+ "ssl": ssl_context,
247
+ **server_config.run_kwargs,
248
+ }
249
+
250
+ try:
251
+ if use_single_process:
252
+ app.run(**common_kwargs, single_process=True)
253
+ else:
254
+ app.prepare(**common_kwargs, dev=dev)
255
+ Sanic.serve(primary=app, app_loader=loader)
256
+ except Exception as e:
257
+ raise click.ClickException(f"Server could not start: {e}") from e
258
+
259
+
260
+ @click.command()
261
+ @click.option(
262
+ "--config",
263
+ "config_path",
264
+ type=click.Path(dir_okay=False, writable=True, path_type=Path),
265
+ metavar="PATH",
266
+ default="config.toml",
267
+ help="Path to the app config file",
268
+ )
269
+ @click.option("--username", type=str, help="Username (prompts if not provided)")
270
+ @click.option("--password", type=str, help="Password (prompts if not provided)")
271
+ def set_credentials(config_path, username, password):
272
+ """Set credentials for the TestBench Defect Service."""
273
+ configure_credentials_only(config_path, username=username, password=password)
274
+
275
+
276
+ cli.add_command(init)
277
+ cli.add_command(configure)
278
+ cli.add_command(set_credentials)
279
+ cli.add_command(start)
280
+
281
+ if __name__ == "__main__":
282
+ cli()
@@ -0,0 +1,13 @@
1
+ from testbench_defect_service.clients.abstract_client import AbstractDefectClient
2
+ from testbench_defect_service.clients.jsonl.client import JsonlDefectClient
3
+
4
+ try: # noqa: SIM105
5
+ from testbench_defect_service.clients.jira.client import JiraDefectClient
6
+ except ImportError:
7
+ pass
8
+
9
+ __all__ = [
10
+ "AbstractDefectClient",
11
+ "JiraDefectClient",
12
+ "JsonlDefectClient",
13
+ ]
@@ -0,0 +1,244 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import ClassVar
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from testbench_defect_service.models.defects import (
7
+ Defect,
8
+ DefectID,
9
+ DefectWithAttributes,
10
+ Protocol,
11
+ ProtocolledDefectSet,
12
+ ProtocolledString,
13
+ Results,
14
+ Settings,
15
+ SyncContext,
16
+ UserDefinedAttribute,
17
+ )
18
+
19
+
20
+ class AbstractDefectClient(ABC):
21
+ """Base class for defect management system clients.
22
+
23
+ This abstract class defines the interface for integrating with various defect
24
+ management systems (e.g., Jira, JSONL). All implementations must conform to
25
+ the API3 Defect Management API specification.
26
+
27
+ Subclasses must:
28
+ - Set CONFIG_CLASS to their specific config model type (e.g., JiraDefectClientConfig)
29
+ - Implement all abstract methods
30
+ - Raise appropriate exceptions for error conditions (NotFound, ServerError, ValidationError)
31
+ - Handle authentication and authorization through the principal in Defect objects
32
+
33
+ Exception Guidelines:
34
+ - Raise sanic.NotFound for missing resources (projects, defects)
35
+ - Raise sanic.ServerError for client/server errors
36
+ - Raise pydantic.ValidationError for invalid data
37
+ """
38
+
39
+ # Subclasses must override this to specify their config model type
40
+ CONFIG_CLASS: ClassVar[type[BaseModel] | None] = None
41
+
42
+ @abstractmethod
43
+ def __init__(self, config: BaseModel):
44
+ """Initialize the defect client with validated configuration.
45
+
46
+ Args:
47
+ config: Validated configuration object (type specified by CONFIG_CLASS)
48
+ """
49
+
50
+ @abstractmethod
51
+ def check_login(self, project: str | None) -> bool:
52
+ """Check whether the current credentials are valid.
53
+
54
+ Args:
55
+ project (str | None): Optional project identifier to scope the credential check.
56
+ If None, validates general system access.
57
+
58
+ Returns:
59
+ bool: True if credentials are valid, False otherwise.
60
+ """
61
+
62
+ @abstractmethod
63
+ def get_settings(self) -> Settings:
64
+ """Retrieve defect management system settings and metadata.
65
+
66
+ Returns:
67
+ Settings: object containing system name, description, and readonly status.
68
+ """
69
+
70
+ @abstractmethod
71
+ def get_projects(self) -> list[str]:
72
+ """Get list of all available project identifiers.
73
+
74
+ Returns:
75
+ list[str]: List of projects that the authenticated user can access.
76
+ """
77
+
78
+ @abstractmethod
79
+ def get_control_fields(self, project: str | None) -> dict[str, list[str]]:
80
+ """Return the allowed values for all defect control fields
81
+ (status, priority, classification).
82
+
83
+ Args:
84
+ project (str | None): Optional project identifier. If None, returns system-wide control fields.
85
+
86
+ Returns:
87
+ dict[str,list[str]]: Dictionary mapping each field name to its list of valid values.
88
+ Example: {"status": ["Open", "Closed"], "priority": ["High", "Low"]}
89
+ """ # noqa: E501
90
+
91
+ @abstractmethod
92
+ def get_defects(self, project: str, sync_context: SyncContext) -> ProtocolledDefectSet:
93
+ """Return all defects for a project together with a sync protocol.
94
+
95
+ Args:
96
+ project (str): Project identifier.
97
+ sync_context (SyncContext): Synchronization context containing sync options and filtering criteria.
98
+
99
+ Returns:
100
+ ProtocolledDefectSet: object containing defects and operation protocol messages.
101
+ """ # noqa: E501
102
+
103
+ @abstractmethod
104
+ def get_defects_batch(
105
+ self, project: str, defect_ids: list[DefectID], sync_context: SyncContext
106
+ ) -> ProtocolledDefectSet:
107
+ """Return a specific set of defects by their IDs.
108
+
109
+ Args:
110
+ project (str): Project identifier.
111
+ defect_ids (list[DefectID]): List of defect IDs to retrieve.
112
+ sync_context (SyncContext): Synchronization context for filtering and options.
113
+
114
+ Returns:
115
+ ProtocolledDefectSet: object containing the requested defects and protocol messages.
116
+ """
117
+
118
+ @abstractmethod
119
+ def create_defect(
120
+ self, project: str, defect: Defect, sync_context: SyncContext
121
+ ) -> ProtocolledString:
122
+ """Create a new defect in the specified project.
123
+
124
+ Args:
125
+ project (str): Project identifier where the defect will be created.
126
+ defect (Defect): Defect data including all required fields.
127
+ sync_context (SyncContext): Synchronization context for creation options.
128
+
129
+ Returns:
130
+ ProtocolledString: object containing the ID of the created defect and an operation protocol.
131
+ """ # noqa: E501
132
+
133
+ @abstractmethod
134
+ def update_defect(
135
+ self, project: str, defect_id: str, defect: Defect, sync_context: SyncContext
136
+ ) -> Protocol:
137
+ """Update an existing defect with new data.
138
+
139
+ Args:
140
+ project (str): Project identifier containing the defect.
141
+ defect_id (str): Unique identifier of the defect to update.
142
+ defect (Defect): Defect object carrying the updated field values.
143
+ sync_context (SyncContext): Synchronization context for update options.
144
+
145
+ Returns:
146
+ Protocol: object containing the operation result and any messages.
147
+ """
148
+
149
+ @abstractmethod
150
+ def delete_defect(
151
+ self, project: str, defect_id: str, defect: Defect, sync_context: SyncContext
152
+ ) -> Protocol:
153
+ """Delete an existing defect.
154
+
155
+ Args:
156
+ project (str): Project identifier containing the defect.
157
+ defect_id (str): Unique identifier of the defect to delete.
158
+ defect (Defect): Defect object carrying principal/authorization information.
159
+ sync_context (SyncContext): Synchronization context for deletion options.
160
+
161
+ Returns:
162
+ Protocol: object containing the operation result and any messages.
163
+ """
164
+
165
+ @abstractmethod
166
+ def get_defect_extended(
167
+ self, project: str, defect_id: str, sync_context: SyncContext
168
+ ) -> DefectWithAttributes:
169
+ """Return a defect together with its extended system-specific attributes.
170
+
171
+ Args:
172
+ project (str): Project identifier containing the defect.
173
+ defect_id (str): Unique identifier of the defect.
174
+ sync_context (SyncContext): Synchronization context used to determine which attributes to include.
175
+
176
+ Returns:
177
+ DefectWithAttributes: object containing the full defect data plus extended attributes.
178
+ """ # noqa: E501
179
+
180
+ @abstractmethod
181
+ def get_user_defined_attributes(self, project: str | None) -> list[UserDefinedAttribute]:
182
+ """Return the user-defined field definitions for a project.
183
+
184
+ Args:
185
+ project (str | None): Optional project identifier. If None, returns system-wide definitions.
186
+
187
+ Returns:
188
+ list[UserDefinedAttribute]: List of user-defined attribute definitions including their value types and constraints.
189
+ """ # noqa: E501
190
+
191
+ @abstractmethod
192
+ def before_sync(self, project: str, sync_type: str, sync_context: SyncContext) -> Protocol:
193
+ """Run pre-synchronization setup and validation hooks.
194
+
195
+ Called once before a sync operation begins. Implementations may use this
196
+ to acquire locks, validate state, or perform any required setup.
197
+
198
+ Args:
199
+ project (str): Project identifier to synchronize.
200
+ sync_type (str): Type of synchronization operation being started.
201
+ sync_context (SyncContext): Synchronization context and options.
202
+
203
+ Returns:
204
+ Protocol: object containing pre-sync validation results and messages.
205
+ """
206
+
207
+ @abstractmethod
208
+ def after_sync(self, project: str, sync_type: str, sync_context: SyncContext) -> Protocol:
209
+ """Run post-synchronization cleanup and finalization hooks.
210
+
211
+ Called once after a sync operation completes. Implementations may use this
212
+ to release locks, persist state, or perform any required cleanup.
213
+
214
+ Args:
215
+ project (str): Project identifier that was synchronized.
216
+ sync_type (str): Type of synchronization operation that completed.
217
+ sync_context (SyncContext): Synchronization context and options.
218
+
219
+ Returns:
220
+ Protocol: object containing post-sync results and messages.
221
+ """
222
+
223
+ @abstractmethod
224
+ def supports_changes_timestamps(self) -> bool:
225
+ """Return whether the defect management system supports change timestamps.
226
+
227
+ Returns:
228
+ bool: True if change tracking with timestamps is supported, False otherwise.
229
+ """
230
+
231
+ @abstractmethod
232
+ def correct_sync_results(self, project: str, body: Results) -> Results:
233
+ """Review and correct proposed synchronization changes before they are applied.
234
+
235
+ Allows the defect management system to filter, adjust, or reject individual
236
+ sync actions in the proposed change set.
237
+
238
+ Args:
239
+ project (str): Project identifier for the sync operation.
240
+ body (Results): Proposed sync actions for local and remote changes.
241
+
242
+ Returns:
243
+ Results: object with corrected/validated sync actions.
244
+ """
File without changes