Coverage for lobster/tools/codebeamer/codebeamer.py: 63%

309 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-15 06:41 +0000

1#!/usr/bin/env python3 

2# 

3# lobster_codebeamer - Extract codebeamer items for LOBSTER 

4# Copyright (C) 2023-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU Affero General Public License as 

8# published by the Free Software Foundation, either version 3 of the 

9# License, or (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, but 

12# WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

14# Affero General Public License for more details. 

15# 

16# You should have received a copy of the GNU Affero General Public 

17# License along with this program. If not, see 

18# <https://www.gnu.org/licenses/>. 

19 

20# This tool is based on the codebeamer Rest API v3, as documented here: 

21# https://codebeamer.com/cb/wiki/11631738 

22# 

23# There are some assumptions encoded here that are not clearly 

24# documented, in that items have a type and the type has a name. 

25# 

26# 

27# Main limitations: 

28# * Item descriptions are ignored right now 

29# * Branches (if they exist) are ignored 

30# * We only ever fetch the HEAD item 

31# 

32# However you _can_ import all the items referenced from another 

33# lobster artefact. 

34 

35import os 

36import sys 

37import argparse 

38import netrc 

39from typing import Dict, Iterable, List, Optional, Sequence, TextIO, Union 

40from urllib.parse import quote, urlparse 

41from enum import Enum 

42from http import HTTPStatus 

43 

44import requests 

45from requests.adapters import HTTPAdapter 

46from requests.exceptions import ( 

47 Timeout, 

48 ConnectionError as RequestsConnectionError, 

49 RequestException, 

50) 

51import yaml 

52from urllib3.util.retry import Retry 

53 

54from lobster.common.items import Tracing_Tag, Requirement, Implementation, Activity 

55from lobster.common.location import Codebeamer_Reference 

56from lobster.common.errors import Message_Handler, LOBSTER_Error 

57from lobster.common.io import lobster_read, lobster_write, ensure_output_directory 

58from lobster.common.meta_data_tool_base import MetaDataToolBase 

59from lobster.tools.codebeamer.bearer_auth import BearerAuth 

60from lobster.tools.codebeamer.config import AuthenticationConfig, Config 

61from lobster.tools.codebeamer.exceptions import ( 

62 MismatchException, NotFileException, QueryException, 

63) 

64 

65 

66TOOL_NAME = "lobster-codebeamer" 

67 

68 

69class SupportedConfigKeys(Enum): 

70 """Helper class to define supported configuration keys.""" 

71 NUM_REQUEST_RETRY = "num_request_retry" 

72 RETRY_ERROR_CODES = "retry_error_codes" 

73 IMPORT_TAGGED = "import_tagged" 

74 IMPORT_QUERY = "import_query" 

75 BASELINE_ID = "baseline_id" 

76 VERIFY_SSL = "verify_ssl" 

77 PAGE_SIZE = "page_size" 

78 REFS = "refs" 

79 SCHEMA = "schema" 

80 CB_TOKEN = "token" 

81 CB_ROOT = "root" 

82 CB_USER = "user" 

83 CB_PASS = "pass" 

84 TIMEOUT = "timeout" 

85 OUT = "out" 

86 

87 @classmethod 

88 def as_set(cls) -> set: 

89 return {parameter.value for parameter in cls} 

90 

91 

92def get_authentication(cb_auth_config: AuthenticationConfig) -> requests.auth.AuthBase: 

93 if cb_auth_config.token: 93 ↛ 95line 93 didn't jump to line 95 because the condition on line 93 was always true

94 return BearerAuth(cb_auth_config.token) 

95 return requests.auth.HTTPBasicAuth(cb_auth_config.user, 

96 cb_auth_config.password) 

97 

98 

99def _get_response_message(response: requests.Response) -> str: 

100 try: 

101 data = response.json() 

102 if isinstance(data, dict) and "message" in data: 

103 return data["message"] 

104 except ValueError: 

105 pass 

106 

107 return response.text.strip() or "Unknown error" 

108 

109 

110def _get_http_reason(response: requests.Response) -> str: 

111 if response.reason: 111 ↛ 113line 111 didn't jump to line 113 because the condition on line 111 was always true

112 return response.reason 

113 try: 

114 return HTTPStatus(response.status_code).phrase 

115 except ValueError: 

116 return "Unknown Status" 

117 

118 

119def query_cb_single(cb_config: Config, url: str): 

120 if cb_config.num_request_retry <= 0: 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true

121 raise ValueError("Retry is disabled (num_request_retry is set to 0). " 

122 "Cannot proceed with retries.") 

123 

124 # Set up a Retry object with exponential backoff 

125 retry_strategy = Retry( 

126 total=cb_config.num_request_retry, 

127 backoff_factor=1, # Exponential backoff: 1s, 2s, 4s, etc. 

128 status_forcelist=cb_config.retry_error_codes, 

129 allowed_methods=["GET"], 

130 raise_on_status=False, 

131 ) 

132 

133 adapter = HTTPAdapter(max_retries=retry_strategy) 

134 session = requests.Session() 

135 session.mount("https://", adapter) 

136 session.mount("http://", adapter) 

137 

138 try: 

139 response = session.get( 

140 url, 

141 auth=get_authentication(cb_config.cb_auth_conf), 

142 timeout=cb_config.timeout, 

143 verify=cb_config.verify_ssl, 

144 ) 

145 except Timeout as ex: 

146 raise QueryException( 

147 "Connection timed out while contacting Codebeamer\n" 

148 f"URL: {url}\n" 

149 f"Reason: {ex}\n" 

150 "\nPossible actions:\n" 

151 "• Increase the timeout using the 'timeout' parameter" 

152 ) from ex 

153 

154 except RequestsConnectionError as ex: 

155 raise QueryException( 

156 "Unable to connect to Codebeamer\n" 

157 f"URL: {url}\n" 

158 f"Reason: {ex}\n" 

159 "\nPossible actions:\n" 

160 "• Check internet connection\n" 

161 "• Increase retries using 'num_request_retry'\n" 

162 "• Check SSL certificates or disable verification by setting " 

163 f"'{SupportedConfigKeys.VERIFY_SSL.value}' to false" 

164 ) from ex 

165 

166 except RequestException as ex: 

167 raise QueryException( 

168 "Unexpected network error while connecting to Codebeamer\n" 

169 f"URL: {url}\n" 

170 f"Reason: {ex}" 

171 "\nPossible actions:\n" 

172 "• Check network stability\n" 

173 ) from ex 

174 

175 if response.status_code == 200: 

176 return response.json() 

177 

178 error_message = _get_response_message(response) 

179 reason = _get_http_reason(response) 

180 

181 raise QueryException( 

182 "Codebeamer request failed:\n" 

183 f" URL: {url}\n" 

184 f" HTTP Status: {response.status_code} ({reason})\n" 

185 f"Reason: {error_message}" 

186 ) 

187 

188 

189def get_single_item(cb_config: Config, item_id: int): 

190 if not isinstance(item_id, int) or (item_id <= 0): 

191 raise ValueError("item_id must be a positive integer") 

192 url = f"{cb_config.base}/items/{item_id}" 

193 return query_cb_single(cb_config, url) 

194 

195 

196def get_many_items(cb_config: Config, item_ids: Iterable[int]): 

197 rv = [] 

198 

199 page_id = 1 

200 query_string = quote(f"item.id IN " 

201 f"({','.join(str(item_id) for item_id in item_ids)})") 

202 

203 while True: 

204 base_url = (f"{cb_config.base}/items/query?page={page_id}" 

205 f"&pageSize={cb_config.page_size}" 

206 f"&queryString={query_string}") 

207 data = query_cb_single(cb_config, base_url) 

208 rv += data["items"] 

209 if len(rv) == data["total"]: 

210 break 

211 page_id += 1 

212 

213 return rv 

214 

215 

216def get_query(cb_config: Config, query: Union[int, str]): 

217 if (not query) or (not isinstance(query, (int, str))): 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 raise ValueError( 

219 "The query must either be a real positive integer or a non-empty string!", 

220 ) 

221 

222 rv = [] 

223 url = "" 

224 page_id = 1 

225 total_items = None 

226 

227 while total_items is None or len(rv) < total_items: 

228 print(f"Fetching page {page_id} of query...") 

229 if isinstance(query, int): 

230 url = (f"{cb_config.base}/reports/{query}/items" 

231 f"?page={page_id}&pageSize={cb_config.page_size}") 

232 elif isinstance(query, str): 232 ↛ 237line 232 didn't jump to line 237 because the condition on line 232 was always true

233 url = (f"{cb_config.base}/items/query?page={page_id}" 

234 f"&pageSize={cb_config.page_size}&queryString={query}") 

235 if cb_config.baseline_id is not None: 

236 url += f"&baselineId={cb_config.baseline_id}" 

237 data = query_cb_single(cb_config, url) 

238 if len(data) != 4: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true

239 raise MismatchException( 

240 f"Expected codebeamer response with 4 data entries, but instead " 

241 f"received {len(data)}!", 

242 ) 

243 

244 if page_id == 1 and len(data["items"]) == 0: 

245 # lobster-trace: codebeamer_req.Get_Query_Zero_Items_Message 

246 print("This query doesn't generate items. Please check:") 

247 print(" * is the number actually correct?") 

248 print(" * do you have permissions to access it?") 

249 print(f"You can try to access '{url}' manually to check.") 

250 

251 if page_id != data["page"]: 251 ↛ 252line 251 didn't jump to line 252 because the condition on line 251 was never true

252 raise MismatchException( 

253 f"Page mismatch in query result: expected page " 

254 f"{page_id} from codebeamer, but got {data['page']}" 

255 ) 

256 

257 if page_id == 1: 

258 total_items = data["total"] 

259 elif total_items != data["total"]: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 raise MismatchException( 

261 f"Item count mismatch in query result: expected " 

262 f"{total_items} items so far, but page " 

263 f"{data['page']} claims to have sent {data['total']} " 

264 f"items in total." 

265 ) 

266 

267 if isinstance(query, int): 

268 rv += [to_lobster(cb_config, cb_item["item"]) 

269 for cb_item in data["items"]] 

270 elif isinstance(query, str): 270 ↛ 274line 270 didn't jump to line 274 because the condition on line 270 was always true

271 rv += [to_lobster(cb_config, cb_item) 

272 for cb_item in data["items"]] 

273 

274 page_id += 1 

275 

276 if total_items != len(rv): 276 ↛ 277line 276 didn't jump to line 277 because the condition on line 276 was never true

277 raise MismatchException( 

278 f"Expected to receive {total_items} items in total from codebeamer, " 

279 f"but actually received {len(rv)}!", 

280 ) 

281 

282 return rv 

283 

284 

285def get_schema_config(cb_config: Config) -> dict: 

286 """ 

287 The function returns a schema map based on the schema mentioned 

288 in the cb_config dictionary. 

289 

290 If there is no match, it raises a KeyError. 

291 

292 Positional arguments: 

293 cb_config -- configuration object containing the schema. 

294 

295 Returns: 

296 A dictionary containing the namespace and class associated with the schema. 

297 

298 Raises: 

299 KeyError -- if the provided schema is not supported. 

300 """ 

301 schema_map = { 

302 'requirement': {"namespace": "req", "class": Requirement}, 

303 'implementation': {"namespace": "imp", "class": Implementation}, 

304 'activity': {"namespace": "act", "class": Activity}, 

305 } 

306 schema = cb_config.schema.lower() 

307 

308 if schema not in schema_map: 308 ↛ 309line 308 didn't jump to line 309 because the condition on line 308 was never true

309 raise KeyError(f"Unsupported SCHEMA '{schema}' provided in configuration.") 

310 

311 return schema_map[schema] 

312 

313 

314def to_lobster(cb_config: Config, cb_item: dict): 

315 if not isinstance(cb_item, dict): 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 raise ValueError("'cb_item' must be of type 'dict'!") 

317 if "id" not in cb_item: 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true

318 raise KeyError("Codebeamer item does not contain ID!") 

319 

320 # This looks like it's business logic, maybe we should make this 

321 # configurable? 

322 

323 categories = cb_item.get("categories") 

324 if categories: 324 ↛ 325line 324 didn't jump to line 325 because the condition on line 324 was never true

325 kind = categories[0].get("name", "codebeamer item") 

326 else: 

327 kind = "codebeamer item" 

328 

329 status = cb_item["status"].get("name", None) if "status" in cb_item else None 

330 

331 # Get item name. Sometimes items do not have one, in which case we 

332 # come up with one. 

333 if "name" in cb_item: 333 ↛ 336line 333 didn't jump to line 336 because the condition on line 333 was always true

334 item_name = cb_item["name"] 

335 else: 

336 item_name = f"Unnamed item {cb_item['id']}" 

337 

338 schema_config = get_schema_config(cb_config) 

339 

340 # Construct the appropriate object based on 'kind' 

341 common_params = _create_common_params( 

342 schema_config["namespace"], cb_item, 

343 cb_config.cb_auth_conf.root, item_name, kind) 

344 item = _create_lobster_item( 

345 schema_config["class"], 

346 common_params, item_name, status) 

347 

348 if cb_config.references: 

349 for displayed_name in cb_config.references: 

350 if cb_item.get(displayed_name): 350 ↛ 355line 350 didn't jump to line 355 because the condition on line 350 was always true

351 item_references = cb_item.get(displayed_name) if ( 

352 isinstance(cb_item.get(displayed_name), list)) \ 

353 else [cb_item.get(displayed_name)] 

354 else: 

355 item_references = [value for custom_field 

356 in cb_item["customFields"] 

357 if custom_field["name"] == displayed_name and 

358 custom_field.get("values") 

359 for value in custom_field["values"]] 

360 

361 for value in item_references: 

362 item.add_tracing_target(Tracing_Tag("req", str(value["id"]))) 

363 

364 return item 

365 

366 

367def _create_common_params(namespace: str, cb_item: dict, cb_root: str, 

368 item_name: str, kind: str): 

369 """ 

370 Creates and returns common parameters for a Codebeamer item. 

371 Args: 

372 namespace (str): Namespace for the tag. 

373 cb_item (dict): Codebeamer item dictionary. 

374 cb_root (str): Root URL or path of Codebeamer. 

375 item_name (str): Name of the item. 

376 kind (str): Type of the item. 

377 Returns: 

378 dict: Common parameters including tag, location, and kind. 

379 """ 

380 return { 

381 'tag': Tracing_Tag( 

382 namespace=namespace, 

383 tag=str(cb_item["id"]), 

384 version=cb_item["version"] 

385 ), 

386 'location': Codebeamer_Reference( 

387 cb_root=cb_root, 

388 tracker=cb_item["tracker"]["id"], 

389 item=cb_item["id"], 

390 version=cb_item["version"], 

391 name=item_name 

392 ), 

393 'kind': kind 

394 } 

395 

396 

397def _create_lobster_item(schema_class, common_params, item_name, status): 

398 """ 

399 Creates and returns a Lobster item based on the schema class. 

400 Args: 

401 schema_class: Class of the schema (Requirement, Implementation, Activity). 

402 common_params (dict): Common parameters for the item. 

403 item_name (str): Name of the item. 

404 status (str): Status of the item. 

405 Returns: 

406 Object: An instance of the schema class with the appropriate parameters. 

407 """ 

408 if schema_class is Requirement: 408 ↛ 417line 408 didn't jump to line 417 because the condition on line 408 was always true

409 return Requirement( 

410 **common_params, 

411 framework="codebeamer", 

412 text=None, 

413 status=status, 

414 name= item_name 

415 ) 

416 

417 if schema_class is Implementation: 

418 return Implementation( 

419 **common_params, 

420 language="python", 

421 name= item_name, 

422 ) 

423 

424 if schema_class is Activity: 

425 return Activity( 

426 **common_params, 

427 framework="codebeamer", 

428 status=status 

429 ) 

430 

431 raise KeyError(f"Unsupported schema class '{schema_class}'!") 

432 

433 

434def import_tagged(cb_config: Config, items_to_import: Iterable[int]): 

435 rv = [] 

436 

437 cb_items = get_many_items(cb_config, items_to_import) 

438 for cb_item in cb_items: 

439 l_item = to_lobster(cb_config, cb_item) 

440 rv.append(l_item) 

441 

442 return rv 

443 

444 

445def ensure_list(instance) -> List: 

446 if isinstance(instance, list): 446 ↛ 448line 446 didn't jump to line 448 because the condition on line 446 was always true

447 return instance 

448 return [instance] 

449 

450 

451def update_authentication_parameters( 

452 auth_conf: AuthenticationConfig, 

453 netrc_path: Optional[str] = None): 

454 if (auth_conf.token is None and 454 ↛ 456line 454 didn't jump to line 456 because the condition on line 454 was never true

455 (auth_conf.user is None or auth_conf.password is None)): 

456 netrc_file = netrc_path or os.path.join(os.path.expanduser("~"), 

457 ".netrc") 

458 if os.path.isfile(netrc_file): 

459 netrc_config = netrc.netrc(netrc_file) 

460 machine = urlparse(auth_conf.root).hostname 

461 auth = netrc_config.authenticators(machine) 

462 if auth is not None: 

463 print(f"Using .netrc login for {auth_conf.root}") 

464 auth_conf.user, _, auth_conf.password = auth 

465 else: 

466 provided_machine = ", ".join(netrc_config.hosts.keys()) or "None" 

467 raise KeyError(f"Error parsing .netrc file." 

468 f"\nExpected '{machine}', but got '{provided_machine}'.") 

469 

470 if (auth_conf.token is None and 470 ↛ 472line 470 didn't jump to line 472 because the condition on line 470 was never true

471 (auth_conf.user is None or auth_conf.password is None)): 

472 raise KeyError("Please add your token to the config file, " 

473 "or use user and pass in the config file, " 

474 "or configure credentials in the .netrc file.") 

475 

476 

477def load_config(file_name: str) -> Config: 

478 """ 

479 Parses a YAML configuration file and returns a validated configuration object. 

480 

481 Args: 

482 file_name (str): Path to the YAML configuration file. 

483 

484 Returns: 

485 Config: validated configuration. 

486 

487 Raises: 

488 ValueError: If `file_name` is not a string. 

489 FileNotFoundError: If the file does not exist. 

490 KeyError: If required fields are missing or unsupported keys are present. 

491 """ 

492 with open(file_name, encoding='utf-8') as file: 

493 return parse_config_data(yaml.safe_load(file) or {}) 

494 

495 

496def parse_config_data(data: dict) -> Config: 

497 # Validate supported keys 

498 provided_config_keys = set(data.keys()) 

499 unsupported_keys = provided_config_keys - SupportedConfigKeys.as_set() 

500 if unsupported_keys: 500 ↛ 501line 500 didn't jump to line 501 because the condition on line 500 was never true

501 raise KeyError( 

502 f"Unsupported config keys: {', '.join(unsupported_keys)}. " 

503 f"Supported keys are: {', '.join(SupportedConfigKeys.as_set())}." 

504 ) 

505 

506 # create config object 

507 config = Config( 

508 references=ensure_list(data.get(SupportedConfigKeys.REFS.value, [])), 

509 import_tagged=data.get(SupportedConfigKeys.IMPORT_TAGGED.value), 

510 import_query=data.get(SupportedConfigKeys.IMPORT_QUERY.value), 

511 baseline_id=data.get(SupportedConfigKeys.BASELINE_ID.value), 

512 verify_ssl=data.get(SupportedConfigKeys.VERIFY_SSL.value, True), 

513 page_size=data.get(SupportedConfigKeys.PAGE_SIZE.value, 100), 

514 schema=data.get(SupportedConfigKeys.SCHEMA.value, "Requirement"), 

515 timeout=data.get(SupportedConfigKeys.TIMEOUT.value, 30), 

516 out=data.get(SupportedConfigKeys.OUT.value), 

517 num_request_retry=data.get(SupportedConfigKeys.NUM_REQUEST_RETRY.value, 5), 

518 retry_error_codes=data.get(SupportedConfigKeys.RETRY_ERROR_CODES.value, []), 

519 cb_auth_conf=AuthenticationConfig( 

520 token=data.get(SupportedConfigKeys.CB_TOKEN.value), 

521 user=data.get(SupportedConfigKeys.CB_USER.value), 

522 password=data.get(SupportedConfigKeys.CB_PASS.value), 

523 root=data.get(SupportedConfigKeys.CB_ROOT.value) 

524 ), 

525 ) 

526 

527 # Ensure consistency of the configuration 

528 if (not config.import_tagged) and (not config.import_query): 528 ↛ 529line 528 didn't jump to line 529 because the condition on line 528 was never true

529 raise KeyError(f"Either {SupportedConfigKeys.IMPORT_TAGGED.value} or " 

530 f"{SupportedConfigKeys.IMPORT_QUERY.value} must be provided!") 

531 

532 if config.cb_auth_conf.root is None: 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true

533 raise KeyError(f"{SupportedConfigKeys.CB_ROOT.value} must be provided!") 

534 

535 if not config.cb_auth_conf.root.startswith("https://"): 535 ↛ 536line 535 didn't jump to line 536 because the condition on line 535 was never true

536 raise KeyError(f"{SupportedConfigKeys.CB_ROOT.value} must start with https://, " 

537 f"but value is {config.cb_auth_conf.root}.") 

538 

539 if config.baseline_id is not None: 

540 if config.import_tagged: 

541 raise KeyError( 

542 f"The keys {SupportedConfigKeys.BASELINE_ID.value} and " 

543 f"{SupportedConfigKeys.IMPORT_TAGGED.value} are both present " 

544 f"in the configuration, but they are mutually exclusive!" 

545 ) 

546 if config.import_query and not isinstance(config.import_query, str): 

547 raise KeyError( 

548 f"The key {SupportedConfigKeys.BASELINE_ID.value} is only " 

549 f"allowed if {SupportedConfigKeys.IMPORT_QUERY.value} is a " 

550 f"cbQL query string, not a numeric report ID!" 

551 ) 

552 try: 

553 config.baseline_id = int(config.baseline_id) 

554 except (TypeError, ValueError) as exc: 

555 raise ValueError( 

556 f"{SupportedConfigKeys.BASELINE_ID.value} must be a positive integer." 

557 ) from exc 

558 if config.baseline_id <= 0: 

559 raise ValueError( 

560 f"{SupportedConfigKeys.BASELINE_ID.value} must be a positive integer." 

561 ) 

562 

563 return config 

564 

565 

566class CodebeamerTool(MetaDataToolBase): 

567 def __init__(self): 

568 super().__init__( 

569 name="codebeamer", 

570 description="Extract codebeamer items for LOBSTER", 

571 official=True, 

572 ) 

573 self._argument_parser.add_argument( 

574 "--config", 

575 help=(f"Path to YAML file with arguments, " 

576 f"by default (codebeamer-config.yaml) " 

577 f"supported references: '{', '.join(SupportedConfigKeys.as_set())}'"), 

578 default=os.path.join(os.getcwd(), "codebeamer-config.yaml")) 

579 

580 self._argument_parser.add_argument( 

581 "--out", 

582 help=("Name of output file"), 

583 default="codebeamer.lobster", 

584 ) 

585 

586 def _run_impl(self, options: argparse.Namespace) -> int: 

587 try: 

588 self._execute(options) 

589 return 0 

590 except NotFileException as ex: 

591 print(ex) 

592 except QueryException as query_ex: 

593 print(query_ex) 

594 except FileNotFoundError as file_ex: 

595 self._print_error(f"File '{file_ex.filename}' not found.") 

596 except IsADirectoryError as isdir_ex: 

597 self._print_error( 

598 f"Path '{isdir_ex.filename}' is a directory, but a file was expected.", 

599 ) 

600 except ValueError as value_error: 

601 self._print_error(value_error) 

602 except KeyError as key_error: 

603 self._print_error(key_error) 

604 except LOBSTER_Error as lobster_error: 

605 self._print_error(lobster_error) 

606 

607 return 1 

608 

609 @staticmethod 

610 def _print_error(error: Union[Exception, str]): 

611 print(f"{TOOL_NAME}: {error}", file=sys.stderr) 

612 

613 def _execute(self, options: argparse.Namespace) -> None: 

614 mh = Message_Handler() 

615 

616 cb_config = load_config(options.config) 

617 

618 if cb_config.out is None: 618 ↛ 619line 618 didn't jump to line 619 because the condition on line 618 was never true

619 cb_config.out = options.out 

620 

621 update_authentication_parameters(cb_config.cb_auth_conf) 

622 

623 items_to_import = set() 

624 

625 if cb_config.import_tagged: 625 ↛ 626line 625 didn't jump to line 626 because the condition on line 625 was never true

626 source_items = {} 

627 lobster_read( 

628 mh = mh, 

629 filename = cb_config.import_tagged, 

630 level = "N/A", 

631 items = source_items, 

632 ) 

633 

634 for item in source_items.values(): 

635 for tag in item.unresolved_references: 

636 if tag.namespace != "req": 

637 continue 

638 try: 

639 item_id = int(tag.tag, 10) 

640 if item_id > 0: 

641 items_to_import.add(item_id) 

642 else: 

643 mh.warning(item.location, 

644 f"invalid codebeamer reference to {item_id}") 

645 except ValueError: 

646 mh.warning( 

647 item.location, 

648 f"cannot convert reference '{tag.tag}' to integer " 

649 f"Codebeamer ID", 

650 ) 

651 

652 items = import_tagged(cb_config, items_to_import) 

653 

654 elif cb_config.import_query is not None: 654 ↛ 674line 654 didn't jump to line 674 because the condition on line 654 was always true

655 try: 

656 if isinstance(cb_config.import_query, str): 

657 if (cb_config.import_query.startswith("-") and 657 ↛ 659line 657 didn't jump to line 659 because the condition on line 657 was never true

658 cb_config.import_query[1:].isdigit()): 

659 self._argument_parser.error( 

660 "import_query must be a positive integer") 

661 elif cb_config.import_query.startswith("-"): 661 ↛ 662line 661 didn't jump to line 662 because the condition on line 661 was never true

662 self._argument_parser.error( 

663 "import_query must be a valid cbQL query") 

664 elif cb_config.import_query == "": 664 ↛ 665line 664 didn't jump to line 665 because the condition on line 664 was never true

665 self._argument_parser.error( 

666 "import_query must either be a query string or a query ID") 

667 elif cb_config.import_query.isdigit(): 667 ↛ 668line 667 didn't jump to line 668 because the condition on line 667 was never true

668 cb_config.import_query = int(cb_config.import_query) 

669 except ValueError as e: 

670 self._argument_parser.error(str(e)) 

671 

672 items = get_query(cb_config, cb_config.import_query) 

673 else: 

674 raise ValueError( 

675 f"Unclear what to do, because neither " 

676 f"'{SupportedConfigKeys.IMPORT_QUERY.value}' nor " 

677 f"'{SupportedConfigKeys.IMPORT_TAGGED.value}' is specified!", 

678 ) 

679 

680 with _get_out_stream(cb_config.out) as out_stream: 

681 _cb_items_to_lobster(items, cb_config, out_stream) 

682 if cb_config.out: 682 ↛ exitline 682 didn't return from function '_execute' because the condition on line 682 was always true

683 print(f"Written {len(items)} requirements to {cb_config.out}") 

684 

685 

686def _get_out_stream(config_out: Optional[str]) -> TextIO: 

687 if config_out: 687 ↛ 690line 687 didn't jump to line 690 because the condition on line 687 was always true

688 ensure_output_directory(config_out) 

689 return open(config_out, "w", encoding="UTF-8") 

690 return sys.stdout 

691 

692 

693def _cb_items_to_lobster(items: List[Dict], config: Config, out_file: TextIO) -> None: 

694 schema_config = get_schema_config(config) 

695 lobster_write(out_file, schema_config["class"], TOOL_NAME.replace("-", "_"), items) 

696 

697 

698def lobster_codebeamer(config: Config, out_file: str) -> None: 

699 """Loads items from codebeamer and serializes them in the LOBSTER interchange 

700 format to the given file. 

701 """ 

702 # This is an API function. 

703 items = get_query(config, config.import_query) 

704 ensure_output_directory(out_file) 

705 with open(out_file, "w", encoding="UTF-8") as fd: 

706 _cb_items_to_lobster(items, config, fd) 

707 

708 

709def main(args: Optional[Sequence[str]] = None) -> int: 

710 return CodebeamerTool().run(args)