get-min-py 1.0.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.
Files changed (31) hide show
  1. get_min_py-1.0.0/PKG-INFO +118 -0
  2. get_min_py-1.0.0/docs/LICENSE.md +21 -0
  3. get_min_py-1.0.0/docs/README.md +65 -0
  4. get_min_py-1.0.0/pyproject.toml +106 -0
  5. get_min_py-1.0.0/setup.cfg +4 -0
  6. get_min_py-1.0.0/src/get_min_py/__init__.py +7 -0
  7. get_min_py-1.0.0/src/get_min_py/api.py +51 -0
  8. get_min_py-1.0.0/src/get_min_py/cli/__init__.py +0 -0
  9. get_min_py-1.0.0/src/get_min_py/cli/__main__.py +31 -0
  10. get_min_py-1.0.0/src/get_min_py/cli/lib/data/__init__.py +3 -0
  11. get_min_py-1.0.0/src/get_min_py/cli/lib/data/csv.py +5 -0
  12. get_min_py-1.0.0/src/get_min_py/cli/lib/data/file.py +23 -0
  13. get_min_py-1.0.0/src/get_min_py/cli/lib/data/json.py +55 -0
  14. get_min_py-1.0.0/src/get_min_py/cli/lib/data/sns.py +8 -0
  15. get_min_py-1.0.0/src/get_min_py/cli/lib/env.py +15 -0
  16. get_min_py-1.0.0/src/get_min_py/cli/lib/init.py +12 -0
  17. get_min_py-1.0.0/src/get_min_py/cli/lib/jsdelivr.py +9 -0
  18. get_min_py-1.0.0/src/get_min_py/cli/lib/language.py +103 -0
  19. get_min_py-1.0.0/src/get_min_py/cli/lib/log.py +101 -0
  20. get_min_py-1.0.0/src/get_min_py/cli/lib/pkg.py +6 -0
  21. get_min_py-1.0.0/src/get_min_py/cli/lib/settings.py +104 -0
  22. get_min_py-1.0.0/src/get_min_py/cli/lib/string.py +2 -0
  23. get_min_py-1.0.0/src/get_min_py/cli/lib/url.py +33 -0
  24. get_min_py-1.0.0/src/get_min_py/data/_locales/en/messages.json +17 -0
  25. get_min_py-1.0.0/src/get_min_py/data/package_data.json +26 -0
  26. get_min_py-1.0.0/src/get_min_py.egg-info/PKG-INFO +118 -0
  27. get_min_py-1.0.0/src/get_min_py.egg-info/SOURCES.txt +29 -0
  28. get_min_py-1.0.0/src/get_min_py.egg-info/dependency_links.txt +1 -0
  29. get_min_py-1.0.0/src/get_min_py.egg-info/entry_points.txt +9 -0
  30. get_min_py-1.0.0/src/get_min_py.egg-info/requires.txt +12 -0
  31. get_min_py-1.0.0/src/get_min_py.egg-info/top_level.txt +1 -0
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: get-min-py
3
+ Version: 1.0.0
4
+ Summary: Get the minimum Python version required for a PyPI package.
5
+ Author-email: Adam Lui <adam@kudoai.com>
6
+ License-Expression: MIT
7
+ Project-URL: Changelog, https://github.com/adamlui/python-utils/releases/tag/get-min-py-1.0.0
8
+ Project-URL: Documentation, https://github.com/adamlui/python-utils/tree/main/get-min-py/docs
9
+ Project-URL: Funding, https://github.com/sponsors/adamlui
10
+ Project-URL: Homepage, https://github.com/adamlui/python-utils/tree/main/get-min-py/#readme
11
+ Project-URL: Issues, https://github.com/adamlui/python-utils/issues
12
+ Project-URL: PyPI Stats, https://pepy.tech/projects/get-min-py
13
+ Project-URL: Releases, https://github.com/adamlui/python-utils/releases
14
+ Project-URL: Repository, https://github.com/adamlui/python-utils
15
+ Keywords: api,auto-detect,classifiers,cli,compatibility,console,dev-tool,minimum-version,package-metadata,pypi,python-version,requires-python,version-check
16
+ Classifier: Development Status :: 5 - Production/Stable
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: Information Technology
20
+ Classifier: Intended Audience :: System Administrators
21
+ Classifier: Natural Language :: English
22
+ Classifier: Operating System :: OS Independent
23
+ Classifier: Programming Language :: Python
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Programming Language :: Python :: 3.8
27
+ Classifier: Programming Language :: Python :: 3.9
28
+ Classifier: Programming Language :: Python :: 3.10
29
+ Classifier: Programming Language :: Python :: 3.11
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: 3.14
33
+ Classifier: Programming Language :: Python :: 3.15
34
+ Classifier: Topic :: Software Development
35
+ Classifier: Topic :: Software Development :: Libraries
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Classifier: Topic :: Utilities
38
+ Classifier: Topic :: Internet
39
+ Classifier: Topic :: Internet :: WWW/HTTP
40
+ Requires-Python: <4,>=3.8
41
+ Description-Content-Type: text/markdown
42
+ License-File: docs/LICENSE.md
43
+ Requires-Dist: colorama~=0.4.6; platform_system == "Windows"
44
+ Requires-Dist: json5~=0.13.0
45
+ Requires-Dist: ucs-detect~=2.0.2
46
+ Provides-Extra: dev
47
+ Requires-Dist: nox>=2026.2.9; extra == "dev"
48
+ Requires-Dist: remove-json-keys~=1.8.3; extra == "dev"
49
+ Requires-Dist: tomli~=2.4.0; extra == "dev"
50
+ Requires-Dist: tomli-w~=1.2.0; extra == "dev"
51
+ Requires-Dist: translate-messages~=1.8.3; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ <a id="top"></a>
55
+
56
+ # > get-min-py
57
+
58
+ <a href="https://github.com/adamlui/python-utils/releases/tag/get-min-py-1.0.0">
59
+ <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>
60
+ <a href="https://github.com/adamlui/python-utils/blob/main/get-min-py/docs/LICENSE.md">
61
+ <img height=31 src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
62
+ <a href="https://www.codefactor.io/repository/github/adamlui/python-utils">
63
+ <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>
64
+ <a href="https://sonarcloud.io/component_measures?metric=new_vulnerabilities&id=adamlui_python-utils">
65
+ <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>
66
+
67
+ > ### _Get the minimum Python version required for a PyPI package._
68
+
69
+ Uses `python-requires`, or classifiers if not found.
70
+
71
+ ## ⚡ Installation
72
+
73
+ ```bash
74
+ pip install get-min-py
75
+ ```
76
+
77
+ ## 💻 Command line usage
78
+
79
+ ```bash
80
+ get-min-py <pkg>[,pkg_b,pkg_c] # or check-min-py
81
+ ```
82
+
83
+ Example:
84
+
85
+ <img src="https://cdn.jsdelivr.net/gh/adamlui/python-utils@97a74a8/get-min-py/assets/images/cli-output.png">
86
+
87
+ CLI options:
88
+
89
+ | Option | Description
90
+ | --------------------------------- | -----------------
91
+ | `-h`, `--help` | Show help screen
92
+ | `-v`, `--version` | Show version
93
+ | `-V`, `--debug [targetConfigKey]` | Show debug logs
94
+ | `--docs` | Open docs URL
95
+
96
+ ## 🔌 API usage
97
+
98
+ ```py
99
+ import get_min_py
100
+
101
+ # Single package
102
+ result = get_min_py('requests')
103
+ print(result) # '3.9'
104
+
105
+ # Multiple packages
106
+ results = get_min_py(['numpy', 'pandas', 'flask'])
107
+ print(results) # ['3.11', '3.11', '3.9']
108
+ ```
109
+
110
+ _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`.
111
+
112
+ ## MIT License
113
+
114
+ Copyright © 2023–2026 [Adam Lui](https://github.com/adamlui).
115
+
116
+ #
117
+
118
+ <a href="#top">Back to top ↑</a>
@@ -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.
@@ -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>
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
@@ -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
File without changes
@@ -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,0 +1,3 @@
1
+ from . import csv, file, json, sns
2
+
3
+ __all__ = ['csv', 'file', 'json', 'sns']
@@ -0,0 +1,5 @@
1
+ from typing import List
2
+
3
+ def parse(val: str) -> List[str]:
4
+ if not val : return []
5
+ return [item.strip() for item in val.split(',') if item.strip()]
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+
4
+ def atomic_write(file_path: Union[Path, str], data: str, encoding: str ='utf-8') -> None: # to prevent TOCTOU
5
+ import os
6
+ file_path = Path(file_path)
7
+ file_path.parent.mkdir(parents=True, exist_ok=True)
8
+ tmp_path = file_path.parent / f'.{file_path.name}.tmp'
9
+ try:
10
+ with open(tmp_path, 'w', encoding=encoding) as file:
11
+ file.write(data) ; file.flush() ; os.fsync(file.fileno())
12
+ os.replace(tmp_path, file_path) # atomic rename
13
+ except Exception:
14
+ if tmp_path.exists() : tmp_path.unlink()
15
+ raise
16
+
17
+ def read(file_path: Union[Path, str], encoding: str = 'utf-8') -> str:
18
+ with open(file_path, 'r', encoding=encoding) as file:
19
+ return file.read()
20
+
21
+ def write(file_path: Union[Path, str], data: str, encoding: str = 'utf-8') -> None:
22
+ with open(file_path, 'w', encoding=encoding) as file:
23
+ file.write(data)
@@ -0,0 +1,55 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Union
4
+
5
+ import json5
6
+
7
+ def flatten(json: Dict[str, Any], key: str = 'message') -> Dict[str, Any]: # eliminate need to ref nested keys
8
+ flat_obj = {}
9
+ for json_key in json:
10
+ val = json[json_key]
11
+ flat_obj[json_key] = val[key] if isinstance(val, dict) and key in val else val
12
+ return flat_obj
13
+
14
+ def is_valid(file_path: Union[Path, str], format: str = 'json') -> bool:
15
+ file_path = Path(file_path)
16
+ if not file_path.exists():
17
+ return False
18
+ try : file_text = file_path.read_text(encoding='utf-8')
19
+ except Exception:
20
+ return False
21
+ if format == 'json':
22
+ try : json.loads(file_text) ; return True
23
+ except Exception : return False
24
+ elif format == 'json5':
25
+ try : json5.loads(file_text) ; return True
26
+ except Exception : return False
27
+ else:
28
+ raise ValueError(f"Unsupported format {format!r}. Expected 'json' or 'json5'")
29
+
30
+ def read(input: Union[Path, str], encoding: str = 'utf-8') -> Any:
31
+ input_str = str(input)
32
+ if input_str.endswith(('.json', '.json5')):
33
+ with open(input_str, 'r', encoding=encoding) as file:
34
+ return json5.load(file)
35
+ else : return json5.loads(input_str)
36
+
37
+ def write(file_path: Union[Path, str], data: Any, encoding: str = 'utf-8', ensure_ascii: bool = False,
38
+ style: str = 'pretty', atomic: bool =True):
39
+ from . import file
40
+ Path(file_path).parent.mkdir(parents=True, exist_ok=True)
41
+ if style == 'pretty': # single key/val spans multi-lines
42
+ json_str = json.dumps(data, indent=2, ensure_ascii=ensure_ascii)
43
+ elif style == 'compact': # single key/val per line
44
+ lines = ['{']
45
+ items = list(data.items())
46
+ for idx, (key, val) in enumerate(items):
47
+ line_end = ',' if idx < len(items) -1 else ''
48
+ inner = f'{{ {json.dumps(val, ensure_ascii=ensure_ascii)[1:-1]} }}'
49
+ lines.append(f' "{key}": {inner}{line_end}')
50
+ lines.append('}')
51
+ json_str = '\n'.join(lines)
52
+ else: # minified to single line
53
+ json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=ensure_ascii)
54
+ json_str += '\n'
55
+ getattr(file, 'atomic_write' if atomic else 'write')(file_path, json_str, encoding=encoding)
@@ -0,0 +1,8 @@
1
+ from types import SimpleNamespace as sn
2
+ from typing import Any, Dict
3
+
4
+ def from_dict(obj: Dict[str, Any]) -> sn:
5
+ for key, val in obj.items():
6
+ if isinstance(val, dict):
7
+ obj[key] = from_dict(val)
8
+ return sn(**obj)
@@ -0,0 +1,15 @@
1
+ import sys
2
+
3
+ def can_render_non_latin_scripts() -> bool: # e.g. ar, dv, zh
4
+ import json, os, subprocess
5
+ try:
6
+ result = subprocess.run(
7
+ ['ucs-detect', '--quick', '--save-json', '-'], # to stdout vs. file
8
+ capture_output=True, text=True, timeout=0.1
9
+ )
10
+ return json.loads(result.stdout).get('wide', False)
11
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
12
+ return os.environ.get('WT_SESSION') is not None if sys.platform == 'win32' else True
13
+
14
+ def is_debug_mode() -> bool:
15
+ return any(arg in ('--debug', '-V') for arg in sys.argv[1:])
@@ -0,0 +1,12 @@
1
+ from pathlib import Path
2
+ from types import SimpleNamespace as sn
3
+
4
+ from . import data
5
+
6
+ def cli() -> sn:
7
+ from . import env, language, settings
8
+ cli = data.sns.from_dict(data.json.read(Path(__file__).parent.parent.parent / 'data/package_data.json'))
9
+ cli.msgs = language.get_msgs(cli,
10
+ language.generate_random_lang(excludes=['en']) if env.is_debug_mode() else language.get_sys_lang())
11
+ settings.load(cli)
12
+ return cli
@@ -0,0 +1,9 @@
1
+ from types import SimpleNamespace as sn
2
+ from typing import Optional
3
+
4
+ def create_pkg_ver_url(cli: sn, version: Optional[str] = None) -> str:
5
+ version = version or cli.version
6
+ return f'{cli.urls.jsdelivr}@{cli.name}-{cli.version}/{cli.name}'
7
+
8
+ def create_commit_url(cli: sn, hash: str = 'latest') -> str:
9
+ return f'{cli.urls.jsdelivr}@{hash}/{cli.name}'
@@ -0,0 +1,103 @@
1
+ from pathlib import Path
2
+ import re, sys
3
+ from types import SimpleNamespace as sn
4
+ from typing import List, Optional
5
+
6
+ from . import data, log
7
+
8
+ def format_code(lang_code: str) -> str: # to match locale dir name (e.g., 'zh-tw' -> 'zh_TW')
9
+ return re.sub(
10
+ r'([a-z]{2,8})[-_]([a-z]{2})',
11
+ lambda m: f'{m.group(1).lower()}_{m.group(2).upper()}',
12
+ lang_code, flags=re.IGNORECASE
13
+ )
14
+
15
+ def generate_random_lang(includes: Optional[List[str]] = None,
16
+ excludes: Optional[List[str]] = None) -> str:
17
+ import random
18
+
19
+ if includes is None : includes = []
20
+ if excludes is None : excludes = []
21
+
22
+ def get_locales() -> List[str]:
23
+
24
+ # Read cache if found
25
+ cache_dir = Path(__file__).parent.parent / '_cache'
26
+ locale_cache = cache_dir / 'locales.json'
27
+ if locale_cache.exists():
28
+ try : return data.json.read(locale_cache)
29
+ except Exception : pass
30
+
31
+ # Discover pkg _locales
32
+ locales_dir = Path(__file__).parent.parent.parent / 'data/_locales'
33
+ if not locales_dir.exists() : return ['en']
34
+ locales = []
35
+ for entry in locales_dir.iterdir():
36
+ if entry.is_dir() and re.match(r'^\w{2}[-_]?\w{0,2}$', entry.name):
37
+ locales.append(entry.name)
38
+
39
+ # Cache result
40
+ cache_dir.mkdir(parents=True, exist_ok=True)
41
+ data.json.write(locale_cache, locales)
42
+
43
+ return locales
44
+
45
+ locales = includes.copy() if includes else get_locales()
46
+
47
+ # Filter out excludes
48
+ exclude_set = set(excludes)
49
+ locales = [locale for locale in locales if locale not in exclude_set]
50
+
51
+ # Get random language
52
+ random_lang = random.choice(locales) if locales else 'en'
53
+ log.debug(f'Random language: {random_lang}')
54
+
55
+ return random_lang
56
+
57
+ def get_msgs(cli: sn, lang_code: str = 'en') -> sn:
58
+ from . import env, jsdelivr, url
59
+
60
+ lang_code = format_code(lang_code)
61
+ if getattr(get_msgs, 'cached', None) and lang_code == get_msgs.cached_lang:
62
+ return get_msgs.cached # don't re-fetch same msgs
63
+
64
+ msgs = data.json.flatten(data.json.read( # local ones
65
+ Path(__file__).parent.parent.parent / 'data/_locales/en/messages.json'))
66
+
67
+ if not lang_code.startswith('en'): # fetch non-English msgs from jsDelivr
68
+ import non_latin_locales
69
+ if lang_code.split('_')[0] in non_latin_locales and not env.can_render_non_latin_scripts(): # type: ignore
70
+ return sn(**msgs) # EN ones cuz non-Latin not supported
71
+ msg_base_url = f'{jsdelivr.create_commit_url(cli, cli.commit_hashes.locales)}' \
72
+ '/src/get_min_py/data/_locales'
73
+ msg_url = f'{msg_base_url}/{lang_code}/messages.json'
74
+ for attempt in range(3):
75
+ try: # fetch remote msgs
76
+ msgs = data.json.flatten(data.json.read(url.get(msg_url)))
77
+ break
78
+ except Exception: # retry up to 2X (region-stripped + EN)
79
+ if attempt == 2 : break
80
+ msg_url = ( re.sub(r'([^_]*)_[^/]*(/.*)', r'\1\2', msg_url) # strip region before retrying
81
+ if attempt == 0 and '-' in lang_code else f'{msg_base_url}/en/messages.json') # else use EN msgs
82
+
83
+ get_msgs.cached = msgs
84
+ get_msgs.cached_lang = lang_code
85
+
86
+ return sn(**msgs)
87
+
88
+ def get_sys_lang(cli: Optional[sn] = None) -> str:
89
+ import os, subprocess
90
+ try:
91
+ if sys.platform == 'win32':
92
+ return subprocess.run(
93
+ ['powershell', '-Command', '(Get-Culture).TwoLetterISOLanguageName'],
94
+ capture_output=True, text=True, check=True
95
+ ).stdout.strip()
96
+ else: # macOS/Linux
97
+ for env_lang_var in ['LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LC_NAME']:
98
+ sys_lang = os.environ.get(env_lang_var)
99
+ if sys_lang : return sys_lang.split('.')[0]
100
+ return 'en'
101
+ except Exception as err:
102
+ if cli : log.error(f'{cli.msgs.err_FAILED_TO_FETCH_SYS_LANG}: {err}')
103
+ return 'en'
@@ -0,0 +1,101 @@
1
+ import os, sys
2
+ from pathlib import Path
3
+ from types import SimpleNamespace as sn
4
+ from typing import Optional
5
+ if sys.platform == 'win32' : import colorama ; colorama.init() # enable ANSI color support
6
+
7
+ from . import data as datalib, pkg
8
+
9
+ try : terminal_width = os.get_terminal_size()[0]
10
+ except OSError : terminal_width = 80
11
+
12
+ current_ver = datalib.json.read(Path(__file__).parent.parent.parent / 'data/package_data.json')['version']
13
+ next_maj_ver = pkg.get_next_maj_ver(current_ver)
14
+ _warned_keys = { 'cli': set(), 'config': set() }
15
+
16
+ colors = sn(
17
+ nc='\x1b[0m', # no color
18
+ br='\x1b[1;91m', # bright red
19
+ by='\x1b[1;33m', # bright yellow
20
+ bo='\x1b[38;5;214m', # bright orange
21
+ bg='\x1b[1;92m', # bright green
22
+ bc='\x1b[1;96m', # bright cyan
23
+ bw='\x1b[1;97m', # bright white
24
+ dg='\x1b[32m', # dark green
25
+ dy='\x1b[33m', # dark yellow
26
+ gry='\x1b[90m' # gray
27
+ )
28
+
29
+ def data(msg: str, *args, no_newline: bool = False, **kwargs) -> None:
30
+ print(f'\n{colors.bw}{msg.format(*args, **kwargs)}{colors.nc}', end='' if no_newline else None)
31
+ def dim(msg: str, *args, no_newline: bool = False, **kwargs) -> None:
32
+ print(f'\n{colors.gry}{msg.format(*args, **kwargs)}{colors.nc}', end='' if no_newline else None)
33
+ def docs_url(cli: sn) -> None : tip(f'{cli.msgs.tip_FOR_MORE_HELP_VISIT}:\n{cli.urls.docs}')
34
+ def error(msg: str, *args, **kwargs) -> None : print(f'\n{colors.br}ERROR: {msg.format(*args, **kwargs)}{colors.nc}')
35
+ def help_cmd(cli: sn) -> None : info(f"{cli.msgs.log_TYPE} '{cli.cmds[0]} --help' {cli.msgs.log_FOR_AVAIL_OPTIONS}\n")
36
+ def info(msg: str, *args, end: str = '', **kwargs) -> None:
37
+ print(f'\n{colors.by}{msg.format(*args, **kwargs)}{colors.nc}', end=end)
38
+ def line_break() : print()
39
+ def overwrite_print(msg: str, *args, **kwargs) -> None:
40
+ sys.stdout.write('\r' + msg.format(*args, **kwargs).ljust(terminal_width)[:terminal_width])
41
+ def success(msg: str, *args, **kwargs) -> None : print(f'\n{colors.bg}{msg.format(*args, **kwargs)}{colors.nc}')
42
+ def tip(msg: str, *args, **kwargs) -> None : print(f'\n{colors.bc}TIP: {msg.format(*args, **kwargs)}{colors.nc}')
43
+ def version(cli: sn) -> None:
44
+ print(f'\n{colors.by}{cli.name}\n{colors.bw}{cli.msgs.log_VERSION.lower()}: {cli.version}{colors.nc}')
45
+ def warn(msg: str, *args, **kwargs) -> None : print(f'\n{colors.bo}WARNING: {msg.format(*args, **kwargs)}{colors.nc}')
46
+
47
+ def warn_legacy_option(cli: sn, flag: str, source: str) -> None:
48
+ from . import settings
49
+ warned_set = _warned_keys[source]
50
+ if flag in warned_set : return
51
+ canonical_key = settings.get_canonical_key(flag)
52
+ msg = f"{ cli.msgs.warn_CONFIG_FILE_KEY if source == 'config' else cli.msgs.warn_CLI_OPTION } {flag!r}"
53
+ if canonical_key:
54
+ canonical_ctrl = getattr(settings.controls, canonical_key, None)
55
+ if source == 'cli' and canonical_ctrl:
56
+ flags = [arg for arg in getattr(canonical_ctrl, 'args', []) if arg.startswith('-')]
57
+ if flag.startswith('-') and len(flag) == 2: # show short flag replacement
58
+ display_key = min(flags, key=len) if flags else f"--{canonical_key.replace('_', '-')}"
59
+ else: # show long flag replacement
60
+ long_flags = [flag for flag in flags if flag.startswith('--')]
61
+ display_key = long_flags[0] if long_flags else f"--{canonical_key.replace('_', '-')}"
62
+ else:
63
+ display_key = canonical_key
64
+ msg += f' {cli.msgs.warn_HAS_BEEN_REPLACED_BY} {display_key!r}'
65
+ else:
66
+ msg += f' {cli.msgs.warn_NO_LONGER_HAS_ANY_EFFECT}'
67
+ msg += f' {cli.msgs.warn_AND_WILL_BE_REMOVED} @ v{next_maj_ver}'
68
+ warn(msg) ; warned_set.add(flag)
69
+
70
+ def cmd_docs_url_exit(cli: sn, msg: str = '', cmd: str = 'help') -> None:
71
+ if msg : error(msg)
72
+ help_cmd(cli)
73
+ docs_url(cli)
74
+ sys.exit(1)
75
+
76
+ def debug(msg: str, cli: Optional[sn] = None, *args, **kwargs) -> None:
77
+ from . import env
78
+ if not env.is_debug_mode() : return
79
+
80
+ # Init --debug [target]
81
+ debug_key=None
82
+ debug_argidx = sys.argv.index('--debug') if '--debug' in sys.argv else sys.argv.index('-V')
83
+ if debug_argidx +1 < len(sys.argv) and not sys.argv[debug_argidx +1].startswith('-'):
84
+ debug_key = sys.argv[debug_argidx +1].replace('-', '_')
85
+
86
+ if cli: # init data line
87
+ if debug_key:
88
+ data_val = getattr(cli.config, debug_key, f'cli.config key {debug_key!r} {cli.msgs.warn_NOT_FOUND.lower()}')
89
+ else:
90
+ data_val = cli.config
91
+ msg += f'\n{colors.gry}{data_val}{colors.nc}'
92
+
93
+ if args: # use 'em
94
+ msg = msg.format(*args, **kwargs)
95
+
96
+ print(f'\n{colors.by}DEBUG: {msg}{colors.nc}')
97
+
98
+ def trunc(msg: str, end: str = '\n') -> None:
99
+ truncated_lines = [
100
+ line if len(line) < terminal_width else line[:terminal_width -4] + '...' for line in msg.splitlines()]
101
+ print('\n'.join(truncated_lines), end=end)
@@ -0,0 +1,6 @@
1
+ import re
2
+
3
+ def get_next_maj_ver(version: str) -> str: # e.g. '1.2.3' -> '2.0.0'
4
+ major = re.match(r'^(\d+)\..*', version)
5
+ if not major : raise ValueError(f'Invalid version string: {version!r}')
6
+ return f'{ int(major.group(1)) +1 }.0.0'
@@ -0,0 +1,104 @@
1
+ import sys
2
+ from types import SimpleNamespace as sn
3
+ from typing import Optional
4
+
5
+ from . import log, string, url
6
+
7
+ controls = sn(
8
+ help=sn(
9
+ args=['-h', '--help'], action='help'),
10
+ version=sn(
11
+ args=['-v', '--version'], action='store_true', exit=True, handler=lambda cli: log.version(cli)),
12
+ docs=sn(
13
+ args=['--docs'], action='store_true', exit=True, handler=lambda cli: url.open(cli.urls.docs)),
14
+ debug=sn(
15
+ args=['-V', '--debug'], nargs='?', const=True, metavar='TARGET_KEY')
16
+ )
17
+
18
+ def get_canonical_key(key: str) -> Optional[str]:
19
+ if key.startswith('-'): # convert CLI arg to full key name
20
+ for ctrl_key, ctrl in vars(controls).items():
21
+ if key in getattr(ctrl, 'args', []):
22
+ key = ctrl_key
23
+ break
24
+ legacy_key = key if key.startswith('legacy_') else f'legacy_{key}'
25
+ legacy_ctrl = getattr(controls, legacy_key, None)
26
+ stripped_key = string.removeprefix(key, 'legacy_')
27
+ return legacy_ctrl.replaced_by if legacy_ctrl and hasattr(legacy_ctrl, 'replaced_by') \
28
+ else stripped_key if hasattr(controls, stripped_key) \
29
+ else None
30
+
31
+ def is_neg_key(key: str) -> bool:
32
+ import re
33
+ return bool(re.match(r'^(?:no|disable|exclude)_', string.removeprefix(key, 'legacy_')))
34
+
35
+ def load(cli: sn) -> None:
36
+ import argparse
37
+ from . import data
38
+
39
+ cli.config = sn()
40
+
41
+ # Assign help tips from cli.msgs
42
+ for ctrl_key, ctrl in vars(controls).items():
43
+ if ctrl_key.startswith('legacy_') : continue
44
+ if not hasattr(ctrl, 'help') : ctrl.help = getattr(cli.msgs, f'help_{ctrl_key.upper()}')
45
+
46
+ # Parse CLI args
47
+ argp = argparse.ArgumentParser(description=cli.description, add_help=False)
48
+ argp.usage = cli.cmd_format
49
+ valid_argparse_kwargs = {
50
+ 'action', 'choices', 'const', 'default', 'dest', 'help', 'metavar', 'nargs', 'required', 'type', 'version'}
51
+ for ctrl_key, ctrl in vars(controls).items(): # add args to argp
52
+ kwargs = ctrl.__dict__.copy()
53
+ args = kwargs.pop('args')
54
+ argparse_kwargs = { key:val for key,val in kwargs.items() if key in valid_argparse_kwargs }
55
+ if ctrl_key.startswith('legacy_'): # copy canonical attrs first
56
+ canonical_key = get_canonical_key(ctrl_key)
57
+ if canonical_key: # adjust argparse_kwargs
58
+ canonical_ctrl = getattr(controls, canonical_key)
59
+ argparse_kwargs.update({
60
+ key:val for key,val in canonical_ctrl.__dict__.items() if key in valid_argparse_kwargs })
61
+ argparse_kwargs['dest'] = canonical_key
62
+ if is_neg_key(ctrl_key) != is_neg_key(canonical_key):
63
+ argparse_kwargs['action'] = 'store_false' if argparse_kwargs['action'] == 'store_true' \
64
+ else 'store_true'
65
+ for arg in args:
66
+ if arg in sys.argv[1:]:
67
+ log.warn_legacy_option(cli, arg, source='cli')
68
+ break
69
+ argp.add_argument(*args, **argparse_kwargs)
70
+ parsed_args, unknown_args = argp.parse_known_args()
71
+ exempt_flags = [] # exempt valid dash-less args from validation
72
+ exempt_flags.extend(arg.lstrip('-') for ctrl_key, ctrl in vars(controls).items()
73
+ if getattr(ctrl, 'subcmd', False)
74
+ for arg in ctrl.args if len(arg) > 2) # skip short flags
75
+ if unknown_args:
76
+ unknown_flags = [arg for arg in unknown_args if arg.startswith('-')]
77
+ if unknown_flags:
78
+ log.cmd_docs_url_exit(cli, f"{cli.msgs.err_UNRECOGNIZED_ARGS}: {' '.join(unknown_flags)}", cmd='help')
79
+ for ctrl_key, ctrl in vars(controls).items(): # process subcmds
80
+ if getattr(ctrl, 'subcmd', False) \
81
+ and next(arg for arg in ctrl.args if arg.startswith('--'))[2:] in sys.argv[1:]:
82
+ setattr(parsed_args, ctrl_key, True)
83
+ applied_args = []
84
+ for arg in sys.argv[1:]:
85
+ if not arg.startswith('-') : continue
86
+ base_arg = arg.split('=')[0]
87
+ for ctrl_key, ctrl in vars(controls).items():
88
+ if base_arg in getattr(ctrl, 'args', []):
89
+ dest = get_canonical_key(ctrl_key) or ctrl_key if ctrl_key.startswith('legacy_') else ctrl_key
90
+ parsed_val = getattr(parsed_args, dest, None)
91
+ if parsed_val is not None:
92
+ setattr(cli.config, dest, parsed_val)
93
+ applied_args.append(arg)
94
+ break
95
+ log.debug(f'Args parsed! {log.colors.bg}{len(applied_args)} args applied {applied_args}', cli)
96
+
97
+ # Apply parsers/default_vals
98
+ for ctrl_key, ctrl in vars(controls).items():
99
+ if not hasattr(cli.config, ctrl_key):
100
+ setattr(cli.config, ctrl_key, ctrl.default_val if hasattr(ctrl, 'default_val') else None)
101
+ config_val = getattr(cli.config, ctrl_key)
102
+ if getattr(ctrl, 'parser', '') == 'csv':
103
+ setattr(cli.config, ctrl_key, data.csv.parse(config_val))
104
+ log.debug('All cli.config vals set!', cli)
@@ -0,0 +1,2 @@
1
+ def removeprefix(str: str, prefix: str) -> str:
2
+ return str[len(prefix):] if str.startswith(prefix) else str
@@ -0,0 +1,33 @@
1
+ from typing import List, Optional, Tuple
2
+
3
+ def get(url: str, timeout: int = 5, encoding: str = 'utf-8') -> str:
4
+ from urllib.error import URLError
5
+ from urllib.request import urlopen
6
+ url = validate(url)
7
+ try:
8
+ with urlopen(url, timeout=timeout) as resp:
9
+ return resp.read().decode(encoding)
10
+ except URLError as err:
11
+ raise RuntimeError(f'Failed to fetch from {url}: {err}')
12
+
13
+ def open(url: str) -> None:
14
+ import webbrowser
15
+ url = validate(url)
16
+ try : webbrowser.open(url)
17
+ except Exception as err:
18
+ raise RuntimeError(f'Failed to open {url} in browser: {err}')
19
+
20
+ def validate(url: str, allowed_schemes: Tuple[str, ...] = ('http', 'https'),
21
+ allowed_domains: Optional[List[str]] = None) -> str:
22
+ from urllib.parse import urlparse
23
+ parsed_url = urlparse(url)
24
+
25
+ if parsed_url.scheme not in allowed_schemes:
26
+ raise ValueError(f'URL scheme {parsed_url.scheme!r} not allowed. Allowed: {allowed_schemes}')
27
+
28
+ if allowed_domains:
29
+ input_domain = parsed_url.netloc.lower()
30
+ if not any(input_domain.endswith(allowed_domain.lower()) for allowed_domain in allowed_domains):
31
+ raise ValueError(f'URL domain {input_domain!r} not allowed. Allowed: {allowed_domains}')
32
+
33
+ return url
@@ -0,0 +1,17 @@
1
+ {
2
+ "log_NO_REQ_FOUND": { "message": "No requirement found" },
3
+ "log_TYPE": { "message": "Type" },
4
+ "log_FOR_AVAIL_OPTIONS": { "message": "for available options" },
5
+ "log_VERSION": { "message": "Version" },
6
+ "tip_FOR_MORE_HELP_VISIT": { "message": "For more help, visit" },
7
+ "warn_NOT_FOUND": { "message": "Not found" },
8
+ "warn_CLI_OPTION": { "message": "CLI option" },
9
+ "warn_HAS_BEEN_REPLACED_BY": { "message": "has been replaced by" },
10
+ "warn_AND_WILL_BE_REMOVED": { "message": "and will be removed" },
11
+ "warn_NO_LONGER_HAS_ANY_EFFECT": { "message": "no longer has any effect" },
12
+ "err_UNRECOGNIZED_ARGS": { "message": "Unrecognized argument(s)" },
13
+ "help_HELP": { "message": "Show help screen" },
14
+ "help_VERSION": { "message": "Show version" },
15
+ "help_DOCS": { "message": "Open docs URL" },
16
+ "help_DEBUG": { "message": "Show debug logs" }
17
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "get-min-py",
3
+ "version": "1.0.0",
4
+ "description": "Get the minimum Python version required for a PyPI package.",
5
+ "cmds": ["get-min-py", "check-min-py"],
6
+ "cmd_format": "get-min-py <pkg>[,pkg_b,pkg_c]",
7
+ "author": { "name": "Adam Lui", "email": "adam@kudoai.com", "url": "https://github.com/adamlui" },
8
+ "urls": {
9
+ "docs": "https://github.com/adamlui/python-utils/tree/main/get-min-py/docs",
10
+ "funding": {
11
+ "cashapp": "https://cash.app/$adamlui",
12
+ "github": "https://github.com/sponsors/adamlui",
13
+ "kofi": "https://ko-fi.com/adamlui",
14
+ "paypal": "https://paypal.me/adamlui"
15
+ },
16
+ "github": "https://github.com/adamlui/python-utils",
17
+ "jsdelivr": "https://cdn.jsdelivr.net/gh/adamlui/python-utils",
18
+ "pypi": "https://pypi.org/project/get-min-py/",
19
+ "pypistats": "https://pypistats.org/packages/get-min-py",
20
+ "support": "https://github.com/adamlui/python-utils/issues"
21
+ },
22
+ "commit_hashes": {
23
+ "data": "50a31e1",
24
+ "locales": "50a31e1"
25
+ }
26
+ }
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: get-min-py
3
+ Version: 1.0.0
4
+ Summary: Get the minimum Python version required for a PyPI package.
5
+ Author-email: Adam Lui <adam@kudoai.com>
6
+ License-Expression: MIT
7
+ Project-URL: Changelog, https://github.com/adamlui/python-utils/releases/tag/get-min-py-1.0.0
8
+ Project-URL: Documentation, https://github.com/adamlui/python-utils/tree/main/get-min-py/docs
9
+ Project-URL: Funding, https://github.com/sponsors/adamlui
10
+ Project-URL: Homepage, https://github.com/adamlui/python-utils/tree/main/get-min-py/#readme
11
+ Project-URL: Issues, https://github.com/adamlui/python-utils/issues
12
+ Project-URL: PyPI Stats, https://pepy.tech/projects/get-min-py
13
+ Project-URL: Releases, https://github.com/adamlui/python-utils/releases
14
+ Project-URL: Repository, https://github.com/adamlui/python-utils
15
+ Keywords: api,auto-detect,classifiers,cli,compatibility,console,dev-tool,minimum-version,package-metadata,pypi,python-version,requires-python,version-check
16
+ Classifier: Development Status :: 5 - Production/Stable
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: Information Technology
20
+ Classifier: Intended Audience :: System Administrators
21
+ Classifier: Natural Language :: English
22
+ Classifier: Operating System :: OS Independent
23
+ Classifier: Programming Language :: Python
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Programming Language :: Python :: 3.8
27
+ Classifier: Programming Language :: Python :: 3.9
28
+ Classifier: Programming Language :: Python :: 3.10
29
+ Classifier: Programming Language :: Python :: 3.11
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Programming Language :: Python :: 3.14
33
+ Classifier: Programming Language :: Python :: 3.15
34
+ Classifier: Topic :: Software Development
35
+ Classifier: Topic :: Software Development :: Libraries
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Classifier: Topic :: Utilities
38
+ Classifier: Topic :: Internet
39
+ Classifier: Topic :: Internet :: WWW/HTTP
40
+ Requires-Python: <4,>=3.8
41
+ Description-Content-Type: text/markdown
42
+ License-File: docs/LICENSE.md
43
+ Requires-Dist: colorama~=0.4.6; platform_system == "Windows"
44
+ Requires-Dist: json5~=0.13.0
45
+ Requires-Dist: ucs-detect~=2.0.2
46
+ Provides-Extra: dev
47
+ Requires-Dist: nox>=2026.2.9; extra == "dev"
48
+ Requires-Dist: remove-json-keys~=1.8.3; extra == "dev"
49
+ Requires-Dist: tomli~=2.4.0; extra == "dev"
50
+ Requires-Dist: tomli-w~=1.2.0; extra == "dev"
51
+ Requires-Dist: translate-messages~=1.8.3; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ <a id="top"></a>
55
+
56
+ # > get-min-py
57
+
58
+ <a href="https://github.com/adamlui/python-utils/releases/tag/get-min-py-1.0.0">
59
+ <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>
60
+ <a href="https://github.com/adamlui/python-utils/blob/main/get-min-py/docs/LICENSE.md">
61
+ <img height=31 src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
62
+ <a href="https://www.codefactor.io/repository/github/adamlui/python-utils">
63
+ <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>
64
+ <a href="https://sonarcloud.io/component_measures?metric=new_vulnerabilities&id=adamlui_python-utils">
65
+ <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>
66
+
67
+ > ### _Get the minimum Python version required for a PyPI package._
68
+
69
+ Uses `python-requires`, or classifiers if not found.
70
+
71
+ ## ⚡ Installation
72
+
73
+ ```bash
74
+ pip install get-min-py
75
+ ```
76
+
77
+ ## 💻 Command line usage
78
+
79
+ ```bash
80
+ get-min-py <pkg>[,pkg_b,pkg_c] # or check-min-py
81
+ ```
82
+
83
+ Example:
84
+
85
+ <img src="https://cdn.jsdelivr.net/gh/adamlui/python-utils@97a74a8/get-min-py/assets/images/cli-output.png">
86
+
87
+ CLI options:
88
+
89
+ | Option | Description
90
+ | --------------------------------- | -----------------
91
+ | `-h`, `--help` | Show help screen
92
+ | `-v`, `--version` | Show version
93
+ | `-V`, `--debug [targetConfigKey]` | Show debug logs
94
+ | `--docs` | Open docs URL
95
+
96
+ ## 🔌 API usage
97
+
98
+ ```py
99
+ import get_min_py
100
+
101
+ # Single package
102
+ result = get_min_py('requests')
103
+ print(result) # '3.9'
104
+
105
+ # Multiple packages
106
+ results = get_min_py(['numpy', 'pandas', 'flask'])
107
+ print(results) # ['3.11', '3.11', '3.9']
108
+ ```
109
+
110
+ _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`.
111
+
112
+ ## MIT License
113
+
114
+ Copyright © 2023–2026 [Adam Lui](https://github.com/adamlui).
115
+
116
+ #
117
+
118
+ <a href="#top">Back to top ↑</a>
@@ -0,0 +1,29 @@
1
+ pyproject.toml
2
+ docs/LICENSE.md
3
+ docs/README.md
4
+ src/get_min_py/__init__.py
5
+ src/get_min_py/api.py
6
+ src/get_min_py.egg-info/PKG-INFO
7
+ src/get_min_py.egg-info/SOURCES.txt
8
+ src/get_min_py.egg-info/dependency_links.txt
9
+ src/get_min_py.egg-info/entry_points.txt
10
+ src/get_min_py.egg-info/requires.txt
11
+ src/get_min_py.egg-info/top_level.txt
12
+ src/get_min_py/cli/__init__.py
13
+ src/get_min_py/cli/__main__.py
14
+ src/get_min_py/cli/lib/env.py
15
+ src/get_min_py/cli/lib/init.py
16
+ src/get_min_py/cli/lib/jsdelivr.py
17
+ src/get_min_py/cli/lib/language.py
18
+ src/get_min_py/cli/lib/log.py
19
+ src/get_min_py/cli/lib/pkg.py
20
+ src/get_min_py/cli/lib/settings.py
21
+ src/get_min_py/cli/lib/string.py
22
+ src/get_min_py/cli/lib/url.py
23
+ src/get_min_py/cli/lib/data/__init__.py
24
+ src/get_min_py/cli/lib/data/csv.py
25
+ src/get_min_py/cli/lib/data/file.py
26
+ src/get_min_py/cli/lib/data/json.py
27
+ src/get_min_py/cli/lib/data/sns.py
28
+ src/get_min_py/data/package_data.json
29
+ src/get_min_py/data/_locales/en/messages.json
@@ -0,0 +1,9 @@
1
+ [console_scripts]
2
+ check-min-py = get_min_py.cli.__main__:main
3
+ check-min-ver = get_min_py.cli.__main__:main
4
+ checkminpy = get_min_py.cli.__main__:main
5
+ checkminver = get_min_py.cli.__main__:main
6
+ get-min-py = get_min_py.cli.__main__:main
7
+ get-min-ver = get_min_py.cli.__main__:main
8
+ getminpy = get_min_py.cli.__main__:main
9
+ getminver = get_min_py.cli.__main__:main
@@ -0,0 +1,12 @@
1
+ json5~=0.13.0
2
+ ucs-detect~=2.0.2
3
+
4
+ [:platform_system == "Windows"]
5
+ colorama~=0.4.6
6
+
7
+ [dev]
8
+ nox>=2026.2.9
9
+ remove-json-keys~=1.8.3
10
+ tomli~=2.4.0
11
+ tomli-w~=1.2.0
12
+ translate-messages~=1.8.3
@@ -0,0 +1 @@
1
+ get_min_py