66import logging
77import hashlib
88import gzip
9+ import re
910from dataclasses import dataclass
1011import time
1112from urllib .parse import quote
1213from xml .etree import ElementTree as ET
1314
1415import aiohttp
1516
17+ from apollo .server .routes .api_updateinfo import PRODUCT_SLUG_MAP
18+
1619logging .basicConfig (level = logging .INFO )
1720logger = logging .getLogger ("apollo_tree" )
1821
2124 "rpm" : "http://linux.duke.edu/metadata/rpm"
2225}
2326
27+ PRODUCT_NAME_TO_SLUG = {v : k for k , v in PRODUCT_SLUG_MAP .items ()}
28+ API_BASE_URL = "https://apollo.build.resf.org/api/v3/updateinfo"
29+
30+ def get_product_slug (product_name : str ) -> str :
31+ """
32+ Convert product name to API slug for v2 endpoint.
33+ Strips version numbers and architecture placeholders before lookup.
34+
35+ Examples:
36+ "Rocky Linux 9 $arch" -> "Rocky Linux"
37+ "Rocky Linux 10.5" -> "Rocky Linux"
38+ "Rocky Linux SIG Cloud" -> "Rocky Linux SIG Cloud"
39+ """
40+ clean_name = product_name .replace ("$arch" , "" ).strip ()
41+
42+ clean_name = re .sub (r'\s+\d+(\.\d+)?$' , '' , clean_name ).strip ()
43+
44+ slug = PRODUCT_NAME_TO_SLUG .get (clean_name )
45+ if not slug :
46+ raise ValueError (
47+ f"Unknown product: { clean_name } . "
48+ f"Valid products: { ', ' .join (PRODUCT_NAME_TO_SLUG .keys ())} "
49+ )
50+ return slug
51+
2452
2553@dataclass
2654class Repository :
@@ -142,16 +170,37 @@ async def fetch_updateinfo_from_apollo(
142170 repo : dict ,
143171 product_name : str ,
144172 api_base : str = None ,
173+ major_version : int = None ,
174+ minor_version : int = None ,
145175) -> str :
146- pname_arch = product_name .replace ("$arch" , repo ["arch" ])
176+ """
177+ Fetch updateinfo.xml from Apollo API.
178+
179+ Args:
180+ repo: Repository dict with 'name' and 'arch' keys
181+ product_name: Product name
182+ api_base: Optional API base URL override
183+ major_version: Required for api_version=2
184+ minor_version: Optional for api_version=2
185+ """
147186 if not api_base :
148- api_base = "https://apollo.build.resf.org/api/v3/updateinfo"
149- api_url = f"{ api_base } /{ quote (pname_arch )} /{ quote (repo ['name' ])} /updateinfo.xml"
150- api_url += f"?req_arch={ repo ['arch' ]} "
187+ api_base = API_BASE_URL
188+
189+ if major_version :
190+ product_slug = get_product_slug (product_name )
191+ api_url = f"{ api_base } /{ product_slug } /{ major_version } /{ quote (repo ['name' ])} /updateinfo.xml"
192+ api_params = {'arch' : repo ['arch' ]}
193+ if minor_version is not None :
194+ api_params ['minor_version' ] = minor_version
195+ logger .info ("Using v2 endpoint: %s with params %s" , api_url , api_params )
196+ else :
197+ pname_arch = product_name .replace ("$arch" , repo ["arch" ])
198+ api_url = f"{ api_base } /{ quote (pname_arch )} /{ quote (repo ['name' ])} /updateinfo.xml"
199+ api_params = {'req_arch' : repo ['arch' ]}
200+ logger .info ("Using legacy endpoint: %s with params %s" , api_url , api_params )
151201
152- logger .info ("Fetching updateinfo from %s" , api_url )
153202 async with aiohttp .ClientSession () as session :
154- async with session .get (api_url ) as resp :
203+ async with session .get (api_url , params = api_params ) as resp :
155204 if resp .status != 200 and resp .status != 404 :
156205 logger .warning (
157206 "Failed to fetch updateinfo from %s, skipping" , api_url
@@ -303,6 +352,9 @@ async def run_apollo_tree(
303352 ignore : list [str ],
304353 ignore_arch : list [str ],
305354 product_name : str ,
355+ major_version : int = None ,
356+ minor_version : int = None ,
357+ api_base : str = None ,
306358):
307359 if manual :
308360 raise Exception ("Manual mode not implemented yet" )
@@ -320,6 +372,9 @@ async def run_apollo_tree(
320372 updateinfo = await fetch_updateinfo_from_apollo (
321373 repo ,
322374 product_name ,
375+ api_base = api_base ,
376+ major_version = major_version ,
377+ minor_version = minor_version ,
323378 )
324379 if not updateinfo :
325380 logger .warning ("No updateinfo found for %s" , repo ["name" ])
@@ -394,13 +449,30 @@ async def run_apollo_tree(
394449 "-n" ,
395450 "--product-name" ,
396451 required = True ,
397- help = "Product name" ,
452+ help = "Product name (e.g., 'Rocky Linux', 'Rocky Linux 8 $arch')" ,
453+ )
454+ parser .add_argument (
455+ "--major-version" ,
456+ type = int ,
457+ help = "Major version (required for --api-version 2)" ,
458+ )
459+ parser .add_argument (
460+ "--minor-version" ,
461+ type = int ,
462+ help = "Minor version filter (optional, only with --api-version 2)" ,
463+ )
464+ parser .add_argument (
465+ "--api-base" ,
466+ help = "API base URL (default: https://apollo.build.resf.org/api/v3/updateinfo)" ,
398467 )
399468
400469 p_args = parser .parse_args ()
401470 if p_args .auto_scan and p_args .manual :
402471 parser .error ("Cannot use --auto-scan and --manual together" )
403472
473+ if p_args .minor_version and not p_args .major_version :
474+ parser .error ("--minor-version can only be used with --major-version" )
475+
404476 if p_args .manual and not p_args .repos :
405477 parser .error ("Must specify repos to publish in manual mode" )
406478
@@ -416,5 +488,8 @@ async def run_apollo_tree(
416488 [y for x in p_args .ignore for y in x ],
417489 [y for x in p_args .ignore_arch for y in x ],
418490 p_args .product_name ,
491+ p_args .major_version ,
492+ p_args .minor_version ,
493+ p_args .api_base ,
419494 )
420495 )
0 commit comments