Skip to content

Commit 888d957

Browse files
committed
Created get-min-py pkg
1 parent 97a74a8 commit 888d957

136 files changed

Lines changed: 2877 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323
[API usage](https://github.com/adamlui/python-utils/tree/main/find-project-root/#api-usage) /
2424
[Discuss](https://github.com/adamlui/python-utils/discussions)
2525

26+
### <a href="https://github.com/adamlui/python-utils/tree/main/get-min-py/#readme">📊 get-min-py</a>
27+
28+
> Get the minimum Python version required for a PyPI package.
29+
<br>[Install](https://github.com/adamlui/python-utils/tree/main/get-min-py/#-installation) /
30+
[Readme](https://github.com/adamlui/python-utils/tree/main/get-min-py/#readme) /
31+
[API usage](https://github.com/adamlui/python-utils/tree/main/get-min-py/#-api-usage) /
32+
[CLI usage](https://github.com/adamlui/python-utils/tree/main/get-min-py/#-cli-usage) /
33+
[Discuss](https://github.com/adamlui/python-utils/discussions)
34+
2635
### <a href="https://github.com/adamlui/python-utils/tree/main/latin-locales/#readme">🇪🇸 latin-locales</a>
2736

2837
> ISO 639-1 (2-letter) codes for Latin locales.

get-min-py/docs/LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# 🏛️ MIT License
2+
3+
**Copyright © 2026 [Adam Lui](https://github.com/adamlui)**
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

get-min-py/docs/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<a id="top"></a>
2+
3+
# > get-min-py
4+
5+
<a href="https://github.com/adamlui/python-utils/releases/tag/get-min-py-1.0.0">
6+
<img height=31 src="https://img.shields.io/badge/Latest_Build-1.0.0-32fcee.svg?logo=icinga&logoColor=white&labelColor=464646&style=for-the-badge"></a>
7+
<a href="https://github.com/adamlui/python-utils/blob/main/get-min-py/docs/LICENSE.md">
8+
<img height=31 src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
9+
<a href="https://www.codefactor.io/repository/github/adamlui/python-utils">
10+
<img height=31 src="https://img.shields.io/codefactor/grade/github/adamlui/python-utils?label=Code+Quality&logo=codefactor&logoColor=white&labelColor=464646&color=a0fc55&style=for-the-badge"></a>
11+
<a href="https://sonarcloud.io/component_measures?metric=new_vulnerabilities&id=adamlui_python-utils">
12+
<img height=31 src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fsonarcloud.io%2Fapi%2Fmeasures%2Fcomponent%3Fcomponent%3Dadamlui_python-utils%26metricKeys%3Dvulnerabilities&query=%24.component.measures.0.value&style=for-the-badge&logo=sonarcloud&logoColor=white&labelColor=464646&label=Vulnerabilities&color=fafc74"></a>
13+
14+
> ### _Get the minimum Python version required for a PyPI package._
15+
16+
Uses `python-requires`, or classifiers if not found.
17+
18+
## ⚡ Installation
19+
20+
```bash
21+
pip install get-min-py
22+
```
23+
24+
## 💻 Command line usage
25+
26+
```bash
27+
get-min-py <pkg>[,pkg_b,pkg_c] # or check-min-py
28+
```
29+
30+
Example:
31+
32+
<img src="https://cdn.jsdelivr.net/gh/adamlui/python-utils@97a74a8/get-min-py/assets/images/cli-output.png">
33+
34+
CLI options:
35+
36+
| Option | Description
37+
| --------------------------------- | -----------------
38+
| `-h`, `--help` | Show help screen
39+
| `-v`, `--version` | Show version
40+
| `-V`, `--debug [targetConfigKey]` | Show debug logs
41+
| `--docs` | Open docs URL
42+
43+
## 🔌 API usage
44+
45+
```py
46+
import get_min_py
47+
48+
# Single package
49+
result = get_min_py('requests')
50+
print(result) # '3.9'
51+
52+
# Multiple packages
53+
results = get_min_py(['numpy', 'pandas', 'flask'])
54+
print(results) # ['3.11', '3.11', '3.9']
55+
```
56+
57+
_Note: Most type checkers will falsely warn_ `get_min_py` _is not a callable module because they are incapable of analyzing runtime behavior (where the module is replaced w/ a function for cleaner, direct access). You can safely suppress such warnings using_ `# type: ignore`.
58+
59+
## MIT License
60+
61+
Copyright © 2023–2026 [Adam Lui](https://github.com/adamlui).
62+
63+
#
64+
65+
<a href="#top">Back to top ↑</a>

get-min-py/docs/SECURITY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# 🛡️ Security Policy
2+
3+
If you find a vulnerability, please e-mail security@tidelift.com and a fix will be coordinated within 2 business days.

get-min-py/noxfile.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from pathlib import Path
2+
from types import SimpleNamespace as sn
3+
4+
import nox
5+
6+
pkg = sn(dir=Path(__file__).parent.name)
7+
pkg.name = pkg.dir.replace('-', '_')
8+
paths = sn(utils=sn(bump='utils/bump.py', clean='utils/clean.py', publish='utils/publish.sh'))
9+
10+
def session(func) : return nox.session(venv_backend='none', name=func.__name__.replace('_', '-'))(func)
11+
12+
# SESSIONS
13+
14+
@session
15+
def dev(session) : session.run('pip', 'install', '-e', '.') ; session.run(pkg.dir, '--help', *session.posargs)
16+
@session
17+
def debug(session) : session.run('py', '-m', pkg.name, '--debug', *session.posargs, env={ 'PYTHONPATH': 'src' })
18+
19+
@session
20+
def lint(session) : session.run('ruff', 'check', '.', *session.posargs)
21+
@session
22+
def lint_fix(session) : session.run('ruff', 'check', '.', '--fix', *session.posargs)
23+
24+
@session
25+
def bump_patch(session, no_push=True):
26+
cmd = ['py', paths.utils.bump, '--patch']
27+
if no_push : cmd.append('--no-push')
28+
session.run(*cmd, *session.posargs)
29+
@session
30+
def bump_minor(session, no_push=True):
31+
cmd = ['py', paths.utils.bump, '--minor']
32+
if no_push : cmd.append('--no-push')
33+
session.run(*cmd, *session.posargs)
34+
@session
35+
def bump_feat(session, no_push=True):
36+
bump_minor(session, no_push)
37+
@session
38+
def bump_major(session, no_push=True):
39+
cmd = ['py', paths.utils.bump, '--major']
40+
if no_push : cmd.append('--no-push')
41+
session.run(*cmd, *session.posargs)
42+
43+
@session
44+
def build(session) : clean(session) ; session.run('py', '-m', 'build') ; print('Build complete!')
45+
@session
46+
def publish(session) : session.run('bash', paths.utils.publish, *session.posargs)
47+
48+
@session
49+
def deploy_patch(session) : bump_patch(session, no_push=False) ; build(session) ; publish(session)
50+
@session
51+
def deploy_minor(session) : bump_minor(session, no_push=False) ; build(session) ; publish(session)
52+
@session
53+
def deploy_feat(session) : deploy_minor(session)
54+
@session
55+
def deploy_major(session) : bump_major(session, no_push=False) ; build(session) ; publish(session)
56+
57+
@session
58+
def clean(session, *args) : session.run('py', paths.utils.clean, *args)

get-min-py/pyproject.toml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
[build-system]
2+
requires = [
3+
"setuptools~=82.0.0",
4+
"wheel",
5+
]
6+
build-backend = "setuptools.build_meta"
7+
8+
[project]
9+
name = "get-min-py"
10+
version = "1.0.0"
11+
description = "Get the minimum Python version required for a PyPI package."
12+
authors = [
13+
{ name = "Adam Lui", email = "adam@kudoai.com" },
14+
]
15+
readme = "docs/README.md"
16+
license = "MIT"
17+
license-files = [
18+
"docs/LICENSE.md",
19+
]
20+
dependencies = [
21+
"colorama~=0.4.6 ; platform_system == 'Windows'",
22+
"json5~=0.13.0",
23+
"ucs-detect~=2.0.2",
24+
]
25+
requires-python = ">=3.8,<4"
26+
keywords = [
27+
"api",
28+
"auto-detect",
29+
"classifiers",
30+
"cli",
31+
"compatibility",
32+
"console",
33+
"dev-tool",
34+
"minimum-version",
35+
"package-metadata",
36+
"pypi",
37+
"python-version",
38+
"requires-python",
39+
"version-check",
40+
]
41+
classifiers = [
42+
"Development Status :: 5 - Production/Stable",
43+
"Environment :: Console",
44+
"Intended Audience :: Developers",
45+
"Intended Audience :: Information Technology",
46+
"Intended Audience :: System Administrators",
47+
"Natural Language :: English",
48+
"Operating System :: OS Independent",
49+
"Programming Language :: Python",
50+
"Programming Language :: Python :: 3",
51+
"Programming Language :: Python :: 3 :: Only",
52+
"Programming Language :: Python :: 3.8",
53+
"Programming Language :: Python :: 3.9",
54+
"Programming Language :: Python :: 3.10",
55+
"Programming Language :: Python :: 3.11",
56+
"Programming Language :: Python :: 3.12",
57+
"Programming Language :: Python :: 3.13",
58+
"Programming Language :: Python :: 3.14",
59+
"Programming Language :: Python :: 3.15",
60+
"Topic :: Software Development",
61+
"Topic :: Software Development :: Libraries",
62+
"Topic :: Software Development :: Libraries :: Python Modules",
63+
"Topic :: Utilities",
64+
"Topic :: Internet",
65+
"Topic :: Internet :: WWW/HTTP",
66+
]
67+
68+
[project.urls]
69+
Changelog = "https://github.com/adamlui/python-utils/releases/tag/get-min-py-1.0.0"
70+
Documentation = "https://github.com/adamlui/python-utils/tree/main/get-min-py/docs"
71+
Funding = "https://github.com/sponsors/adamlui"
72+
Homepage = "https://github.com/adamlui/python-utils/tree/main/get-min-py/#readme"
73+
Issues = "https://github.com/adamlui/python-utils/issues"
74+
"PyPI Stats" = "https://pepy.tech/projects/get-min-py"
75+
Releases = "https://github.com/adamlui/python-utils/releases"
76+
Repository = "https://github.com/adamlui/python-utils"
77+
78+
[project.scripts]
79+
get-min-py = "get_min_py.cli.__main__:main"
80+
get-min-ver = "get_min_py.cli.__main__:main"
81+
check-min-py = "get_min_py.cli.__main__:main"
82+
check-min-ver = "get_min_py.cli.__main__:main"
83+
getminpy = "get_min_py.cli.__main__:main"
84+
getminver = "get_min_py.cli.__main__:main"
85+
checkminpy = "get_min_py.cli.__main__:main"
86+
checkminver = "get_min_py.cli.__main__:main"
87+
88+
[project.optional-dependencies]
89+
dev = [
90+
"nox>=2026.2.9",
91+
"remove-json-keys~=1.8.3",
92+
"tomli~=2.4.0",
93+
"tomli-w~=1.2.0",
94+
"translate-messages~=1.8.3",
95+
]
96+
97+
[tool.setuptools.packages.find]
98+
where = [
99+
"src",
100+
]
101+
102+
[tool.setuptools.package-data]
103+
get_min_py = [
104+
"data/*.json",
105+
"data/_locales/en/messages.json",
106+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import sys
2+
3+
from .api import get_min_py
4+
from . import cli
5+
6+
sys.modules[__name__].cli = cli # type: ignore
7+
sys.modules[__name__] = get_min_py # type: ignore

get-min-py/src/get_min_py/api.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json, re, urllib.request as http
2+
from pathlib import Path
3+
from typing import Union, List, Optional
4+
5+
api = json.loads((Path(__file__).parent / 'data/package_data.json').read_text())
6+
7+
def get_min_py(pkg_names: Union[str, List[str]]) -> Union[Optional[str], List[Optional[str]]]:
8+
if isinstance(pkg_names, str) : pkg_names = [pkg_names]
9+
results: List[Optional[str]] = []
10+
11+
for pkg_name in pkg_names: # get min py
12+
try:
13+
req = http.Request(f'https://pypi.org/pypi/{pkg_name}/json')
14+
req.add_header('User-Agent', f"{api['name']}/{api['version']}")
15+
resp = http.urlopen(req, timeout=5)
16+
pkg_info = json.loads(resp.read())['info']
17+
18+
# Check `requires_python`
19+
requires_python = pkg_info.get('requires_python')
20+
if requires_python:
21+
ver_match = re.search(r'(>|>=|==|~=)\s*(\d+\.\d+(?:\.\d+)?)', requires_python)
22+
if ver_match:
23+
op, version = ver_match.group(1), ver_match.group(2)
24+
if op == '>': # return minor-bumped
25+
major, minor = version.split('.')[:2]
26+
results.append(f'{major}.{int(minor) + 1}')
27+
else: # >=|==|~=
28+
results.append(version) # as-is
29+
continue # to next pkg
30+
31+
# Check classifiers
32+
classifiers, versions = pkg_info.get('classifiers', []), []
33+
for classifier in classifiers:
34+
if classifier.startswith('Programming Language :: Python ::'):
35+
ver_match = re.search(r'(\d+(?:\.\d+)?)', classifier)
36+
if ver_match : versions.append(ver_match.group())
37+
if versions: # append lowest
38+
decimal_vers = [ver for ver in versions if '.' in ver]
39+
if decimal_vers:
40+
decimal_vers.sort(key=lambda ver: [int(x) for x in ver.split('.')])
41+
results.append(decimal_vers[0])
42+
else:
43+
results.append(min(versions, key=int))
44+
continue # to next pkg
45+
else : results.append(None)
46+
47+
except Exception as err:
48+
print(f'Error fetching data for {pkg_name}: {err}')
49+
results.append(None)
50+
51+
return results[0] if len(pkg_names) == 1 else results

get-min-py/src/get_min_py/cli/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import sys
2+
3+
from ..api import get_min_py
4+
from .lib import init, log, settings
5+
6+
def main():
7+
if len(sys.argv) == 1 : sys.argv.append('--help')
8+
cli = init.cli()
9+
10+
# Process early-exit args (e.g. --help, --version)
11+
for ctrl_name, ctrl in vars(settings.controls).items():
12+
if getattr(ctrl, 'exit', False) and getattr(cli.config, ctrl_name, False):
13+
if hasattr(ctrl, 'handler') : ctrl.handler(cli)
14+
sys.exit(0)
15+
16+
# Process pkgs
17+
pkgs = []
18+
for arg in sys.argv[1:]:
19+
if arg.startswith('-') : continue
20+
pkgs.extend([pkg.strip() for pkg in arg.split(',') if pkg.strip()])
21+
if pkgs:
22+
results = get_min_py(pkgs)
23+
results = [results] if not isinstance(results, list) else results
24+
for pkg, version in zip(pkgs, results):
25+
if version:
26+
log.info(f'{log.colors.bw}{pkg}:{log.colors.nc} Python {version}')
27+
else:
28+
log.dim(f'{pkg}: {cli.msgs.log_NO_REQ_FOUND}', no_newline=True)
29+
log.line_break()
30+
31+
if __name__ == '__main__' : main()

0 commit comments

Comments
 (0)