cookieplone 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.
- cookieplone-0.1.0/PKG-INFO +59 -0
- cookieplone-0.1.0/README.md +26 -0
- cookieplone-0.1.0/cookieplone/__init__.py +1 -0
- cookieplone-0.1.0/cookieplone/__main__.py +6 -0
- cookieplone-0.1.0/cookieplone/cli.py +172 -0
- cookieplone-0.1.0/cookieplone/data.py +62 -0
- cookieplone-0.1.0/cookieplone/exceptions.py +18 -0
- cookieplone-0.1.0/cookieplone/filters/__init__.py +84 -0
- cookieplone-0.1.0/cookieplone/generator.py +118 -0
- cookieplone-0.1.0/cookieplone/repository.py +44 -0
- cookieplone-0.1.0/cookieplone/settings.py +31 -0
- cookieplone-0.1.0/cookieplone/utils/__init__.py +3 -0
- cookieplone-0.1.0/cookieplone/utils/commands/__init__.py +85 -0
- cookieplone-0.1.0/cookieplone/utils/console.py +134 -0
- cookieplone-0.1.0/cookieplone/utils/containers.py +6 -0
- cookieplone-0.1.0/cookieplone/utils/files.py +19 -0
- cookieplone-0.1.0/cookieplone/utils/sanity.py +28 -0
- cookieplone-0.1.0/cookieplone/utils/validators.py +134 -0
- cookieplone-0.1.0/cookieplone/utils/versions.py +92 -0
- cookieplone-0.1.0/pyproject.toml +105 -0
- cookieplone-0.1.0/setup.py +39 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: cookieplone
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Create Plone projects, addons, documentation with ease!
|
|
5
|
+
Home-page: https://github.com/plone/cookieplone
|
|
6
|
+
Author: Plone Community
|
|
7
|
+
Author-email: dev@plone.org
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
24
|
+
Classifier: Topic :: Software Development
|
|
25
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
26
|
+
Requires-Dist: cookiecutter (>=2.6.0,<3.0.0)
|
|
27
|
+
Requires-Dist: semver (>=3.0.2,<4.0.0)
|
|
28
|
+
Requires-Dist: typer[all] (>=0.12.3,<0.13.0)
|
|
29
|
+
Project-URL: Documentation, https://plone.github.io/cookieplone/
|
|
30
|
+
Project-URL: Repository, https://github.com/plone/cookieplone
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img alt="Plone Logo" width="200px" src="https://raw.githubusercontent.com/plone/.github/main/plone-logo.png">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h1 align="center">
|
|
38
|
+
cookieplone
|
|
39
|
+
</h1>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/cookieplone/)
|
|
45
|
+
[](https://pypi.org/project/cookieplone/)
|
|
46
|
+
[](https://pypi.org/project/cookieplone/)
|
|
47
|
+
[](https://pypi.org/project/cookieplone/)
|
|
48
|
+
[](https://pypi.org/project/cookieplone/)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
[](https://pypi.org/project/cookieplone/)
|
|
52
|
+
|
|
53
|
+
[](https://github.com/plone/cookieplone/actions/workflows/main.yml)
|
|
54
|
+
|
|
55
|
+
[](https://github.com/plone/cookieplone)
|
|
56
|
+
[](https://github.com/plone/cookieplone)
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="Plone Logo" width="200px" src="https://raw.githubusercontent.com/plone/.github/main/plone-logo.png">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">
|
|
6
|
+
cookieplone
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
|
|
12
|
+
[](https://pypi.org/project/cookieplone/)
|
|
13
|
+
[](https://pypi.org/project/cookieplone/)
|
|
14
|
+
[](https://pypi.org/project/cookieplone/)
|
|
15
|
+
[](https://pypi.org/project/cookieplone/)
|
|
16
|
+
[](https://pypi.org/project/cookieplone/)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[](https://pypi.org/project/cookieplone/)
|
|
20
|
+
|
|
21
|
+
[](https://github.com/plone/cookieplone/actions/workflows/main.yml)
|
|
22
|
+
|
|
23
|
+
[](https://github.com/plone/cookieplone)
|
|
24
|
+
[](https://github.com/plone/cookieplone)
|
|
25
|
+
|
|
26
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Main `cookieplone` CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from cookiecutter import __version__ as __cookiecutter_version__
|
|
10
|
+
from cookiecutter.log import configure_logger
|
|
11
|
+
from rich import print
|
|
12
|
+
from rich.prompt import Prompt
|
|
13
|
+
|
|
14
|
+
from cookieplone import __version__, data
|
|
15
|
+
from cookieplone.exceptions import GeneratorException
|
|
16
|
+
from cookieplone.generator import generate
|
|
17
|
+
from cookieplone.repository import get_base_repository, get_template_options
|
|
18
|
+
from cookieplone.utils import console
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_extra_context(value: list[str] | None = None):
|
|
22
|
+
"""Validate extra content follows the correct pattern."""
|
|
23
|
+
if not value:
|
|
24
|
+
return {}
|
|
25
|
+
for string in value:
|
|
26
|
+
if "=" not in string:
|
|
27
|
+
raise typer.BadParameter(
|
|
28
|
+
f"EXTRA_CONTEXT should contain items of the form key=value; "
|
|
29
|
+
f"'{string}' doesn't match that form"
|
|
30
|
+
)
|
|
31
|
+
# Convert list -- e.g.: ['program_name=foobar', 'startsecs=66']
|
|
32
|
+
# to dict -- e.g.: {'program_name': 'foobar', 'startsecs': '66'}
|
|
33
|
+
return dict([s.split("=", 1) for s in value])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def prompt_for_template(base_path: Path) -> str:
|
|
37
|
+
"""Parse cookiecutter.json in base_path and prompt user to choose."""
|
|
38
|
+
templates = get_template_options(base_path)
|
|
39
|
+
choices = {i[0]: i[1] for i in templates}
|
|
40
|
+
console.welcome_screen(templates)
|
|
41
|
+
answer = Prompt.ask("Select a template", choices=list(choices.keys()), default=1)
|
|
42
|
+
return choices[answer]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def version_info() -> str:
|
|
46
|
+
"""Return the Cookieplone version, location and Python powering it."""
|
|
47
|
+
python_version = sys.version
|
|
48
|
+
location = Path(__file__).parent
|
|
49
|
+
return (
|
|
50
|
+
f"Cookieplone {__version__} from {location} "
|
|
51
|
+
f"(Cookiecutter {__cookiecutter_version__}, "
|
|
52
|
+
f"Python {python_version})"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cli(
|
|
57
|
+
template: Annotated[str, typer.Argument(help="Template to be used.")] = "",
|
|
58
|
+
extra_context: Annotated[
|
|
59
|
+
data.OptionalListStr,
|
|
60
|
+
typer.Argument(callback=validate_extra_context, help="Extra context."),
|
|
61
|
+
] = None,
|
|
62
|
+
output_dir: Annotated[
|
|
63
|
+
data.OptionalPath,
|
|
64
|
+
typer.Option("--output-dir", "-o", help="Where to generate the code."),
|
|
65
|
+
] = None,
|
|
66
|
+
tag: Annotated[str, typer.Option(help="Tag.")] = "",
|
|
67
|
+
version: Annotated[
|
|
68
|
+
bool, typer.Option("--version", help="Display the version of cookieplone.")
|
|
69
|
+
] = False,
|
|
70
|
+
no_input: Annotated[
|
|
71
|
+
bool,
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--no_input",
|
|
74
|
+
help=(
|
|
75
|
+
"Do not prompt for parameters and only use cookiecutter.json "
|
|
76
|
+
"file content. Defaults to deleting any cached resources and "
|
|
77
|
+
"redownloading them. Cannot be combined with the --replay flag."
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
] = False,
|
|
81
|
+
replay: Annotated[bool, typer.Option("--replay", "-r")] = False,
|
|
82
|
+
replay_file: Annotated[data.OptionalPath, typer.Option("--replay-file")] = None,
|
|
83
|
+
skip_if_file_exists: Annotated[
|
|
84
|
+
bool,
|
|
85
|
+
typer.Option(
|
|
86
|
+
"--skip-if-file-exists",
|
|
87
|
+
"-s",
|
|
88
|
+
help=(
|
|
89
|
+
"Skip the files in the corresponding directories "
|
|
90
|
+
"if they already exist"
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
] = False,
|
|
94
|
+
overwrite_if_exists: Annotated[
|
|
95
|
+
bool, typer.Option("--overwrite-if-exists", "-f")
|
|
96
|
+
] = False,
|
|
97
|
+
config_file: Annotated[
|
|
98
|
+
data.OptionalPath, typer.Option("--config-file", help="User configuration file")
|
|
99
|
+
] = None,
|
|
100
|
+
default_config: Annotated[
|
|
101
|
+
bool,
|
|
102
|
+
typer.Option(
|
|
103
|
+
"--default-config",
|
|
104
|
+
help="Do not load a config file. Use the defaults instead",
|
|
105
|
+
),
|
|
106
|
+
] = False,
|
|
107
|
+
keep_project_on_failure: Annotated[
|
|
108
|
+
bool,
|
|
109
|
+
typer.Option(
|
|
110
|
+
"--keep-project-on-failure", help="Do not delete project folder on failure"
|
|
111
|
+
),
|
|
112
|
+
] = False,
|
|
113
|
+
debug_file: Annotated[
|
|
114
|
+
data.OptionalPath,
|
|
115
|
+
typer.Option(
|
|
116
|
+
"--debug-file", help="File to be used as a stream for DEBUG logging"
|
|
117
|
+
),
|
|
118
|
+
] = None,
|
|
119
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False,
|
|
120
|
+
):
|
|
121
|
+
"""Generate a new Plone codebase."""
|
|
122
|
+
if version:
|
|
123
|
+
info = version_info
|
|
124
|
+
print(info)
|
|
125
|
+
raise typer.Exit()
|
|
126
|
+
repository = os.environ.get("COOKIEPLONE_REPOSITORY")
|
|
127
|
+
if not repository:
|
|
128
|
+
repository = "gh:plone/cookiecutter-plone"
|
|
129
|
+
|
|
130
|
+
if not template:
|
|
131
|
+
# Display template options
|
|
132
|
+
repo_path = get_base_repository(repository)
|
|
133
|
+
template = prompt_for_template(Path(repo_path))
|
|
134
|
+
else:
|
|
135
|
+
console.welcome_screen()
|
|
136
|
+
|
|
137
|
+
if replay_file:
|
|
138
|
+
replay = replay_file
|
|
139
|
+
passwd = os.environ.get(
|
|
140
|
+
"COOKIECUTTER_REPO_PASSWORD", os.environ.get("COOKIEPLONE_REPO_PASSWORD")
|
|
141
|
+
)
|
|
142
|
+
if not output_dir:
|
|
143
|
+
output_dir = Path().cwd()
|
|
144
|
+
configure_logger(stream_level="DEBUG" if verbose else "INFO", debug_file=debug_file)
|
|
145
|
+
# Run generator
|
|
146
|
+
try:
|
|
147
|
+
generate(
|
|
148
|
+
repository,
|
|
149
|
+
tag,
|
|
150
|
+
no_input,
|
|
151
|
+
extra_context,
|
|
152
|
+
replay,
|
|
153
|
+
overwrite_if_exists,
|
|
154
|
+
output_dir,
|
|
155
|
+
config_file,
|
|
156
|
+
default_config,
|
|
157
|
+
passwd,
|
|
158
|
+
template,
|
|
159
|
+
skip_if_file_exists,
|
|
160
|
+
keep_project_on_failure,
|
|
161
|
+
)
|
|
162
|
+
except GeneratorException:
|
|
163
|
+
# TODO: Handle error
|
|
164
|
+
raise typer.Exit(1) # noQA:B904
|
|
165
|
+
except Exception:
|
|
166
|
+
# TODO: Handle error
|
|
167
|
+
raise typer.Exit(1) # noQA:B904
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
"""Run the cli."""
|
|
172
|
+
typer.run(cli)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Literal, Optional, TypeAlias
|
|
5
|
+
|
|
6
|
+
OptionalPath: TypeAlias = Optional[Path] # noQA:UP007
|
|
7
|
+
OptionalListStr: TypeAlias = Optional[list[str]] # noQA:UP007
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SanityCheck:
|
|
12
|
+
"""Definition of a sanity check."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
func: Callable
|
|
16
|
+
args: list[Any]
|
|
17
|
+
level: Literal["info", "warning", "error"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SanityCheckResult:
|
|
22
|
+
"""Result of a sanity check."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
status: bool
|
|
26
|
+
message: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SanityCheckResults:
|
|
31
|
+
"""Results of all sanity checks."""
|
|
32
|
+
|
|
33
|
+
status: bool
|
|
34
|
+
checks: list[SanityCheckResult]
|
|
35
|
+
message: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ItemValidator:
|
|
40
|
+
"""Validate an answer provided by the user."""
|
|
41
|
+
|
|
42
|
+
key: str
|
|
43
|
+
func: Callable
|
|
44
|
+
level: Literal["info", "warning", "error"] = "error"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ItemValidatorResult:
|
|
49
|
+
"""Result of an item validation."""
|
|
50
|
+
|
|
51
|
+
key: str
|
|
52
|
+
status: bool
|
|
53
|
+
message: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ContextValidatorResult:
|
|
58
|
+
"""Results of all validations checks."""
|
|
59
|
+
|
|
60
|
+
status: bool
|
|
61
|
+
validations: list[ItemValidatorResult]
|
|
62
|
+
message: str = ""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CookieploneException(Exception):
|
|
2
|
+
"""Cookieplone base exception."""
|
|
3
|
+
|
|
4
|
+
message: str
|
|
5
|
+
|
|
6
|
+
def __init__(self, message: str):
|
|
7
|
+
self.message = message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeneratorException(CookieploneException):
|
|
11
|
+
"""Cookieplone generator exception."""
|
|
12
|
+
|
|
13
|
+
message: str
|
|
14
|
+
original: Exception | None = None
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, original: Exception | None = None):
|
|
17
|
+
self.message = message
|
|
18
|
+
self.original = original
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from cookiecutter.utils import simple_filter
|
|
4
|
+
|
|
5
|
+
from cookieplone.utils import containers, versions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@simple_filter
|
|
9
|
+
def package_name(v) -> str:
|
|
10
|
+
"""Return the Python package name (without namespace)."""
|
|
11
|
+
return v.split(".")[1]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@simple_filter
|
|
15
|
+
def package_namespace(v) -> str:
|
|
16
|
+
"""Return the Python package namespace."""
|
|
17
|
+
return v.split(".")[0]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@simple_filter
|
|
21
|
+
def pascal_case(package_name: str) -> str:
|
|
22
|
+
"""Return the package name as a string in the PascalCase format ."""
|
|
23
|
+
parts = [name.title() for name in package_name.split("_")]
|
|
24
|
+
return "".join(parts)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@simple_filter
|
|
28
|
+
def extract_host(hostname: str) -> str:
|
|
29
|
+
"""Get the host part of a hostname."""
|
|
30
|
+
parts = hostname.split(".")
|
|
31
|
+
return parts[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@simple_filter
|
|
35
|
+
def use_prerelease_versions(_: str) -> str:
|
|
36
|
+
"""Should we use prerelease versions of packages."""
|
|
37
|
+
use_prerelease_versions = "USE_PRERELEASE" in os.environ
|
|
38
|
+
return "Yes" if use_prerelease_versions else "No"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@simple_filter
|
|
42
|
+
def latest_volto(use_prerelease_versions: str) -> str:
|
|
43
|
+
"""Return the latest released version of Volto."""
|
|
44
|
+
allow_prerelease = use_prerelease_versions == "Yes"
|
|
45
|
+
return versions.latest_volto(allow_prerelease=allow_prerelease)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@simple_filter
|
|
49
|
+
def latest_plone(use_prerelease_versions: str) -> str:
|
|
50
|
+
"""Return the latest released version of Plone."""
|
|
51
|
+
allow_prerelease = use_prerelease_versions == "Yes"
|
|
52
|
+
return versions.latest_plone(allow_prerelease=allow_prerelease)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@simple_filter
|
|
56
|
+
def node_version_for_volto(volto_version: str) -> int:
|
|
57
|
+
"""Return the Node Version to be used."""
|
|
58
|
+
return versions.node_version_for_volto(volto_version)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@simple_filter
|
|
62
|
+
def gs_language_code(code: str) -> str:
|
|
63
|
+
"""Return the language code as expected by Generic Setup."""
|
|
64
|
+
gs_code = code.lower()
|
|
65
|
+
if "-" in code:
|
|
66
|
+
base_language, country = code.split("-")
|
|
67
|
+
gs_code = f"{base_language}-{country.lower()}"
|
|
68
|
+
return gs_code
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@simple_filter
|
|
72
|
+
def locales_language_code(code: str) -> str:
|
|
73
|
+
"""Return the language code as expected by gettext."""
|
|
74
|
+
gs_code = code.lower()
|
|
75
|
+
if "-" in code:
|
|
76
|
+
base_language, country = code.split("-")
|
|
77
|
+
gs_code = f"{base_language}_{country.upper()}"
|
|
78
|
+
return gs_code
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@simple_filter
|
|
82
|
+
def image_prefix(registry: str) -> str:
|
|
83
|
+
"""Return the a prefix to be used with all Docker images."""
|
|
84
|
+
return containers.image_prefix(registry)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from cookiecutter import exceptions as exc
|
|
7
|
+
from cookiecutter.main import cookiecutter
|
|
8
|
+
|
|
9
|
+
from cookieplone.exceptions import GeneratorException
|
|
10
|
+
from cookieplone.utils import console, files
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _remove_internal_keys(context: OrderedDict) -> dict:
|
|
14
|
+
"""Remove internal and computed keys."""
|
|
15
|
+
new_context = {
|
|
16
|
+
key: value for key, value in context.items() if not key.startswith("_")
|
|
17
|
+
}
|
|
18
|
+
return new_context
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate(
|
|
22
|
+
repository,
|
|
23
|
+
tag,
|
|
24
|
+
no_input,
|
|
25
|
+
extra_context,
|
|
26
|
+
replay,
|
|
27
|
+
overwrite_if_exists,
|
|
28
|
+
output_dir,
|
|
29
|
+
config_file,
|
|
30
|
+
default_config,
|
|
31
|
+
passwd,
|
|
32
|
+
template,
|
|
33
|
+
skip_if_file_exists,
|
|
34
|
+
keep_project_on_failure,
|
|
35
|
+
) -> Path:
|
|
36
|
+
try:
|
|
37
|
+
result = cookiecutter(
|
|
38
|
+
repository,
|
|
39
|
+
tag,
|
|
40
|
+
no_input,
|
|
41
|
+
extra_context=extra_context,
|
|
42
|
+
replay=replay,
|
|
43
|
+
overwrite_if_exists=overwrite_if_exists,
|
|
44
|
+
output_dir=output_dir,
|
|
45
|
+
config_file=config_file,
|
|
46
|
+
default_config=default_config,
|
|
47
|
+
password=passwd,
|
|
48
|
+
directory=template,
|
|
49
|
+
skip_if_file_exists=skip_if_file_exists,
|
|
50
|
+
accept_hooks=True,
|
|
51
|
+
keep_project_on_failure=keep_project_on_failure,
|
|
52
|
+
)
|
|
53
|
+
except (
|
|
54
|
+
exc.ContextDecodingException,
|
|
55
|
+
exc.OutputDirExistsException,
|
|
56
|
+
exc.InvalidModeException,
|
|
57
|
+
exc.FailedHookException,
|
|
58
|
+
exc.UnknownExtension,
|
|
59
|
+
exc.InvalidZipRepository,
|
|
60
|
+
exc.RepositoryNotFound,
|
|
61
|
+
exc.RepositoryCloneFailed,
|
|
62
|
+
) as e:
|
|
63
|
+
raise GeneratorException(message=str(e), original=e) # noQA:B904
|
|
64
|
+
except exc.UndefinedVariableInTemplate as undefined_err:
|
|
65
|
+
context_str = json.dumps(undefined_err.context, indent=2, sort_keys=True)
|
|
66
|
+
msg = f"""{undefined_err.message}
|
|
67
|
+
Error message: {undefined_err.error.message}
|
|
68
|
+
Context: {context_str}
|
|
69
|
+
"""
|
|
70
|
+
raise GeneratorException(message=msg, original=undefined_err) # noQA:B904
|
|
71
|
+
else:
|
|
72
|
+
return Path(result)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def generate_subtemplate(
|
|
76
|
+
template: str, output_dir: Path, folder_name: str, context: OrderedDict
|
|
77
|
+
) -> Path:
|
|
78
|
+
# Extract path to repository
|
|
79
|
+
repository = context.get("_checkout") or context.get("_template")
|
|
80
|
+
|
|
81
|
+
if not repository or not (Path(repository) / template).exists():
|
|
82
|
+
# TODO: Error message
|
|
83
|
+
raise typer.Exit(code=1)
|
|
84
|
+
# Cleanup context
|
|
85
|
+
extra_context = _remove_internal_keys(context)
|
|
86
|
+
## Add folder name again
|
|
87
|
+
extra_context["__folder_name"] = folder_name
|
|
88
|
+
## Disable GHA for subcomponent
|
|
89
|
+
extra_context["__gha_enable"] = False
|
|
90
|
+
# Enable quiet mode
|
|
91
|
+
console.enable_quiet_mode()
|
|
92
|
+
# Call generate
|
|
93
|
+
try:
|
|
94
|
+
result = generate(
|
|
95
|
+
repository,
|
|
96
|
+
None, # We should have the tag already locally
|
|
97
|
+
True, # No input
|
|
98
|
+
extra_context,
|
|
99
|
+
False, # Not running a replay
|
|
100
|
+
True, # overwrite_if_exists
|
|
101
|
+
output_dir,
|
|
102
|
+
None, # config_file
|
|
103
|
+
None, # default_config,
|
|
104
|
+
None, # password
|
|
105
|
+
template,
|
|
106
|
+
False, # skip_if_file_exists,
|
|
107
|
+
False, # keep_project_on_failure
|
|
108
|
+
)
|
|
109
|
+
except GeneratorException as exc:
|
|
110
|
+
console.disable_quiet_mode()
|
|
111
|
+
raise exc
|
|
112
|
+
else:
|
|
113
|
+
console.disable_quiet_mode()
|
|
114
|
+
path = Path(result)
|
|
115
|
+
# Remove GHA folder
|
|
116
|
+
files.remove_gha(path)
|
|
117
|
+
# Return path
|
|
118
|
+
return path
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from cookiecutter.config import get_user_config
|
|
5
|
+
from cookiecutter.repository import determine_repo_dir
|
|
6
|
+
|
|
7
|
+
from cookieplone import data
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_base_repository(
|
|
11
|
+
repository: str,
|
|
12
|
+
tag: str | None = None,
|
|
13
|
+
password: str = "",
|
|
14
|
+
config_file: data.OptionalPath = None,
|
|
15
|
+
default_config: bool = False,
|
|
16
|
+
):
|
|
17
|
+
config_dict = get_user_config(
|
|
18
|
+
config_file=config_file,
|
|
19
|
+
default_config=default_config,
|
|
20
|
+
)
|
|
21
|
+
base_repo_dir, _ = determine_repo_dir(
|
|
22
|
+
template=repository,
|
|
23
|
+
abbreviations=config_dict["abbreviations"],
|
|
24
|
+
clone_to_dir=config_dict["cookiecutters_dir"],
|
|
25
|
+
checkout=tag,
|
|
26
|
+
no_input=True, # Force download
|
|
27
|
+
password=password,
|
|
28
|
+
directory="",
|
|
29
|
+
)
|
|
30
|
+
return base_repo_dir
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_template_options(base_path: Path) -> list[list[str]]:
|
|
34
|
+
"""Parse cookiecutter.json and return a list of template options."""
|
|
35
|
+
config = json.loads((base_path / "cookiecutter.json").read_text())
|
|
36
|
+
available_templates = config.get("templates", {})
|
|
37
|
+
templates = []
|
|
38
|
+
for idx, name in enumerate(available_templates, start=1):
|
|
39
|
+
idx = str(idx)
|
|
40
|
+
value = available_templates[name]
|
|
41
|
+
title = value["title"]
|
|
42
|
+
description = value["description"]
|
|
43
|
+
templates.append((idx, name, title, description))
|
|
44
|
+
return templates
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
PLONE_MIN_VERSION = "6"
|
|
2
|
+
|
|
3
|
+
SUPPORTED_PYTHON_VERSIONS = [
|
|
4
|
+
"3.8",
|
|
5
|
+
"3.9",
|
|
6
|
+
"3.10",
|
|
7
|
+
"3.11",
|
|
8
|
+
"3.12",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
DEFAULT_NODE = 18
|
|
12
|
+
SUPPORTED_NODE_VERSIONS = [
|
|
13
|
+
"16",
|
|
14
|
+
"17",
|
|
15
|
+
"18",
|
|
16
|
+
"19",
|
|
17
|
+
"20",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
VOLTO_MIN_VERSION = "16"
|
|
22
|
+
VOLTO_NODE = {
|
|
23
|
+
16: 16,
|
|
24
|
+
17: DEFAULT_NODE,
|
|
25
|
+
18: 20,
|
|
26
|
+
}
|
|
27
|
+
MIN_DOCKER_VERSION = "20.10"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Config
|
|
31
|
+
QUIET_MODE_VAR = "COOKIEPLONE_QUIET_MODE_SWITCH"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from cookieplone import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_command_version(cmd: str) -> str:
|
|
9
|
+
"""Get the reported version of a command line utility."""
|
|
10
|
+
try:
|
|
11
|
+
raw_version = (
|
|
12
|
+
subprocess.run([cmd, "--version"], capture_output=True) # noQA: S603
|
|
13
|
+
.stdout.decode()
|
|
14
|
+
.strip()
|
|
15
|
+
)
|
|
16
|
+
except FileNotFoundError:
|
|
17
|
+
raw_version = ""
|
|
18
|
+
return raw_version
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_node_major_version(value: str) -> str:
|
|
22
|
+
"""Parse value and return the major version of Node."""
|
|
23
|
+
match = re.match(r"v(\d{1,3})\.\d{1,3}.\d{1,3}", value)
|
|
24
|
+
return match.groups()[0] if match else ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_docker_version(value: str) -> str:
|
|
28
|
+
"""Parse value and return the docker version."""
|
|
29
|
+
value = value.strip()
|
|
30
|
+
match = re.match(r"Docker version (\d{2}).(\d{1,2}).(\d{1,2})", value)
|
|
31
|
+
if match:
|
|
32
|
+
groups = match.groups()
|
|
33
|
+
return f"{groups[0]}.{groups[1]}"
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_python_version(supported_versions: list[str] | None = None) -> str:
|
|
38
|
+
"""Check if Python version is supported."""
|
|
39
|
+
supported = (
|
|
40
|
+
supported_versions if supported_versions else settings.SUPPORTED_PYTHON_VERSIONS
|
|
41
|
+
)
|
|
42
|
+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
43
|
+
return (
|
|
44
|
+
""
|
|
45
|
+
if version in supported
|
|
46
|
+
else f"Python version is not supported: Got {sys.version}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_node_version(supported_versions: list[str] | None = None) -> str:
|
|
51
|
+
"""Check if node version is supported."""
|
|
52
|
+
supported = (
|
|
53
|
+
supported_versions if supported_versions else settings.SUPPORTED_NODE_VERSIONS
|
|
54
|
+
)
|
|
55
|
+
raw_version = _get_command_version("node")
|
|
56
|
+
if not raw_version:
|
|
57
|
+
return "NodeJS not found."
|
|
58
|
+
else:
|
|
59
|
+
version = _parse_node_major_version(raw_version)
|
|
60
|
+
return (
|
|
61
|
+
""
|
|
62
|
+
if version in supported
|
|
63
|
+
else f"Node version is not supported: Got {raw_version}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_docker_version(min_version: str) -> str:
|
|
68
|
+
"""Check if docker version is supported."""
|
|
69
|
+
min_version = min_version if min_version else settings.MIN_DOCKER_VERSION
|
|
70
|
+
raw_version = _get_command_version("docker")
|
|
71
|
+
if not raw_version:
|
|
72
|
+
return "Docker not found."
|
|
73
|
+
else:
|
|
74
|
+
version = _parse_docker_version(raw_version)
|
|
75
|
+
return (
|
|
76
|
+
""
|
|
77
|
+
if version >= settings.MIN_DOCKER_VERSION
|
|
78
|
+
else f"Docker version is not supported: Got {raw_version}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_command_is_available(command: str) -> str:
|
|
83
|
+
"""Check if a command line utility is available."""
|
|
84
|
+
raw_version = _get_command_version(command)
|
|
85
|
+
return "" if raw_version else f"Command {command} is not available."
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from textwrap import dedent
|
|
3
|
+
|
|
4
|
+
from rich import print as base_print
|
|
5
|
+
from rich.markup import escape
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from cookieplone.settings import QUIET_MODE_VAR
|
|
10
|
+
|
|
11
|
+
BANNER = """
|
|
12
|
+
.xxxxxxxxxxxxxx.
|
|
13
|
+
;xxxxxxxxxxxxxxxxxxxxxx;
|
|
14
|
+
;xxxxxxxxxxxxxxxxxxxxxxxxxxxx;
|
|
15
|
+
xxxxxxxxxx xxxxxxxxxx
|
|
16
|
+
xxxxxxxx. .xxxxxxxx
|
|
17
|
+
xxxxxxx xxxxxxx: xxxxxxx
|
|
18
|
+
:xxxxxx xxxxxxxxxx xxxxxx:
|
|
19
|
+
:xxxxx+ xxxxxxxxxxx +xxxxx:
|
|
20
|
+
.xxxxx. :xxxxxxxxxx .xxxxx.
|
|
21
|
+
xxxxx+ ;xxxxxxxx +xxxxx
|
|
22
|
+
xxxxx +xx. xxxxx.
|
|
23
|
+
xxxxx: .xxxxxxxx :xxxxx
|
|
24
|
+
xxxxx .xxxxxxxxxx xxxxx
|
|
25
|
+
xxxxx xxxxxxxxxxx xxxxx
|
|
26
|
+
xxxxx .xxxxxxxxxx xxxxx
|
|
27
|
+
xxxxx: .xxxxxxxx :xxxxx
|
|
28
|
+
.xxxxx ;xx. ... xxxxx.
|
|
29
|
+
xxxxx+ :xxxxxxxx +xxxxx
|
|
30
|
+
.xxxxx. :xxxxxxxxxx .xxxxx.
|
|
31
|
+
:xxxxx+ xxxxxxxxxxx ;xxxxx:
|
|
32
|
+
:xxxxxx xxxxxxxxxx xxxxxx:
|
|
33
|
+
xxxxxxx xxxxxxx; xxxxxxx
|
|
34
|
+
xxxxxxxx. .xxxxxxxx
|
|
35
|
+
xxxxxxxxxx xxxxxxxxxx
|
|
36
|
+
;xxxxxxxxxxxxxxxxxxxxxxxxxxxx+
|
|
37
|
+
;xxxxxxxxxxxxxxxxxxxxxx;
|
|
38
|
+
.xxxxxxxxxxxxxx.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _print(msg: str):
|
|
43
|
+
"""Wrapper around rich.print."""
|
|
44
|
+
if not os.environ.get(QUIET_MODE_VAR):
|
|
45
|
+
base_print(msg)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print(msg: str, style: str = "", color: str = ""): # noQA:A001
|
|
49
|
+
"""Print to console, using style and color.
|
|
50
|
+
|
|
51
|
+
style: https://rich.readthedocs.io/en/latest/reference/style.html#rich.style.Style
|
|
52
|
+
"""
|
|
53
|
+
tag_open = ""
|
|
54
|
+
tag_close = ""
|
|
55
|
+
markup = " ".join([item.strip() for item in (style, color) if item.strip()])
|
|
56
|
+
if markup:
|
|
57
|
+
tag_open = f"[{markup}]"
|
|
58
|
+
tag_close = f"[/{markup}]"
|
|
59
|
+
_print(f"{tag_open}{escape(msg)}{tag_close}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_plone_banner():
|
|
63
|
+
"""Print Plone banner."""
|
|
64
|
+
style: str = "bold"
|
|
65
|
+
color: str = "blue"
|
|
66
|
+
print(BANNER, style, color)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def info(msg: str):
|
|
70
|
+
style: str = "bold"
|
|
71
|
+
color: str = "white"
|
|
72
|
+
print(msg, style, color)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def success(msg: str):
|
|
76
|
+
style: str = "bold"
|
|
77
|
+
color: str = "green"
|
|
78
|
+
print(msg, style, color)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def error(msg: str):
|
|
82
|
+
style: str = "bold"
|
|
83
|
+
color: str = "red"
|
|
84
|
+
print(msg, style, color)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def warning(msg: str):
|
|
88
|
+
style: str = "bold"
|
|
89
|
+
color: str = "yellow"
|
|
90
|
+
print(msg, style, color)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def panel(title: str, msg: str = "", subtitle: str = "", url: str = ""):
|
|
94
|
+
msg = dedent(msg)
|
|
95
|
+
if url:
|
|
96
|
+
msg = f"{msg}\n[link]{url}[/link]"
|
|
97
|
+
_print(
|
|
98
|
+
Panel(
|
|
99
|
+
msg,
|
|
100
|
+
title=title,
|
|
101
|
+
subtitle=subtitle,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def table_available_templates(title: str, rows: list[list[str]]):
|
|
107
|
+
"""Display a table of options."""
|
|
108
|
+
table = Table(title=title, expand=True)
|
|
109
|
+
|
|
110
|
+
table.add_column("#", justify="center", style="cyan", no_wrap=True)
|
|
111
|
+
table.add_column("Title", style="blue")
|
|
112
|
+
table.add_column("Description", justify="left", style="blue")
|
|
113
|
+
|
|
114
|
+
for idx, _, title, description in rows:
|
|
115
|
+
table.add_row(idx, title, description)
|
|
116
|
+
|
|
117
|
+
return table
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def welcome_screen(templates: list[list[str]] | None = None):
|
|
121
|
+
print_plone_banner()
|
|
122
|
+
if templates:
|
|
123
|
+
table = table_available_templates("Templates", templates)
|
|
124
|
+
_print(table)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def enable_quiet_mode():
|
|
128
|
+
"""Enable quiet mode."""
|
|
129
|
+
os.environ[QUIET_MODE_VAR] = "1"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def disable_quiet_mode():
|
|
133
|
+
"""Disable quiet mode."""
|
|
134
|
+
os.environ.pop(QUIET_MODE_VAR, "")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from cookiecutter.utils import rmtree
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def remove_files(base_path: Path, paths: list[str]):
|
|
7
|
+
"""Remove files."""
|
|
8
|
+
for filepath in paths:
|
|
9
|
+
path = base_path / filepath
|
|
10
|
+
exists = path.exists()
|
|
11
|
+
if exists and path.is_dir():
|
|
12
|
+
rmtree(path)
|
|
13
|
+
elif exists and path.is_file():
|
|
14
|
+
path.unlink()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def remove_gha(base_path: Path):
|
|
18
|
+
"""Remove GHA folder."""
|
|
19
|
+
remove_files(base_path=base_path, paths=[".github"])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from cookieplone import data
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def run_sanity_checks(checks: list[data.SanityCheck]) -> data.SanityCheckResults:
|
|
5
|
+
"""Run sanity checks."""
|
|
6
|
+
global_status = True
|
|
7
|
+
results = []
|
|
8
|
+
for check in checks:
|
|
9
|
+
name = check.name
|
|
10
|
+
func = check.func
|
|
11
|
+
args = check.args
|
|
12
|
+
level = check.level
|
|
13
|
+
message = func(*args)
|
|
14
|
+
if not message:
|
|
15
|
+
status = True
|
|
16
|
+
message = "✓"
|
|
17
|
+
elif level == "warning":
|
|
18
|
+
status = True
|
|
19
|
+
elif level == "error":
|
|
20
|
+
status = False
|
|
21
|
+
global_status = global_status and status
|
|
22
|
+
results.append(data.SanityCheckResult(name, status, message))
|
|
23
|
+
global_message = (
|
|
24
|
+
f"Ran {len(checks)} checks and they {'passed' if global_status else 'failed'}."
|
|
25
|
+
)
|
|
26
|
+
return data.SanityCheckResults(
|
|
27
|
+
status=global_status, message=global_message, checks=results
|
|
28
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from packaging.version import InvalidVersion, Version
|
|
6
|
+
|
|
7
|
+
from cookieplone import data, settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _version_from_str(value: str) -> Version | None:
|
|
11
|
+
"""Parse a value and return a Version"""
|
|
12
|
+
try:
|
|
13
|
+
version = Version(value)
|
|
14
|
+
except InvalidVersion:
|
|
15
|
+
version = None
|
|
16
|
+
return version
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_not_empty(value: Any, key: str = "") -> str:
|
|
20
|
+
"""Value should not be empty."""
|
|
21
|
+
status = True
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
status = bool(value.strip())
|
|
24
|
+
elif isinstance(value, int | float):
|
|
25
|
+
# We accept 0 as valid
|
|
26
|
+
status = True
|
|
27
|
+
else:
|
|
28
|
+
status = bool(value)
|
|
29
|
+
|
|
30
|
+
return "" if status else f"{key} should be provided"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_component_version(component: str, version: str, min_version: str) -> str:
|
|
34
|
+
"""Validate if a component version is bigger than the min_version."""
|
|
35
|
+
version_ = _version_from_str(version)
|
|
36
|
+
min_version = _version_from_str(min_version)
|
|
37
|
+
check = version_ and min_version and (version_ >= min_version)
|
|
38
|
+
return "" if check else f"{version} is not a valid {component} version."
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_language_code(value: str) -> str:
|
|
42
|
+
"""Language code should be valid."""
|
|
43
|
+
pattern = r"^([a-z]{2}|[a-z]{2}-[a-z]{2})$"
|
|
44
|
+
return (
|
|
45
|
+
"" if re.match(pattern, value) else f"'{value}' is not a valid language code."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_python_package_name(value: str) -> str:
|
|
50
|
+
"""Validate python_package_name is an identifier."""
|
|
51
|
+
status = False
|
|
52
|
+
if "." in value:
|
|
53
|
+
namespace, package = value.split(".")
|
|
54
|
+
status = namespace.isidentifier() and package.isidentifier()
|
|
55
|
+
else:
|
|
56
|
+
status = value.isidentifier()
|
|
57
|
+
return "" if status else f"'{value}' is not a valid Python identifier."
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_hostname(value: str) -> str:
|
|
61
|
+
"""Check if hostname is valid."""
|
|
62
|
+
valid = False
|
|
63
|
+
if value and value.strip():
|
|
64
|
+
value_with_protocol = f"https://{value}"
|
|
65
|
+
result = urlparse(value_with_protocol)
|
|
66
|
+
valid = str(result.hostname) == value
|
|
67
|
+
return "" if valid else f"'{value}' is not a valid hostname."
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_volto_addon_name(value: str) -> str:
|
|
71
|
+
"""Validate the volto addon name is valid."""
|
|
72
|
+
pattern = "^[a-z0-9-~][a-z0-9-._~]*$"
|
|
73
|
+
return "" if re.match(pattern, value) else f"'{value}' is not a valid name."
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_npm_package_name(value: str) -> str:
|
|
77
|
+
"""Validate the npm package name is valid."""
|
|
78
|
+
pattern = r"^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$"
|
|
79
|
+
return "" if re.match(pattern, value) else f"'{value}' is not a valid package name."
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_plone_version(value: str) -> str:
|
|
83
|
+
"""Validate Plone Version."""
|
|
84
|
+
status = False
|
|
85
|
+
version = _version_from_str(value)
|
|
86
|
+
if version:
|
|
87
|
+
status = version >= _version_from_str(settings.PLONE_MIN_VERSION)
|
|
88
|
+
return "" if status else f"{value} is not a valid Plone version."
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_volto_version(value: str) -> str:
|
|
92
|
+
"""Validate Volto Version."""
|
|
93
|
+
status = False
|
|
94
|
+
version = _version_from_str(value)
|
|
95
|
+
if version:
|
|
96
|
+
status = version >= _version_from_str(settings.VOLTO_MIN_VERSION)
|
|
97
|
+
return "" if status else f"{value} is not a valid Volto version."
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_context_validations(
|
|
101
|
+
context: dict, validations: list[data.ItemValidator], allow_empty: bool = False
|
|
102
|
+
) -> data.ContextValidatorResult:
|
|
103
|
+
"""Run validations for context."""
|
|
104
|
+
global_status = True
|
|
105
|
+
results = []
|
|
106
|
+
if not allow_empty:
|
|
107
|
+
func = validate_not_empty
|
|
108
|
+
for key in context:
|
|
109
|
+
if key.startswith("_"):
|
|
110
|
+
# Ignore computed values
|
|
111
|
+
continue
|
|
112
|
+
validations.append(data.ItemValidator(key, func, "error"))
|
|
113
|
+
for validation in validations:
|
|
114
|
+
key = validation.key
|
|
115
|
+
func = validation.func
|
|
116
|
+
value = context.get(key, "")
|
|
117
|
+
level = validation.level
|
|
118
|
+
message = func(value, key) if func == validate_not_empty else func(value)
|
|
119
|
+
if not message:
|
|
120
|
+
status = True
|
|
121
|
+
message = "✓"
|
|
122
|
+
elif level == "warning":
|
|
123
|
+
status = True
|
|
124
|
+
elif level == "error":
|
|
125
|
+
status = False
|
|
126
|
+
global_status = global_status and status
|
|
127
|
+
results.append(data.ItemValidatorResult(key, status, message))
|
|
128
|
+
global_message = (
|
|
129
|
+
f"Ran {len(results)} validations and "
|
|
130
|
+
f"they {'passed' if global_status else 'failed'}."
|
|
131
|
+
)
|
|
132
|
+
return data.ContextValidatorResult(
|
|
133
|
+
status=global_status, message=global_message, validations=results
|
|
134
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from packaging.version import Version
|
|
3
|
+
|
|
4
|
+
from cookieplone import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_npm_package_versions(package: str) -> list[str]:
|
|
8
|
+
"""Get versions for a NPM package."""
|
|
9
|
+
url: str = f"https://registry.npmjs.org/{package}"
|
|
10
|
+
resp = requests.get(url, headers={"Accept": "application/vnd.npm.install-v1+json"}) # noQA: S113
|
|
11
|
+
data = resp.json()
|
|
12
|
+
return list(data["dist-tags"].values())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_pypi_package_versions(package: str) -> list[str]:
|
|
16
|
+
"""Get versions for a PyPi package."""
|
|
17
|
+
url: str = f"https://pypi.org/pypi/{package}/json"
|
|
18
|
+
resp = requests.get(url) # noQA: S113
|
|
19
|
+
data = resp.json()
|
|
20
|
+
return list(data.get("releases").keys())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_valid_version(
|
|
24
|
+
version: Version,
|
|
25
|
+
min_version: Version | None = None,
|
|
26
|
+
max_version: Version | None = None,
|
|
27
|
+
allow_prerelease: bool = False,
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""Check if version is valid."""
|
|
30
|
+
status = True
|
|
31
|
+
if version.is_prerelease:
|
|
32
|
+
status = allow_prerelease
|
|
33
|
+
if status and min_version:
|
|
34
|
+
status = version >= min_version
|
|
35
|
+
if status and max_version:
|
|
36
|
+
status = version < max_version
|
|
37
|
+
return status
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def latest_version(
|
|
41
|
+
versions: list[str],
|
|
42
|
+
min_version: str | None = None,
|
|
43
|
+
max_version: str | None = None,
|
|
44
|
+
allow_prerelease: bool = False,
|
|
45
|
+
) -> str | None:
|
|
46
|
+
min_version = Version(min_version) if min_version else None
|
|
47
|
+
max_version = Version(max_version) if max_version else None
|
|
48
|
+
versions_ = sorted(
|
|
49
|
+
[(Version(v.replace("v", "")), v) for v in versions], reverse=True
|
|
50
|
+
)
|
|
51
|
+
valid = [
|
|
52
|
+
(version, raw_version)
|
|
53
|
+
for version, raw_version in versions_
|
|
54
|
+
if is_valid_version(version, min_version, max_version, allow_prerelease)
|
|
55
|
+
]
|
|
56
|
+
return valid[0][1] if valid else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def latest_volto(
|
|
60
|
+
min_version: str | None = None,
|
|
61
|
+
max_version: str | None = None,
|
|
62
|
+
allow_prerelease: bool = False,
|
|
63
|
+
) -> str | None:
|
|
64
|
+
"""Return the latest volto version."""
|
|
65
|
+
versions = get_npm_package_versions("@plone/volto")
|
|
66
|
+
return latest_version(
|
|
67
|
+
versions,
|
|
68
|
+
min_version=min_version,
|
|
69
|
+
max_version=max_version,
|
|
70
|
+
allow_prerelease=allow_prerelease,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def latest_plone(
|
|
75
|
+
min_version: str | None = None,
|
|
76
|
+
max_version: str | None = None,
|
|
77
|
+
allow_prerelease: bool = False,
|
|
78
|
+
) -> str | None:
|
|
79
|
+
"""Return the latest Plone version."""
|
|
80
|
+
versions = get_pypi_package_versions("Plone")
|
|
81
|
+
return latest_version(
|
|
82
|
+
versions,
|
|
83
|
+
min_version=min_version,
|
|
84
|
+
max_version=max_version,
|
|
85
|
+
allow_prerelease=allow_prerelease,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def node_version_for_volto(volto_version: str) -> int:
|
|
90
|
+
"""Return the Node Version to be used with Volto."""
|
|
91
|
+
major = Version(volto_version).major
|
|
92
|
+
return settings.VOLTO_NODE.get(major, settings.DEFAULT_NODE)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "cookieplone"
|
|
3
|
+
version = "0"
|
|
4
|
+
description = "Create Plone projects, addons, documentation with ease!"
|
|
5
|
+
authors = ["Plone Community <dev@plone.org>"]
|
|
6
|
+
repository = "https://github.com/plone/cookieplone"
|
|
7
|
+
documentation = "https://plone.github.io/cookieplone/"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Natural Language :: English",
|
|
14
|
+
"License :: OSI Approved :: BSD License",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
21
|
+
"Programming Language :: Python",
|
|
22
|
+
"Topic :: Software Development",
|
|
23
|
+
"Topic :: Software Development :: Code Generators"
|
|
24
|
+
]
|
|
25
|
+
packages = [
|
|
26
|
+
{include = "cookieplone"}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.poetry.dependencies]
|
|
30
|
+
python = "^3.10"
|
|
31
|
+
cookiecutter = "^2.6.0"
|
|
32
|
+
semver = "^3.0.2"
|
|
33
|
+
typer = {extras = ["all"], version = "^0.12.3"}
|
|
34
|
+
|
|
35
|
+
[tool.poetry.group.dev.dependencies]
|
|
36
|
+
pytest = "^8.1.1"
|
|
37
|
+
pytest-cov = "^5.0.0"
|
|
38
|
+
pre-commit = "^3.7.0"
|
|
39
|
+
tox = "^4.14.2"
|
|
40
|
+
|
|
41
|
+
[tool.poetry.scripts]
|
|
42
|
+
cookieplone = 'cookieplone.__main__:main'
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
target-version = "py310"
|
|
49
|
+
line-length = 88
|
|
50
|
+
fix = true
|
|
51
|
+
lint.select = [
|
|
52
|
+
# flake8-2020
|
|
53
|
+
"YTT",
|
|
54
|
+
# flake8-bandit
|
|
55
|
+
"S",
|
|
56
|
+
# flake8-bugbear
|
|
57
|
+
"B",
|
|
58
|
+
# flake8-builtins
|
|
59
|
+
"A",
|
|
60
|
+
# flake8-comprehensions
|
|
61
|
+
"C4",
|
|
62
|
+
# flake8-debugger
|
|
63
|
+
"T10",
|
|
64
|
+
# flake8-simplify
|
|
65
|
+
"SIM",
|
|
66
|
+
# isort
|
|
67
|
+
"I",
|
|
68
|
+
# mccabe
|
|
69
|
+
"C90",
|
|
70
|
+
# pycodestyle
|
|
71
|
+
"E", "W",
|
|
72
|
+
# pyflakes
|
|
73
|
+
"F",
|
|
74
|
+
# pygrep-hooks
|
|
75
|
+
"PGH",
|
|
76
|
+
# pyupgrade
|
|
77
|
+
"UP",
|
|
78
|
+
# ruff
|
|
79
|
+
"RUF",
|
|
80
|
+
]
|
|
81
|
+
lint.ignore = [
|
|
82
|
+
# DoNotAssignLambda
|
|
83
|
+
"E731",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.format]
|
|
87
|
+
preview = true
|
|
88
|
+
|
|
89
|
+
[tool.coverage.report]
|
|
90
|
+
skip_empty = true
|
|
91
|
+
|
|
92
|
+
[tool.coverage.run]
|
|
93
|
+
branch = true
|
|
94
|
+
source = ["cookieplone"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.per-file-ignores]
|
|
98
|
+
"tests/*" = ["S101"]
|
|
99
|
+
|
|
100
|
+
[tool.poetry-version-plugin]
|
|
101
|
+
source = "init"
|
|
102
|
+
|
|
103
|
+
[build-system]
|
|
104
|
+
requires = ["poetry-core"]
|
|
105
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
packages = \
|
|
5
|
+
['cookieplone',
|
|
6
|
+
'cookieplone.filters',
|
|
7
|
+
'cookieplone.utils',
|
|
8
|
+
'cookieplone.utils.commands']
|
|
9
|
+
|
|
10
|
+
package_data = \
|
|
11
|
+
{'': ['*']}
|
|
12
|
+
|
|
13
|
+
install_requires = \
|
|
14
|
+
['cookiecutter>=2.6.0,<3.0.0',
|
|
15
|
+
'semver>=3.0.2,<4.0.0',
|
|
16
|
+
'typer[all]>=0.12.3,<0.13.0']
|
|
17
|
+
|
|
18
|
+
entry_points = \
|
|
19
|
+
{'console_scripts': ['cookieplone = cookieplone.__main__:main']}
|
|
20
|
+
|
|
21
|
+
setup_kwargs = {
|
|
22
|
+
'name': 'cookieplone',
|
|
23
|
+
'version': '0.1.0',
|
|
24
|
+
'description': 'Create Plone projects, addons, documentation with ease!',
|
|
25
|
+
'long_description': '<p align="center">\n <img alt="Plone Logo" width="200px" src="https://raw.githubusercontent.com/plone/.github/main/plone-logo.png">\n</p>\n\n<h1 align="center">\n cookieplone\n</h1>\n\n\n<div align="center">\n\n[](https://pypi.org/project/cookieplone/)\n[](https://pypi.org/project/cookieplone/)\n[](https://pypi.org/project/cookieplone/)\n[](https://pypi.org/project/cookieplone/)\n[](https://pypi.org/project/cookieplone/)\n\n\n[](https://pypi.org/project/cookieplone/)\n\n[](https://github.com/plone/cookieplone/actions/workflows/main.yml)\n\n[](https://github.com/plone/cookieplone)\n[](https://github.com/plone/cookieplone)\n\n</div>\n',
|
|
26
|
+
'author': 'Plone Community',
|
|
27
|
+
'author_email': 'dev@plone.org',
|
|
28
|
+
'maintainer': 'None',
|
|
29
|
+
'maintainer_email': 'None',
|
|
30
|
+
'url': 'https://github.com/plone/cookieplone',
|
|
31
|
+
'packages': packages,
|
|
32
|
+
'package_data': package_data,
|
|
33
|
+
'install_requires': install_requires,
|
|
34
|
+
'entry_points': entry_points,
|
|
35
|
+
'python_requires': '>=3.10,<4.0',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
setup(**setup_kwargs)
|