cici-tools 0.15.0__py3-none-any.whl → 0.16.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.
cici/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.15.0'
32
- __version_tuple__ = version_tuple = (0, 15, 0)
31
+ __version__ = version = '0.16.0'
32
+ __version_tuple__ = version_tuple = (0, 16, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g858d2662b'
34
+ __commit_id__ = commit_id = 'gc5321d184'
cici/cli/bundle.py CHANGED
@@ -1,18 +1,22 @@
1
1
  # SPDX-FileCopyrightText: UL Research Institutes
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+
4
5
  import logging
5
6
  import re
6
7
  from importlib import import_module
7
8
  from io import StringIO
8
9
  from pathlib import Path
10
+ from typing import Optional
9
11
 
10
12
  from msgspec.structs import replace
11
13
  from termcolor import colored
12
14
 
13
- from ..config.project.serializers import load as load_cici_config
15
+ from cici.config.project import models as cici_config
16
+ from cici.config.project import serializers as cici_config_serializers
17
+
14
18
  from ..constants import DEFAULT_PROVIDER
15
- from ..paths import get_cici_config_file_path, get_cici_config_path
19
+ from ..paths import get_cici_config_path
16
20
 
17
21
 
18
22
  def get_bundle_name(text):
@@ -34,16 +38,40 @@ def get_bundle_content(provider, bundle, cici_config_file=None):
34
38
  return content.getvalue()
35
39
 
36
40
 
41
+ def load_cici_or_nothing(base_path: Path) -> Optional[cici_config.File]:
42
+ # try to load and resolve .cici/config.yaml
43
+ # - if config.yaml is there, load it, if not load nothing
44
+ # - if config.yaml does not exist return None
45
+
46
+ base_path = Path(base_path).resolve()
47
+
48
+ if base_path.name == ".cici":
49
+ base_path = base_path.parent
50
+
51
+ config_path_root = base_path / ".cici" / "config.yaml"
52
+
53
+ # load config.yaml if it exists
54
+ config_path = None
55
+ if config_path_root.exists():
56
+ config_path = config_path_root
57
+
58
+ if config_path:
59
+ config_file = cici_config_serializers.load(config_path)
60
+ # if load() succeeds then resove targets
61
+ return cici_config_serializers.resolve_targets(config_file, config_path)
62
+
63
+ return None
64
+
65
+
37
66
  def bundle_command(parser, args):
38
67
  logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
39
68
 
40
69
  provider = import_module(f".{DEFAULT_PROVIDER}", "cici.providers")
41
70
 
42
- # preserve backwards compatibility (i.e. no .cici/config.yaml)
43
- try:
44
- cici_config_file = load_cici_config(get_cici_config_file_path())
45
- except FileNotFoundError:
46
- cici_config_file = None
71
+ cici_config_file = load_cici_or_nothing(args.config_path)
72
+
73
+ if not cici_config_file:
74
+ logging.warning(f"No config.yaml found in .cici")
47
75
 
48
76
  ci_file_path = args.config_path / provider.CI_FILE
49
77
 
@@ -1,7 +1,11 @@
1
1
  # SPDX-FileCopyrightText: UL Research Institutes
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ # Loads .cici/config.yaml
5
+ # Defines targes, variables and metadata about the project.
4
6
 
7
+
8
+ import logging
5
9
  import re
6
10
  from pathlib import Path
7
11
  from typing import Any, Optional, Union
@@ -13,22 +17,97 @@ from msgspec.structs import replace
13
17
  from . import models as cici_config
14
18
 
15
19
  decoder = msgspec.json.Decoder(type=cici_config.File)
20
+ target_decoder = msgspec.json.Decoder(type=cici_config.Target)
16
21
  image_fqdn_regex = re.compile(r"^[\w\.-]+/")
17
22
 
18
23
 
19
- def inject_variable_names(variables: dict[str, dict]) -> dict[str, dict]:
20
- # make sure each variable has its 'name' field set from its key
24
+ # make sure each variable has its 'name' field set from its key
25
+ def inject_variable_names(variables: dict[str, Union[str, dict]]) -> dict[str, dict]:
21
26
  patched = {}
22
27
  for key, value in variables.items():
28
+ if isinstance(value, str):
29
+ value = {"value": value}
23
30
  # if value is None or not a dict, treat it as empty dict
24
31
  if not isinstance(value, dict):
25
- value = {}
32
+ raise TypeError(f"Expected dict for variable {key}, got {type(value)}")
26
33
  # only add name if missing
27
34
  value.setdefault("name", key)
28
35
  patched[key] = value
29
36
  return patched
30
37
 
31
38
 
39
+ def load_targets_from_dir(target_dir: Path) -> list[cici_config.Target]:
40
+ if not target_dir.exists() or not target_dir.is_dir():
41
+ return []
42
+
43
+ yaml = ruamel.yaml.YAML(typ="safe")
44
+ targets: list[cici_config.Target] = []
45
+
46
+ # checking for .yaml
47
+ for file in sorted(target_dir.glob("*.yaml")):
48
+ try:
49
+ with open(file, "r", encoding="utf-8") as f:
50
+ data = yaml.load(f) or {}
51
+
52
+ # make sure name exists before decoding
53
+ data.setdefault("name", file.stem)
54
+
55
+ # decode using msgspec for validation and type safety
56
+ target_obj = target_decoder.decode(msgspec.json.encode(data))
57
+ targets.append(target_obj)
58
+
59
+ except msgspec.ValidationError as e:
60
+ logging.warning(f"Validation failed for {file.name}: {e}")
61
+ except Exception as e:
62
+ logging.warning(f"Failed to load target {file.name}: {e}")
63
+
64
+ logging.info(f"Loaded {len(targets)} valid targets from {target_dir}")
65
+ return targets
66
+
67
+
68
+ def resolve_targets(
69
+ cici_config_file: Optional[cici_config.File], config_path: Path
70
+ ) -> Optional[cici_config.File]:
71
+ # Resolve and merge targets from .cici/config.yaml and from .cici/targets/*.yaml
72
+
73
+ # - Directory targets override config.yaml targets with the same name
74
+ # - If no targets dir exists, use the targets defined in the config.yaml
75
+ # - returns updated 'cici_config.File' with merged targets
76
+
77
+ if not cici_config_file:
78
+ return None
79
+
80
+ # get directory that could exist targets
81
+ targets_dir = config_path.parent / "targets"
82
+
83
+ # get targets from config.yaml
84
+ config_targets: list[cici_config.Target] = list(cici_config_file.targets)
85
+
86
+ # load targets from '.cici/targets/' if there are any
87
+ dir_targets: list[cici_config.Target] = []
88
+ if targets_dir.exists() and any(targets_dir.glob("*.yaml")):
89
+ logging.info(f"loading targets from directory: {targets_dir}")
90
+ dir_targets = load_targets_from_dir(targets_dir)
91
+
92
+ # combine all targets
93
+ all_targets = list(config_targets) + list(dir_targets)
94
+
95
+ # get target names
96
+ target_names = [
97
+ target.name for target in all_targets if getattr(target, "name", None)
98
+ ]
99
+
100
+ # check duplicates
101
+ if len(target_names) != len(set(target_names)):
102
+ raise ValueError(
103
+ f"Duplicate target names found: {', '.join(sorted(target_names))}"
104
+ )
105
+
106
+ # create new file with merged target list
107
+ resolved_file = replace(cici_config_file, targets=all_targets)
108
+ return resolved_file
109
+
110
+
32
111
  def patch_image(image: str, container_proxy: str = "${CONTAINER_PROXY}") -> str:
33
112
  """Patch in $CONTAINER_PROXY to image unless the following are true:
34
113
 
@@ -64,15 +143,21 @@ def loads(
64
143
 
65
144
  yaml = ruamel.yaml.YAML(typ="safe")
66
145
  data = yaml.load(text)
146
+
67
147
  # verify targets exists even if empty
68
148
  data.setdefault("targets", [])
69
149
 
70
150
  # Inject precommit/gitlab includes into each target
151
+
152
+ # Debug to test injection
153
+ # print("Before injection:", data["targets"])
154
+
71
155
  for target in data["targets"]:
72
- if target["name"] in precommit_hooks:
73
- target["precommit_hook"] = {"name": target["name"]}
74
- if target["name"] in gitlab_ci_jobs:
75
- target["gitlab_include"] = {"name": target["name"]}
156
+ target["precommit_hook"] = {"name": target["name"]}
157
+ target["gitlab_include"] = {"name": target["name"]}
158
+
159
+ # Debug to test injection
160
+ # print("After injection:", data["targets"])
76
161
 
77
162
  if "variables" in data:
78
163
  data["variables"] = inject_variable_names(data["variables"])
@@ -1,6 +1,8 @@
1
1
  # SPDX-FileCopyrightText: UL Research Institutes
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ # Loads and serializes .gitlab-ci.yml and CI job definitions
5
+
4
6
  import io
5
7
  import typing
6
8
  from pathlib import Path
@@ -249,41 +251,33 @@ def loads(
249
251
  yaml = ruamel.yaml.YAML()
250
252
  data = yaml.load(text)
251
253
 
252
- # 1. Normalize scalars
254
+ # Normalize scalars
253
255
  data = normalize_scalars(data)
254
256
 
255
- # 2. merge top-level config variables
257
+ # merge top-level config variables
256
258
  if cici_config_file:
257
259
  data = add_config_variables(data, cici_config_file=cici_config_file)
258
260
 
259
- # DEBUG
260
- # print("CONFIG FILE TYPE:", type(cici_config_file))
261
- # print("CONFIG FILE CONTENT:", getattr(cici_config_file, "variables", None))
262
-
263
- # 4. normalize jobs
261
+ # normalize jobs
264
262
  data = normalize_jobs_in_data(data)
265
263
 
266
- # 5. normalize variables format
264
+ # normalize variables format
267
265
  data = normalize_variables(data)
268
266
 
269
- # 6. inject variable names
267
+ # inject variable names
270
268
  if "variables" in data:
271
269
  data["variables"] = inject_variable_names(data["variables"])
272
270
 
273
- # 7. pack the jobs
271
+ # pack the jobs
274
272
  data = pack_jobs(data)
275
273
 
276
- # DEBUG
277
- # print("Keys at top level before decode:", data.keys())
278
- # print("Variables at top level:", data.get("variables"))
279
-
280
- # 8. decode into msgspec struct
274
+ # decode into msgspec struct
281
275
  file_struct = decode_file(data)
282
276
 
283
- # 9. expand extends and anchors
277
+ # expand extends and anchors
284
278
  file_struct = expand_jobs(file_struct)
285
279
 
286
- # 10. inject container defaults
280
+ # inject container defaults
287
281
  return inject_container_into_job(file_struct, cici_config_file)
288
282
 
289
283
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cici-tools
3
- Version: 0.15.0
3
+ Version: 0.16.0
4
4
  Summary: Continuous Integration Catalog Interface
5
5
  Author-email: Digital Safety Research Institute <contact@dsri.org>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  cici/__init__.py,sha256=wdYYpZKxK2omc_mRJgPj86Ec-QwoB4noZzlk71sBjnk,87
2
2
  cici/__main__.py,sha256=TWDBrcS5vXSclLWLm0Iu3uUXOkDw0PzcJ8a0zd7_ClU,150
3
- cici/_version.py,sha256=yVM3H6SCPkszqQN_Lm-MropXHFmwVrg3EdCRYTwK0gw,714
3
+ cici/_version.py,sha256=nFweIwOtVfmijqw6TBaMFicDb1QpkX-juuqHfIWXZJw,714
4
4
  cici/constants.py,sha256=ydShHTAbuanWWXeg9tJZ5GLm65M41AKmIibUheKV5nw,913
5
5
  cici/exceptions.py,sha256=JMpcD5YjYwKdn65D0iOEchXiAgIVsKApAyngCTwVgI4,139
6
6
  cici/main.py,sha256=ceH9s7i2XdB-mpUtDWuzLVn9TZHiwmsq-daZOgB9oRI,748
@@ -8,7 +8,7 @@ cici/paths.py,sha256=R-zBlX70uUJKR-ttGmZ7gI8iIKdxz9hLN1bP1pSXZxI,1103
8
8
  cici/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  cici/utils.py,sha256=E6GrD6qCvsFqaR-fhsuBf8qotxyUO2ZdOoKj2vRUUHs,1917
10
10
  cici/cli/__init__.py,sha256=wdYYpZKxK2omc_mRJgPj86Ec-QwoB4noZzlk71sBjnk,87
11
- cici/cli/bundle.py,sha256=Xv0mlUidcrwx3cf4k2awRZcvQYDgz4d9EjDFq2BYsPw,3152
11
+ cici/cli/bundle.py,sha256=XTsyXdlJHTK4Ca0i0S637pOIendaAI6hra5GdRgrYsw,3928
12
12
  cici/cli/readme.py,sha256=th7PjmsckF3d0roVmV6Y4-1Azz9TGbaDXcyvPX3tjZQ,2496
13
13
  cici/cli/schema.py,sha256=tQNirJrMyUf96iaA8XBmALeo4EvdRGqWLkTKCtvWR5M,1623
14
14
  cici/cli/update.py,sha256=8PZpd6HqJucoZDhLLX8eHAMSFh-Ngox6UYiVqsy6ZCs,4825
@@ -16,13 +16,13 @@ cici/config/__init__.py,sha256=TIewA-VCl2VYl_d0EviE3EEQSmBtDwjU26R68pDT38E,141
16
16
  cici/config/user.py,sha256=UBz21P-Vj41rKy2DAZwdAp5vcliAEEQlEQeEfHk-EZ4,841
17
17
  cici/config/project/__init__.py,sha256=wdYYpZKxK2omc_mRJgPj86Ec-QwoB4noZzlk71sBjnk,87
18
18
  cici/config/project/models.py,sha256=Ao8MGliXihoYRgqplWQAIyC3r4Jb0aj53F7EhgZGcu4,5873
19
- cici/config/project/serializers.py,sha256=sQxRzMlXTWfUbkJBiu5huvkUc8z5xYVfsoYs8W55Aqw,3202
19
+ cici/config/project/serializers.py,sha256=Mlqodt4j2Q4KXK-LjxAA8jdopCenbBMokQ-LfITtJRc,6119
20
20
  cici/providers/__init__.py,sha256=wdYYpZKxK2omc_mRJgPj86Ec-QwoB4noZzlk71sBjnk,87
21
21
  cici/providers/gitlab/__init__.py,sha256=8ej6k3hd25UFOGFhHymLa2mvz9hEwMjdxyu66hnJwOQ,259
22
22
  cici/providers/gitlab/constants.py,sha256=nA36aqGvwBBood8lTFJueEY8lKEsOVjkp6ciMjAeKJA,1786
23
23
  cici/providers/gitlab/models.py,sha256=zPOLSfb811yVYgkKNirAEhcAPkoBwE-r_fmxMfxn8GU,7084
24
24
  cici/providers/gitlab/normalizers.py,sha256=EFQiJzRDkoahes0zwIMahuVC3DOy9A97eZDhMXdamno,4762
25
- cici/providers/gitlab/serializers.py,sha256=RHqy5ZI061U41PzMxBeOITOGIErxRIS6GW1iWOzTKFw,10835
25
+ cici/providers/gitlab/serializers.py,sha256=WmIFhztf5-oLhitJwzezKcKp4hMdctZgm9BaJWbELtw,10581
26
26
  cici/providers/gitlab/utils.py,sha256=NUZ25rAosSWQyWh7m3xhiHRotqVl1krckZL58yR0LKQ,651
27
27
  cici/providers/gitlab/yaml_style.py,sha256=Obqm6JxmGgksCG_KYOFL9JH3S-bwzH9qFRvtD68mNjs,13409
28
28
  cici/schema/LICENSE.gitlab,sha256=0N7FHuinuoGBqZE6aUlD84yw3CGylx8lyfzertgmtY0,2001
@@ -39,10 +39,10 @@ cici/templates/target-table.md.j2,sha256=NYspLbP3ap1706WnehZRtu6a2uxL_mKf7zfm3y0
39
39
  cici/templates/targets.md.j2,sha256=k-lkfReFJMCu9t2SjuD5YJ-Q_KECJxdY7v-0TWDTaCM,116
40
40
  cici/templates/variable-list.md.j2,sha256=M9r3j7d8H171okQNj8j4pc92kMdeWSRoeZQ5QB3NUTQ,613
41
41
  cici/templates/variables.md.j2,sha256=2DquXagNupqBkD95_3ZI4guJOK-eG_igaaMdRIUDrcE,84
42
- cici_tools-0.15.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
43
- cici_tools-0.15.0.dist-info/licenses/NOTICE,sha256=dIBr-7sfvnoYXYvgHIozlRJTLD4EHORRtRJtIe4znW8,55
44
- cici_tools-0.15.0.dist-info/METADATA,sha256=sRoea8HJWGpWRw_55L3T4-uAHgOxWWuDPR-rSf-bdKk,4178
45
- cici_tools-0.15.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
- cici_tools-0.15.0.dist-info/entry_points.txt,sha256=CuBAwYG3z7qOKm4IYARjZzdX5fytMLKyr59HLLJx_Is,44
47
- cici_tools-0.15.0.dist-info/top_level.txt,sha256=sv8xIjFuuqtyBMoyzueczNvZo_--q12r64Zc0lGKKF8,5
48
- cici_tools-0.15.0.dist-info/RECORD,,
42
+ cici_tools-0.16.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
43
+ cici_tools-0.16.0.dist-info/licenses/NOTICE,sha256=dIBr-7sfvnoYXYvgHIozlRJTLD4EHORRtRJtIe4znW8,55
44
+ cici_tools-0.16.0.dist-info/METADATA,sha256=-rzT_wwEirgiEcT2jelxH3ikrkXUuqhO91-K9b97CtM,4178
45
+ cici_tools-0.16.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ cici_tools-0.16.0.dist-info/entry_points.txt,sha256=CuBAwYG3z7qOKm4IYARjZzdX5fytMLKyr59HLLJx_Is,44
47
+ cici_tools-0.16.0.dist-info/top_level.txt,sha256=sv8xIjFuuqtyBMoyzueczNvZo_--q12r64Zc0lGKKF8,5
48
+ cici_tools-0.16.0.dist-info/RECORD,,