44
55from pathlib import Path
66from typing import Any
7+ from urllib .parse import urljoin , urlparse
78
89import yaml
910from django .core .management .base import BaseCommand , CommandError
1011
12+ import requests
1113from kernelCI_app .models import (
1214 HardwareRegistryPlatform ,
1315 HardwareRegistryPlatformVendor ,
@@ -34,57 +36,105 @@ def get_update_fields(model):
3436)
3537
3638
39+ def _is_url (value : str ) -> bool :
40+ parsed = urlparse (value )
41+ return parsed .scheme in ("http" , "https" )
42+
43+
3744class Command (BaseCommand ):
3845 help = (
3946 "Parse a KernelCI hardware registry YAML file (same shape as "
40- "kernelci-pipeline config/hardware_registry/*.yaml) and print its data."
47+ "kernelci-pipeline config/hardware_registry/*.yaml) and print its data. "
48+ "Accepts a local file path or a URL."
4149 )
4250
4351 def add_arguments (self , parser ):
4452 parser .add_argument (
4553 "registry_file" ,
54+ nargs = "?" ,
55+ type = str ,
56+ help = "Path or URL to a single hardware registry YAML file." ,
57+ )
58+ parser .add_argument (
59+ "--index" ,
4660 type = str ,
47- help = "Path to the hardware registry YAML file." ,
61+ help = (
62+ "Path or URL to an index YAML listing registry files "
63+ "(e.g. hardware_registry/index.yaml). Entries in the "
64+ "'registries' key are resolved relative to the index location."
65+ ),
4866 )
4967
50- def load_yaml (self , path : str ) -> dict :
68+ def _fetch_url (self , url : str ) -> str :
69+ try :
70+ response = requests .get (url , timeout = 30 )
71+ response .raise_for_status ()
72+ return response .text
73+ except requests .RequestException as exc :
74+ raise CommandError (f"Failed to fetch { url } : { exc } " ) from exc
75+
76+ def _read_file (self , path : Path ) -> str :
5177 if not path .is_file ():
5278 raise CommandError (f"Registry file not found: { path } " )
5379 try :
54- raw = path .read_text (encoding = "utf-8" )
80+ return path .read_text (encoding = "utf-8" )
5581 except OSError as exc :
5682 raise CommandError (f"Cannot read registry file: { exc } " ) from exc
5783
84+ def load_yaml (self , source : str ) -> dict :
85+ if _is_url (source ):
86+ raw = self ._fetch_url (source )
87+ else :
88+ raw = self ._read_file (Path (source ).expanduser ().resolve ())
89+
5890 try :
59- data = yaml .safe_load (raw )
60- return data
91+ return yaml .safe_load (raw )
6192 except yaml .YAMLError as exc :
6293 raise CommandError (f"Invalid YAML: { exc } " ) from exc
6394
64- def handle (self , * args : Any , registry_file : str , ** options : Any ) -> None :
65- path = Path (registry_file ).expanduser ().resolve ()
66- data = self .load_yaml (path )
95+ def _resolve_relative (self , base : str , relative : str ) -> str :
96+ """Resolve a relative filename against the base index location."""
97+ if _is_url (base ):
98+ return urljoin (base , relative )
99+ return str (Path (base ).expanduser ().resolve ().parent / relative )
100+
101+ def _resolve_index (self , index_source : str ) -> list [str ]:
102+ """Load an index YAML and return resolved paths/URLs for each registry."""
103+ data = self .load_yaml (index_source )
104+ if not isinstance (data , dict ) or "registries" not in data :
105+ raise CommandError (
106+ "Index YAML must contain a 'registries' key with a list of filenames."
107+ )
108+ entries = data ["registries" ]
109+ if not isinstance (entries , list ) or not entries :
110+ raise CommandError ("Index 'registries' must be a non-empty list." )
111+ return [self ._resolve_relative (index_source , entry ) for entry in entries ]
112+
113+ def _process_registry (self , source : str ) -> None :
114+ """Load and persist a single registry YAML."""
115+ data = self .load_yaml (source )
67116
68117 if data is None :
69- raise CommandError ("Registry file is empty or parses to null. " )
118+ raise CommandError (f "Registry is empty or parses to null: { source } " )
70119 if not isinstance (data , dict ):
71120 raise CommandError (
72- f"Expected a YAML mapping at root, got { type (data ).__name__ } . "
121+ f"Expected a YAML mapping at root, got { type (data ).__name__ } : { source } "
73122 )
74123
75124 missing = [k for k in EXPECTED_SECTIONS if k not in data ]
76125 if missing :
77126 self .stderr .write (
78127 self .style .WARNING (
79- f"Missing typical hardware-registry sections : { ', ' .join (missing )} "
128+ f"Missing typical hardware-registry sections in { source } : "
129+ f"{ ', ' .join (missing )} "
80130 )
81131 )
82132
83- silicon_vendors = data ["silicon_vendors" ] or []
84- platform_vendors = data ["platform_vendors" ] or []
85- processors = data ["processors" ] or []
86- system_modules = data ["system_modules" ] or []
87- platforms = data ["platforms" ] or []
133+ silicon_vendors = data ["silicon_vendors" ] or {}
134+ platform_vendors = data ["platform_vendors" ] or {}
135+ processors = data ["processors" ] or {}
136+ system_modules = data ["system_modules" ] or {}
137+ platforms = data ["platforms" ] or {}
88138
89139 HardwareRegistrySiliconVendor .objects .bulk_create (
90140 [HardwareRegistrySiliconVendor (** sv ) for sv in silicon_vendors .values ()],
@@ -120,3 +170,22 @@ def handle(self, *args: Any, registry_file: str, **options: Any) -> None:
120170 unique_fields = ["id" ],
121171 update_fields = get_update_fields (HardwareRegistryPlatform ),
122172 )
173+
174+ self .stdout .write (self .style .SUCCESS (f"Processed: { source } " ))
175+
176+ def handle (self , * args : Any , ** options : Any ) -> None :
177+ registry_file = options .get ("registry_file" )
178+ index = options .get ("index" )
179+
180+ if not registry_file and not index :
181+ raise CommandError ("Provide either a registry_file or --index." )
182+ if registry_file and index :
183+ raise CommandError ("Provide either a registry_file or --index, not both." )
184+
185+ if index :
186+ sources = self ._resolve_index (index )
187+ else :
188+ sources = [registry_file ]
189+
190+ for source in sources :
191+ self ._process_registry (source )
0 commit comments