1919import asyncio
2020import logging
2121import random
22+ import base64
23+ import re
2224from typing import Any , Dict , List , Optional , Tuple , Union
2325
2426from .constants import (
3537 SUPPORTED_SYSTEMS_QUERY ,
3638)
3739from .exceptions import APIError
38- from .utils import encode_url_param , validate_hash , validate_system
40+ from .utils import (
41+ encode_url_param ,
42+ normalize_package ,
43+ validate_hash ,
44+ validate_system ,
45+ )
3946
4047JSONType = Union [Dict [str , Any ], List [Any ]]
48+ PEP503_NORMALIZE = re .compile (r"[-_.]+" )
4149
4250logger : logging .Logger = logging .getLogger (__name__ )
4351handler = logging .StreamHandler ()
@@ -216,6 +224,13 @@ async def fetch_data(
216224 def _build_path (self , * segments : str , suffix : str = "" ) -> str :
217225 """
218226 Build a full URL from base + path segments + optional suffix.
227+
228+ Args:
229+ *segments (str): Path segments to join with '/'.
230+ suffix (str): Optional suffix (e.g. ":dependencies").
231+
232+ Returns:
233+ str: Full URL.
219234 """
220235 path = "/" .join (segments )
221236 if suffix :
@@ -230,6 +245,14 @@ async def _get(
230245 ) -> JSONType :
231246 """
232247 Internal helper for GET requests.
248+
249+ Args:
250+ *segments (str): Path segments.
251+ suffix (str): Optional suffix (appended to last segment).
252+ query_params (Optional[Dict[str, str]]): URL query parameters.
253+
254+ Returns:
255+ JSONType: Parsed JSON response.
233256 """
234257 url = self ._build_path (* segments , suffix = suffix )
235258 return await self .fetch_data (url , query_params )
@@ -239,16 +262,25 @@ async def _post(
239262 ) -> JSONType :
240263 """
241264 Internal helper for POST requests.
265+
266+ Args:
267+ *segments (str): Path segments.
268+ suffix (str): Optional suffix.
269+ json_body (Any): Object to send as JSON body.
270+
271+ Returns:
272+ JSONType: Parsed JSON response.
242273 """
243274 url = self ._build_path (* segments , suffix = suffix )
244275 return await self .fetch_data (url , method = "POST" , json_body = json_body )
245276
277+ @normalize_package
246278 async def get_package (self , system_name : str , package_name : str ) -> JSONType :
247279 """
248280 Fetch basic package info including available versions.
249281
250282 Args:
251- system_name (str): Package system (e.g. "NPM ", "PYPI ").
283+ system_name (str): Package system (e.g. "npm ", "pypi ").
252284 package_name (str): Name of the package.
253285
254286 Returns:
@@ -262,14 +294,15 @@ async def get_package(self, system_name: str, package_name: str) -> JSONType:
262294 name_enc = encode_url_param (package_name )
263295 return await self ._get ("systems" , system_name , "packages" , name_enc )
264296
297+ @normalize_package
265298 async def get_version (
266299 self , system_name : str , package_name : str , version : str
267300 ) -> JSONType :
268301 """
269302 Fetch detailed info about a specific package version.
270303
271304 Args:
272- system_name (str): Package system (e.g. "NPM ", "PYPI ").
305+ system_name (str): Package system (e.g. "npm ", "pypi ").
273306 package_name (str): Name of the package.
274307 version (str): Version identifier.
275308
@@ -323,14 +356,14 @@ async def get_version_batch(
323356 raise ValueError ("version_requests may not exceed 5000 entries" )
324357
325358 payload : Dict [str , Any ] = {"requests" : []}
326- for system , pkg , ver in version_requests :
327- validate_system (system )
359+ for system_name , package , version in version_requests :
360+ validate_system (system_name )
328361 payload ["requests" ].append (
329362 {
330363 "versionKey" : {
331- "system" : system ,
332- "name" : pkg ,
333- "version" : ver ,
364+ "system" : system_name . upper () ,
365+ "name" : package ,
366+ "version" : version ,
334367 }
335368 }
336369 )
@@ -366,6 +399,7 @@ async def get_all_versions_batch(
366399 break
367400 return all_responses
368401
402+ @normalize_package
369403 async def get_requirements (
370404 self , system_name : str , package_name : str , version : str
371405 ) -> JSONType :
@@ -397,6 +431,7 @@ async def get_requirements(
397431 suffix = ":requirements" ,
398432 )
399433
434+ @normalize_package
400435 async def get_dependencies (
401436 self , system_name : str , package_name : str , version : str
402437 ) -> JSONType :
@@ -428,6 +463,7 @@ async def get_dependencies(
428463 suffix = ":dependencies" ,
429464 )
430465
466+ @normalize_package
431467 async def get_dependents (
432468 self , system_name : str , package_name : str , version : str
433469 ) -> JSONType :
@@ -463,6 +499,7 @@ async def get_dependents(
463499 suffix = ":dependents" ,
464500 )
465501
502+ @normalize_package
466503 async def get_capabilities (
467504 self , system_name : str , package_name : str , version : str
468505 ) -> JSONType :
@@ -559,8 +596,7 @@ async def get_project_batch(
559596 return await self ._post ("projectbatch" , json_body = payload )
560597
561598 async def get_all_projects_batch (
562- self ,
563- project_ids : List [str ],
599+ self , project_ids : List [str ]
564600 ) -> List [Dict [str , Any ]]:
565601 """
566602 Convenience wrapper to retrieve all pages for a given project
@@ -618,6 +654,7 @@ async def get_advisory(self, advisory_id: str) -> JSONType:
618654 id_enc = encode_url_param (advisory_id )
619655 return await self ._get ("advisories" , id_enc )
620656
657+ @normalize_package
621658 async def get_similarly_named_packages (
622659 self , system_name : str , package_name : str
623660 ) -> JSONType :
@@ -673,12 +710,20 @@ async def query_package_versions(
673710 if hash_type and hash_value :
674711 validate_hash (hash_type )
675712 if version_system :
713+ version_system = version_system .upper ()
676714 validate_system (version_system , SUPPORTED_SYSTEMS_QUERY )
677715
716+ if version_name :
717+ if version_system == "NUGET" :
718+ version_name = version_name .lower ()
719+ elif version_system == "PYPI" :
720+ version_name = PEP503_NORMALIZE .sub ("-" , version_name ).lower ()
721+
678722 params : Dict [str , str ] = {}
679723 if hash_type and hash_value :
724+ b64_hash = base64 .b64encode (hash_value .encode ("utf-8" )).decode ("ascii" )
680725 params ["hash.type" ] = hash_type
681- params ["hash.value" ] = hash_value
726+ params ["hash.value" ] = b64_hash
682727 if version_system :
683728 params ["versionKey.system" ] = version_system
684729 if version_name :
0 commit comments