infraclass 1.0.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.
- infraclass-1.0.0/PKG-INFO +11 -0
- infraclass-1.0.0/README.md +3 -0
- infraclass-1.0.0/infraclass/__init__.py +1 -0
- infraclass-1.0.0/infraclass/cli.py +97 -0
- infraclass-1.0.0/infraclass/compiler.py +203 -0
- infraclass-1.0.0/infraclass/merging.py +54 -0
- infraclass-1.0.0/infraclass.egg-info/PKG-INFO +11 -0
- infraclass-1.0.0/infraclass.egg-info/SOURCES.txt +12 -0
- infraclass-1.0.0/infraclass.egg-info/dependency_links.txt +1 -0
- infraclass-1.0.0/infraclass.egg-info/entry_points.txt +2 -0
- infraclass-1.0.0/infraclass.egg-info/requires.txt +1 -0
- infraclass-1.0.0/infraclass.egg-info/top_level.txt +1 -0
- infraclass-1.0.0/pyproject.toml +21 -0
- infraclass-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infraclass
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A fast, predictable, top-down configuration inventory compiler.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pyyaml>=6.0
|
|
8
|
+
|
|
9
|
+
# Infraclass
|
|
10
|
+
|
|
11
|
+
A fast, predictable, top-down configuration inventory compiler for infrastructure management workflows.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .compiler import compile_node_data
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# infraclass/cli.py
|
|
2
|
+
import os, sys, yaml, argparse, getpass, secrets, string
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import infraclass.compiler as xc
|
|
5
|
+
|
|
6
|
+
class NoAliasDumper(yaml.SafeDumper):
|
|
7
|
+
def ignore_aliases(self, data): return True
|
|
8
|
+
|
|
9
|
+
yaml.add_representer(str, lambda d, data: d.represent_scalar('tag:yaml.org,2002:str', data, style='|' if '\n' in data else None), Dumper=NoAliasDumper)
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description="Infraclass: Top-Down Infrastructure Inventory Compiler")
|
|
13
|
+
parser.add_argument("-b", "--base-dir", default="reclass", help="Path to reclass root folder")
|
|
14
|
+
parser.add_argument("node", nargs="?", help="Name of the node to compile")
|
|
15
|
+
parser.add_argument("--add-secret", action="store_true", help="Add a new encrypted value securely")
|
|
16
|
+
parser.add_argument("-f", "--file", help="Optional path to secret file input (used with --add-secret)")
|
|
17
|
+
parser.add_argument("--remove-secret", metavar="ID", help="Remove a tracking key from the vault")
|
|
18
|
+
parser.add_argument("--show-secret", metavar="ID", help="Reveal the plaintext value of a tracking key")
|
|
19
|
+
parser.add_argument("--re-encrypt", action="store_true", help="Re-encrypt the vault using the current recipients file")
|
|
20
|
+
parser.add_argument("--audit-secrets", action="store_true", help="Check for stale or missing configuration secrets")
|
|
21
|
+
args = parser.parse_args()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# ----------------------------------------------------
|
|
25
|
+
# VAULT MANAGEMENT ROUTES
|
|
26
|
+
# ----------------------------------------------------
|
|
27
|
+
if args.add_secret:
|
|
28
|
+
if not sys.stdin.isatty():
|
|
29
|
+
secret_value = sys.stdin.read().strip()
|
|
30
|
+
if not secret_value: raise ValueError("Piped input stream was empty.")
|
|
31
|
+
print("--> Reading secret value directly from piped input.")
|
|
32
|
+
elif args.file:
|
|
33
|
+
with open(args.file, "r") as f: secret_value = f.read().strip()
|
|
34
|
+
else:
|
|
35
|
+
secret_value = getpass.getpass("Enter passphrase (leave empty to autogenerate a secure one): ").strip()
|
|
36
|
+
if not secret_value:
|
|
37
|
+
secret_value = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
|
|
38
|
+
print("--> No passphrase provided. Autogenerated a secure 32-character secret.")
|
|
39
|
+
|
|
40
|
+
new_id = xc.add_secret_to_vault(secret_value)
|
|
41
|
+
print(f"\nMaster vault decrypted safely via in-memory stream.\nGenerated unique tracking identifier: {new_id}\nRe-encrypted master vault back to vault/vault.age successfully.\n\nPaste this reference identifier into your reclass configuration:\n !secret {new_id}")
|
|
42
|
+
|
|
43
|
+
elif args.remove_secret:
|
|
44
|
+
xc.remove_secret_from_vault(args.remove_secret)
|
|
45
|
+
print(f"Successfully purged tracker '{args.remove_secret}' from vault/vault.age.")
|
|
46
|
+
|
|
47
|
+
elif args.show_secret:
|
|
48
|
+
print(xc.show_secret_from_vault(args.show_secret))
|
|
49
|
+
|
|
50
|
+
elif args.re_encrypt:
|
|
51
|
+
xc.update_vault_recipients()
|
|
52
|
+
print("Successfully re-encrypted vault/vault.age using the current recipients file.")
|
|
53
|
+
|
|
54
|
+
elif args.audit_secrets:
|
|
55
|
+
stale, missing = xc.find_stale_secrets(base_dir=args.base_dir)
|
|
56
|
+
print(f"Auditing vault references inside '{args.base_dir}' against master vault...\n")
|
|
57
|
+
if stale:
|
|
58
|
+
print("Stale Secrets (Found in vault/vault.age but NEVER referenced in reclass files):")
|
|
59
|
+
for k in stale: print(f" - {k} (Safe to remove via: Infraclass --remove-secret {k})")
|
|
60
|
+
else:
|
|
61
|
+
print("✔ No stale secrets found in master vault.")
|
|
62
|
+
if missing:
|
|
63
|
+
print("\nMissing Secrets (Tagged as !secret in reclass but NOT found in vault/vault.age!):")
|
|
64
|
+
for m in missing: print(f" - {m}")
|
|
65
|
+
|
|
66
|
+
# ----------------------------------------------------
|
|
67
|
+
# COMPILATION ROUTE
|
|
68
|
+
# ----------------------------------------------------
|
|
69
|
+
elif args.node:
|
|
70
|
+
node_data = xc.compile_node_data(args.node, base_dir=args.base_dir)
|
|
71
|
+
hostname = node_data["node_name"]
|
|
72
|
+
|
|
73
|
+
output_blueprint = {
|
|
74
|
+
"__reclass__": {
|
|
75
|
+
"environment": node_data["environment"], "name": hostname, "node": hostname,
|
|
76
|
+
"timestamp": datetime.now().strftime("%a %b %d %H:%M:%S %Y"),
|
|
77
|
+
"uri": f"yaml_fs:///{os.path.abspath(args.base_dir)}/nodes/{hostname}.yml"
|
|
78
|
+
},
|
|
79
|
+
"applications": node_data["applications"], "classes": node_data["classes"],
|
|
80
|
+
"environment": node_data["environment"], "exports": node_data["exports"],
|
|
81
|
+
"parameters": {
|
|
82
|
+
"_reclass_": {"environment": node_data["environment"], "name": {"full": hostname, "short": hostname.split(".")[0]}},
|
|
83
|
+
**{k: node_data["parameters"][k] for k in sorted(node_data["parameters"].keys()) if k != "_reclass_"}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
print(yaml.dump(output_blueprint, default_flow_style=False, sort_keys=False, Dumper=NoAliasDumper))
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
parser.print_help()
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"ERROR: {str(e)}", file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# infraclass/compiler.py
|
|
2
|
+
import os, subprocess, sys, yaml, uuid
|
|
3
|
+
from .merging import deep_merge, evaluate_rubouts
|
|
4
|
+
|
|
5
|
+
_COMPILER_CONTEXT = {"root_dir": os.getcwd()}
|
|
6
|
+
_VAULT_CACHE = None
|
|
7
|
+
|
|
8
|
+
# =====================================================================
|
|
9
|
+
# INTERNAL SECRETS ENGINE UTILITIES
|
|
10
|
+
# =====================================================================
|
|
11
|
+
|
|
12
|
+
def _get_vault_path():
|
|
13
|
+
"""Traverses upward from the current working directory to discover vault/vault.age."""
|
|
14
|
+
project_root = os.getcwd()
|
|
15
|
+
while project_root and project_root != os.path.dirname(project_root):
|
|
16
|
+
if os.path.exists(os.path.join(project_root, "reclass")) or \
|
|
17
|
+
os.path.exists(os.path.join(project_root, "vault", "vault.age")):
|
|
18
|
+
break
|
|
19
|
+
project_root = os.path.dirname(project_root)
|
|
20
|
+
return os.path.join(project_root, "vault", "vault.age")
|
|
21
|
+
|
|
22
|
+
def _get_age_key_path():
|
|
23
|
+
"""Resolves the age encryption key path based on systemic environment tags."""
|
|
24
|
+
creds_dir = os.environ.get("CREDENTIALS_DIRECTORY")
|
|
25
|
+
if creds_dir and os.path.exists(os.path.join(creds_dir, "age-pq-key")):
|
|
26
|
+
return os.path.join(creds_dir, "age-pq-key")
|
|
27
|
+
return os.environ.get("INFRACLASS_AGE_KEY_FILE", os.path.expanduser("~/.config/age/identity.age"))
|
|
28
|
+
|
|
29
|
+
def _crypt_vault(ciphertext=None, plaintext=None, key_path=None):
|
|
30
|
+
"""Handles core in-memory age encryption and decryption subroutines."""
|
|
31
|
+
if not os.path.exists(key_path):
|
|
32
|
+
raise FileNotFoundError(f"Missing age identity key/envelope file at {key_path}")
|
|
33
|
+
|
|
34
|
+
if ciphertext is not None:
|
|
35
|
+
proc = subprocess.run(
|
|
36
|
+
["age", "--decrypt", "-i", key_path],
|
|
37
|
+
input=ciphertext.strip() + "\n", capture_output=True, text=True, check=True
|
|
38
|
+
)
|
|
39
|
+
return yaml.load(proc.stdout, Loader=yaml.SafeLoader) or {}
|
|
40
|
+
|
|
41
|
+
vault_path = _get_vault_path()
|
|
42
|
+
recipients_file = os.path.join(os.path.dirname(vault_path), "recipients")
|
|
43
|
+
encrypt_flags = ["-R", recipients_file] if os.path.exists(recipients_file) else ["-i", key_path]
|
|
44
|
+
|
|
45
|
+
class VaultDumper(yaml.SafeDumper): pass
|
|
46
|
+
yaml.add_representer(str, lambda d, data: d.represent_scalar('tag:yaml.org,2002:str', data, style='|' if '\n' in data else None), Dumper=VaultDumper)
|
|
47
|
+
|
|
48
|
+
proc = subprocess.run(
|
|
49
|
+
["age", "--encrypt"] + encrypt_flags + ["-a"],
|
|
50
|
+
input=yaml.dump(plaintext, default_flow_style=False, Dumper=VaultDumper),
|
|
51
|
+
capture_output=True, text=True, check=True
|
|
52
|
+
)
|
|
53
|
+
return proc.stdout
|
|
54
|
+
|
|
55
|
+
def _get_cached_vault():
|
|
56
|
+
"""Decrypts the vault once and caches it in memory."""
|
|
57
|
+
global _VAULT_CACHE
|
|
58
|
+
if _VAULT_CACHE is not None:
|
|
59
|
+
return _VAULT_CACHE
|
|
60
|
+
|
|
61
|
+
vault_path = _get_vault_path()
|
|
62
|
+
if os.path.exists(vault_path):
|
|
63
|
+
with open(vault_path, "r") as f:
|
|
64
|
+
_VAULT_CACHE = _crypt_vault(ciphertext=f.read(), key_path=_get_age_key_path())
|
|
65
|
+
else:
|
|
66
|
+
_VAULT_CACHE = {}
|
|
67
|
+
return _VAULT_CACHE
|
|
68
|
+
|
|
69
|
+
def _write_vault(vault_dict, error_msg="Failed to write vault"):
|
|
70
|
+
"""Helper subroutine to sync memory modifications cleanly out to disk."""
|
|
71
|
+
global _VAULT_CACHE
|
|
72
|
+
vault_path = _get_vault_path()
|
|
73
|
+
os.makedirs(os.path.dirname(vault_path), exist_ok=True)
|
|
74
|
+
try:
|
|
75
|
+
ciphertext = _crypt_vault(plaintext=vault_dict, key_path=_get_age_key_path())
|
|
76
|
+
with open(vault_path, "w") as f:
|
|
77
|
+
f.write(ciphertext)
|
|
78
|
+
_VAULT_CACHE = vault_dict
|
|
79
|
+
except subprocess.CalledProcessError as e:
|
|
80
|
+
raise RuntimeError(f"{error_msg}: {e.stderr.strip()}")
|
|
81
|
+
|
|
82
|
+
# =====================================================================
|
|
83
|
+
# PUBLIC CORE INTERFACE FUNCTIONS
|
|
84
|
+
# =====================================================================
|
|
85
|
+
|
|
86
|
+
def add_secret_to_vault(plaintext_value):
|
|
87
|
+
"""Assigns a unique UUID identifier to a secret value, then commits it to the vault."""
|
|
88
|
+
vault_dict = _get_cached_vault()
|
|
89
|
+
while True:
|
|
90
|
+
new_uuid = str(uuid.uuid4())
|
|
91
|
+
if new_uuid not in vault_dict: break
|
|
92
|
+
vault_dict[new_uuid] = plaintext_value
|
|
93
|
+
_write_vault(vault_dict, "Failed to re-encrypt vault using age")
|
|
94
|
+
return new_uuid
|
|
95
|
+
|
|
96
|
+
def remove_secret_from_vault(lookup_key):
|
|
97
|
+
"""Pops a specified secret tracking identifier out of the master vault mapping."""
|
|
98
|
+
vault_dict = _get_cached_vault()
|
|
99
|
+
clean_key = str(lookup_key).strip().strip("'\"")
|
|
100
|
+
if clean_key not in vault_dict:
|
|
101
|
+
raise KeyError(f"Secret key '{clean_key}' does not exist inside the vault.")
|
|
102
|
+
vault_dict.pop(clean_key)
|
|
103
|
+
_write_vault(vault_dict, "Failed to re-encrypt vault")
|
|
104
|
+
|
|
105
|
+
def show_secret_from_vault(lookup_key):
|
|
106
|
+
"""Returns the plaintext configuration value behind an isolated reference ID."""
|
|
107
|
+
vault_dict = _get_cached_vault()
|
|
108
|
+
clean_key = str(lookup_key).strip().strip("'\"")
|
|
109
|
+
if clean_key not in vault_dict:
|
|
110
|
+
raise KeyError(f"Secret key '{clean_key}' does not exist inside the vault.")
|
|
111
|
+
return vault_dict[clean_key]
|
|
112
|
+
|
|
113
|
+
def update_vault_recipients():
|
|
114
|
+
"""Forces an immediate rewrite pass of the vault data block to register updated keys."""
|
|
115
|
+
_write_vault(_get_cached_vault(), "Failed to re-encrypt vault with updated recipients")
|
|
116
|
+
|
|
117
|
+
def find_stale_secrets(base_dir="reclass"):
|
|
118
|
+
"""Audits reclass inventory against vault.age to trace unreferenced or broken keys."""
|
|
119
|
+
referenced_keys = set()
|
|
120
|
+
class AuditLoader(yaml.SafeLoader): pass
|
|
121
|
+
AuditLoader.add_constructor('!secret', lambda loader, node: referenced_keys.add(str(loader.construct_scalar(node)).strip().strip("'\"")) or f"!secret {node.value}")
|
|
122
|
+
|
|
123
|
+
for root, _, files in os.walk(base_dir):
|
|
124
|
+
for file in files:
|
|
125
|
+
if file.endswith((".yml", ".yaml")):
|
|
126
|
+
try:
|
|
127
|
+
with open(os.path.join(root, file), "r") as f: yaml.load(f, Loader=AuditLoader)
|
|
128
|
+
except Exception: continue
|
|
129
|
+
|
|
130
|
+
vault_dict = _get_cached_vault()
|
|
131
|
+
vault_keys = set(str(k).strip() for k in vault_dict.keys())
|
|
132
|
+
return sorted(list(vault_keys - referenced_keys)), sorted(list(referenced_keys - vault_keys))
|
|
133
|
+
|
|
134
|
+
# =====================================================================
|
|
135
|
+
# PYYAML INVENTORY COMPILER HOOKS
|
|
136
|
+
# =====================================================================
|
|
137
|
+
|
|
138
|
+
def age_decrypt_constructor(loader, node):
|
|
139
|
+
"""Dynamic parser tag constructor used during real-time reclass file loading phases."""
|
|
140
|
+
lookup_key = str(loader.construct_scalar(node)).strip().strip("'\"")
|
|
141
|
+
try:
|
|
142
|
+
vault_dict = _get_cached_vault()
|
|
143
|
+
return str(vault_dict.get(lookup_key, f"ERROR: Secret key '{lookup_key}' not found in vault.")).strip()
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return f"ERROR: Infraclass vault breakdown: {str(e)}"
|
|
146
|
+
|
|
147
|
+
yaml.add_constructor('!secret', age_decrypt_constructor, Loader=yaml.SafeLoader)
|
|
148
|
+
|
|
149
|
+
# =====================================================================
|
|
150
|
+
# RECLASS COMPILATION LOGIC ENGINE
|
|
151
|
+
# =====================================================================
|
|
152
|
+
|
|
153
|
+
def load_yaml_file(filepath):
|
|
154
|
+
if not os.path.exists(filepath): return {}
|
|
155
|
+
with open(filepath, "r") as f: return yaml.load(f, Loader=yaml.SafeLoader) or {}
|
|
156
|
+
|
|
157
|
+
def resolve_class_path(classes_dir, class_name):
|
|
158
|
+
base_path = os.path.join(classes_dir, *class_name.split("."))
|
|
159
|
+
return base_path + ".yml" if os.path.exists(base_path + ".yml") else (os.path.join(base_path, "init.yml") if os.path.exists(os.path.join(base_path, "init.yml")) else None)
|
|
160
|
+
|
|
161
|
+
def compile_node_data(node_name, base_dir="reclass"):
|
|
162
|
+
classes_dir, nodes_dir = os.path.join(base_dir, "classes"), os.path.join(base_dir, "nodes")
|
|
163
|
+
filename = node_name if node_name.endswith((".yml", ".yaml")) else f"{node_name}.yml"
|
|
164
|
+
node_path = os.path.join(nodes_dir, filename)
|
|
165
|
+
|
|
166
|
+
if not os.path.exists(node_path): raise FileNotFoundError(f"Missing node definition file: {node_path}")
|
|
167
|
+
|
|
168
|
+
node_raw = load_yaml_file(node_path)
|
|
169
|
+
merged_parameters, param_origins = {}, {}
|
|
170
|
+
applications, final_ordered_classes, seen_classes = [], [], set()
|
|
171
|
+
|
|
172
|
+
def visit_class(class_name, parent="the Node file"):
|
|
173
|
+
if class_name in seen_classes: return
|
|
174
|
+
class_file = resolve_class_path(classes_dir, class_name)
|
|
175
|
+
if not class_file:
|
|
176
|
+
raise FileNotFoundError(f"\n[Infraclass Class Missing Error]\n➔ Could not resolve class path for: '{class_name}'\n - Requested by: {parent}")
|
|
177
|
+
|
|
178
|
+
class_data = load_yaml_file(class_file)
|
|
179
|
+
current_short_name = os.path.relpath(class_file, base_dir)
|
|
180
|
+
|
|
181
|
+
for sub_class in class_data.get("classes", []): visit_class(sub_class, parent=current_short_name)
|
|
182
|
+
for app in class_data.get("applications", []):
|
|
183
|
+
if app not in applications: applications.append(app)
|
|
184
|
+
|
|
185
|
+
deep_merge(merged_parameters, class_data.get("parameters", {}), current_context="", source_b=current_short_name, origin_map=param_origins)
|
|
186
|
+
if class_name not in final_ordered_classes: final_ordered_classes.append(class_name)
|
|
187
|
+
seen_classes.add(class_name)
|
|
188
|
+
|
|
189
|
+
for starting_class in node_raw.get("classes", []): visit_class(starting_class)
|
|
190
|
+
for app in node_raw.get("applications", []):
|
|
191
|
+
if app not in applications: applications.append(app)
|
|
192
|
+
|
|
193
|
+
short_node_name = os.path.relpath(node_path, base_dir)
|
|
194
|
+
deep_merge(merged_parameters, node_raw.get("parameters", {}), current_context="", source_b=short_node_name, origin_map=param_origins)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"node_name": filename.rsplit(".yml", 1)[0].rsplit(".yaml", 1)[0],
|
|
198
|
+
"classes": final_ordered_classes,
|
|
199
|
+
"applications": applications,
|
|
200
|
+
"environment": node_raw.get("environment", "base"),
|
|
201
|
+
"exports": node_raw.get("exports", {}),
|
|
202
|
+
"parameters": evaluate_rubouts(merged_parameters)
|
|
203
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
def deep_merge(dict_a, dict_b, current_context="", source_b="Incoming", origin_map=None):
|
|
2
|
+
if origin_map is None:
|
|
3
|
+
origin_map = {}
|
|
4
|
+
|
|
5
|
+
for key, value in dict_b.items():
|
|
6
|
+
path = f"{current_context}.{key}" if current_context else str(key)
|
|
7
|
+
|
|
8
|
+
if key in dict_a:
|
|
9
|
+
type_a = type(dict_a[key])
|
|
10
|
+
type_b = type(value)
|
|
11
|
+
|
|
12
|
+
if isinstance(dict_a[key], dict) and isinstance(value, dict):
|
|
13
|
+
deep_merge(dict_a[key], value, current_context=path, source_b=source_b, origin_map=origin_map)
|
|
14
|
+
|
|
15
|
+
elif isinstance(dict_a[key], list) and isinstance(value, list):
|
|
16
|
+
dict_a[key].extend([x for x in value if x not in dict_a[key]])
|
|
17
|
+
origin_map[path] = source_b
|
|
18
|
+
|
|
19
|
+
elif type_a != type_b and dict_a[key] is not None and value is not None:
|
|
20
|
+
original_file = origin_map.get(path, "An earlier class")
|
|
21
|
+
raise TypeError(
|
|
22
|
+
f"\n[Wifty Data Type Mismatch Detected]\n"
|
|
23
|
+
f"➔ Parameter path: '{path}'\n"
|
|
24
|
+
f" - File 1 ({original_file}) defined a [{type_a.__name__}]:\n"
|
|
25
|
+
f" {dict_a[key]}\n"
|
|
26
|
+
f" - File 2 ({source_b}) tried to overwrite it with a [{type_b.__name__}]:\n"
|
|
27
|
+
f" {value}\n\n"
|
|
28
|
+
f"Fix: Check your class ordering or make sure you aren't missing dashes (-) or brackets []!"
|
|
29
|
+
)
|
|
30
|
+
else:
|
|
31
|
+
dict_a[key] = value
|
|
32
|
+
origin_map[path] = source_b
|
|
33
|
+
else:
|
|
34
|
+
dict_a[key] = value
|
|
35
|
+
origin_map[path] = source_b
|
|
36
|
+
|
|
37
|
+
return dict_a
|
|
38
|
+
|
|
39
|
+
def evaluate_rubouts(data):
|
|
40
|
+
if not isinstance(data, dict):
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
processed = {}
|
|
44
|
+
for key, value in data.items():
|
|
45
|
+
evaluated_value = evaluate_rubouts(value)
|
|
46
|
+
if str(key).startswith('~'):
|
|
47
|
+
clean_key = str(key)[1:]
|
|
48
|
+
processed[clean_key] = evaluated_value
|
|
49
|
+
else:
|
|
50
|
+
if key in processed and isinstance(processed[key], dict) and isinstance(evaluated_value, dict):
|
|
51
|
+
processed[key].update(evaluated_value)
|
|
52
|
+
else:
|
|
53
|
+
processed[key] = evaluated_value
|
|
54
|
+
return processed
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infraclass
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A fast, predictable, top-down configuration inventory compiler.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pyyaml>=6.0
|
|
8
|
+
|
|
9
|
+
# Infraclass
|
|
10
|
+
|
|
11
|
+
A fast, predictable, top-down configuration inventory compiler for infrastructure management workflows.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
infraclass/__init__.py
|
|
4
|
+
infraclass/cli.py
|
|
5
|
+
infraclass/compiler.py
|
|
6
|
+
infraclass/merging.py
|
|
7
|
+
infraclass.egg-info/PKG-INFO
|
|
8
|
+
infraclass.egg-info/SOURCES.txt
|
|
9
|
+
infraclass.egg-info/dependency_links.txt
|
|
10
|
+
infraclass.egg-info/entry_points.txt
|
|
11
|
+
infraclass.egg-info/requires.txt
|
|
12
|
+
infraclass.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyyaml>=6.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
infraclass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "infraclass"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A fast, predictable, top-down configuration inventory compiler."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pyyaml>=6.0"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
infraclass = "infraclass.cli:main"
|
|
17
|
+
|
|
18
|
+
# Explicitly tell setuptools to look for your flat layout module folder
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["."]
|
|
21
|
+
include = ["infraclass*"]
|