-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Project definition v2 entity schemas: application and application package #1280
Changes from 18 commits
0eaf420
f7dc71f
1e806be
65d62f5
8126435
1858673
244483a
984a046
240fb53
d618d8f
cb7b090
35617a7
df209b3
2b85a04
4c84b9d
c66479e
2c9793d
fd475e9
1fd8975
79f06fb
56b9847
bd79cd0
a6c6ab2
90e6f2c
5227eda
d5188dc
6b3faa6
c25b4fb
1beb393
f4fa56a
c49d6d6
e0555b0
9665a6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Copyright (c) 2024 Snowflake Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Literal, Optional | ||
|
||
from pydantic import AliasChoices, Field | ||
from snowflake.cli.api.project.schemas.entities.application_package_entity import ( | ||
ApplicationPackageEntity, | ||
) | ||
from snowflake.cli.api.project.schemas.entities.common import ( | ||
EntityBase, | ||
TargetField, | ||
) | ||
from snowflake.cli.api.project.schemas.updatable_model import ( | ||
UpdatableModel, | ||
) | ||
|
||
|
||
class ApplicationEntity(EntityBase): | ||
type: Literal["application"] | ||
name: str = Field( | ||
title="Name of the application created when this entity is deployed" | ||
) | ||
from_: ApplicationFromField = Field( | ||
validation_alias=AliasChoices("from"), | ||
title="An application package this entity should be created from", | ||
) | ||
debug: Optional[bool] = Field( | ||
title="Whether to enable debug mode when using a named stage to create an application object", | ||
default=None, | ||
) | ||
|
||
|
||
class ApplicationFromField(UpdatableModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Non-blocking) In the future, we should pull the generic type attr up, e.g.
|
||
target: TargetField[ApplicationPackageEntity] = Field( | ||
title="Reference to an application package entity", | ||
) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,61 @@ | ||||||
# Copyright (c) 2024 Snowflake Inc. | ||||||
# | ||||||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
# you may not use this file except in compliance with the License. | ||||||
# You may obtain a copy of the License at | ||||||
# | ||||||
# http://www.apache.org/licenses/LICENSE-2.0 | ||||||
# | ||||||
# Unless required by applicable law or agreed to in writing, software | ||||||
# distributed under the License is distributed on an "AS IS" BASIS, | ||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
# See the License for the specific language governing permissions and | ||||||
# limitations under the License. | ||||||
|
||||||
from __future__ import annotations | ||||||
|
||||||
from typing import List, Literal, Optional, Union | ||||||
|
||||||
from pydantic import Field | ||||||
from snowflake.cli.api.project.schemas.entities.common import ( | ||||||
EntityBase, | ||||||
) | ||||||
from snowflake.cli.api.project.schemas.native_app.package import DistributionOptions | ||||||
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping | ||||||
|
||||||
|
||||||
class ApplicationPackageEntity(EntityBase): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are some properties not accounted for here, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added these as well. While we maintain both versions we will have to remember to update v2 schema when we make v1 changes. |
||||||
type: Literal["application package"] | ||||||
name: str = Field( | ||||||
title="Name of the application package created when this entity is deployed" | ||||||
) | ||||||
artifacts: List[Union[PathMapping, str]] = Field( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why union? Won't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated all of these to |
||||||
title="List of file source and destination pairs to add to the deploy root", | ||||||
) | ||||||
bundle_root: Optional[str] = Field( | ||||||
title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored.", | ||||||
default="output/bundle/", | ||||||
) | ||||||
deploy_root: Optional[str] = Field( | ||||||
title="Folder at the root of your project where the build step copies the artifacts", | ||||||
default="output/deploy/", | ||||||
) | ||||||
generated_root: Optional[str] = Field( | ||||||
title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written.", | ||||||
default="__generated/", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should default be a Path or conversion happens automagicaly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Automagically, but made it |
||||||
) | ||||||
stage: Optional[str] = Field( | ||||||
title="Identifier of the stage that stores the application artifacts.", | ||||||
default="app_src.stage", | ||||||
) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we consider introducing I think same can be applied to Edit: It seems we do have |
||||||
scratch_stage: Optional[str] = Field( | ||||||
title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI.", | ||||||
default="app_src.stage_snowflake_cli_scratch", | ||||||
) | ||||||
distribution: Optional[DistributionOptions] = Field( | ||||||
title="Distribution of the application package created by the Snowflake CLI", | ||||||
default="internal", | ||||||
) | ||||||
manifest: str = Field( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Or |
||||||
title="Path to manifest.yml", | ||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# Copyright (c) 2024 Snowflake Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from __future__ import annotations | ||
|
||
from abc import ABC | ||
from enum import Enum | ||
from typing import Generic, List, Optional, TypeVar | ||
|
||
from pydantic import AliasChoices, Field, GetCoreSchemaHandler, ValidationInfo | ||
from pydantic_core import core_schema | ||
from snowflake.cli.api.project.schemas.native_app.application import ( | ||
ApplicationPostDeployHook, | ||
) | ||
from snowflake.cli.api.project.schemas.updatable_model import ( | ||
IdentifierField, | ||
UpdatableModel, | ||
) | ||
|
||
|
||
class EntityType(Enum): | ||
APPLICATION = "application" | ||
APPLICATION_PACKAGE = "application package" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have those strings as literal on entities classes and we have them here. Should we consider using this Enum as source of truth? Or should we use entities to build Enum / something? Taking into account that we are constructing _v2_entity_types_map = {
EntityType.APPLICATION.value: ApplicationEntity,
EntityType.APPLICATION_PACKAGE.value: ApplicationPackageEntity,
} It means that adding new entity requires it to be registered in at least two places. it may be wise to have KNOWN_ENTITIES = [
ApplicationEntity,
ApplicationPackageEntity
]
ENTITIES_TYPE = Union[*KNOWN_ENTITIES]
_v2_entity_types_map = {get_args(eval(inspect.get_annotations(e)["type"]))[0]: e for e in KNOWN_ENTITIES} The dict comprehension is a bit of a magic, but probably there's something smarter we can do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's what I tried at first but I couldn't make pydantic understand enums as discriminators and reverted to literal strings. I'm not sure what I think about this dict comprehension, but I get the idea. I'll try to come up with a cleaner way to have a single source of truth. |
||
|
||
|
||
class MetaField(UpdatableModel): | ||
warehouse: Optional[str] = IdentifierField( | ||
title="Warehouse used to run the scripts", default=None | ||
) | ||
role: Optional[str] = IdentifierField( | ||
title="Role to use when creating the entity object", | ||
default=None, | ||
) | ||
post_deploy: Optional[List[ApplicationPostDeployHook]] = Field( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this common for all entities? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep |
||
title="Actions that will be executed after the application object is created/upgraded", | ||
default=None, | ||
) | ||
|
||
|
||
class DefaultsField(UpdatableModel): | ||
schema_: Optional[str] = Field( | ||
title="Schema.", | ||
validation_alias=AliasChoices("schema"), | ||
default=None, | ||
) | ||
stage: Optional[str] = Field( | ||
title="Stage.", | ||
default=None, | ||
) | ||
|
||
|
||
class EntityBase(ABC, UpdatableModel): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Non-blocking) I would like to see e.g. |
||
meta: Optional[MetaField] = Field(title="Meta fields", default=None) | ||
|
||
|
||
TargetType = TypeVar("TargetType") | ||
|
||
|
||
class TargetField(Generic[TargetType]): | ||
def __init__(self, entity_target_key: str): | ||
self.value = entity_target_key | ||
|
||
def __repr__(self): | ||
return self.value | ||
|
||
@classmethod | ||
def validate(cls, value: str, info: ValidationInfo) -> TargetField: | ||
return cls(value) | ||
|
||
@classmethod | ||
def __get_pydantic_core_schema__( | ||
cls, source_type, handler: GetCoreSchemaHandler | ||
) -> core_schema.CoreSchema: | ||
return core_schema.with_info_after_validator_function( | ||
cls.validate, handler(str), field_name=handler.field_name | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Copyright (c) 2024 Snowflake Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Union | ||
|
||
from snowflake.cli.api.project.schemas.entities.application_entity import ( | ||
ApplicationEntity, | ||
) | ||
from snowflake.cli.api.project.schemas.entities.application_package_entity import ( | ||
ApplicationPackageEntity, | ||
) | ||
|
||
Entity = Union[ApplicationEntity, ApplicationPackageEntity] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,15 +18,35 @@ | |
from typing import Dict, Optional, Union | ||
|
||
from packaging.version import Version | ||
from pydantic import Field, ValidationError, field_validator | ||
from pydantic import Field, ValidationError, field_validator, model_validator | ||
from snowflake.cli.api.feature_flags import FeatureFlag | ||
from snowflake.cli.api.project.errors import SchemaValidationError | ||
from snowflake.cli.api.project.schemas.entities.application_entity import ( | ||
ApplicationEntity, | ||
) | ||
from snowflake.cli.api.project.schemas.entities.application_package_entity import ( | ||
ApplicationPackageEntity, | ||
) | ||
from snowflake.cli.api.project.schemas.entities.common import ( | ||
DefaultsField, | ||
EntityType, | ||
TargetField, | ||
) | ||
from snowflake.cli.api.project.schemas.entities.entities import ( | ||
Entity, | ||
) | ||
from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp | ||
from snowflake.cli.api.project.schemas.snowpark.snowpark import Snowpark | ||
from snowflake.cli.api.project.schemas.streamlit.streamlit import Streamlit | ||
from snowflake.cli.api.project.schemas.updatable_model import UpdatableModel | ||
from snowflake.cli.api.utils.models import ProjectEnvironment | ||
from snowflake.cli.api.utils.types import Context | ||
from typing_extensions import Annotated | ||
|
||
_v2_entity_types_map = { | ||
EntityType.APPLICATION.value: ApplicationEntity, | ||
EntityType.APPLICATION_PACKAGE.value: ApplicationPackageEntity, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Next up for us is to figure out how these pydantic models map to classes with e.g. deploy, drop, bundle methods |
||
|
||
|
||
@dataclass | ||
|
@@ -105,8 +125,55 @@ def _convert_env( | |
|
||
|
||
class DefinitionV20(_ProjectDefinitionBase): | ||
entities: Dict = Field( | ||
title="Entity definitions.", | ||
entities: Dict[str, Annotated[Entity, Field(discriminator="type")]] = Field( | ||
title="Entity definitions." | ||
) | ||
|
||
@model_validator(mode="before") | ||
@classmethod | ||
def apply_defaults(cls, data: Dict) -> Dict: | ||
""" | ||
Applies default values that exist on the model but not specified in yml | ||
""" | ||
if "defaults" in data: | ||
if "entities" in data: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
for key, entity in data["entities"].items(): | ||
entity_type = entity["type"] | ||
if entity_type not in _v2_entity_types_map: | ||
continue | ||
entity_model = _v2_entity_types_map[entity_type] | ||
for default_key, default_value in data["defaults"].items(): | ||
if ( | ||
default_key in entity_model.model_fields | ||
and default_key not in entity | ||
): | ||
entity[default_key] = default_value | ||
return data | ||
|
||
@field_validator("entities", mode="after") | ||
@classmethod | ||
def validate_entities(cls, entities: Dict[str, Entity]) -> Dict[str, Entity]: | ||
for key, entity in entities.items(): | ||
# TODO Automatically detect TargetFields to validate | ||
if entity.type == EntityType.APPLICATION.value: | ||
if isinstance(entity.from_.target, TargetField): | ||
target = str(entity.from_.target) | ||
if target not in entities: | ||
raise ValueError(f"No such target: {target}") | ||
else: | ||
# Validate the target type | ||
target_cls = entity.from_.__class__.model_fields["target"] | ||
target_type = target_cls.annotation.__args__[0] | ||
actual_target_type = entities[target].__class__ | ||
if target_type and target_type is not actual_target_type: | ||
raise ValueError( | ||
f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}" | ||
) | ||
return entities | ||
|
||
defaults: Optional[DefaultsField] = Field( | ||
title="Default key/value entity values that are merged recursively for each entity.", | ||
default=None, | ||
) | ||
|
||
env: Union[Dict[str, str], ProjectEnvironment, None] = Field( | ||
|
@@ -125,12 +192,6 @@ def _convert_env( | |
return env | ||
return ProjectEnvironment(default_env=(env or {}), override_env={}) | ||
|
||
@field_validator("entities") | ||
@classmethod | ||
def validate_entities(cls, entities: Dict) -> Dict: | ||
# TODO Add entities validation logic | ||
return entities | ||
|
||
|
||
def build_project_definition(**data): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignoring flake8 A003 and removing some
noqa: A003
comments. This prevents us from adding atype
field on Entity classes. Using an alias prevents us from using discriminators (not supported by Pydantic).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like it, it's a known source of errors. It's not also limited to
type
but also other builtins.I think
noqa: A003
on single line in model (well, every model) is safer.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair. I tried to avoid having
noqa
on each entity type. Reverted.