Skip to content

Commit dbb7eb6

Browse files
add custom index arguments (#606)
add custom index arguments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7bbd66b commit dbb7eb6

File tree

8 files changed

+454
-18
lines changed

8 files changed

+454
-18
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,32 @@ grayskull pypi --pypi-mirror-url https://pypi.example.com --pypi-metadata-url ht
9797

9898
> *Note:* `--pypi-metadata-url` is a replacement for `--pypi-url`; `--pypi-url` is deprecated and will be removed in a future release.
9999
100+
### Checking package availability against custom indexes
101+
102+
By default, Grayskull checks if packages are available on conda-forge and highlights missing dependencies. You can specify custom package indexes to check against using the `--package-indexes` argument:
103+
104+
```bash
105+
grayskull pypi --package-indexes my-channel company-channel conda-forge pytest
106+
```
107+
108+
This will check if packages exist in `my-channel`, `company-channel`, or `conda-forge` (in that order) and mark them accordingly in the output.
109+
110+
You can also specify full URLs for internal package indexes that don't use anaconda.org:
111+
112+
```bash
113+
grayskull pypi --package-indexes https://internal-conda.example.com http://another-conda.example.com conda-forge pytest
114+
```
115+
116+
Both HTTP and HTTPS protocols are supported for custom package indexes. This is particularly useful for internal networks that don't have access to anaconda.org.
117+
118+
For internal package indexes with custom API structures, you can use the `{pkg_name}` placeholder in your URL:
119+
120+
```bash
121+
grayskull pypi --package-indexes "https://internal-conda.example.com/api/{pkg_name}/available" conda-forge pytest
122+
```
123+
124+
This allows you to specify exactly how your internal package index API works, rather than using the default `/pkg_name/files` path structure.
125+
100126
### Online Grayskull
101127

102128
It is also possible to use Grayskull without any installation. You can go to this website [marcelotrevisani.com/grayskull](https://www.marcelotrevisani.com/grayskull) and inform the name and the version (optional) of the package and it will create the recipe for you.

grayskull/base/pkg_info.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,49 @@
1+
import re
12
from functools import lru_cache
23

34
import requests
45

6+
from grayskull.cli import CLIConfig
7+
8+
9+
def build_package_url(channel_or_url: str, pkg_name: str) -> str:
10+
"""Build the URL to check for package availability.
11+
12+
:param channel_or_url: Channel name or full URL
13+
:param pkg_name: Package name
14+
:return: Full URL to check for package availability
15+
"""
16+
# Check if the channel is a full URL
17+
if re.match(r"^https?://", channel_or_url):
18+
return f"{channel_or_url.rstrip('/')}/{pkg_name}/files"
19+
else:
20+
# Default to anaconda.org if not a full URL
21+
return f"https://anaconda.org/{channel_or_url}/{pkg_name}/files"
22+
523

624
@lru_cache(maxsize=35)
7-
def is_pkg_available(pkg_name: str, channel: str = "conda-forge") -> bool:
8-
"""Verify if the package is available on Anaconda for a specific channel.
25+
def is_pkg_available(pkg_name: str, channel: str = None) -> bool:
26+
"""Verify if the package is available on Anaconda or custom package indexes.
927
1028
:param pkg_name: Package name
1129
:param channel: Anaconda channel
12-
:return: Return True if the package is present on the given channel
30+
or full URL (if None, will use channels from CLIConfig)
31+
:return: Return True if the package is present on any of the given channels or URLs
1332
"""
14-
try:
15-
response = requests.get(
16-
url=f"https://anaconda.org/{channel}/{pkg_name}/files",
17-
allow_redirects=False,
18-
)
19-
except requests.exceptions.SSLError:
20-
return False
21-
return response.status_code == 200
33+
channels_to_check = [channel] if channel else CLIConfig().package_indexes
34+
35+
for channel_to_check in channels_to_check:
36+
try:
37+
url = build_package_url(channel_to_check, pkg_name)
38+
response = requests.get(
39+
url=url,
40+
allow_redirects=False,
41+
)
42+
if response.status_code == 200:
43+
return True
44+
except requests.exceptions.RequestException:
45+
continue
46+
return False
2247

2348

2449
def normalize_pkg_name(pkg_name: str) -> str:

grayskull/cli/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020
class CLIConfig:
2121
__instance: Self | None = None
2222

23-
def __new__(cls, stdout: bool = False, list_missing_deps: bool = False):
23+
def __new__(
24+
cls,
25+
stdout: bool = False,
26+
list_missing_deps: bool = False,
27+
package_indexes: list[str] = None,
28+
):
2429
if CLIConfig.__instance is None:
2530
CLIConfig.__instance = object.__new__(cls)
2631
CLIConfig.__instance.stdout = stdout
2732
CLIConfig.__instance.list_missing_deps = list_missing_deps
33+
CLIConfig.__instance.package_indexes = package_indexes or ["conda-forge"]
2834
return CLIConfig.__instance

grayskull/cli/stdout.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,29 @@ def print_req(list_pkg):
108108
print_req(req_list)
109109

110110
print_msg(
111-
f"\n{Fore.RED}RED{Style.RESET_ALL}: Package names not available on conda-forge"
111+
f"\n{Fore.RED}RED{Style.RESET_ALL}: "
112+
"Package names not available on specified package indexes"
112113
)
113114
print_msg(
114115
f"{Fore.YELLOW}YELLOW{Style.RESET_ALL}: "
115116
"PEP-725 PURLs that did not map to known package"
116117
)
117-
print_msg(f"{Fore.GREEN}GREEN{Style.RESET_ALL}: Packages available on conda-forge")
118+
print_msg(
119+
f"{Fore.GREEN}GREEN{Style.RESET_ALL}: "
120+
"Packages available on specified package indexes"
121+
)
118122

119123
if CLIConfig().list_missing_deps:
120124
if all_missing_deps:
121-
print_msg(f"Missing dependencies: {', '.join(all_missing_deps)}")
125+
indexes = ", ".join(f"'{idx}'" for idx in CLIConfig().package_indexes)
126+
print_msg(
127+
f"Missing dependencies (not found in {indexes}): "
128+
f"{', '.join(all_missing_deps)}"
129+
)
122130
else:
123-
print_msg("All dependencies are already on conda-forge.")
131+
indexes = ", ".join(f"'{idx}'" for idx in CLIConfig().package_indexes)
132+
print_msg(
133+
f"All dependencies are already available "
134+
f"in the specified package indexes ({indexes})."
135+
)
124136
return all_missing_deps

grayskull/main.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ def init_parser():
4444
dest="list_missing_deps",
4545
help="After the execution Grayskull will print all the missing dependencies.",
4646
)
47+
cran_parser.add_argument(
48+
"--package-indexes",
49+
default=["conda-forge"],
50+
nargs="+",
51+
dest="package_indexes",
52+
help="""
53+
List of package indexes to check for existing packages.
54+
Can be channel names (e.g., conda-forge) or full URLs
55+
(e.g., https://internal-conda.example.com).
56+
For custom API structures, use the {pkg_name} placeholder
57+
(e.g., https://internal-conda.example.com/api/{pkg_name}/available).
58+
Default is conda-forge.",
59+
""",
60+
)
4761
cran_parser.add_argument(
4862
"--download",
4963
"-d",
@@ -163,6 +177,19 @@ def init_parser():
163177
dest="list_missing_deps",
164178
help="After the execution Grayskull will print all the missing dependencies.",
165179
)
180+
pypi_parser.add_argument(
181+
"--package-indexes",
182+
default=["conda-forge"],
183+
nargs="+",
184+
dest="package_indexes",
185+
help="""
186+
List of package indexes to check for existing packages.
187+
Can be channel names (e.g., conda-forge) or full URLs (e.g., https://internal-conda.example.com).
188+
For custom API structures, use the {pkg_name} placeholder
189+
(e.g., https://internal-conda.example.com/api/{pkg_name}/available).
190+
Default is conda-forge.",
191+
""",
192+
)
166193
pypi_parser.add_argument(
167194
"--strict-conda-forge",
168195
default=False,
@@ -304,6 +331,8 @@ def main(args=None):
304331

305332
CLIConfig().stdout = args.stdout
306333
CLIConfig().list_missing_deps = args.list_missing_deps
334+
if hasattr(args, "package_indexes"):
335+
CLIConfig().package_indexes = args.package_indexes
307336

308337
print_msg(Style.RESET_ALL)
309338

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ testing = [
3939
"pytest-cov",
4040
"pytest-mock",
4141
"setuptools-scm",
42+
"numpy",
4243
]
4344

4445
docs = [

0 commit comments

Comments
 (0)