hatch-openzim 0.1.0__py3-none-any.whl
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.
- hatch_openzim/__about__.py +1 -0
- hatch_openzim/__init__.py +0 -0
- hatch_openzim/build_hook.py +19 -0
- hatch_openzim/files_install.py +236 -0
- hatch_openzim/hooks.py +16 -0
- hatch_openzim/metadata.py +63 -0
- hatch_openzim/metadata_hook.py +19 -0
- hatch_openzim/shared.py +16 -0
- hatch_openzim/utils.py +80 -0
- hatch_openzim-0.1.0.dist-info/METADATA +307 -0
- hatch_openzim-0.1.0.dist-info/RECORD +14 -0
- hatch_openzim-0.1.0.dist-info/WHEEL +4 -0
- hatch_openzim-0.1.0.dist-info/entry_points.txt +2 -0
- hatch_openzim-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
2
|
+
|
|
3
|
+
from hatch_openzim.files_install import process as process_files_install
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenzimBuildHook(BuildHookInterface):
|
|
7
|
+
"""Hatch build hook to perform custom openzim actions
|
|
8
|
+
|
|
9
|
+
This hook performs:
|
|
10
|
+
- files installation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
PLUGIN_NAME = "openzim-build"
|
|
14
|
+
|
|
15
|
+
def initialize(self, version, build_data): # noqa: ARG002
|
|
16
|
+
if "toml-config" in self.config:
|
|
17
|
+
process_files_install(openzim_toml_location=self.config["toml-config"])
|
|
18
|
+
else:
|
|
19
|
+
process_files_install()
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
from urllib.request import urlopen
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import tomllib
|
|
11
|
+
except ImportError: # pragma: no cover
|
|
12
|
+
import toml as tomllib
|
|
13
|
+
|
|
14
|
+
from hatch_openzim.shared import logger
|
|
15
|
+
|
|
16
|
+
DEFAULT_OPENZIM_TOML_LOCATION = "openzim.toml"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def process(openzim_toml_location: str = DEFAULT_OPENZIM_TOML_LOCATION):
|
|
20
|
+
"""performs openZIM operations on files, i.e. download and extract ZIPs
|
|
21
|
+
|
|
22
|
+
Configuration is read from openzim.toml which must be in root of the hatch project,
|
|
23
|
+
next to pyproject.toml
|
|
24
|
+
|
|
25
|
+
openzim_toml_location: location of the openzim.toml file with instructions about
|
|
26
|
+
files to install locally
|
|
27
|
+
"""
|
|
28
|
+
config_path = Path(openzim_toml_location)
|
|
29
|
+
if not config_path.exists():
|
|
30
|
+
if openzim_toml_location != DEFAULT_OPENZIM_TOML_LOCATION:
|
|
31
|
+
raise Exception(f"File is missing at {openzim_toml_location}")
|
|
32
|
+
else:
|
|
33
|
+
return
|
|
34
|
+
config = tomllib.loads(config_path.read_text())
|
|
35
|
+
files_config = config.get("files", None)
|
|
36
|
+
if not files_config:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
for section_name, section_data in files_config.items():
|
|
40
|
+
_process_section(section_name=section_name, section_data=section_data)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _process_section(section_name: str, section_data: Dict[str, Any]):
|
|
44
|
+
"""processes all actions required for one section (i.e. one target folder)"""
|
|
45
|
+
|
|
46
|
+
logger.info(f"Processing {section_name} section")
|
|
47
|
+
section_config = section_data.get("config", None)
|
|
48
|
+
if not section_config:
|
|
49
|
+
raise Exception("config table is mandatory")
|
|
50
|
+
base_target_dir = section_config.get("target_dir", None)
|
|
51
|
+
if not base_target_dir:
|
|
52
|
+
raise Exception("target_dir is mandatory in config table")
|
|
53
|
+
base_target_dir = Path(base_target_dir)
|
|
54
|
+
section_actions = section_data.get("actions", None)
|
|
55
|
+
if not section_actions:
|
|
56
|
+
logger.info(" No actions to process")
|
|
57
|
+
return
|
|
58
|
+
base_target_dir = Path(section_config["target_dir"])
|
|
59
|
+
base_target_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
logger.info(f" Installing files in {base_target_dir}")
|
|
61
|
+
for action_name, action_config in section_actions.items():
|
|
62
|
+
_process_one_action(base_target_dir, action_name, action_config)
|
|
63
|
+
|
|
64
|
+
execute_after = section_config.get("execute_after", None)
|
|
65
|
+
if execute_after:
|
|
66
|
+
_process_execute_after(base_target_dir=base_target_dir, actions=execute_after)
|
|
67
|
+
|
|
68
|
+
logger.info(" All done")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _process_one_action(
|
|
72
|
+
base_target_dir: Path, action_name: str, action_data: Dict[str, Any]
|
|
73
|
+
):
|
|
74
|
+
"""processes one action (basically trigger the right kind of action)"""
|
|
75
|
+
logger.info(f" Processing {action_name} action")
|
|
76
|
+
source = action_data.get("source", None)
|
|
77
|
+
if not source:
|
|
78
|
+
raise Exception("source is not configured")
|
|
79
|
+
action = action_data.get("action", None)
|
|
80
|
+
if not action:
|
|
81
|
+
raise Exception("action is not configured")
|
|
82
|
+
|
|
83
|
+
if action == "get_file":
|
|
84
|
+
_process_get_file_action(
|
|
85
|
+
base_target_dir=base_target_dir, source=source, action_data=action_data
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
elif action == "extract_all":
|
|
89
|
+
_process_extract_all_action(
|
|
90
|
+
base_target_dir=base_target_dir, source=source, action_data=action_data
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
elif action == "extract_items":
|
|
94
|
+
_process_extract_items_action(
|
|
95
|
+
base_target_dir=base_target_dir, source=source, action_data=action_data
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
else:
|
|
99
|
+
raise Exception(f"Unsupported action '{action}'")
|
|
100
|
+
|
|
101
|
+
execute_after = action_data.get("execute_after", None)
|
|
102
|
+
if execute_after:
|
|
103
|
+
_process_execute_after(base_target_dir=base_target_dir, actions=execute_after)
|
|
104
|
+
|
|
105
|
+
logger.info(" Done")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _process_execute_after(base_target_dir: Path, actions: List[str]):
|
|
109
|
+
"""execute actions after file(s) installation"""
|
|
110
|
+
|
|
111
|
+
for action in actions:
|
|
112
|
+
logger.info(f" Executing '{action}'")
|
|
113
|
+
process = subprocess.run(
|
|
114
|
+
action,
|
|
115
|
+
shell=True, # noqa: S602 # nosec: B602
|
|
116
|
+
cwd=base_target_dir,
|
|
117
|
+
text=True,
|
|
118
|
+
check=True,
|
|
119
|
+
capture_output=True,
|
|
120
|
+
)
|
|
121
|
+
if process.stdout:
|
|
122
|
+
logger.info(f" stdout:\n{process.stdout}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _process_get_file_action(
|
|
126
|
+
base_target_dir: Path, source: str, action_data: Dict[str, Any]
|
|
127
|
+
):
|
|
128
|
+
"""downloads one file to a given location"""
|
|
129
|
+
target_file = action_data.get("target_file", None)
|
|
130
|
+
if not target_file:
|
|
131
|
+
raise Exception("target_file is mandatory when action='get_file'")
|
|
132
|
+
target_dir = action_data.get("target_dir", None)
|
|
133
|
+
local_dir = base_target_dir
|
|
134
|
+
if target_dir:
|
|
135
|
+
local_dir = base_target_dir / str(target_dir)
|
|
136
|
+
local_dir.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
local_file = local_dir / str(target_file)
|
|
138
|
+
if local_file.exists():
|
|
139
|
+
logger.info(" Skipping, local_file is already present")
|
|
140
|
+
return
|
|
141
|
+
_download_file(source, local_file)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _process_extract_all_action(
|
|
145
|
+
base_target_dir: Path, source: str, action_data: Dict[str, Any]
|
|
146
|
+
):
|
|
147
|
+
"""extacts all Zip content to a given location
|
|
148
|
+
|
|
149
|
+
Supports the remove attribute which allows to delete some Zip content
|
|
150
|
+
"""
|
|
151
|
+
target_dir = action_data.get("target_dir", None)
|
|
152
|
+
if not target_dir:
|
|
153
|
+
raise Exception("target_dir is mandatory when action='extract_all'")
|
|
154
|
+
target_dir = base_target_dir / str(target_dir)
|
|
155
|
+
if target_dir.exists():
|
|
156
|
+
logger.info(" Skipping, target_dir is already present")
|
|
157
|
+
return
|
|
158
|
+
if not target_dir.parent.exists():
|
|
159
|
+
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
_extract_zip_from_url(url=source, extract_to=target_dir)
|
|
161
|
+
if "remove" in action_data:
|
|
162
|
+
_remove_items(globs=action_data["remove"], directory=target_dir)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _process_extract_items_action(
|
|
166
|
+
base_target_dir: Path, source: str, action_data: Dict[str, Any]
|
|
167
|
+
):
|
|
168
|
+
"""extacts some items of a content to some given locations
|
|
169
|
+
|
|
170
|
+
Supports the remove attribute which allows to delete some items content after
|
|
171
|
+
extraction
|
|
172
|
+
"""
|
|
173
|
+
zip_paths = action_data.get("zip_paths", None)
|
|
174
|
+
if not zip_paths:
|
|
175
|
+
raise Exception("zip_paths is mandatory when action='extract_items'")
|
|
176
|
+
target_paths = action_data.get("target_paths", None)
|
|
177
|
+
if not target_paths:
|
|
178
|
+
raise Exception("target_paths is mandatory when action='extract_items'")
|
|
179
|
+
if len(zip_paths) != len(target_paths):
|
|
180
|
+
raise Exception(
|
|
181
|
+
f"zip_paths and target_paths must have same length ({len(zip_paths)} !="
|
|
182
|
+
f" {len(target_paths)})"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# do not re-install if asset has already been installed
|
|
186
|
+
if any(
|
|
187
|
+
(base_target_dir / str(target_path)).exists() for target_path in target_paths
|
|
188
|
+
):
|
|
189
|
+
logger.info(" Skipping, at least one target path is already present")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
193
|
+
_extract_zip_from_url(url=source, extract_to=tempdir)
|
|
194
|
+
for index, zip_path in enumerate(zip_paths):
|
|
195
|
+
item_src = Path(tempdir) / str(zip_path)
|
|
196
|
+
item_dst = base_target_dir / str(target_paths[index])
|
|
197
|
+
shutil.move(src=str(item_src), dst=item_dst)
|
|
198
|
+
|
|
199
|
+
if "remove" in action_data:
|
|
200
|
+
_remove_items(globs=action_data["remove"], directory=base_target_dir)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _remove_items(directory: Path, globs: List[str]):
|
|
204
|
+
"""removes all files in directory matching one of the provided glob patterns"""
|
|
205
|
+
for pattern in globs:
|
|
206
|
+
matches = directory.glob(pattern)
|
|
207
|
+
for match in matches:
|
|
208
|
+
if match.is_file():
|
|
209
|
+
match.unlink()
|
|
210
|
+
elif match.is_dir(): # pragma: no branch
|
|
211
|
+
shutil.rmtree(match)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _download_file(url, download_to):
|
|
215
|
+
"""downloads a file to a given location"""
|
|
216
|
+
if not url.startswith(("http:", "https:")):
|
|
217
|
+
raise ValueError("URL must start with 'http:' or 'https:'")
|
|
218
|
+
with urlopen(url) as response, open(download_to, "wb") as file: # noqa: S310
|
|
219
|
+
file.write(response.read())
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _extract_zip_from_url(url, extract_to):
|
|
223
|
+
"""downloads ZIP from URL and extract in given directory
|
|
224
|
+
|
|
225
|
+
Nota: the ZIP is temporarily saved on disk (there is no convenient function
|
|
226
|
+
to stream the web resource to the ZIP extractor in Python standard library)
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
if not url.startswith(("http:", "https:")):
|
|
230
|
+
raise ValueError("URL must start with 'http:' or 'https:'")
|
|
231
|
+
with urlopen(url) as response: # noqa: S310
|
|
232
|
+
with tempfile.NamedTemporaryFile(delete=False) as temp_zip:
|
|
233
|
+
shutil.copyfileobj(response, temp_zip)
|
|
234
|
+
with zipfile.ZipFile(temp_zip.name, "r") as zip_file:
|
|
235
|
+
zip_file.extractall(extract_to)
|
|
236
|
+
Path(temp_zip.name).unlink()
|
hatch_openzim/hooks.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from hatchling.plugin import hookimpl
|
|
4
|
+
|
|
5
|
+
from hatch_openzim.build_hook import OpenzimBuildHook
|
|
6
|
+
from hatch_openzim.metadata_hook import OpenzimMetadataHook
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@hookimpl
|
|
10
|
+
def hatch_register_build_hook() -> Type[OpenzimBuildHook]:
|
|
11
|
+
return OpenzimBuildHook
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@hookimpl
|
|
15
|
+
def hatch_register_metadata_hook() -> Type[OpenzimMetadataHook]:
|
|
16
|
+
return OpenzimMetadataHook
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from hatch_openzim.utils import get_github_info, get_python_versions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def update(root: str, config: dict, metadata: dict):
|
|
7
|
+
"""Update the project table's metadata."""
|
|
8
|
+
|
|
9
|
+
# Check for absence of metadata we will set + presence in the dynamic property
|
|
10
|
+
for metadata_key in ["urls", "authors", "keywords", "license", "classifiers"]:
|
|
11
|
+
if config.get(f"preserve-{metadata_key}", False):
|
|
12
|
+
# Do not check if we intend to preserve the value set manually
|
|
13
|
+
continue
|
|
14
|
+
|
|
15
|
+
if metadata_key in metadata:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"'{metadata_key}' must not be listed in the 'project' table when using"
|
|
18
|
+
" openzim metadata hook."
|
|
19
|
+
)
|
|
20
|
+
if metadata_key not in metadata.get("dynamic", []):
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"'{metadata_key}' must be listed in 'project.dynamic' when using "
|
|
23
|
+
"openzim metadata hook."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
github_info = get_github_info(git_config_path=Path(root) / ".git/config")
|
|
27
|
+
|
|
28
|
+
organization = config.get("organization", github_info.organization)
|
|
29
|
+
|
|
30
|
+
if not config.get("preserve-urls", False):
|
|
31
|
+
metadata["urls"] = {
|
|
32
|
+
"Donate": "https://www.kiwix.org/en/support-us/",
|
|
33
|
+
"Homepage": github_info.homepage,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if not config.get("preserve-authors", False):
|
|
37
|
+
if str(organization).lower() in ("kiwix", "offspot"):
|
|
38
|
+
authors = [{"name": "Kiwix", "email": "dev@kiwix.org"}]
|
|
39
|
+
else:
|
|
40
|
+
authors = [{"name": "openZIM", "email": "dev@openzim.org"}]
|
|
41
|
+
authors.extend(config.get("additional-authors", []))
|
|
42
|
+
metadata["authors"] = authors
|
|
43
|
+
|
|
44
|
+
if not config.get("preserve-keywords", False):
|
|
45
|
+
if str(organization).lower() in ("kiwix", "offspot"):
|
|
46
|
+
keywords = ["kiwix"]
|
|
47
|
+
else:
|
|
48
|
+
keywords = ["openzim"]
|
|
49
|
+
if config.get("kind", "") == "scraper":
|
|
50
|
+
keywords.extend(["zim", "offline"])
|
|
51
|
+
keywords.extend(config.get("additional-keywords", []))
|
|
52
|
+
metadata["keywords"] = keywords
|
|
53
|
+
|
|
54
|
+
if not config.get("preserve-license", False):
|
|
55
|
+
metadata["license"] = {"text": "GPL-3.0-or-later"}
|
|
56
|
+
|
|
57
|
+
if not config.get("preserve-classifiers", False):
|
|
58
|
+
classifiers = [
|
|
59
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"
|
|
60
|
+
]
|
|
61
|
+
for python_version in get_python_versions(metadata["requires-python"]):
|
|
62
|
+
classifiers.append(f"Programming Language :: Python :: {python_version}")
|
|
63
|
+
metadata["classifiers"] = classifiers
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
|
4
|
+
|
|
5
|
+
from hatch_openzim.metadata import update
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenzimMetadataHook(MetadataHookInterface):
|
|
9
|
+
"""Hatch metadata hook to populate 'project' data
|
|
10
|
+
|
|
11
|
+
This hook populates:
|
|
12
|
+
- project urls
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
PLUGIN_NAME = "openzim-metadata"
|
|
16
|
+
|
|
17
|
+
def update(self, metadata: dict):
|
|
18
|
+
"""Update the project table's metadata."""
|
|
19
|
+
update(root=self.root, config=self.config, metadata=metadata)
|
hatch_openzim/shared.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
DEFAULT_FORMAT = "%(name)s:%(levelname)s:%(message)s"
|
|
6
|
+
|
|
7
|
+
# create logger
|
|
8
|
+
logger = logging.getLogger("hatch_openzim")
|
|
9
|
+
logger.setLevel(logging.DEBUG)
|
|
10
|
+
|
|
11
|
+
# setup console logging
|
|
12
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
13
|
+
console_handler.setFormatter(logging.Formatter(DEFAULT_FORMAT))
|
|
14
|
+
log_level = logging.getLevelName(os.getenv("HATCH_OPENZIM_LOG_LEVEL", "INFO"))
|
|
15
|
+
console_handler.setLevel(log_level)
|
|
16
|
+
logger.addHandler(console_handler)
|
hatch_openzim/utils.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import re
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from packaging.specifiers import SpecifierSet
|
|
8
|
+
from packaging.version import Version
|
|
9
|
+
|
|
10
|
+
from hatch_openzim.shared import logger
|
|
11
|
+
|
|
12
|
+
REMOTE_REGEXP = re.compile(
|
|
13
|
+
r"""^(?:git@|https:\/\/)github.com[:\/](?P<organization>.*?)\/"""
|
|
14
|
+
r"""(?P<repository>.*?)(?:.git)?$"""
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
GithubInfo = namedtuple("GithubInfo", ["homepage", "organization", "repository"])
|
|
18
|
+
|
|
19
|
+
DEFAULT_GITHUB_INFO = GithubInfo(
|
|
20
|
+
homepage="https://www.kiwix.org", organization=None, repository=None
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_github_info(git_config_path: Path, remote: str = "origin") -> GithubInfo:
|
|
25
|
+
if not git_config_path.exists() or not git_config_path.is_file():
|
|
26
|
+
return DEFAULT_GITHUB_INFO
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
config = configparser.ConfigParser()
|
|
30
|
+
config.read(git_config_path)
|
|
31
|
+
git_remote_url = config[f'remote "{remote}"']["url"]
|
|
32
|
+
match = REMOTE_REGEXP.match(git_remote_url)
|
|
33
|
+
if not match:
|
|
34
|
+
raise Exception(f"Unexpected remote url: {git_remote_url}")
|
|
35
|
+
return GithubInfo(
|
|
36
|
+
homepage=f"https://github.com/{match.group('organization')}/"
|
|
37
|
+
f"{match.group('repository')}",
|
|
38
|
+
organization=match.group("organization"),
|
|
39
|
+
repository=match.group("repository"),
|
|
40
|
+
)
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
logger.error("Failed to read Github URL", exc_info=exc)
|
|
43
|
+
return DEFAULT_GITHUB_INFO
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_python_versions(requires_python: str) -> List[str]:
|
|
47
|
+
"""
|
|
48
|
+
Returns the list of major and major.minor versions compatible with the specifier
|
|
49
|
+
|
|
50
|
+
E.g. if requires_python is ">=3.10,<3.12", the result is "3", "3.10", "3.11"
|
|
51
|
+
|
|
52
|
+
Nota: this does not work for requirements overlapping 3.x and 4.x, or later, because
|
|
53
|
+
latest 3.x version is not yet known
|
|
54
|
+
"""
|
|
55
|
+
specifier_set = SpecifierSet(requires_python)
|
|
56
|
+
|
|
57
|
+
last_py1_minor = 6
|
|
58
|
+
last_py2_minor = 7
|
|
59
|
+
|
|
60
|
+
major_versions = []
|
|
61
|
+
minor_versions = []
|
|
62
|
+
for major in range(1, 10): # this will work up to Python 10 ...
|
|
63
|
+
major_added = False
|
|
64
|
+
last_minor = 100 # this supposes we will never have Python x.100
|
|
65
|
+
if major == 1:
|
|
66
|
+
last_minor = last_py1_minor
|
|
67
|
+
elif major == 2: # noqa: PLR2004
|
|
68
|
+
last_minor = last_py2_minor
|
|
69
|
+
for minor in range(last_minor + 1):
|
|
70
|
+
if specifier_set.contains(Version(f"{major}.{minor}")):
|
|
71
|
+
if not major_added:
|
|
72
|
+
if len(major_versions) > 0 and major >= 4: # noqa: PLR2004
|
|
73
|
+
raise Exception(
|
|
74
|
+
"Multiple major versions is not supported for 3 and up"
|
|
75
|
+
)
|
|
76
|
+
major_added = True
|
|
77
|
+
major_versions.append(f"{major}")
|
|
78
|
+
minor_versions.append(f"{major}.{minor}")
|
|
79
|
+
|
|
80
|
+
return major_versions + minor_versions
|