Skip to content
This repository was archived by the owner on Sep 25, 2025. It is now read-only.

Commit 9c4dead

Browse files
committed
Add parameters normalizations
1 parent c03f462 commit 9c4dead

File tree

5 files changed

+328
-139
lines changed

5 files changed

+328
-139
lines changed

pydepsdev/api.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import asyncio
2020
import logging
2121
import random
22+
import base64
23+
import re
2224
from typing import Any, Dict, List, Optional, Tuple, Union
2325

2426
from .constants import (
@@ -35,9 +37,15 @@
3537
SUPPORTED_SYSTEMS_QUERY,
3638
)
3739
from .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

4047
JSONType = Union[Dict[str, Any], List[Any]]
48+
PEP503_NORMALIZE = re.compile(r"[-_.]+")
4149

4250
logger: logging.Logger = logging.getLogger(__name__)
4351
handler = 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:

pydepsdev/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616
#
1717

1818
BASE_URL = "https://api.deps.dev/v3alpha"
19+
20+
1921
SUPPORTED_SYSTEMS = ["GO", "RUBYGEMS", "NPM", "CARGO", "MAVEN", "PYPI", "NUGET"]
2022
SUPPORTED_SYSTEMS_REQUIREMENTS = ["RUBYGEMS", "NPM", "MAVEN", "NUGET"]
2123
SUPPORTED_SYSTEMS_DEPENDENCIES = ["NPM", "CARGO", "MAVEN", "PYPI"]
2224
SUPPORTED_SYSTEMS_DEPENDENTS = ["NPM", "CARGO", "MAVEN", "PYPI"]
2325
SUPPORTED_SYSTEMS_CAPABILITIES = ["GO"]
2426
SUPPORTED_SYSTEMS_QUERY = ["RUBYGEMS", "NPM", "CARGO", "MAVEN", "PYPI", "NUGET"]
27+
28+
2529
SUPPORTED_HASHES = ["MD5", "SHA1", "SHA256", "SHA512"]
2630

31+
2732
DEFAULT_MAX_RETRIES = 3
2833
DEFAULT_BASE_BACKOFF = 1
2934
DEFAULT_MAX_BACKOFF = 5

pydepsdev/utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
# limitations under the License.
1616
#
1717

18+
import functools
19+
import re
1820
import urllib.parse
1921
from typing import Optional, Sequence
22+
2023
from .constants import (
2124
SUPPORTED_SYSTEMS,
2225
SUPPORTED_SYSTEMS_REQUIREMENTS,
@@ -27,6 +30,8 @@
2730
SUPPORTED_HASHES,
2831
)
2932

33+
PEP503_NORMALIZE = re.compile(r"[-_.]+")
34+
3035

3136
def encode_url_param(param: str) -> str:
3237
"""
@@ -41,6 +46,31 @@ def encode_url_param(param: str) -> str:
4146
return urllib.parse.quote_plus(param)
4247

4348

49+
def normalize_package(fn):
50+
"""
51+
Decorator for any async method whose second argument is `package_name`.
52+
It:
53+
- upper-cases system_name,
54+
- lowercases NuGet names,
55+
- PEP503-normalizes PyPI names,
56+
- leaves other systems untouched.
57+
Then calls the wrapped fn with (self, system, normalized_pkg, *rest).
58+
"""
59+
60+
@functools.wraps(fn)
61+
async def wrapper(self, system_name: str, package_name: str, *args, **kwargs):
62+
sys = system_name.upper()
63+
if sys == "NUGET":
64+
pkg = package_name.lower()
65+
elif sys == "PYPI":
66+
pkg = PEP503_NORMALIZE.sub("-", package_name).lower()
67+
else:
68+
pkg = package_name
69+
return await fn(self, sys, pkg, *args, **kwargs)
70+
71+
return wrapper
72+
73+
4474
def validate_system(
4575
system: str,
4676
allowed_systems: Optional[Sequence[str]] = None,

0 commit comments

Comments
 (0)