Skip to content

Commit

Permalink
LOBSTER-Codebeamer tool to use configuration files
Browse files Browse the repository at this point in the history
- Replaced command-line arguments with a YAML-based configuration file for improved usability and maintainability.
- Simplifies parameter management and enhances readability.
  • Loading branch information
SurajBDeore committed Dec 5, 2024
1 parent 0a90610 commit 4d1e10c
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 176 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

### 0.9.20-dev

* Enhanced Configuration Management: Transitioned from command-line arguments to YAML
configuration files for LOBSTER-Codebeamer tools.

**Rationale:** Managing numerous parameters via command-line arguments was cumbersome and error-prone for users.

**Benefits:** Improved configurability, better readability, and simplified management of tool settings.

* `lobster-gtest` accepts XML nodes other than `testcase`, but ignores them.

### 0.9.19
Expand Down
216 changes: 108 additions & 108 deletions lobster/tools/codebeamer/codebeamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
import netrc
from urllib.parse import quote
from enum import Enum
import json
import requests
import yaml

from lobster.items import Tracing_Tag, Requirement, Implementation, Activity
from lobster.location import Codebeamer_Reference
Expand All @@ -55,15 +55,26 @@


class SupportedConfigKeys(Enum):
REFS = "refs"
SCHEMA = "schema"
"""Helper class to define supported configuration keys."""
IMPORT_TAGGED = "import_tagged"
IMPORT_QUERY = "import_query"
VERIFY_SSL = "verify_ssl"
PAGE_SIZE = "page_size"
REFS = "refs"
SCHEMA = "schema"
CB_TOKEN = "token"
CB_ROOT = "root"
CB_USER = "user"
CB_PASS = "pass"
TIMEOUT = "timeout"
OUT = "out"

@classmethod
def as_set(cls) -> set:
return {parameter.value for parameter in cls}


def add_refs_refrences(req, flat_values_list):
def add_refs_references(req, flat_values_list):
# refs
for value in flat_values_list:
if value.get("id"):
Expand All @@ -72,7 +83,7 @@ def add_refs_refrences(req, flat_values_list):


map_reference_name_to_function = {
SupportedConfigKeys.REFS.value: add_refs_refrences
SupportedConfigKeys.REFS.value: add_refs_references
}


Expand Down Expand Up @@ -234,12 +245,7 @@ def to_lobster(cb_config, cb_item):
else:
kind = "codebeamer item"

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

# TODO: Parse item text
status = cb_item["status"].get("name", None) if "status" in cb_item else None

# Get item name. Sometimes items do not have one, in which case we
# come up with one.
Expand Down Expand Up @@ -270,12 +276,11 @@ def to_lobster(cb_config, cb_item):
isinstance(cb_item.get(displayed_name), list)) \
else [cb_item.get(displayed_name)]
else:
flat_values_list = (
list(value for custom_field
in cb_item["customFields"]
if custom_field["name"] == displayed_name and
custom_field.get("values")
for value in custom_field["values"]))
flat_values_list = [value for custom_field
in cb_item["customFields"]
if custom_field["name"] == displayed_name and
custom_field.get("values")
for value in custom_field["values"]]
if not flat_values_list:
continue

Expand Down Expand Up @@ -373,108 +378,102 @@ def ensure_array_of_strings(instance):
return [str(instance)]


def parse_cb_config(file_name):
assert isinstance(file_name, str)
assert os.path.isfile(file_name)
def parse_yaml_config(file_name: str):
"""
Parses a YAML configuration file and returns a validated configuration dictionary.
with open(file_name, "r", encoding='utf-8') as file:
data = json.loads(file.read())
Args:
file_name (str): Path to the YAML configuration file.
json_config = {REFERENCES: {}}
Returns:
Dict[str, Any]: Parsed and validated configuration.
if TOKEN in data:
json_config["token"] = data.pop(TOKEN)
Raises:
ValueError: If `file_name` is not a string.
FileNotFoundError: If the file does not exist.
KeyError: If required fields are missing or unsupported keys are present.
"""
if not isinstance(file_name, str):
raise ValueError("File name must be a string.")
if not os.path.isfile(file_name):
raise FileNotFoundError(f"File not found: {file_name}")

default_values = {
'timeout': 30,
'page_size': 100,
'verify_ssl': False,
'schema': 'Requirement',
}

provided_config_keys = set(data.keys())
schema = data.get("schema", "Requirement").lower()
required_fields = {"import_tagged", "import_query"}

if not provided_config_keys.issubset(SupportedConfigKeys.as_set()):
raise KeyError("The provided config keys are not supported! "
"supported keys: '%s'" %
', '.join(SupportedConfigKeys.as_set()))
with open(file_name, "r", encoding='utf-8') as file:
data = yaml.safe_load(file) or {}

# Ensure at least one required field is present
if not required_fields & data.keys():
raise KeyError(f"One of the required fields "
f"must be present: {', '.join(required_fields)}")

# Build the configuration dictionary
json_config = {
"references": {
"refs": ensure_array_of_strings(data["refs"])
} if "refs" in data else {},
"token": data.pop("token", None),
"base": f"{data.get('root', '')}/cb/api/v3",
}

for key, value in data.items():
json_config[REFERENCES][key] = ensure_array_of_strings(value)
# Validate supported keys
provided_config_keys = set(data.keys())
unsupported_keys = provided_config_keys - SupportedConfigKeys.as_set()
if unsupported_keys:
raise KeyError(
f"Unsupported config keys: {', '.join(unsupported_keys)}. "
f"Supported keys are: {', '.join(SupportedConfigKeys.as_set())}."
)

json_config[key] = ensure_array_of_strings(value)
# Merge with default values
json_config.update({key: data.get(key, default_values.get(key))
for key in SupportedConfigKeys.as_set()})

json_config["schema"] = schema
print(json_config)

return json_config


def main():
# lobster-trace: codebeamer_req.Dummy_Requirement
ap = argparse.ArgumentParser()

modes = ap.add_mutually_exclusive_group(required=True)
modes.add_argument("--import-tagged",
metavar="LOBSTER_FILE",
default=None)

modes.add_argument("--import-query",
metavar="CB_QUERY_ID",
default=None)

ap.add_argument("--config",
help=("name of codebeamer "
"config file, supported references: '%s'" %
help=("Path to YAML file with arguments, "
"by default (codebeamer-config.yaml) "
"supported references: '%s'" %
', '.join(SupportedConfigKeys.as_set())),
default=None)

ap.add_argument("--ignore-ssl-errors",
action="store_true",
default=False,
help="ignore ssl errors and accept any certificate")

ap.add_argument("--query-size",
type=int,
default=100,
help=("Fetch this many cb items at once (by default 100),"
" reduce if you get too many timeouts."))

ap.add_argument("--timeout",
type=int,
default=30,
help="Timeout in s (by default 30) for each REST call.")

ap.add_argument("--schema",
default='requirement',
help="Specify the output schema"
"(Requirement, Implementation, Activity).")

ap.add_argument("--cb-root", default=os.environ.get("CB_ROOT", None))
ap.add_argument("--cb-user", default=os.environ.get("CB_USERNAME", None))
ap.add_argument("--cb-pass", default=os.environ.get("CB_PASSWORD", None))
ap.add_argument("--cb-token", default=None)
ap.add_argument("--out", default=None)
default=os.path.join(os.getcwd(), "codebeamer-config.yaml"))

ap.add_argument("--out",
help=("Name of output file"),
default="codebeamer.lobster")

options = ap.parse_args()

mh = Message_Handler()

cb_config = {
'schema' : options.schema,
"root" : options.cb_root,
"base" : "%s/cb/api/v3" % options.cb_root,
"user" : options.cb_user,
"pass" : options.cb_pass,
"token" : options.cb_token,
"verify_ssl" : not options.ignore_ssl_errors,
"page_size" : options.query_size,
"timeout" : options.timeout,
}
if not os.path.isfile(options.config):
print((f"lobster-codebeamer: Config file '{options.config}' not found."))
return 1

if options.config:
if os.path.isfile(options.config):
cb_config.update(parse_cb_config(options.config))
else:
ap.error("cannot open config file '%s'" % options.config)
cb_config = parse_yaml_config(options.config)

if cb_config["out"] is None:
cb_config["out"] = options.out

if cb_config["root"] is None:
ap.error("please set CB_ROOT or use --cb-root")
sys.exit("lobster-codebeamer: Please set 'root' in the config file")

if not cb_config["root"].startswith("https://"):
ap.error("codebeamer root %s must start with https://")
sys.exit(f"Codebeamer root {cb_config['root']} must start with https://")

if (cb_config["token"] is None and
(cb_config["user"] is None or cb_config["pass"] is None)):
Expand All @@ -484,23 +483,24 @@ def main():
netrc_config = netrc.netrc()
auth = netrc_config.authenticators(cb_config["root"][8:])
if auth is not None:
print("using .netrc login for %s" % cb_config["root"])
print("Using .netrc login for %s" % cb_config["root"])
cb_config["user"], _, cb_config["pass"] = auth

if (cb_config["token"] is None and
(cb_config["user"] is None or cb_config["pass"] is None)):
ap.error("please set --cb-token or add your token to the config-file"
sys.exit("lobster-codebeamer: please set --cb-token"
"or add your token to the config-file"
"or use --cb-user and --cb-pass")

items_to_import = set()

if options.import_tagged:
if not os.path.isfile(options.import_tagged):
ap.error("%s is not a file" % options.import_tagged)
if cb_config.get("import_tagged"):
if not os.path.isfile(cb_config["import_tagged"]):
sys.exit(f"lobster-codebeamer: {cb_config['import_tagged']} is not a file.")
items = {}
try:
lobster_read(mh = mh,
filename = options.import_tagged,
filename = cb_config["import_tagged"],
level = "N/A",
items = items)
except LOBSTER_Error:
Expand All @@ -520,31 +520,31 @@ def main():
except ValueError:
pass

elif options.import_query:
elif cb_config.get("import_query"):
try:
query_id = int(options.import_query)
query_id = int(cb_config["import_query"])
except ValueError:
ap.error("query-id must be an integer")
sys.exit("lobster-codebeamer: Query ID must be an integer.")
if query_id < 1:
ap.error("query-id must be a positive")
sys.exit("lobster-codebeamer: Query ID must be a positive integer.")

try:
if options.import_tagged:
if cb_config.get("import_tagged"):
items = import_tagged(mh, cb_config, items_to_import)
elif options.import_query:
elif cb_config.get("import_query"):
items = get_query(mh, cb_config, query_id)
except LOBSTER_Error:
return 1

schema_config = get_schema_config(cb_config)

if options.out is None:
if cb_config["out"] is None:
with sys.stdout as fd:
lobster_write(fd, schema_config["class"], "lobster_codebeamer", items)
else:
with open(options.out, "w", encoding="UTF-8") as fd:
with open(cb_config["out"], "w", encoding="UTF-8") as fd:
lobster_write(fd, schema_config["class"], "lobster_codebeamer", items)
print(f"Written {len(items)} requirements to {options.out}")
print(f"Written {len(items)} requirements to {cb_config['out']}")

return 0

Expand Down
Loading

0 comments on commit 4d1e10c

Please sign in to comment.