Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,4 @@ pyrightconfig.json

# Claude Code
.claude/
workspace/
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ metadata:

imports: #Optional
git:
- env_token: e.g. GITLAB_TOKEN or empty
- token: "${GITLAB_TOKEN}" # use ${VAR} interpolation to supply from environment
ref:
url: url
path: path
maven:
- env_token: e.g. MAVEN_TOKEN or empty
- token: "${MAVEN_TOKEN}" # use ${VAR} interpolation to supply from environment
url: x.se
group_id: x
artifact_id: y
classifier: s
version: 1.0.0
pypi:
- env_token: e.g. PYPI_TOKEN or empty
- token: "${PYPI_TOKEN}" # use ${VAR} interpolation to supply from environment
url: url
package: package-name
version: 1.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ metadata:

imports:
git:
- env_token: e.g. GITLAB_TOKEN or empty
- token: "${GITLAB_TOKEN}" # use ${VAR} interpolation to supply from environment
ref:
url:
path:
Expand All @@ -26,7 +26,7 @@ filters:

implementations: # only used with type = system
git:
- env_token: e.g. GITLAB_TOKEN or empty
- token: "${GITLAB_TOKEN}" # use ${VAR} interpolation to supply from environment
ref:
url:
path:
Expand Down
28 changes: 20 additions & 8 deletions src/reqstool/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,21 @@
{"flags": ["-u", "--url"], "kwargs": {"help": "git repository URL", "required": True}},
{"flags": ["-p", "--path"], "kwargs": {"help": "path within the repository", "required": True}},
{"flags": ["-r", "--ref"], "kwargs": {"help": "git branch, tag, or commit SHA", "required": True}},
{"flags": ["-t", "--env_token"], "kwargs": {"help": "env var name holding the access token"}},
{
"flags": ["-t", "--token"],
"kwargs": {"help": "authentication token value (use ${VAR} in YAML or shell expansion for CLI)"},
},
],
},
{
"name": "maven",
"help": "maven source",
"args": [
{"flags": ["-u", "--url"], "kwargs": {"help": "Maven repository URL", "required": False}},
{"flags": ["-t", "--env_token"], "kwargs": {"help": "env var name holding the access token"}},
{
"flags": ["-t", "--token"],
"kwargs": {"help": "authentication token value (use ${VAR} in YAML or shell expansion for CLI)"},
},
{"flags": ["--group_id"], "kwargs": {"help": "Maven group ID", "required": True}},
{"flags": ["--artifact_id"], "kwargs": {"help": "Maven artifact ID", "required": True}},
{"flags": ["--version"], "kwargs": {"help": "artifact version (e.g. 1.2.3)", "required": True}},
Expand All @@ -100,7 +106,10 @@
"required": False,
},
},
{"flags": ["-t", "--env_token"], "kwargs": {"help": "env var name holding the Bearer token"}},
{
"flags": ["-t", "--token"],
"kwargs": {"help": "authentication token value (use ${VAR} in YAML or shell expansion for CLI)"},
},
{"flags": ["--package"], "kwargs": {"help": "npm package name (e.g. @scope/package)", "required": True}},
{"flags": ["--version"], "kwargs": {"help": "package version (e.g. 1.2.3)", "required": True}},
],
Expand All @@ -110,7 +119,10 @@
"help": "pypi source",
"args": [
{"flags": ["-u", "--url"], "kwargs": {"help": "PyPI index URL", "required": False}},
{"flags": ["-t", "--env_token"], "kwargs": {"help": "env var name holding the access token"}},
{
"flags": ["-t", "--token"],
"kwargs": {"help": "authentication token value (use ${VAR} in YAML or shell expansion for CLI)"},
},
{"flags": ["--package"], "kwargs": {"help": "PyPI package name", "required": True}},
{"flags": ["--version"], "kwargs": {"help": "package version (e.g. 1.2.3)", "required": True}},
],
Expand Down Expand Up @@ -437,28 +449,28 @@ def _get_initial_source(self, args_source: argparse.Namespace) -> LocationInterf
artifact_id=args_source.artifact_id,
version=args_source.version,
classifier=args_source.classifier if args_source.classifier else None,
env_token=args_source.env_token if args_source.env_token else None,
token=args_source.token or None,
)
elif args_source.source == "npm":
location = NpmLocation(
url=args_source.url if args_source.url else "https://registry.npmjs.org",
package=args_source.package,
version=args_source.version,
env_token=args_source.env_token if args_source.env_token else None,
token=args_source.token or None,
)
elif args_source.source == "pypi":
location = PypiLocation(
url=args_source.url if args_source.url else None,
package=args_source.package,
version=args_source.version,
env_token=args_source.env_token if args_source.env_token else None,
token=args_source.token or None,
)
elif args_source.source == "git":
location = GitLocation(
url=args_source.url,
path=args_source.path,
ref=args_source.ref,
env_token=args_source.env_token if args_source.env_token else None,
token=args_source.token or None,
)
elif args_source.source == "local":
if args_source.maven:
Expand Down
10 changes: 5 additions & 5 deletions src/reqstool/locations/git_location.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# Copyright © LFV

import logging
import os
import re
from typing import Optional

from pydantic import field_validator
from pydantic import SecretStr, field_validator
from pygit2 import Commit, GitError, RemoteCallbacks, UserPass, clone_repository
from reqstool_python_decorators.decorators.decorators import Requirements

Expand All @@ -21,7 +20,7 @@
class GitLocation(LocationInterface):
url: str
ref: str
env_token: Optional[str] = None
token: Optional[SecretStr] = None
path: str = ""

@field_validator("ref")
Expand All @@ -44,9 +43,10 @@ def tmpdir_key(self) -> str:
return make_safe_tmpdir_suffix("git", f"{urlunparse(parsed)}@{self.ref}")

def _make_available_on_localdisk(self, dst_path: str) -> str:
api_token = os.getenv(self.env_token) if self.env_token else None
api_token = self.token.get_secret_value() if self.token else None
callbacks = self.MyRemoteCallbacks(api_token) if api_token else None

repo = clone_repository(url=self.url, path=dst_path, callbacks=self.MyRemoteCallbacks(api_token))
repo = clone_repository(url=self.url, path=dst_path, callbacks=callbacks)

# Try direct lookup first (tag, default branch, or commit SHA), then fall back to the
# remote-tracking ref — non-default branches only exist as origin/<ref> after a plain clone.
Expand Down
6 changes: 3 additions & 3 deletions src/reqstool/locations/maven_location.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Copyright © LFV

import logging
import os
from typing import Optional

from maven_artifact import Artifact, Downloader, RequestException
from pydantic import SecretStr
from reqstool_python_decorators.decorators.decorators import Requirements

from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError
Expand All @@ -19,13 +19,13 @@ class MavenLocation(LocationInterface):
artifact_id: str
version: str
classifier: str = "reqstool"
env_token: Optional[str] = None
token: Optional[SecretStr] = None

def tmpdir_key(self) -> str:
return make_safe_tmpdir_suffix("maven", f"{self.group_id}:{self.artifact_id}:{self.version}")

def _make_available_on_localdisk(self, dst_path: str):
token = os.getenv(self.env_token) if self.env_token else None
token = self.token.get_secret_value() if self.token else None

# assume OAuth Bearer, see: https://georgearisty.dev/posts/oauth2-token-bearer-usage/
downloader = Downloader(base=self.url, token=token)
Expand Down
9 changes: 4 additions & 5 deletions src/reqstool/locations/npm_location.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# Copyright © LFV

import logging
import os
import tarfile
from typing import Optional
from urllib.parse import quote, urlparse

import requests
from pydantic import field_validator
from pydantic import SecretStr, field_validator

from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError
from reqstool.common.utils import Utils
Expand All @@ -21,7 +20,7 @@ class NpmLocation(LocationInterface):
url: str = "https://registry.npmjs.org"
package: str
version: str
env_token: Optional[str] = None
token: Optional[SecretStr] = None

@field_validator("url")
@classmethod
Expand All @@ -34,8 +33,8 @@ def tmpdir_key(self) -> str:
return make_safe_tmpdir_suffix("npm", f"{self.package}@{self.version}")

def _make_available_on_localdisk(self, dst_path: str):
"""Resolve token → fetch tarball URL → SSRF check → download → extract."""
token = os.getenv(self.env_token) if self.env_token else None
"""Fetch tarball URL → SSRF check → download → extract."""
token = self.token.get_secret_value() if self.token else None

try:
tarball_url = self._get_tarball_url(token)
Expand Down
16 changes: 7 additions & 9 deletions src/reqstool/locations/pypi_location.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Copyright © LFV

import logging
import os
import re
import tarfile
from typing import Optional
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup
from pydantic import SecretStr
from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError
from reqstool.common.utils import Utils
from reqstool.locations.location import LocationInterface, make_safe_tmpdir_suffix
Expand All @@ -16,7 +18,7 @@ class PypiLocation(LocationInterface):
url: str = "https://pypi.org/simple"
package: str
version: str
env_token: Optional[str] = None
token: Optional[SecretStr] = None

@staticmethod
def normalize_pypi_package_name(package_name):
Expand All @@ -26,19 +28,15 @@ def tmpdir_key(self) -> str:
return make_safe_tmpdir_suffix("pypi", f"{self.package}=={self.version}")

def _make_available_on_localdisk(self, dst_path: str):
"""
Download the PyPI package and extract it to the local disk.
"""
# Retrieve token from environment variable
token = os.getenv(self.env_token) if self.env_token else None
token = self.token.get_secret_value() if self.token else None

if token:
logging.debug("Using OAuth Bearer token for authentication")

package_url = self.get_package_url(self.package, self.version, self.url, token)

if not package_url:
token_info = f"(with token in environment variable '{self.env_token}')" if self.env_token else ""
token_info = "(with authentication token)" if token else ""
raise RuntimeError(
f"Unable to find a sdist pypi package for {self.package} == {self.version} in repo {self.url}{token_info}"
)
Expand All @@ -54,7 +52,7 @@ def _make_available_on_localdisk(self, dst_path: str):
except Exception as e:
raise ArtifactDownloadError(
f"Error when downloading etc sdist pypi package for {self.package}=={self.version}"
f" in repo {self.url} {'with token' if token else ''}: {e}"
f" in repo {self.url} {'with token' if token else ''}: {type(e).__name__}"
) from e

@staticmethod
Expand Down
8 changes: 4 additions & 4 deletions src/reqstool/model_generators/requirements_model_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def __parse_location_maven(self, locations_obj, instance_type, locations):
maven_location = instance_type(
parent=self.parent,
current_unresolved=MavenLocation(
env_token=maven.env_token,
token=maven.token,
url=maven.url.root if maven.url else MAVEN_CENTRAL_REPO_URL,
group_id=maven.group_id,
artifact_id=maven.artifact_id,
Expand All @@ -227,7 +227,7 @@ def __parse_location_pypi(self, locations_obj, instance_type, locations):
pypi_location = instance_type(
parent=self.parent,
current_unresolved=PypiLocation(
env_token=pypi.env_token,
token=pypi.token,
url=pypi.url.root if pypi.url else PYPI_ORG_SIMPLE_API_URL,
package=pypi.package,
version=pypi.version,
Expand All @@ -239,7 +239,7 @@ def __parse_location_pypi(self, locations_obj, instance_type, locations):
def __parse_location_npm(self, locations_obj, instance_type, locations):
if locations_obj.npm is not None:
for npm in locations_obj.npm:
npm_kwargs = {"env_token": npm.env_token, "package": npm.package, "version": npm.version}
npm_kwargs = {"token": npm.token, "package": npm.package, "version": npm.version}
if npm.url:
npm_kwargs["url"] = npm.url.root
npm_location = instance_type(
Expand All @@ -262,7 +262,7 @@ def __parse_location_git(self, locations_obj, instance_type, locations):
git_location = instance_type(
parent=self.parent,
current_unresolved=GitLocation(
env_token=git.env_token,
token=git.token,
url=git.url,
ref=git.ref,
path=git.path or "",
Expand Down
16 changes: 8 additions & 8 deletions src/reqstool/models/generated/requirements_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ class Git(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
env_token: str | None = None
token: str | None = None
"""
Token to authenticate. E.g. GITLAB_TOKEN or empty.
Authentication token value. Use ${VAR} interpolation to supply from environment, e.g. ${GITLAB_TOKEN}.
"""
ref: str
"""
Expand Down Expand Up @@ -211,9 +211,9 @@ class Maven(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
env_token: str | None = None
token: str | None = None
"""
Token to authenticate. E.g. MAVEN_TOKEN or empty.
Authentication token value. Use ${VAR} interpolation to supply from environment, e.g. ${MAVEN_TOKEN}.
"""
url: Url | None = None
"""
Expand Down Expand Up @@ -241,9 +241,9 @@ class Npm(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
env_token: str | None = None
token: str | None = None
"""
Token to authenticate. E.g. NPM_TOKEN or empty.
Authentication token value. Use ${VAR} interpolation to supply from environment, e.g. ${NPM_TOKEN}.
"""
url: Url | None = None
"""
Expand All @@ -263,9 +263,9 @@ class Pypi(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
env_token: str | None = None
token: str | None = None
"""
Token to authenticate. E.g. PYPI_TOKEN or empty.
Authentication token value. Use ${VAR} interpolation to supply from environment, e.g. ${PYPI_TOKEN}.
"""
url: Url | None = None
"""
Expand Down
Loading