pip-fund 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: pip-fund
3
+ Version: 0.1.0
4
+ Summary: Enumerate funding links for Python packages
5
+ Author-email: Gaurav Sood <gsood07@gmail.com>
6
+ Maintainer-email: Gaurav Sood <gsood07@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/gojiplus/pip-fund
9
+ Project-URL: Repository, https://github.com/gojiplus/pip-fund.git
10
+ Project-URL: Documentation, https://github.com/gojiplus/pip-fund#readme
11
+ Project-URL: Bug Tracker, https://github.com/gojiplus/pip-fund/issues
12
+ Project-URL: Funding, https://github.com/sponsors/soodoku
13
+ Keywords: funding,sponsorship,open-source,packaging,pypi,donations,npm-fund
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Software Distribution
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ Provides-Extra: github
28
+ Requires-Dist: PyGithub>=1.59; extra == "github"
29
+ Requires-Dist: PyYAML>=6.0; extra == "github"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: black>=23.0; extra == "dev"
34
+ Requires-Dist: flake8>=6.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.0; extra == "dev"
36
+
37
+ # pip‑fund
38
+
39
+ `pip‑fund` is a small command‑line tool that scans your Python
40
+ environment (or a list of package names) and reports any funding or
41
+ sponsorship links it can find. It is inspired by Node.js’s
42
+ `npm fund` command and uses Python’s packaging metadata to discover
43
+ funding information. The goal is to make it easy for developers to
44
+ discover how to support the open‑source libraries they depend on.
45
+
46
+ ## Features
47
+
48
+ * **Normalises labels and URLs:** Funding links are detected by
49
+ examining `Project‑URL` metadata and matching normalised labels
50
+ against a set of known aliases such as `funding`, `sponsor`,
51
+ `donate` and `donation`. Labels containing the words `fund` or
52
+ `sponsor` are also matched. Query strings and fragments are
53
+ stripped from URLs before grouping, so links that differ only by
54
+ tracking parameters are treated as the same.
55
+ * **Groups duplicate links:** When multiple packages share the same
56
+ funding URL, the tool groups them into a single entry and lists
57
+ all associated package names.
58
+ * **Multiple output formats:** Choose human‑readable text (default),
59
+ JSON (`--json`) or Markdown (`--markdown`).
60
+ * **Remote lookups:** Use the `--remote` flag to query the PyPI JSON
61
+ API for funding links of packages that are not installed locally.
62
+ * **Optional GitHub sponsor support:** When installed with the
63
+ `github` extra (`pip install pip‑fund[github]`), the tool can
64
+ attempt to fetch sponsor links from a repository’s
65
+ `.github/FUNDING.yml` file on GitHub if no funding metadata is
66
+ present. This requires a GitHub access token.
67
+
68
+ ## Installation
69
+
70
+ ### Basic usage
71
+
72
+ ```bash
73
+ pip install pip-fund
74
+
75
+ # Scan all installed packages for funding information
76
+ pip-fund
77
+
78
+ # Check specific packages (installed or not) and query PyPI if needed
79
+ pip-fund --remote requests flask
80
+
81
+ # Output Markdown instead of plain text
82
+ pip-fund --markdown
83
+ ```
84
+
85
+ ### Enabling GitHub sponsor integration
86
+
87
+ To enable GitHub sponsor discovery, install the `github` extra and
88
+ provide a GitHub personal access token via the `GITHUB_TOKEN` environment
89
+ variable. Without a token the GitHub API will operate with very
90
+ limited rate limits.
91
+
92
+ ```bash
93
+ pip install pip-fund[github]
94
+
95
+ export GITHUB_TOKEN=ghp_yourtokenhere
96
+ pip-fund --github flask
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ Running `pip-fund` without arguments scans all installed
102
+ distributions in the current Python environment. For each unique
103
+ funding link found, it prints the label, the URL and the names of
104
+ packages that declare it. If no funding information is discovered,
105
+ the tool explains why this might be the case.
106
+
107
+ Use the `--json` or `--markdown` flags to produce machine‑readable
108
+ output. Combine `--remote` with package names to query funding
109
+ information for packages that aren’t installed.
110
+
111
+ ## Project structure
112
+
113
+ This project follows the modern Python packaging guidelines:
114
+
115
+ * A `pyproject.toml` file declares the build system (`setuptools`) and
116
+ metadata such as the project name, version and console script
117
+ entry point. The `[project.scripts]` table instructs the build
118
+ backend to generate a `pip-fund` command that invokes the
119
+ `main()` function in `pip_fund/fund.py`【917575989859582†L503-L507】.
120
+ * Source code lives in the `src/pip_fund/` package. The main module
121
+ `fund.py` implements the logic of detecting funding links.
122
+ * Optional dependencies for GitHub sponsor support are defined under
123
+ `[project.optional-dependencies]` in `pyproject.toml`. Installing
124
+ with `pip install pip-fund[github]` adds the `PyGithub` and
125
+ `PyYAML` libraries.
126
+
127
+ ## Contributing
128
+
129
+ Contributions are welcome! Feature requests, bug reports and pull
130
+ requests are greatly appreciated. Areas of interest include:
131
+
132
+ * Improving detection of funding links in package metadata
133
+ * Supporting additional donation platforms
134
+ * Enhancing the GitHub sponsor integration
135
+ * Adding interactive prompts or browser integration to open funding
136
+ pages directly from the CLI
137
+
138
+ ## License
139
+
140
+ This project is released under the MIT License.
@@ -0,0 +1,104 @@
1
+ # pip‑fund
2
+
3
+ `pip‑fund` is a small command‑line tool that scans your Python
4
+ environment (or a list of package names) and reports any funding or
5
+ sponsorship links it can find. It is inspired by Node.js’s
6
+ `npm fund` command and uses Python’s packaging metadata to discover
7
+ funding information. The goal is to make it easy for developers to
8
+ discover how to support the open‑source libraries they depend on.
9
+
10
+ ## Features
11
+
12
+ * **Normalises labels and URLs:** Funding links are detected by
13
+ examining `Project‑URL` metadata and matching normalised labels
14
+ against a set of known aliases such as `funding`, `sponsor`,
15
+ `donate` and `donation`. Labels containing the words `fund` or
16
+ `sponsor` are also matched. Query strings and fragments are
17
+ stripped from URLs before grouping, so links that differ only by
18
+ tracking parameters are treated as the same.
19
+ * **Groups duplicate links:** When multiple packages share the same
20
+ funding URL, the tool groups them into a single entry and lists
21
+ all associated package names.
22
+ * **Multiple output formats:** Choose human‑readable text (default),
23
+ JSON (`--json`) or Markdown (`--markdown`).
24
+ * **Remote lookups:** Use the `--remote` flag to query the PyPI JSON
25
+ API for funding links of packages that are not installed locally.
26
+ * **Optional GitHub sponsor support:** When installed with the
27
+ `github` extra (`pip install pip‑fund[github]`), the tool can
28
+ attempt to fetch sponsor links from a repository’s
29
+ `.github/FUNDING.yml` file on GitHub if no funding metadata is
30
+ present. This requires a GitHub access token.
31
+
32
+ ## Installation
33
+
34
+ ### Basic usage
35
+
36
+ ```bash
37
+ pip install pip-fund
38
+
39
+ # Scan all installed packages for funding information
40
+ pip-fund
41
+
42
+ # Check specific packages (installed or not) and query PyPI if needed
43
+ pip-fund --remote requests flask
44
+
45
+ # Output Markdown instead of plain text
46
+ pip-fund --markdown
47
+ ```
48
+
49
+ ### Enabling GitHub sponsor integration
50
+
51
+ To enable GitHub sponsor discovery, install the `github` extra and
52
+ provide a GitHub personal access token via the `GITHUB_TOKEN` environment
53
+ variable. Without a token the GitHub API will operate with very
54
+ limited rate limits.
55
+
56
+ ```bash
57
+ pip install pip-fund[github]
58
+
59
+ export GITHUB_TOKEN=ghp_yourtokenhere
60
+ pip-fund --github flask
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ Running `pip-fund` without arguments scans all installed
66
+ distributions in the current Python environment. For each unique
67
+ funding link found, it prints the label, the URL and the names of
68
+ packages that declare it. If no funding information is discovered,
69
+ the tool explains why this might be the case.
70
+
71
+ Use the `--json` or `--markdown` flags to produce machine‑readable
72
+ output. Combine `--remote` with package names to query funding
73
+ information for packages that aren’t installed.
74
+
75
+ ## Project structure
76
+
77
+ This project follows the modern Python packaging guidelines:
78
+
79
+ * A `pyproject.toml` file declares the build system (`setuptools`) and
80
+ metadata such as the project name, version and console script
81
+ entry point. The `[project.scripts]` table instructs the build
82
+ backend to generate a `pip-fund` command that invokes the
83
+ `main()` function in `pip_fund/fund.py`【917575989859582†L503-L507】.
84
+ * Source code lives in the `src/pip_fund/` package. The main module
85
+ `fund.py` implements the logic of detecting funding links.
86
+ * Optional dependencies for GitHub sponsor support are defined under
87
+ `[project.optional-dependencies]` in `pyproject.toml`. Installing
88
+ with `pip install pip-fund[github]` adds the `PyGithub` and
89
+ `PyYAML` libraries.
90
+
91
+ ## Contributing
92
+
93
+ Contributions are welcome! Feature requests, bug reports and pull
94
+ requests are greatly appreciated. Areas of interest include:
95
+
96
+ * Improving detection of funding links in package metadata
97
+ * Supporting additional donation platforms
98
+ * Enhancing the GitHub sponsor integration
99
+ * Adding interactive prompts or browser integration to open funding
100
+ pages directly from the CLI
101
+
102
+ ## License
103
+
104
+ This project is released under the MIT License.
@@ -0,0 +1,98 @@
1
+ ## pyproject.toml for the pip‑fund package
2
+ ## Build system configuration. Setuptools is sufficient for this small
3
+ ## command‑line utility. See the Python Packaging User Guide for more
4
+ ## details on the `[build-system]` table and specifying build
5
+ ## dependencies.
6
+ [build-system]
7
+ requires = ["setuptools>=61.0"]
8
+ build-backend = "setuptools.build_meta"
9
+
10
+ ## Project metadata. The name should be unique on PyPI. Update
11
+ ## author information and the version as you iterate.
12
+ [project]
13
+ name = "pip-fund"
14
+ version = "0.1.0"
15
+ description = "Enumerate funding links for Python packages"
16
+ readme = "README.md"
17
+ requires-python = ">=3.8"
18
+ license = { text = "MIT" }
19
+ authors = [
20
+ { name = "Gaurav Sood", email = "gsood07@gmail.com" }
21
+ ]
22
+ maintainers = [
23
+ { name = "Gaurav Sood", email = "gsood07@gmail.com" }
24
+ ]
25
+ keywords = [
26
+ "funding",
27
+ "sponsorship",
28
+ "open-source",
29
+ "packaging",
30
+ "pypi",
31
+ "donations",
32
+ "npm-fund"
33
+ ]
34
+ classifiers = [
35
+ "Development Status :: 4 - Beta",
36
+ "Environment :: Console",
37
+ "Intended Audience :: Developers",
38
+ "License :: OSI Approved :: MIT License",
39
+ "Operating System :: OS Independent",
40
+ "Programming Language :: Python :: 3",
41
+ "Programming Language :: Python :: 3.11",
42
+ "Programming Language :: Python :: 3.12",
43
+ "Topic :: Software Development :: Libraries :: Python Modules",
44
+ "Topic :: System :: Software Distribution",
45
+ "Topic :: Utilities"
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/gojiplus/pip-fund"
50
+ Repository = "https://github.com/gojiplus/pip-fund.git"
51
+ Documentation = "https://github.com/gojiplus/pip-fund#readme"
52
+ "Bug Tracker" = "https://github.com/gojiplus/pip-fund/issues"
53
+ Funding = "https://github.com/sponsors/soodoku"
54
+
55
+ ## Define a console script so that `pip install pip-fund` creates a
56
+ ## `pip-fund` command. The value points to the `main` function in
57
+ ## `pip_fund/fund.py`.
58
+ [project.scripts]
59
+ pip-fund = "pip_fund.fund:main"
60
+
61
+ ## Optional dependencies. Installing with `pip install pip-fund[github]`
62
+ ## will pull in PyGithub and PyYAML, enabling GitHub sponsor
63
+ ## integration.
64
+ [project.optional-dependencies]
65
+ github = [
66
+ "PyGithub>=1.59",
67
+ "PyYAML>=6.0"
68
+ ]
69
+ dev = [
70
+ "pytest>=7.0",
71
+ "pytest-cov>=4.0",
72
+ "black>=23.0",
73
+ "flake8>=6.0",
74
+ "mypy>=1.0"
75
+ ]
76
+
77
+ ## Tool-specific configuration
78
+ [tool.setuptools]
79
+ package-dir = {"" = "src"}
80
+
81
+ [tool.setuptools.packages.find]
82
+ where = ["src"]
83
+
84
+ [tool.black]
85
+ line-length = 88
86
+ target-version = ["py38"]
87
+
88
+ [tool.mypy]
89
+ python_version = "3.8"
90
+ warn_return_any = true
91
+ warn_unused_configs = true
92
+ disallow_untyped_defs = true
93
+
94
+ [tool.pytest.ini_options]
95
+ testpaths = ["tests"]
96
+ python_files = ["test_*.py"]
97
+ python_classes = ["Test*"]
98
+ python_functions = ["test_*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """
2
+ pip_fund package
3
+ =================
4
+
5
+ This package exposes the ``pip_fund`` command line utility via the
6
+ console script defined in ``pyproject.toml``. Users should import
7
+ functionality from :mod:`pip_fund.fund` rather than this top‑level
8
+ package.
9
+ """
10
+
11
+ __all__ = ["fund"]
@@ -0,0 +1,568 @@
1
+ """
2
+ Core implementation for the ``pip_fund`` command.
3
+
4
+ This module provides a command‑line interface for discovering funding
5
+ links associated with Python packages. Funding information is
6
+ discovered by inspecting package metadata (``Project‑URL`` entries)
7
+ and, optionally, by querying the PyPI JSON API and GitHub’s
8
+ ``.github/FUNDING.yml`` file when the optional ``github`` feature is
9
+ enabled. Duplicate links are normalised and grouped across
10
+ packages so that each unique funding source is printed once with a
11
+ list of packages that declare it.
12
+
13
+ The default output is a simple human‑readable report, but JSON and
14
+ Markdown formats are available via command‑line switches.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import re
23
+ from collections import defaultdict
24
+ from typing import Dict, Iterable, List, Mapping, Optional, Tuple
25
+ from urllib.parse import urlparse, urlunparse
26
+
27
+ try:
28
+ # ``importlib.metadata`` is in the standard library for Python 3.8+
29
+ from importlib import metadata as importlib_metadata
30
+ except ImportError: # pragma: no cover
31
+ import importlib_metadata # type: ignore
32
+
33
+ try:
34
+ import requests # type: ignore[import]
35
+ except ImportError:
36
+ requests = None # type: ignore[assignment]
37
+
38
+ # Optional GitHub dependencies. These are only imported when the
39
+ # ``--github`` flag is provided *and* the extras have been installed.
40
+ try:
41
+ from github import Github # type: ignore[import]
42
+ import yaml # type: ignore[import]
43
+ except ImportError:
44
+ Github = None # type: ignore[assignment]
45
+ yaml = None # type: ignore[assignment]
46
+
47
+
48
+ # Normalised labels that count as “funding”. See the Python Packaging
49
+ # User Guide’s “Well-known Project URLs” section for discussion of
50
+ # funding aliases.
51
+ FUNDING_ALIASES: set[str] = {
52
+ "funding",
53
+ "sponsor",
54
+ "donate",
55
+ "donation",
56
+ }
57
+
58
+
59
+ def normalise_label(label: str) -> str:
60
+ """Normalise a Project‑URL label for comparison.
61
+
62
+ Remove punctuation and whitespace and convert to lowercase. This
63
+ follows the rules described in the core metadata specification and
64
+ PEP 753 for normalising project URL labels.
65
+
66
+ Args:
67
+ label: The label from a Project‑URL entry.
68
+
69
+ Returns:
70
+ The normalised label.
71
+ """
72
+ cleaned = re.sub(r"[^A-Za-z0-9]", "", label)
73
+ return cleaned.lower()
74
+
75
+
76
+ def normalise_url(url: str) -> str:
77
+ """Strip query parameters and fragments from a URL for grouping.
78
+
79
+ Query strings and fragments often contain tracking parameters that
80
+ should not affect whether two URLs refer to the same funding page.
81
+
82
+ Args:
83
+ url: The URL to normalise.
84
+
85
+ Returns:
86
+ The URL without ``?`` query parameters or ``#`` fragments.
87
+ """
88
+ try:
89
+ parsed = urlparse(url)
90
+ cleaned = parsed._replace(query="", fragment="")
91
+ return urlunparse(cleaned)
92
+ except Exception:
93
+ return url
94
+
95
+
96
+ def extract_funding_urls_from_dist(
97
+ dist: importlib_metadata.Distribution,
98
+ ) -> List[Tuple[str, str]]:
99
+ """Extract funding URLs from a distribution's metadata.
100
+
101
+ Args:
102
+ dist: An ``importlib.metadata`` Distribution object.
103
+
104
+ Returns:
105
+ A list of ``(label, url)`` pairs representing funding‑related
106
+ Project‑URL entries. Labels are preserved as they appear in
107
+ metadata; if an entry lacks a label, ``"Generic Link"`` is used.
108
+ """
109
+ funding_entries: List[Tuple[str, str]] = []
110
+ try:
111
+ project_urls = dist.metadata.get_all("Project-URL", []) or []
112
+ except Exception:
113
+ project_urls = []
114
+ for url_entry in project_urls:
115
+ # Format: "Label, URL"
116
+ if "," in url_entry:
117
+ label, url = url_entry.split(",", 1)
118
+ label = label.strip()
119
+ url = url.strip()
120
+ norm = normalise_label(label)
121
+ if (
122
+ norm in FUNDING_ALIASES
123
+ or "fund" in norm
124
+ or "sponsor" in norm
125
+ ):
126
+ funding_entries.append((label, url))
127
+ else:
128
+ url = url_entry.strip()
129
+ if re.search(r"fund|sponsor", url, re.IGNORECASE):
130
+ funding_entries.append(("Generic Link", url))
131
+ return funding_entries
132
+
133
+
134
+ def query_pypi_project_urls(package_name: str) -> Dict[str, str]:
135
+ """Query the PyPI JSON API for a package's project URLs.
136
+
137
+ When ``--remote`` is specified, this helper uses the public PyPI
138
+ JSON API at ``https://pypi.org/pypi/<package>/json`` to retrieve
139
+ the ``project_urls`` mapping. The API returns a dictionary of
140
+ well‑known URLs, including a possible "Funding" entry.
141
+
142
+ Args:
143
+ package_name: The name of the package on PyPI.
144
+
145
+ Returns:
146
+ A mapping of ``{label: url}``. If the package does not exist
147
+ or an error occurs, an empty dict is returned. Requires
148
+ ``requests`` to be installed.
149
+ """
150
+ if requests is None:
151
+ return {}
152
+ url = f"https://pypi.org/pypi/{package_name}/json"
153
+ try:
154
+ resp = requests.get(url, timeout=5)
155
+ if resp.status_code == 200:
156
+ data = resp.json()
157
+ project_urls: Mapping[str, str] = (
158
+ data.get("info", {}).get("project_urls", {}) or {}
159
+ )
160
+ return {
161
+ str(k): str(v)
162
+ for k, v in project_urls.items()
163
+ if k and v
164
+ }
165
+ except Exception:
166
+ pass
167
+ return {}
168
+
169
+
170
+ def extract_github_repo_from_metadata(
171
+ dist: importlib_metadata.Distribution,
172
+ ) -> Optional[str]:
173
+ """Attempt to find a GitHub repository URL in a distribution's metadata.
174
+
175
+ This helper inspects the ``Home-page`` field and ``Project-URL``
176
+ entries for a link to a GitHub repository. If found, it returns
177
+ the repository in ``owner/repo`` form. Otherwise it returns
178
+ ``None``.
179
+
180
+ Args:
181
+ dist: A distribution to inspect.
182
+
183
+ Returns:
184
+ A string like ``"octocat/Hello-World"`` or ``None``.
185
+ """
186
+ # Check the Home-page field
187
+ homepage = dist.metadata.get("Home-page")
188
+ candidates: List[str] = []
189
+ if homepage:
190
+ candidates.append(homepage)
191
+ # Also check Project-URL entries for labels like "Source" or
192
+ # "Homepage" that might include the repository. We don't use
193
+ # funding labels here.
194
+ try:
195
+ project_urls = dist.metadata.get_all("Project-URL", []) or []
196
+ for entry in project_urls:
197
+ if "," in entry:
198
+ label, url = entry.split(",", 1)
199
+ label = label.strip().lower()
200
+ url = url.strip()
201
+ if label in {"source", "homepage", "code", "repository"}:
202
+ candidates.append(url)
203
+ else:
204
+ candidates.append(entry)
205
+ except Exception:
206
+ pass
207
+ # Examine candidates for GitHub URLs
208
+ for url in candidates:
209
+ match = re.match(
210
+ r"https?://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)",
211
+ url,
212
+ )
213
+ if match:
214
+ owner, repo = match.groups()
215
+ return f"{owner}/{repo}"
216
+ return None
217
+
218
+
219
+ def fetch_github_funding(repo_path: str, token: Optional[str]) -> List[Tuple[str, str]]:
220
+ """Fetch funding links from a GitHub repository’s FUNDING.yml.
221
+
222
+ This helper uses the GitHub API via the PyGithub library to fetch
223
+ the contents of ``.github/FUNDING.yml`` for a given repository.
224
+ If successful, it returns a list of ``(platform, url)`` pairs
225
+ representing the configured funding sources. If PyGithub or
226
+ PyYAML is not installed, or the file does not exist, an empty
227
+ list is returned.
228
+
229
+ Args:
230
+ repo_path: A string like ``"owner/repo"`` specifying the
231
+ repository.
232
+ token: Optional GitHub personal access token. Without a
233
+ token, unauthenticated requests may be subject to very
234
+ strict rate limits.
235
+
236
+ Returns:
237
+ A list of ``(label, url)`` tuples.
238
+ """
239
+ if Github is None or yaml is None:
240
+ return []
241
+ try:
242
+ gh = Github(login_or_token=token)
243
+ repo = gh.get_repo(repo_path)
244
+ contents = repo.get_contents(".github/FUNDING.yml")
245
+ data = yaml.safe_load(contents.decoded_content)
246
+ if not isinstance(data, dict):
247
+ return []
248
+ results: List[Tuple[str, str]] = []
249
+ for platform, entries in data.items():
250
+ # Platform names correspond to GitHub sponsors ("github"),
251
+ # Patreon ("patreon"), etc. Entries may be strings or
252
+ # lists of identifiers. Construct a URL for each.
253
+ if not entries:
254
+ continue
255
+ if isinstance(entries, str):
256
+ entries = [entries]
257
+ for entry in entries:
258
+ # Map known platforms to URL patterns. Unknown
259
+ # platforms fall back to None and are skipped.
260
+ if platform == "github":
261
+ url = f"https://github.com/sponsors/{entry}"
262
+ label = "GitHub Sponsors"
263
+ elif platform == "patreon":
264
+ url = f"https://www.patreon.com/{entry}"
265
+ label = "Patreon"
266
+ elif platform == "tidelift":
267
+ url = f"https://tidelift.com/funding/github/{entry}"
268
+ label = "Tidelift"
269
+ elif platform == "custom":
270
+ url = entry
271
+ label = "Custom"
272
+ else:
273
+ # Unknown platform type
274
+ continue
275
+ results.append((label, url))
276
+ return results
277
+ except Exception:
278
+ return []
279
+
280
+
281
+ def gather_funding_info(
282
+ package_names: Iterable[str],
283
+ use_remote: bool,
284
+ use_github: bool,
285
+ github_token: Optional[str],
286
+ ) -> Dict[str, List[Tuple[str, str]]]:
287
+ """Gather funding information for the given packages.
288
+
289
+ Args:
290
+ package_names: Names of packages to inspect. If empty, all
291
+ installed distributions are scanned.
292
+ use_remote: Whether to query the PyPI JSON API for funding
293
+ information about packages (useful when a package is not
294
+ installed). Requires ``requests``.
295
+ use_github: Whether to attempt to discover funding links via
296
+ GitHub’s ``FUNDING.yml`` file when no funding metadata is
297
+ found locally or remotely. Requires the ``github`` extra
298
+ and a valid access token.
299
+ github_token: Personal access token for the GitHub API.
300
+
301
+ Returns:
302
+ A mapping from package name to a list of ``(label, url)`` tuples.
303
+ """
304
+ results: Dict[str, List[Tuple[str, str]]] = defaultdict(list)
305
+ if package_names:
306
+ # Explicit list of packages
307
+ for pkg in package_names:
308
+ try:
309
+ dist = importlib_metadata.distribution(pkg)
310
+ entries = extract_funding_urls_from_dist(dist)
311
+ results[pkg].extend(entries)
312
+ # Optionally query PyPI for more links even if the
313
+ # package is installed
314
+ if use_remote:
315
+ pypi_urls = query_pypi_project_urls(pkg)
316
+ for lbl, url in pypi_urls.items():
317
+ norm = normalise_label(lbl)
318
+ if (
319
+ norm in FUNDING_ALIASES
320
+ or "fund" in norm
321
+ or "sponsor" in norm
322
+ ):
323
+ if (lbl, url) not in results[pkg]:
324
+ results[pkg].append((lbl, url))
325
+ except importlib_metadata.PackageNotFoundError:
326
+ # Not installed: try PyPI
327
+ if use_remote:
328
+ pypi_urls = query_pypi_project_urls(pkg)
329
+ for lbl, url in pypi_urls.items():
330
+ norm = normalise_label(lbl)
331
+ if (
332
+ norm in FUNDING_ALIASES
333
+ or "fund" in norm
334
+ or "sponsor" in norm
335
+ ):
336
+ results[pkg].append((lbl, url))
337
+ # If we still have no funding info and GitHub support is
338
+ # enabled, attempt to fetch funding.yml
339
+ if use_github and not results[pkg]:
340
+ try:
341
+ dist = importlib_metadata.distribution(pkg)
342
+ except importlib_metadata.PackageNotFoundError:
343
+ dist = None # type: ignore
344
+ repo_path = None
345
+ if dist:
346
+ repo_path = extract_github_repo_from_metadata(dist)
347
+ if not repo_path and use_remote:
348
+ # Try remote metadata for repository URL
349
+ # using the PyPI JSON API
350
+ pypi_urls = query_pypi_project_urls(pkg)
351
+ for lbl, url in pypi_urls.items():
352
+ # look for GitHub repo in URL
353
+ match = re.match(
354
+ r"https?://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)",
355
+ url,
356
+ )
357
+ if match:
358
+ owner, repo = match.groups()
359
+ repo_path = f"{owner}/{repo}"
360
+ break
361
+ if repo_path:
362
+ entries = fetch_github_funding(repo_path, github_token)
363
+ results[pkg].extend(entries)
364
+ else:
365
+ # Scan all installed distributions
366
+ dists = list(importlib_metadata.distributions())
367
+ for dist in dists:
368
+ name = dist.metadata.get("Name", dist.metadata.get("Summary", "Unknown"))
369
+ entries = extract_funding_urls_from_dist(dist)
370
+ if entries:
371
+ results[name].extend(entries)
372
+ if use_remote and not entries:
373
+ pypi_urls = query_pypi_project_urls(name)
374
+ for lbl, url in pypi_urls.items():
375
+ norm = normalise_label(lbl)
376
+ if (
377
+ norm in FUNDING_ALIASES
378
+ or "fund" in norm
379
+ or "sponsor" in norm
380
+ ):
381
+ results[name].append((lbl, url))
382
+ if use_github and not results[name]:
383
+ repo_path = extract_github_repo_from_metadata(dist)
384
+ if repo_path:
385
+ entries = fetch_github_funding(repo_path, github_token)
386
+ results[name].extend(entries)
387
+ return results
388
+
389
+
390
+ def group_by_url(
391
+ results: Mapping[str, List[Tuple[str, str]]],
392
+ ) -> Mapping[Tuple[str, str], List[str]]:
393
+ """Group funding results by URL.
394
+
395
+ Normalises URLs before grouping so that links differing only in
396
+ query parameters or fragments are treated as the same.
397
+
398
+ Args:
399
+ results: Mapping from package name to funding entries.
400
+
401
+ Returns:
402
+ A mapping where each key is a (label, canonical_url) tuple and
403
+ the value is a list of packages that declare that funding
404
+ entry.
405
+ """
406
+ grouped: Dict[Tuple[str, str], List[str]] = defaultdict(list)
407
+ for pkg, entries in results.items():
408
+ for lbl, url in entries:
409
+ canon = normalise_url(url)
410
+ grouped[(lbl, canon)].append(pkg)
411
+ return grouped
412
+
413
+
414
+ def format_as_plain(results: Mapping[str, List[Tuple[str, str]]]) -> str:
415
+ """Format funding results as a human‑readable string.
416
+
417
+ Funding entries are grouped by canonical URL. Each unique entry
418
+ appears once with the list of packages that declare it. If no
419
+ funding information is found, an explanatory message is returned.
420
+
421
+ Args:
422
+ results: Mapping from package to funding entries.
423
+
424
+ Returns:
425
+ A string representation of the report.
426
+ """
427
+ if not results:
428
+ return (
429
+ "No funding links found for any packages.\n"
430
+ "This could mean:\n"
431
+ " - No packages declare funding links in their metadata.\n"
432
+ " - The packages use an older metadata format that doesn't support 'Project-URL'.\n"
433
+ " - The funding links are present but use a different, unrecognised label."
434
+ )
435
+ grouped = group_by_url(results)
436
+ lines: List[str] = []
437
+ lines.append("--- Funding Information Found ---")
438
+ for (lbl, url), packages in sorted(grouped.items(), key=lambda kv: kv[0][1]):
439
+ lines.append(f"{lbl}: {url}")
440
+ lines.append(" Packages: " + ", ".join(sorted(packages)))
441
+ lines.append("".join("-" for _ in range(30)))
442
+ return "\n".join(lines)
443
+
444
+
445
+ def format_as_json(results: Mapping[str, List[Tuple[str, str]]]) -> str:
446
+ """Format funding results as a JSON string.
447
+
448
+ Funding entries are grouped by canonical URL. The JSON maps each
449
+ unique link to an object containing the label, canonical URL and
450
+ the list of packages that declare it.
451
+
452
+ Args:
453
+ results: Mapping from package to funding entries.
454
+
455
+ Returns:
456
+ A JSON string representation of the grouped report.
457
+ """
458
+ grouped = group_by_url(results)
459
+ jsonable = {
460
+ f"{lbl}|{url}": {
461
+ "label": lbl,
462
+ "url": url,
463
+ "packages": sorted(packages),
464
+ }
465
+ for (lbl, url), packages in grouped.items()
466
+ }
467
+ return json.dumps(jsonable, indent=2)
468
+
469
+
470
+ def format_as_markdown(results: Mapping[str, List[Tuple[str, str]]]) -> str:
471
+ """Format funding results as a Markdown document.
472
+
473
+ Funding entries are grouped by canonical URL. Each entry is
474
+ listed once with the packages that declare it. Long lines are
475
+ avoided to ensure readability.
476
+
477
+ Args:
478
+ results: Mapping from package to funding entries.
479
+
480
+ Returns:
481
+ A Markdown string representing the report.
482
+ """
483
+ if not results:
484
+ return "No funding links found."
485
+ grouped = group_by_url(results)
486
+ lines: List[str] = []
487
+ lines.append("# Funding Information\n")
488
+ for (lbl, url), packages in sorted(grouped.items(), key=lambda kv: kv[0][1]):
489
+ lines.append(f"* **{lbl}**: {url}")
490
+ lines.append(f" - Packages: {', '.join(sorted(packages))}\n")
491
+ return "\n".join(lines)
492
+
493
+
494
+ def parse_arguments(argv: Optional[List[str]] = None) -> argparse.Namespace:
495
+ """Parse command‑line arguments.
496
+
497
+ Args:
498
+ argv: Optional list of arguments (for testing). If omitted,
499
+ defaults to ``sys.argv[1:]``.
500
+
501
+ Returns:
502
+ An ``argparse.Namespace`` containing parsed options.
503
+ """
504
+ parser = argparse.ArgumentParser(
505
+ description=(
506
+ "Enumerate funding links for Python packages by reading their metadata "
507
+ "(and optionally PyPI project_urls and GitHub FUNDING.yml).\n"
508
+ "Without arguments the script scans all installed distributions; "
509
+ "otherwise supply package names to inspect them individually."
510
+ )
511
+ )
512
+ parser.add_argument(
513
+ "packages",
514
+ nargs="*",
515
+ help="Names of packages to inspect. If omitted, all installed packages are scanned.",
516
+ )
517
+ parser.add_argument(
518
+ "--json",
519
+ action="store_true",
520
+ help="Output the results as JSON instead of human‑readable text.",
521
+ )
522
+ parser.add_argument(
523
+ "--markdown",
524
+ action="store_true",
525
+ help="Output the results as Markdown instead of human‑readable text.",
526
+ )
527
+ parser.add_argument(
528
+ "--remote",
529
+ action="store_true",
530
+ help="Query the PyPI JSON API for packages (requires network and requests).",
531
+ )
532
+ parser.add_argument(
533
+ "--github",
534
+ action="store_true",
535
+ help=(
536
+ "Attempt to fetch funding links from a repository’s .github/FUNDING.yml via the GitHub API. "
537
+ "Requires the github extra and a personal access token set via the GITHUB_TOKEN environment variable."
538
+ ),
539
+ )
540
+ return parser.parse_args(argv)
541
+
542
+
543
+ def main(argv: Optional[List[str]] = None) -> None:
544
+ """Entry point for the ``pip_fund`` command. Parses arguments and
545
+ prints the formatted funding report.
546
+
547
+ Args:
548
+ argv: Optional list of arguments to parse (for testing). If
549
+ omitted, ``sys.argv[1:]`` is used.
550
+ """
551
+ args = parse_arguments(argv)
552
+ token = os.environ.get("GITHUB_TOKEN") if args.github else None
553
+ results = gather_funding_info(
554
+ args.packages,
555
+ use_remote=args.remote,
556
+ use_github=args.github,
557
+ github_token=token,
558
+ )
559
+ if args.json:
560
+ print(format_as_json(results))
561
+ elif args.markdown:
562
+ print(format_as_markdown(results))
563
+ else:
564
+ print(format_as_plain(results))
565
+
566
+
567
+ if __name__ == "__main__": # pragma: no cover
568
+ main()
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: pip-fund
3
+ Version: 0.1.0
4
+ Summary: Enumerate funding links for Python packages
5
+ Author-email: Gaurav Sood <gsood07@gmail.com>
6
+ Maintainer-email: Gaurav Sood <gsood07@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/gojiplus/pip-fund
9
+ Project-URL: Repository, https://github.com/gojiplus/pip-fund.git
10
+ Project-URL: Documentation, https://github.com/gojiplus/pip-fund#readme
11
+ Project-URL: Bug Tracker, https://github.com/gojiplus/pip-fund/issues
12
+ Project-URL: Funding, https://github.com/sponsors/soodoku
13
+ Keywords: funding,sponsorship,open-source,packaging,pypi,donations,npm-fund
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Software Distribution
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ Provides-Extra: github
28
+ Requires-Dist: PyGithub>=1.59; extra == "github"
29
+ Requires-Dist: PyYAML>=6.0; extra == "github"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: black>=23.0; extra == "dev"
34
+ Requires-Dist: flake8>=6.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.0; extra == "dev"
36
+
37
+ # pip‑fund
38
+
39
+ `pip‑fund` is a small command‑line tool that scans your Python
40
+ environment (or a list of package names) and reports any funding or
41
+ sponsorship links it can find. It is inspired by Node.js’s
42
+ `npm fund` command and uses Python’s packaging metadata to discover
43
+ funding information. The goal is to make it easy for developers to
44
+ discover how to support the open‑source libraries they depend on.
45
+
46
+ ## Features
47
+
48
+ * **Normalises labels and URLs:** Funding links are detected by
49
+ examining `Project‑URL` metadata and matching normalised labels
50
+ against a set of known aliases such as `funding`, `sponsor`,
51
+ `donate` and `donation`. Labels containing the words `fund` or
52
+ `sponsor` are also matched. Query strings and fragments are
53
+ stripped from URLs before grouping, so links that differ only by
54
+ tracking parameters are treated as the same.
55
+ * **Groups duplicate links:** When multiple packages share the same
56
+ funding URL, the tool groups them into a single entry and lists
57
+ all associated package names.
58
+ * **Multiple output formats:** Choose human‑readable text (default),
59
+ JSON (`--json`) or Markdown (`--markdown`).
60
+ * **Remote lookups:** Use the `--remote` flag to query the PyPI JSON
61
+ API for funding links of packages that are not installed locally.
62
+ * **Optional GitHub sponsor support:** When installed with the
63
+ `github` extra (`pip install pip‑fund[github]`), the tool can
64
+ attempt to fetch sponsor links from a repository’s
65
+ `.github/FUNDING.yml` file on GitHub if no funding metadata is
66
+ present. This requires a GitHub access token.
67
+
68
+ ## Installation
69
+
70
+ ### Basic usage
71
+
72
+ ```bash
73
+ pip install pip-fund
74
+
75
+ # Scan all installed packages for funding information
76
+ pip-fund
77
+
78
+ # Check specific packages (installed or not) and query PyPI if needed
79
+ pip-fund --remote requests flask
80
+
81
+ # Output Markdown instead of plain text
82
+ pip-fund --markdown
83
+ ```
84
+
85
+ ### Enabling GitHub sponsor integration
86
+
87
+ To enable GitHub sponsor discovery, install the `github` extra and
88
+ provide a GitHub personal access token via the `GITHUB_TOKEN` environment
89
+ variable. Without a token the GitHub API will operate with very
90
+ limited rate limits.
91
+
92
+ ```bash
93
+ pip install pip-fund[github]
94
+
95
+ export GITHUB_TOKEN=ghp_yourtokenhere
96
+ pip-fund --github flask
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ Running `pip-fund` without arguments scans all installed
102
+ distributions in the current Python environment. For each unique
103
+ funding link found, it prints the label, the URL and the names of
104
+ packages that declare it. If no funding information is discovered,
105
+ the tool explains why this might be the case.
106
+
107
+ Use the `--json` or `--markdown` flags to produce machine‑readable
108
+ output. Combine `--remote` with package names to query funding
109
+ information for packages that aren’t installed.
110
+
111
+ ## Project structure
112
+
113
+ This project follows the modern Python packaging guidelines:
114
+
115
+ * A `pyproject.toml` file declares the build system (`setuptools`) and
116
+ metadata such as the project name, version and console script
117
+ entry point. The `[project.scripts]` table instructs the build
118
+ backend to generate a `pip-fund` command that invokes the
119
+ `main()` function in `pip_fund/fund.py`【917575989859582†L503-L507】.
120
+ * Source code lives in the `src/pip_fund/` package. The main module
121
+ `fund.py` implements the logic of detecting funding links.
122
+ * Optional dependencies for GitHub sponsor support are defined under
123
+ `[project.optional-dependencies]` in `pyproject.toml`. Installing
124
+ with `pip install pip-fund[github]` adds the `PyGithub` and
125
+ `PyYAML` libraries.
126
+
127
+ ## Contributing
128
+
129
+ Contributions are welcome! Feature requests, bug reports and pull
130
+ requests are greatly appreciated. Areas of interest include:
131
+
132
+ * Improving detection of funding links in package metadata
133
+ * Supporting additional donation platforms
134
+ * Enhancing the GitHub sponsor integration
135
+ * Adding interactive prompts or browser integration to open funding
136
+ pages directly from the CLI
137
+
138
+ ## License
139
+
140
+ This project is released under the MIT License.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/pip_fund/__init__.py
4
+ src/pip_fund/fund.py
5
+ src/pip_fund.egg-info/PKG-INFO
6
+ src/pip_fund.egg-info/SOURCES.txt
7
+ src/pip_fund.egg-info/dependency_links.txt
8
+ src/pip_fund.egg-info/entry_points.txt
9
+ src/pip_fund.egg-info/requires.txt
10
+ src/pip_fund.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pip-fund = pip_fund.fund:main
@@ -0,0 +1,11 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
4
+ pytest-cov>=4.0
5
+ black>=23.0
6
+ flake8>=6.0
7
+ mypy>=1.0
8
+
9
+ [github]
10
+ PyGithub>=1.59
11
+ PyYAML>=6.0
@@ -0,0 +1 @@
1
+ pip_fund