cfglock 0.1.0__tar.gz

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.
cfglock-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.3
2
+ Name: cfglock
3
+ Version: 0.1.0
4
+ Summary: The official Python engine for the ConfigLock ecosystem
5
+ Author: Philipp.Stahlberg
6
+ Author-email: Philipp.Stahlberg <131917964+PhilippStahlbergGit@users.noreply.github.com>
7
+ Requires-Dist: dotenv>=0.9.9
8
+ Requires-Dist: pyyaml>=6.0.3
9
+ Requires-Dist: requests>=2.34.2
10
+ Requires-Dist: typer>=0.25.1
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+
14
+ <p align="center">
15
+ <img src="https://github.com/phalberg/configlock/actions/workflows/ci-cd.yaml/badge.svg" alt="CI Status" />
16
+ <img src="https://img.shields.io/badge/python-3.13-blue.svg" alt="Python Version" />
17
+ <a href="https://codecov.io/github/phalberg/configlock" >
18
+ <img src="https://codecov.io/github/phalberg/configlock/graph/badge.svg?token=60JBRC22NB"/>
19
+ </a>
20
+ </p>
21
+
22
+ # ConfigLock
23
+ ConfigLock is a lightweight CLI tool designed to prevent production outages by configuration errors. It brings the concept of Lockfiles (inspiration from uv) to your application’s .yaml or .json configurations.
24
+
25
+ # Quick start
26
+
27
+ ```
28
+ # Clone the repo
29
+ git clone https://github.com/phalberg/configlock
30
+ cd configlock
31
+
32
+ # Install dependencies
33
+ uv sync
34
+
35
+ # Initialize a lockfile
36
+ uv run configlock init my_config.yaml
37
+
38
+ # Sync after changes
39
+ uv run configlock sync my_config.yaml
40
+ ```
41
+
42
+ If you wish to not write `uv` each time, you can do as such:
43
+
44
+ ## CLI Usage
45
+
46
+ Install the project in editable mode while developing:
47
+
48
+ ```bash
49
+ pip install -e .
50
+ ```
51
+
52
+ Run the command directly after install:
53
+
54
+ ```bash
55
+ configlock --help
56
+ configlock init {path_to_your_file}
57
+ configlock lock {path_to_your_file}
58
+ configlock sync {path_to_your_file}
59
+ ```
60
+
61
+ Preview the docs locally with hot reload:
62
+
63
+ ```bash
64
+ uv run docs-serve
65
+ ```
66
+
67
+ # Status
68
+ ConfigLock is a **personal hobby project** focused on learning robust CLI development and structural validation logic.
69
+
70
+ > [!NOTE]
71
+ > This project is in an early prototype stage. It is a learning exercise in building developer tools with Python and Typer.
72
+
73
+ ### Roadmap
74
+ - [x] Basic CLI integration with Typer
75
+ - [x] GitHub Actions CI/CD pipeline
76
+ - [x] Recursive Type & Structure checking
77
+ - [ ] GitHub API integration (Fetch remote configs)
78
+ - [ ] Web-based UI for configuration visualization
79
+
80
+ # The problem
81
+ In modern DevOps, non-technical team members often need to edit configuration files (YAML/JSON). One missing key or a wrong data type (e.g., entering a string where a boolean is expected) may crash a production environment.
82
+
83
+ # Init
84
+ ```bash
85
+ init: Analyzes your YAML/JSON and creates a config.lock.json that stores the required structure and types.
86
+ ```
87
+ _Note: ConfigLock generates one unique lockfile corresponding to the file path provided._
88
+
89
+ # Sync
90
+
91
+ ```bash
92
+ sync: Compares your current YAML/JSON against the lockfile. If a key is missing or a type has changed, you get an error.
93
+ ```
94
+
95
+ # Lock
96
+ ```bash
97
+ lock: Checks your current YAML/JSON and tries to replace the locked file with the new changed current file, if the change is not compatible, you get an error.
98
+
99
+ ```
100
+ ## Lock with strict ordering
101
+ Please use the command:
102
+ ```bash
103
+ configlock lock {path_to_your_file} --order-matters
104
+ ```
105
+ If the order of the keys matter, if not the default:
106
+ ```bash
107
+ configlock lock {path_to_your_file} --no-order-matters
108
+ ```
109
+ will be set.
110
+
111
+ # License
112
+
113
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,100 @@
1
+ <p align="center">
2
+ <img src="https://github.com/phalberg/configlock/actions/workflows/ci-cd.yaml/badge.svg" alt="CI Status" />
3
+ <img src="https://img.shields.io/badge/python-3.13-blue.svg" alt="Python Version" />
4
+ <a href="https://codecov.io/github/phalberg/configlock" >
5
+ <img src="https://codecov.io/github/phalberg/configlock/graph/badge.svg?token=60JBRC22NB"/>
6
+ </a>
7
+ </p>
8
+
9
+ # ConfigLock
10
+ ConfigLock is a lightweight CLI tool designed to prevent production outages by configuration errors. It brings the concept of Lockfiles (inspiration from uv) to your application’s .yaml or .json configurations.
11
+
12
+ # Quick start
13
+
14
+ ```
15
+ # Clone the repo
16
+ git clone https://github.com/phalberg/configlock
17
+ cd configlock
18
+
19
+ # Install dependencies
20
+ uv sync
21
+
22
+ # Initialize a lockfile
23
+ uv run configlock init my_config.yaml
24
+
25
+ # Sync after changes
26
+ uv run configlock sync my_config.yaml
27
+ ```
28
+
29
+ If you wish to not write `uv` each time, you can do as such:
30
+
31
+ ## CLI Usage
32
+
33
+ Install the project in editable mode while developing:
34
+
35
+ ```bash
36
+ pip install -e .
37
+ ```
38
+
39
+ Run the command directly after install:
40
+
41
+ ```bash
42
+ configlock --help
43
+ configlock init {path_to_your_file}
44
+ configlock lock {path_to_your_file}
45
+ configlock sync {path_to_your_file}
46
+ ```
47
+
48
+ Preview the docs locally with hot reload:
49
+
50
+ ```bash
51
+ uv run docs-serve
52
+ ```
53
+
54
+ # Status
55
+ ConfigLock is a **personal hobby project** focused on learning robust CLI development and structural validation logic.
56
+
57
+ > [!NOTE]
58
+ > This project is in an early prototype stage. It is a learning exercise in building developer tools with Python and Typer.
59
+
60
+ ### Roadmap
61
+ - [x] Basic CLI integration with Typer
62
+ - [x] GitHub Actions CI/CD pipeline
63
+ - [x] Recursive Type & Structure checking
64
+ - [ ] GitHub API integration (Fetch remote configs)
65
+ - [ ] Web-based UI for configuration visualization
66
+
67
+ # The problem
68
+ In modern DevOps, non-technical team members often need to edit configuration files (YAML/JSON). One missing key or a wrong data type (e.g., entering a string where a boolean is expected) may crash a production environment.
69
+
70
+ # Init
71
+ ```bash
72
+ init: Analyzes your YAML/JSON and creates a config.lock.json that stores the required structure and types.
73
+ ```
74
+ _Note: ConfigLock generates one unique lockfile corresponding to the file path provided._
75
+
76
+ # Sync
77
+
78
+ ```bash
79
+ sync: Compares your current YAML/JSON against the lockfile. If a key is missing or a type has changed, you get an error.
80
+ ```
81
+
82
+ # Lock
83
+ ```bash
84
+ lock: Checks your current YAML/JSON and tries to replace the locked file with the new changed current file, if the change is not compatible, you get an error.
85
+
86
+ ```
87
+ ## Lock with strict ordering
88
+ Please use the command:
89
+ ```bash
90
+ configlock lock {path_to_your_file} --order-matters
91
+ ```
92
+ If the order of the keys matter, if not the default:
93
+ ```bash
94
+ configlock lock {path_to_your_file} --no-order-matters
95
+ ```
96
+ will be set.
97
+
98
+ # License
99
+
100
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "cfglock"
3
+ version = "0.1.0"
4
+ description = "The official Python engine for the ConfigLock ecosystem"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Philipp.Stahlberg", email = "131917964+PhilippStahlbergGit@users.noreply.github.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "dotenv>=0.9.9",
12
+ "pyyaml>=6.0.3",
13
+ "requests>=2.34.2",
14
+ "typer>=0.25.1",
15
+ ]
16
+
17
+ [project.scripts]
18
+ configlock = "cfglock.cli:app"
19
+ cfglock = "cfglock.cli:app"
20
+ cfl = "cfglock.cli:app"
21
+ docs-serve = "cfglock.docs_server:main"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.11.14,<0.12.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pre-commit>=4.6.0",
30
+ "pyright>=1.1.410",
31
+ "pytest>=9.0.3",
32
+ "pytest-cov>=7.1.0",
33
+ "radon>=6.0.1",
34
+ "reloadserver>=1.0.0",
35
+ "ruff>=0.15.15",
36
+ ]
37
+
38
+ [tool.pytest.ini_options]
39
+ pythonpath = ["src"]
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+ from dotenv import load_dotenv
3
+
4
+ env_file = Path(".env")
5
+ if env_file.exists():
6
+ load_dotenv(env_file)
7
+ else:
8
+ load_dotenv(Path(".env.example"))
@@ -0,0 +1,68 @@
1
+ import typer
2
+
3
+ from cfglock.validator import ConfigLockError
4
+
5
+ from .helper import (
6
+ write_json,
7
+ check_file_exists,
8
+ check_file_identicality,
9
+ check_comp_cli,
10
+ check_file_and_read_file,
11
+ )
12
+ from typing import Annotated
13
+
14
+ app = typer.Typer(help="ConfigLock: Secure GitOps YAML validation engine.")
15
+
16
+
17
+ @app.command()
18
+ def init(
19
+ file_path: Annotated[
20
+ str, typer.Argument(help="the path for the newly proposed file")
21
+ ],
22
+ ) -> None:
23
+ """
24
+ Reads a YAML config and generates a lockfile.
25
+ """
26
+ if check_file_exists():
27
+ typer.echo("File already exists!")
28
+ else:
29
+ data = check_file_and_read_file(file_path)
30
+ write_json(data)
31
+
32
+
33
+ @app.command()
34
+ def sync(
35
+ file_path: Annotated[
36
+ str, typer.Argument(help="the path for the newly proposed file")
37
+ ],
38
+ ) -> None:
39
+ """
40
+ Used to check if lock file and proposed file are out of sync
41
+ """
42
+ if check_file_identicality(file_path):
43
+ typer.echo("The file has not changed.")
44
+ else:
45
+ raise ConfigLockError(
46
+ "The lock file is outdated, run sync to update the lock file!", error_code=1
47
+ )
48
+
49
+
50
+ @app.command()
51
+ def lock(
52
+ file_path: Annotated[
53
+ str, typer.Argument(help="the path for the newly proposed file")
54
+ ],
55
+ order_matters: bool = typer.Option(
56
+ False,
57
+ "--order-matters/--no-order-matters",
58
+ help="choose if the order of the keys matter or not",
59
+ ),
60
+ ) -> None:
61
+ """
62
+ Used to update the lock file, IF compatible
63
+ """
64
+
65
+ check_comp_cli(file_path, order_matters)
66
+ data = check_file_and_read_file(file_path)
67
+ write_json(data)
68
+
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import contextlib
5
+ import http.server
6
+ import threading
7
+ from pathlib import Path
8
+
9
+ from watchdog.events import FileSystemEventHandler
10
+ from watchdog.observers import Observer
11
+
12
+
13
+ RELOAD_SCRIPT = b"""
14
+ <script>
15
+ async function waitForReload() {
16
+ try {
17
+ const response = await fetch('/__reload__', { cache: 'no-store' });
18
+ if (response.status === 204) {
19
+ location.reload();
20
+ return;
21
+ }
22
+ } catch (error) {
23
+ console.log('reload poll failed', error);
24
+ }
25
+ setTimeout(waitForReload, 1000);
26
+ }
27
+ waitForReload();
28
+ </script>
29
+ """
30
+
31
+
32
+ class ReloadState:
33
+ def __init__(self) -> None:
34
+ self.condition = threading.Condition()
35
+ self.generation = 0
36
+
37
+ def bump(self) -> None:
38
+ with self.condition:
39
+ self.generation += 1
40
+ self.condition.notify_all()
41
+
42
+ def wait_for_next(self, generation: int) -> None:
43
+ with self.condition:
44
+ self.condition.wait_for(lambda: self.generation > generation)
45
+
46
+
47
+ class ReloadHandler(FileSystemEventHandler):
48
+ def __init__(self, state: ReloadState) -> None:
49
+ self.state = state
50
+
51
+ def on_any_event(self, event) -> None:
52
+ if not event.is_directory:
53
+ self.state.bump()
54
+
55
+
56
+ class DocsHandler(http.server.SimpleHTTPRequestHandler):
57
+ def __init__(self, *args, docs_root: Path, state: ReloadState, **kwargs):
58
+ self.docs_root = docs_root
59
+ self.state = state
60
+ super().__init__(*args, directory=str(docs_root), **kwargs)
61
+
62
+ def end_headers(self) -> None:
63
+ if self.path.endswith(".html") or self.path in {"/", ""}:
64
+ self.send_header("Cache-Control", "no-store")
65
+ super().end_headers()
66
+
67
+ def do_GET(self) -> None:
68
+ if self.path == "/__reload__":
69
+ generation = self.state.generation
70
+ self.state.wait_for_next(generation)
71
+ self.send_response(204)
72
+ self.end_headers()
73
+ return
74
+ super().do_GET()
75
+
76
+ def copyfile(self, source, outputfile) -> None:
77
+ content = source.read()
78
+ if b"</html>" in content:
79
+ content = content.replace(b"</html>", RELOAD_SCRIPT + b"</html>")
80
+ elif self.path.endswith(".html") or self.path in {"/", ""}:
81
+ content += RELOAD_SCRIPT
82
+ outputfile.write(content)
83
+
84
+
85
+ def main() -> None:
86
+ parser = argparse.ArgumentParser(description="Serve docs/ with live reload.")
87
+ parser.add_argument("port", nargs="?", type=int, default=8000)
88
+ parser.add_argument("--bind", default="127.0.0.1")
89
+ args = parser.parse_args()
90
+
91
+ docs_root = Path(__file__).resolve().parents[2] / "docs"
92
+ state = ReloadState()
93
+
94
+ observer = Observer()
95
+ observer.schedule(ReloadHandler(state), str(docs_root), recursive=True)
96
+ observer.start()
97
+
98
+ def handler_factory(*handler_args, **handler_kwargs):
99
+ return DocsHandler(
100
+ *handler_args,
101
+ docs_root=docs_root,
102
+ state=state,
103
+ **handler_kwargs,
104
+ )
105
+
106
+ try:
107
+ with contextlib.suppress(KeyboardInterrupt):
108
+ server = http.server.ThreadingHTTPServer(
109
+ (args.bind, args.port), handler_factory
110
+ )
111
+ print(f"Serving {docs_root} at http://{args.bind}:{args.port}/")
112
+ server.serve_forever()
113
+ finally:
114
+ observer.stop()
115
+ observer.join()
@@ -0,0 +1,24 @@
1
+ import requests
2
+ import json
3
+
4
+
5
+ def ext_fr_gh_public(
6
+ user: str | None = "phalberg",
7
+ repo: str | None = "configlock",
8
+ branch: str | None = "main",
9
+ file: str | None = "config.lock.json",
10
+ ):
11
+ """
12
+ Extract from a public github repo in GitHub, the text content
13
+ """
14
+ raw_gh_url = "https://raw.githubusercontent.com"
15
+ com_url = f"{raw_gh_url}/{user}/{repo}/{branch}/{file}"
16
+ response = requests.get(com_url)
17
+ data = response.json()
18
+ return data
19
+
20
+
21
+ if __name__ == "__main__":
22
+ json_data = json.dumps(ext_fr_gh_public(), indent=4)
23
+ with open("test_config.json", "w", encoding="utf-8") as f:
24
+ f.write(json_data)
@@ -0,0 +1,127 @@
1
+ import typer
2
+ import yaml
3
+ import json
4
+ from pathlib import Path
5
+ import filecmp
6
+ from dotenv import load_dotenv
7
+ import os
8
+
9
+ from cfglock.validator import (
10
+ ValidationContext,
11
+ walk_yaml_in_order,
12
+ walk_yaml_with_no_order,
13
+ )
14
+
15
+ load_dotenv()
16
+ CONFIG_LOG_FILE_PATH: str = os.environ.get("CONFIG_LOG_FILE_PATH", "config.lock.json")
17
+
18
+
19
+ def check_file_identicality(
20
+ file_path: str, config_file_path: str = CONFIG_LOG_FILE_PATH
21
+ ):
22
+ """Checks if files are identical, if they are it returns True, False otherwise"""
23
+ try:
24
+ a = check_file_and_read_file(file_path)
25
+ b = read_json(config_file_path)
26
+ if a == b:
27
+ return True
28
+ filecmp.clear_cache()
29
+ res = filecmp.cmp(file_path, config_file_path, shallow=False)
30
+ return res
31
+ except Exception:
32
+ filecmp.clear_cache()
33
+ res = filecmp.cmp(file_path, config_file_path, shallow=False)
34
+ return res
35
+
36
+
37
+ def check_file_exists(file_path: str = CONFIG_LOG_FILE_PATH) -> bool:
38
+ path = Path(file_path)
39
+ exists = path.exists()
40
+ if not exists:
41
+ typer.echo(f"The path does not exist: {path}")
42
+ return exists
43
+
44
+
45
+ def read_yaml(file_path: str) -> dict:
46
+ try:
47
+ with open(file_path, "r") as f:
48
+ data = yaml.safe_load(f)
49
+ except FileNotFoundError:
50
+ raise
51
+ else:
52
+ typer.echo("Sucessfully read file")
53
+ return data
54
+
55
+
56
+ def read_json(file_path: str) -> dict:
57
+ try:
58
+ with open(file_path, "r") as f:
59
+ data = json.load(f)
60
+ except FileNotFoundError:
61
+ raise
62
+ else:
63
+ typer.echo("Sucessfully read file")
64
+ return data
65
+
66
+
67
+ def write_json(data: dict, file_path: str = CONFIG_LOG_FILE_PATH) -> None:
68
+ data.update({"version": 1})
69
+ try:
70
+ with open(file_path, "w") as json_file:
71
+ json.dump(data, json_file, indent=4)
72
+ except TypeError:
73
+ raise
74
+ except Exception:
75
+ raise
76
+ else:
77
+ typer.echo("Sucessfully wrote file")
78
+
79
+
80
+ def check_file_and_read_file(file_path: str) -> dict:
81
+ typer.echo(f"Reading {file_path}...")
82
+
83
+ path = Path(file_path)
84
+ suffix = path.suffix.lower()
85
+
86
+ reader_by_suffix = {
87
+ ".yaml": read_yaml,
88
+ ".yml": read_yaml,
89
+ ".json": read_json,
90
+ }
91
+
92
+ reader = reader_by_suffix.get(suffix)
93
+ if reader is None:
94
+ typer.echo(
95
+ f"File not suppported: {suffix}. Use .yaml, .yml, or .json.",
96
+ err=True,
97
+ )
98
+ raise ValueError("Error not able to read the file")
99
+
100
+ data = reader(file_path)
101
+
102
+ return data
103
+
104
+
105
+ def check_comp_cli(new_file_path: str, order_matters: bool = False) -> None:
106
+ """ ""
107
+ Check compatiblity for the cli version
108
+ Keys => same names (must be the same, and (!) in the same (?order?)/precedence)
109
+ Values => must be of the same types
110
+ What about adding New entries? -> Fine
111
+ Deleting entries should not be allowed as it will obviously destroy Things. -> Fail
112
+ """
113
+
114
+ current_file_path = CONFIG_LOG_FILE_PATH
115
+ context = ValidationContext(
116
+ new_path=new_file_path,
117
+ current_path=current_file_path,
118
+ order_matters=bool(order_matters),
119
+ )
120
+
121
+ current_data = read_json(current_file_path)
122
+ new_data = check_file_and_read_file(new_file_path)
123
+
124
+ if order_matters:
125
+ walk_yaml_in_order(current_data, new_data, context)
126
+ else:
127
+ walk_yaml_with_no_order(current_data, new_data, context)
@@ -0,0 +1,9 @@
1
+ {
2
+ "project_name": "ConfigLock",
3
+ "status": "Alpha",
4
+ "version": 1,
5
+ "team": {
6
+ "lead": "Philipp Stahlberg",
7
+ "contributors": []
8
+ }
9
+ }
@@ -0,0 +1,159 @@
1
+ from dataclasses import dataclass
2
+ from itertools import zip_longest
3
+
4
+ keys_to_ignore = {"version"}
5
+
6
+
7
+ """
8
+ Note:
9
+ This is a strict class, meaning that webassembly will use this class.
10
+ Beware of the contents, and try to be efficient, keep the class minimal.
11
+ """
12
+
13
+
14
+ @dataclass
15
+ class ValidationContext:
16
+ new_path: str
17
+ current_path: str
18
+ order_matters: bool
19
+
20
+
21
+ # add other metadata if needed.
22
+
23
+
24
+ class ConfigLockError(Exception):
25
+ """Basic Error"""
26
+
27
+ def __init__(self, message, error_code=1):
28
+ super().__init__(message)
29
+ self.message = message
30
+ self.error_code = error_code
31
+
32
+
33
+ class ValidationError(ConfigLockError):
34
+ """Error for validation for syncing file"""
35
+
36
+ def __init__(
37
+ self,
38
+ path,
39
+ expected_value,
40
+ actual_value,
41
+ message="Validation Failed",
42
+ order_matters=False,
43
+ ):
44
+ super().__init__(message, error_code=100)
45
+ self.path = path
46
+ self.expected_value = expected_value
47
+ self.actual_value = actual_value
48
+ self.order_matters = order_matters
49
+
50
+ def __str__(self):
51
+ base_msg = f"""
52
+ In path: {self.path}
53
+ Expected: {self.expected_value}
54
+ Found: {self.actual_value}
55
+ """
56
+ if self.order_matters:
57
+ base_msg += "Additional: remember that order matters for keys!"
58
+ return f"{base_msg}\n(Error Code: {self.error_code})"
59
+
60
+
61
+ def walk_yaml_with_no_order(
62
+ current_data, new_data, context: ValidationContext, depth=0
63
+ ):
64
+ """Recursively walks through a YAML-loaded object with no order."""
65
+
66
+ if isinstance(current_data, dict):
67
+ if not isinstance(new_data, dict):
68
+ accept_new_value(
69
+ current_value=current_data, new_value=new_data, context=context
70
+ )
71
+ return
72
+
73
+ for curr_k, curr_v in current_data.items():
74
+ # specific values for our own interpretation of versionings etc.
75
+ if curr_k in keys_to_ignore:
76
+ continue
77
+
78
+ if curr_k not in new_data:
79
+ accept_new_keys(curr_k, None, context)
80
+
81
+ new_v = new_data[curr_k]
82
+
83
+ walk_yaml_with_no_order(curr_v, new_v, context, depth + 1)
84
+
85
+ else:
86
+ accept_new_value(
87
+ current_value=current_data, new_value=new_data, context=context
88
+ )
89
+
90
+
91
+ def walk_yaml_in_order(current_data, new_data, context: ValidationContext, depth=0):
92
+ """Recursively walks through a YAML-loaded object in order."""
93
+
94
+ if isinstance(current_data, dict):
95
+ for new_pair, curr_pair in zip_longest(
96
+ new_data.items(), current_data.items(), fillvalue=(None, None)
97
+ ):
98
+ new_k, new_v = new_pair
99
+ curr_k, curr_v = curr_pair
100
+
101
+ # specific values for our own interpretation of versionings etc.
102
+ if curr_k in keys_to_ignore:
103
+ continue
104
+
105
+ accept_new_keys(curr_k, new_k, context)
106
+
107
+ walk_yaml_with_no_order(new_v, curr_v, context, depth + 1)
108
+
109
+ else:
110
+ accept_new_value(
111
+ current_value=current_data, new_value=new_data, context=context
112
+ )
113
+
114
+
115
+ # Not sure about this solution to this problem...
116
+ def accept_new_keys(
117
+ current_key: str | None, new_key: str | None, context: ValidationContext
118
+ ) -> None:
119
+ """
120
+ General logic for accepting new keys:
121
+ 1) the name of new_key cannot be different than the name of current_key
122
+ 2) the order of new_key cannot be different than the current_key
123
+ 3) the precedence (i.e indentation) cannot be different than the current_key
124
+ """
125
+
126
+ if current_key != new_key:
127
+ if context.order_matters:
128
+ raise ValidationError(
129
+ path=context.new_path,
130
+ expected_value=current_key,
131
+ actual_value=new_key,
132
+ order_matters=True,
133
+ )
134
+ else:
135
+ raise ValidationError(
136
+ path=context.new_path,
137
+ expected_value=current_key,
138
+ actual_value=new_key,
139
+ order_matters=False,
140
+ )
141
+
142
+
143
+ def accept_new_value(current_value, new_value, context: ValidationContext) -> None:
144
+ """
145
+ General logic for accepting new values:
146
+ 1) the type of the new_value cannot be different than the type of the current_value
147
+ """
148
+ type_curr = type(current_value)
149
+ type_new = type(new_value)
150
+
151
+ if type_curr != type_new:
152
+ type_curr_val = f"<{type_curr.__name__}> with value: {current_value}"
153
+ type_next_val = f"<{type_new.__name__}> with value: {new_value}"
154
+
155
+ raise ValidationError(
156
+ path=context.new_path,
157
+ expected_value=type_curr_val,
158
+ actual_value=type_next_val,
159
+ )