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.
- testbench_defect_service/__init__.py +8 -0
- testbench_defect_service/__main__.py +8 -0
- testbench_defect_service/app.py +73 -0
- testbench_defect_service/cli.py +282 -0
- testbench_defect_service/clients/__init__.py +13 -0
- testbench_defect_service/clients/abstract_client.py +244 -0
- testbench_defect_service/clients/jira/__init__.py +0 -0
- testbench_defect_service/clients/jira/client.py +928 -0
- testbench_defect_service/clients/jira/config.py +333 -0
- testbench_defect_service/clients/jira/defect_mapping_service.py +207 -0
- testbench_defect_service/clients/jira/jira_client.py +639 -0
- testbench_defect_service/clients/jira/utils.py +218 -0
- testbench_defect_service/clients/jsonl/__init__.py +0 -0
- testbench_defect_service/clients/jsonl/client.py +500 -0
- testbench_defect_service/clients/jsonl/config.py +85 -0
- testbench_defect_service/clients/jsonl/utils.py +356 -0
- testbench_defect_service/clients/utils.py +178 -0
- testbench_defect_service/config.py +151 -0
- testbench_defect_service/exceptions.py +86 -0
- testbench_defect_service/log.py +294 -0
- testbench_defect_service/middleware.py +166 -0
- testbench_defect_service/models/__init__.py +0 -0
- testbench_defect_service/models/config.py +110 -0
- testbench_defect_service/models/defects.py +242 -0
- testbench_defect_service/models/logging.py +21 -0
- testbench_defect_service/openapi.yaml +716 -0
- testbench_defect_service/routes.py +209 -0
- testbench_defect_service/static/swagger-ui/favicon-16x16.png +0 -0
- testbench_defect_service/static/swagger-ui/favicon-32x32.png +0 -0
- testbench_defect_service/static/swagger-ui/index.css +16 -0
- testbench_defect_service/static/swagger-ui/index.html +19 -0
- testbench_defect_service/static/swagger-ui/swagger-initializer.js +21 -0
- testbench_defect_service/static/swagger-ui/swagger-ui-bundle.min.js +17 -0
- testbench_defect_service/static/swagger-ui/swagger-ui-standalone-preset.min.js +1 -0
- testbench_defect_service/static/swagger-ui/swagger-ui.min.css +1 -0
- testbench_defect_service/utils/__init__.py +0 -0
- testbench_defect_service/utils/auth.py +120 -0
- testbench_defect_service/utils/config.py +344 -0
- testbench_defect_service/utils/config_wizard.py +631 -0
- testbench_defect_service/utils/dependencies.py +121 -0
- testbench_defect_service/utils/import_utils.py +209 -0
- testbench_defect_service/utils/validation.py +42 -0
- testbench_defect_service/utils/wizard.py +1148 -0
- testbench_defect_service-0.1.0.dist-info/METADATA +127 -0
- testbench_defect_service-0.1.0.dist-info/RECORD +48 -0
- testbench_defect_service-0.1.0.dist-info/WHEEL +4 -0
- testbench_defect_service-0.1.0.dist-info/entry_points.txt +3 -0
- testbench_defect_service-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|