snowflake-cli-labs 2.5.0rc3__py3-none-any.whl → 2.6.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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/api/cli_global_context.py +31 -3
- snowflake/cli/api/commands/decorators.py +21 -6
- snowflake/cli/api/commands/flags.py +60 -51
- snowflake/cli/api/commands/snow_typer.py +24 -0
- snowflake/cli/api/commands/typer_pre_execute.py +26 -0
- snowflake/cli/api/console/abc.py +8 -0
- snowflake/cli/api/console/console.py +29 -4
- snowflake/cli/api/constants.py +3 -0
- snowflake/cli/api/project/definition.py +17 -35
- snowflake/cli/api/project/definition_manager.py +22 -19
- snowflake/cli/api/project/errors.py +9 -6
- snowflake/cli/api/project/schemas/identifier_model.py +1 -1
- snowflake/cli/api/project/schemas/native_app/application.py +15 -3
- snowflake/cli/api/project/schemas/native_app/native_app.py +5 -1
- snowflake/cli/api/project/schemas/native_app/path_mapping.py +14 -3
- snowflake/cli/api/project/schemas/project_definition.py +37 -6
- snowflake/cli/api/project/schemas/streamlit/streamlit.py +3 -0
- snowflake/cli/api/project/schemas/updatable_model.py +2 -6
- snowflake/cli/api/rest_api.py +113 -0
- snowflake/cli/api/sanitizers.py +43 -0
- snowflake/cli/api/sql_execution.py +7 -0
- snowflake/cli/api/utils/definition_rendering.py +95 -25
- snowflake/cli/api/utils/models.py +31 -26
- snowflake/cli/api/utils/rendering.py +24 -3
- snowflake/cli/app/cli_app.py +2 -0
- snowflake/cli/app/commands_registration/command_plugins_loader.py +8 -0
- snowflake/cli/app/dev/docs/commands_docs_generator.py +100 -0
- snowflake/cli/app/dev/docs/generator.py +8 -67
- snowflake/cli/app/dev/docs/project_definition_docs_generator.py +58 -0
- snowflake/cli/app/dev/docs/project_definition_generate_json_schema.py +227 -0
- snowflake/cli/app/dev/docs/template_utils.py +23 -0
- snowflake/cli/app/dev/docs/templates/definition_description.rst.jinja2 +38 -0
- snowflake/cli/app/dev/docs/templates/usage.rst.jinja2 +6 -1
- snowflake/cli/app/loggers.py +25 -0
- snowflake/cli/app/printing.py +7 -5
- snowflake/cli/app/telemetry.py +11 -0
- snowflake/cli/plugins/nativeapp/artifacts.py +78 -9
- snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +3 -11
- snowflake/cli/plugins/nativeapp/codegen/compiler.py +6 -24
- snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +27 -27
- snowflake/cli/plugins/nativeapp/commands.py +23 -12
- snowflake/cli/plugins/nativeapp/constants.py +2 -0
- snowflake/cli/plugins/nativeapp/errno.py +15 -0
- snowflake/cli/plugins/nativeapp/feature_flags.py +24 -0
- snowflake/cli/plugins/nativeapp/init.py +5 -0
- snowflake/cli/plugins/nativeapp/manager.py +101 -103
- snowflake/cli/plugins/nativeapp/project_model.py +181 -0
- snowflake/cli/plugins/nativeapp/run_processor.py +178 -110
- snowflake/cli/plugins/nativeapp/teardown_processor.py +89 -64
- snowflake/cli/plugins/nativeapp/utils.py +2 -2
- snowflake/cli/plugins/nativeapp/version/commands.py +3 -3
- snowflake/cli/plugins/object/commands.py +70 -4
- snowflake/cli/plugins/object/manager.py +44 -3
- snowflake/cli/plugins/snowpark/commands.py +2 -2
- snowflake/cli/plugins/sql/commands.py +2 -10
- snowflake/cli/plugins/sql/manager.py +4 -2
- snowflake/cli/plugins/stage/commands.py +23 -4
- snowflake/cli/plugins/stage/diff.py +81 -51
- snowflake/cli/plugins/stage/manager.py +2 -1
- snowflake/cli/plugins/streamlit/commands.py +2 -1
- snowflake/cli/plugins/streamlit/manager.py +6 -0
- {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/METADATA +15 -9
- {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/RECORD +67 -56
- {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/WHEEL +1 -1
- {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -63,10 +63,31 @@ def _env_bootstrap(env: Environment) -> Environment:
|
|
|
63
63
|
return env
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
class IgnoreAttrEnvironment(Environment):
|
|
67
|
+
"""
|
|
68
|
+
extend Environment class and ignore attributes during rendering.
|
|
69
|
+
This ensures that attributes of classes
|
|
70
|
+
do not get used during rendering (e.g. __class__, get, etc).
|
|
71
|
+
Only dict items can be used for rendering.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def getattr(self, obj, attribute): # noqa: A003
|
|
75
|
+
try:
|
|
76
|
+
return obj[attribute]
|
|
77
|
+
except (TypeError, LookupError, AttributeError):
|
|
78
|
+
return self.undefined(obj=obj, name=attribute)
|
|
79
|
+
|
|
80
|
+
def getitem(self, obj, argument):
|
|
81
|
+
try:
|
|
82
|
+
return obj[argument]
|
|
83
|
+
except (AttributeError, TypeError, LookupError):
|
|
84
|
+
return self.undefined(obj=obj, name=argument)
|
|
85
|
+
|
|
86
|
+
|
|
66
87
|
def get_snowflake_cli_jinja_env() -> Environment:
|
|
67
88
|
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
68
89
|
return _env_bootstrap(
|
|
69
|
-
|
|
90
|
+
IgnoreAttrEnvironment(
|
|
70
91
|
loader=loaders.BaseLoader(),
|
|
71
92
|
keep_trailing_newline=True,
|
|
72
93
|
variable_start_string=_YML_TEMPLATE_START,
|
|
@@ -81,7 +102,7 @@ def get_snowflake_cli_jinja_env() -> Environment:
|
|
|
81
102
|
def get_sql_cli_jinja_env():
|
|
82
103
|
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
83
104
|
return _env_bootstrap(
|
|
84
|
-
|
|
105
|
+
IgnoreAttrEnvironment(
|
|
85
106
|
loader=loaders.BaseLoader(),
|
|
86
107
|
keep_trailing_newline=True,
|
|
87
108
|
variable_start_string="&{",
|
|
@@ -108,7 +129,7 @@ def jinja_render_from_file(
|
|
|
108
129
|
None if file path is provided, else returns the rendered string.
|
|
109
130
|
"""
|
|
110
131
|
env = _env_bootstrap(
|
|
111
|
-
|
|
132
|
+
IgnoreAttrEnvironment(
|
|
112
133
|
loader=loaders.FileSystemLoader(template_path.parent),
|
|
113
134
|
keep_trailing_newline=True,
|
|
114
135
|
undefined=StrictUndefined,
|
snowflake/cli/app/cli_app.py
CHANGED
|
@@ -30,6 +30,7 @@ from snowflake.cli.api.config import config_init
|
|
|
30
30
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
31
31
|
from snowflake.cli.api.output.types import CollectionResult
|
|
32
32
|
from snowflake.cli.api.secure_path import SecurePath
|
|
33
|
+
from snowflake.cli.app import loggers
|
|
33
34
|
from snowflake.cli.app.api_impl.plugin.plugin_config_provider_impl import (
|
|
34
35
|
PluginConfigProviderImpl,
|
|
35
36
|
)
|
|
@@ -88,6 +89,7 @@ def _commands_registration_callback(value: bool):
|
|
|
88
89
|
@_commands_registration.before
|
|
89
90
|
def _config_init_callback(configuration_file: Optional[Path]):
|
|
90
91
|
config_init(configuration_file)
|
|
92
|
+
loggers.create_initial_loggers()
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
@_commands_registration.before
|
|
@@ -100,6 +100,10 @@ class CommandPluginsLoader:
|
|
|
100
100
|
self._loaded_command_paths[
|
|
101
101
|
loaded_plugin.command_spec.full_command_path
|
|
102
102
|
] = loaded_plugin
|
|
103
|
+
|
|
104
|
+
if self._is_external_plugin(loaded_plugin):
|
|
105
|
+
log.info("Loaded external plugin: %s", plugin_name)
|
|
106
|
+
|
|
103
107
|
return loaded_plugin
|
|
104
108
|
|
|
105
109
|
def _load_plugin_spec(
|
|
@@ -145,6 +149,10 @@ class CommandPluginsLoader:
|
|
|
145
149
|
)
|
|
146
150
|
return None
|
|
147
151
|
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _is_external_plugin(plugin) -> bool:
|
|
154
|
+
return isinstance(plugin, LoadedExternalCommandPlugin)
|
|
155
|
+
|
|
148
156
|
|
|
149
157
|
def load_only_builtin_command_plugins() -> List[LoadedCommandPlugin]:
|
|
150
158
|
loader = CommandPluginsLoader()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
from click import Command
|
|
21
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
22
|
+
from snowflake.cli.app.dev.docs.template_utils import get_template_environment
|
|
23
|
+
from typer.core import TyperArgument
|
|
24
|
+
|
|
25
|
+
log = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
CMD_USAGE_TMPL = "usage.rst.jinja2"
|
|
28
|
+
OVERVIEW_TMPL = "overview.rst.jinja2"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_command_docs(
|
|
32
|
+
root: SecurePath, command: Command, cmd_parts: Optional[List] = None
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Iterates recursively through commands info. Creates a file structure resembling
|
|
36
|
+
commands structure. For each terminal command creates a "usage" rst file.
|
|
37
|
+
"""
|
|
38
|
+
if getattr(command, "hidden", False):
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
root.mkdir(exist_ok=True)
|
|
42
|
+
if cmd_parts is None:
|
|
43
|
+
_render_command_usage(command, root, cmd_parts, template_name=OVERVIEW_TMPL)
|
|
44
|
+
|
|
45
|
+
cmd_parts = cmd_parts or []
|
|
46
|
+
if hasattr(command, "commands"):
|
|
47
|
+
for command_name, command_info in command.commands.items():
|
|
48
|
+
path = root / command.name if command.name != "default" else root
|
|
49
|
+
generate_command_docs(path, command_info, [*cmd_parts, command_name])
|
|
50
|
+
else:
|
|
51
|
+
_render_command_usage(command, root, cmd_parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_main_option(options: List[str]) -> str:
|
|
55
|
+
long_options = [option for option in options if option.startswith("--")]
|
|
56
|
+
if long_options:
|
|
57
|
+
return long_options[0]
|
|
58
|
+
|
|
59
|
+
short_options = [option for option in options if option.startswith("-")]
|
|
60
|
+
if short_options:
|
|
61
|
+
return short_options[0]
|
|
62
|
+
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _render_command_usage(
|
|
67
|
+
command: Command,
|
|
68
|
+
root: SecurePath,
|
|
69
|
+
path: Optional[List] = None,
|
|
70
|
+
template_name: str = CMD_USAGE_TMPL,
|
|
71
|
+
):
|
|
72
|
+
# This is end command
|
|
73
|
+
command_name = command.name
|
|
74
|
+
env = get_template_environment()
|
|
75
|
+
env.filters[get_main_option.__name__] = get_main_option
|
|
76
|
+
template = env.get_template(template_name)
|
|
77
|
+
arguments = []
|
|
78
|
+
options = []
|
|
79
|
+
for param in command.params:
|
|
80
|
+
if isinstance(param, TyperArgument):
|
|
81
|
+
arguments.append(param)
|
|
82
|
+
else:
|
|
83
|
+
options.append(param)
|
|
84
|
+
|
|
85
|
+
# RST files are presumed to be standalone pages in the docs with a matching item in the left nav.
|
|
86
|
+
# Included files, which these are, need to use the .txt extension.
|
|
87
|
+
file_path = root / f"usage-{command_name}.txt"
|
|
88
|
+
log.info("Creating %s", file_path)
|
|
89
|
+
with file_path.open("w+") as fh:
|
|
90
|
+
fh.write(
|
|
91
|
+
template.render(
|
|
92
|
+
{
|
|
93
|
+
"help": command.help,
|
|
94
|
+
"name": command_name,
|
|
95
|
+
"options": options,
|
|
96
|
+
"arguments": arguments,
|
|
97
|
+
"path": path,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
)
|
|
@@ -15,80 +15,21 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import List, Optional
|
|
20
18
|
|
|
21
19
|
from click import Command
|
|
22
|
-
from jinja2 import Environment, FileSystemLoader
|
|
23
20
|
from snowflake.cli.api.secure_path import SecurePath
|
|
24
|
-
from
|
|
21
|
+
from snowflake.cli.app.dev.docs.commands_docs_generator import generate_command_docs
|
|
22
|
+
from snowflake.cli.app.dev.docs.project_definition_docs_generator import (
|
|
23
|
+
generate_project_definition_docs,
|
|
24
|
+
)
|
|
25
25
|
|
|
26
26
|
log = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
|
-
CMD_USAGE_TMPL = "usage.rst.jinja2"
|
|
29
|
-
OVERVIEW_TMPL = "overview.rst.jinja2"
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
def generate_docs(root: SecurePath, command: Command, cmd_parts: Optional[List] = None):
|
|
29
|
+
def generate_docs(root: SecurePath, command: Command):
|
|
33
30
|
"""
|
|
34
|
-
|
|
35
|
-
commands structure. For each terminal command creates a "usage" rst file.
|
|
31
|
+
Generates documentation for each command, its options and for the project definition.
|
|
36
32
|
"""
|
|
37
|
-
if getattr(command, "hidden", False):
|
|
38
|
-
return
|
|
39
|
-
|
|
40
33
|
root.mkdir(exist_ok=True)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
cmd_parts = cmd_parts or []
|
|
45
|
-
if hasattr(command, "commands"):
|
|
46
|
-
for command_name, command_info in command.commands.items():
|
|
47
|
-
path = root / command.name if command.name != "default" else root
|
|
48
|
-
generate_docs(path, command_info, [*cmd_parts, command_name])
|
|
49
|
-
else:
|
|
50
|
-
_render_usage(command, root, cmd_parts)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_main_option(options: List[str]) -> str:
|
|
54
|
-
long_options = [option for option in options if option.startswith("--")]
|
|
55
|
-
short_options = [option for option in options if option.startswith("-")]
|
|
56
|
-
if long_options:
|
|
57
|
-
return long_options[0]
|
|
58
|
-
if short_options:
|
|
59
|
-
return short_options[0]
|
|
60
|
-
return ""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _render_usage(
|
|
64
|
-
command: Command,
|
|
65
|
-
root: SecurePath,
|
|
66
|
-
path: Optional[List] = None,
|
|
67
|
-
template_name: str = CMD_USAGE_TMPL,
|
|
68
|
-
):
|
|
69
|
-
# This is end command
|
|
70
|
-
command_name = command.name
|
|
71
|
-
env = Environment(loader=FileSystemLoader(Path(__file__).parent / "templates"))
|
|
72
|
-
env.filters[get_main_option.__name__] = get_main_option
|
|
73
|
-
template = env.get_template(template_name)
|
|
74
|
-
arguments = []
|
|
75
|
-
options = []
|
|
76
|
-
for param in command.params:
|
|
77
|
-
if isinstance(param, TyperArgument):
|
|
78
|
-
arguments.append(param)
|
|
79
|
-
else:
|
|
80
|
-
options.append(param)
|
|
81
|
-
file_path = root / f"usage-{command_name}.txt"
|
|
82
|
-
log.info("Creating %s", file_path)
|
|
83
|
-
with file_path.open("w+") as fh:
|
|
84
|
-
fh.write(
|
|
85
|
-
template.render(
|
|
86
|
-
{
|
|
87
|
-
"help": command.help,
|
|
88
|
-
"name": command_name,
|
|
89
|
-
"options": options,
|
|
90
|
-
"arguments": arguments,
|
|
91
|
-
"path": path,
|
|
92
|
-
}
|
|
93
|
-
)
|
|
94
|
-
)
|
|
34
|
+
generate_command_docs(root / "commands", command)
|
|
35
|
+
generate_project_definition_docs(root / "project_definition")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Dict
|
|
19
|
+
|
|
20
|
+
from pydantic.json_schema import model_json_schema
|
|
21
|
+
from snowflake.cli.api.project.schemas.project_definition import ProjectDefinition
|
|
22
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
23
|
+
from snowflake.cli.app.dev.docs.project_definition_generate_json_schema import (
|
|
24
|
+
ProjectDefinitionGenerateJsonSchema,
|
|
25
|
+
)
|
|
26
|
+
from snowflake.cli.app.dev.docs.template_utils import get_template_environment
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
DEFINITION_DESCRIPTION = "definition_description.rst.jinja2"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_project_definition_docs(root: SecurePath):
|
|
34
|
+
"""
|
|
35
|
+
Recursively traverses the generated project definition schema,
|
|
36
|
+
creating a file for each section that mirrors the YAML structure.
|
|
37
|
+
Each file contains the definition for every field within that section.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
root.mkdir(exist_ok=True)
|
|
41
|
+
list_of_sections = model_json_schema(
|
|
42
|
+
ProjectDefinition, schema_generator=ProjectDefinitionGenerateJsonSchema
|
|
43
|
+
)["result"]
|
|
44
|
+
for section in list_of_sections:
|
|
45
|
+
_render_definition_description(root, section)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _render_definition_description(root: SecurePath, section: Dict[str, Any]) -> None:
|
|
50
|
+
env = get_template_environment()
|
|
51
|
+
|
|
52
|
+
# RST files are presumed to be standalone pages in the docs with a matching item in the left nav.
|
|
53
|
+
# Included files, which these are, need to use the .txt extension.
|
|
54
|
+
file_path = root / f"definition_{section['name']}.txt"
|
|
55
|
+
log.info("Creating %s", file_path)
|
|
56
|
+
template = env.get_template(DEFINITION_DESCRIPTION)
|
|
57
|
+
with file_path.open("w+") as fh:
|
|
58
|
+
fh.write(template.render(section))
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import asdict, dataclass
|
|
18
|
+
from typing import Any, Dict, List, Tuple
|
|
19
|
+
|
|
20
|
+
from pydantic.json_schema import GenerateJsonSchema
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ProjectDefinitionProperty:
|
|
25
|
+
"""
|
|
26
|
+
Class for storing data of properties to be used in project definition generators.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
path: str
|
|
30
|
+
title: str
|
|
31
|
+
indents: int
|
|
32
|
+
item_index: int
|
|
33
|
+
required: bool
|
|
34
|
+
name: str
|
|
35
|
+
description: str
|
|
36
|
+
add_types: bool
|
|
37
|
+
types: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProjectDefinitionGenerateJsonSchema(GenerateJsonSchema):
|
|
41
|
+
def __init__(self, by_alias: bool = False, ref_template: str = ""):
|
|
42
|
+
super().__init__(by_alias, "{model}")
|
|
43
|
+
self._remapped_definitions: Dict[str, Any] = {}
|
|
44
|
+
|
|
45
|
+
def generate(self, schema, mode="validation"):
|
|
46
|
+
"""
|
|
47
|
+
Transforms the generated json from the model to a list of project definition sections with its properties.
|
|
48
|
+
For example:
|
|
49
|
+
{
|
|
50
|
+
"result": [
|
|
51
|
+
{
|
|
52
|
+
"title": "Native app definitions for the project",
|
|
53
|
+
"name": "native_app",
|
|
54
|
+
"properties": [
|
|
55
|
+
{
|
|
56
|
+
"path": "Version of the project definition schema, which is currently 1",
|
|
57
|
+
"title": "Title of field A",
|
|
58
|
+
"indents": 0,
|
|
59
|
+
"item_index": 0,
|
|
60
|
+
"required": True,
|
|
61
|
+
"name": "definition_version",
|
|
62
|
+
"add_types": True,
|
|
63
|
+
"types": "string | integer",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"path": "native_app.name",
|
|
67
|
+
"title": "Project identifier",
|
|
68
|
+
"indents": 1,
|
|
69
|
+
"item_index": 0,
|
|
70
|
+
"required": True,
|
|
71
|
+
"name": "name",
|
|
72
|
+
"add_types": True,
|
|
73
|
+
"types": "string",
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
"""
|
|
80
|
+
json_schema = super().generate(schema, mode=mode)
|
|
81
|
+
self._remapped_definitions = json_schema["$defs"]
|
|
82
|
+
return {"result": self._get_definition_sections(json_schema)}
|
|
83
|
+
|
|
84
|
+
def _get_definition_sections(
|
|
85
|
+
self, current_definition: Dict[str, Any]
|
|
86
|
+
) -> List[Dict[str, Any]]:
|
|
87
|
+
required_properties: List[Dict[str, Any]] = []
|
|
88
|
+
sections: List[Dict[str, Any]] = []
|
|
89
|
+
|
|
90
|
+
for property_name, property_model in current_definition["properties"].items():
|
|
91
|
+
is_required = (
|
|
92
|
+
"required" in current_definition
|
|
93
|
+
and property_name in current_definition["required"]
|
|
94
|
+
)
|
|
95
|
+
children_properties = self._get_children_properties(
|
|
96
|
+
property_model, property_name
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
new_property = ProjectDefinitionProperty(
|
|
100
|
+
path=property_name,
|
|
101
|
+
title=property_model.get("title", ""),
|
|
102
|
+
description=property_model.get("description", ""),
|
|
103
|
+
indents=0,
|
|
104
|
+
item_index=0,
|
|
105
|
+
required=is_required,
|
|
106
|
+
name=property_name,
|
|
107
|
+
add_types=len(children_properties) == 0,
|
|
108
|
+
types=" | ".join(self._get_property_types(property_model)),
|
|
109
|
+
)
|
|
110
|
+
properties = [asdict(new_property)] + children_properties
|
|
111
|
+
|
|
112
|
+
if is_required:
|
|
113
|
+
required_properties.extend(properties)
|
|
114
|
+
else:
|
|
115
|
+
sections.append(
|
|
116
|
+
{
|
|
117
|
+
"properties": properties,
|
|
118
|
+
"title": property_model["title"],
|
|
119
|
+
"name": property_name,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for section in sections:
|
|
124
|
+
section["properties"] = required_properties + section["properties"]
|
|
125
|
+
|
|
126
|
+
return sections
|
|
127
|
+
|
|
128
|
+
def _get_section_properties(
|
|
129
|
+
self,
|
|
130
|
+
current_definition: Dict[str, Any],
|
|
131
|
+
current_path: str = "",
|
|
132
|
+
depth: int = 0,
|
|
133
|
+
is_array_item: bool = False,
|
|
134
|
+
) -> List[Dict[str, Any]]:
|
|
135
|
+
required_properties: List[Dict[str, Any]] = []
|
|
136
|
+
optional_properties: List[Dict[str, Any]] = []
|
|
137
|
+
item_index = 0
|
|
138
|
+
|
|
139
|
+
for property_name, property_model in current_definition["properties"].items():
|
|
140
|
+
item_index += 1 if is_array_item else 0
|
|
141
|
+
is_required = (
|
|
142
|
+
"required" in current_definition
|
|
143
|
+
and property_name in current_definition["required"]
|
|
144
|
+
)
|
|
145
|
+
new_current_path = (
|
|
146
|
+
property_name
|
|
147
|
+
if current_path == ""
|
|
148
|
+
else current_path + "." + property_name
|
|
149
|
+
)
|
|
150
|
+
children_properties = self._get_children_properties(
|
|
151
|
+
property_model, new_current_path, depth
|
|
152
|
+
)
|
|
153
|
+
new_property = ProjectDefinitionProperty(
|
|
154
|
+
path=new_current_path,
|
|
155
|
+
title=property_model.get("title", ""),
|
|
156
|
+
description=property_model.get("description", ""),
|
|
157
|
+
indents=depth,
|
|
158
|
+
item_index=item_index,
|
|
159
|
+
required=is_required,
|
|
160
|
+
name=property_name,
|
|
161
|
+
add_types=len(children_properties) == 0,
|
|
162
|
+
types=" | ".join(self._get_property_types(property_model)),
|
|
163
|
+
)
|
|
164
|
+
properties = [asdict(new_property)] + children_properties
|
|
165
|
+
if is_required:
|
|
166
|
+
required_properties.extend(properties)
|
|
167
|
+
else:
|
|
168
|
+
optional_properties.extend(properties)
|
|
169
|
+
return required_properties + optional_properties
|
|
170
|
+
|
|
171
|
+
def _get_children_properties(
|
|
172
|
+
self,
|
|
173
|
+
property_model: Dict[str, Any],
|
|
174
|
+
current_path: str,
|
|
175
|
+
depth: int = 0,
|
|
176
|
+
) -> List[Dict[str, Any]]:
|
|
177
|
+
child_properties: List[Dict[str, Any]] = []
|
|
178
|
+
references: List[Tuple[str, bool]] = self._get_property_references(
|
|
179
|
+
property_model
|
|
180
|
+
)
|
|
181
|
+
for reference, is_array_item in references:
|
|
182
|
+
child_properties.extend(
|
|
183
|
+
self._get_section_properties(
|
|
184
|
+
self._remapped_definitions[reference],
|
|
185
|
+
current_path,
|
|
186
|
+
depth + 1,
|
|
187
|
+
is_array_item,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return child_properties
|
|
192
|
+
|
|
193
|
+
def _get_property_references(
|
|
194
|
+
self,
|
|
195
|
+
model_with_type: Dict[str, Any],
|
|
196
|
+
is_array_item: bool = False,
|
|
197
|
+
) -> list[tuple[str, bool]]:
|
|
198
|
+
result: List[Tuple[str, bool]] = []
|
|
199
|
+
|
|
200
|
+
if "$ref" in model_with_type:
|
|
201
|
+
return [(model_with_type["$ref"], is_array_item)]
|
|
202
|
+
|
|
203
|
+
if "type" in model_with_type and model_with_type["type"] == "array":
|
|
204
|
+
result.extend(self._get_property_references(model_with_type["items"], True))
|
|
205
|
+
|
|
206
|
+
if "anyOf" in model_with_type:
|
|
207
|
+
for property_type in model_with_type["anyOf"]:
|
|
208
|
+
result.extend(
|
|
209
|
+
self._get_property_references(property_type, is_array_item)
|
|
210
|
+
)
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
def _get_property_types(self, model_with_type: Dict[str, Any]) -> list[str]:
|
|
214
|
+
types_result: List[str] = []
|
|
215
|
+
if "type" in model_with_type:
|
|
216
|
+
if model_with_type["type"] == "array":
|
|
217
|
+
items_types = self._get_property_types(model_with_type["items"])
|
|
218
|
+
if len(items_types) > 0:
|
|
219
|
+
types_result.append(f"array[{' | '.join(items_types)}]")
|
|
220
|
+
|
|
221
|
+
elif model_with_type["type"] != "null":
|
|
222
|
+
types_result.append(model_with_type["type"])
|
|
223
|
+
elif "anyOf" in model_with_type:
|
|
224
|
+
for property_type in model_with_type["anyOf"]:
|
|
225
|
+
types = self._get_property_types(property_type)
|
|
226
|
+
types_result.extend(types)
|
|
227
|
+
return types_result
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from jinja2 import Environment, FileSystemLoader
|
|
18
|
+
|
|
19
|
+
_TEMPLATE_PATH = Path(__file__).parent / "templates"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_template_environment() -> Environment:
|
|
23
|
+
return Environment(loader=FileSystemLoader(_TEMPLATE_PATH))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{{ title }}
|
|
2
|
+
|
|
3
|
+
Project definition structure
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
.. code-block::
|
|
7
|
+
|
|
8
|
+
{% for property in properties %}
|
|
9
|
+
{{ " "*(property["indents"] + 1) + ("- " if property["item_index"] == 1 else "") + (" " if property["item_index"] > 1 else "") + property["name"] }}: {% if property["add_types"] %}<{{ property["types"] }}>{% endif %}
|
|
10
|
+
{%- endfor %}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Project definition properties
|
|
14
|
+
===============================================================================
|
|
15
|
+
{%- if properties %}
|
|
16
|
+
The following table describes the project definition properties.
|
|
17
|
+
|
|
18
|
+
.. list-table:: Project definition properties
|
|
19
|
+
:widths: 30 70
|
|
20
|
+
:header-rows: 1
|
|
21
|
+
|
|
22
|
+
* - Property
|
|
23
|
+
- Definition
|
|
24
|
+
{% for property in properties %}
|
|
25
|
+
* - **{{property["path"]}}**
|
|
26
|
+
|
|
27
|
+
*{{"Required" if property["required"] else "Optional"}}*{{ (", *"+property["types"]+"*") if property["types"]}}
|
|
28
|
+
|
|
29
|
+
- {{property["title"]}}
|
|
30
|
+
{% if property["description"] %}
|
|
31
|
+
{{ property["description"] }}
|
|
32
|
+
{% else %}{% endif -%}
|
|
33
|
+
{%+ endfor %}
|
|
34
|
+
{% else %}
|
|
35
|
+
|
|
36
|
+
None
|
|
37
|
+
|
|
38
|
+
{% endif -%}
|
|
@@ -11,6 +11,7 @@ Syntax
|
|
|
11
11
|
{%- endfor %}
|
|
12
12
|
{%- for param in options if not param.hidden %}
|
|
13
13
|
{{ (param.opts | get_main_option) }}{% if not param.is_flag %} <{{ param.name }}>{% endif %}
|
|
14
|
+
{%- if param.type.name == "boolean" and param.secondary_opts|length > 0 %} {{ "/ " }}{{ param.secondary_opts[0] }}{% endif %}
|
|
14
15
|
{%- endfor %}
|
|
15
16
|
|
|
16
17
|
Arguments
|
|
@@ -38,7 +39,11 @@ Options
|
|
|
38
39
|
|
|
39
40
|
{%- if options %}
|
|
40
41
|
{% for param in options if not param.hidden %}
|
|
41
|
-
:samp:`{%
|
|
42
|
+
:samp:`{% if param.type.name == "boolean" %}
|
|
43
|
+
{%- for p in param.opts %}{{ p }}{{ ", " if not loop.last }}{% endfor %}{% if param.secondary_opts|length > 0 %} {{ "/ " }}{{ param.secondary_opts[0] }}{% endif %}
|
|
44
|
+
{%- else %}
|
|
45
|
+
{%- for p in param.opts %}{{ p }}{{ ", " if not loop.last }}{% endfor %}
|
|
46
|
+
{%- endif %}
|
|
42
47
|
{%- if not param.is_flag %}
|
|
43
48
|
{%- if param.type.name != "choice" %}{{ ' {' }}{% else %} {% endif %}{{ param.make_metavar() }}{% if param.type.name != "choice" %}{{ '}' }}
|
|
44
49
|
{%- endif %}
|