Skip to content
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

Add optional deploy files argument #933

Merged
merged 82 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
ad43ea1
add files argument to deploy
sfc-gh-gbloom Mar 22, 2024
041b385
add integration test
sfc-gh-gbloom Mar 25, 2024
159752b
stage files relative to deploy root
sfc-gh-gbloom Mar 25, 2024
dc4c967
add unit tests
sfc-gh-gbloom Mar 26, 2024
b95962d
check for empty list
sfc-gh-gbloom Mar 26, 2024
6265837
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Mar 26, 2024
2149053
release notes update
sfc-gh-gbloom Mar 26, 2024
f8b8c5e
update command description
sfc-gh-gbloom Mar 26, 2024
7a07388
unit test grooming
sfc-gh-gbloom Mar 26, 2024
9fa9f44
pr comments
sfc-gh-gbloom Mar 26, 2024
1e29c4b
add types
sfc-gh-gbloom Mar 26, 2024
e2119c3
fix ignore only on stage files
sfc-gh-gbloom Mar 27, 2024
5616e01
update snapshot
sfc-gh-gbloom Mar 27, 2024
9c4a902
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Mar 27, 2024
3a1e46d
fix set type for python3.8
sfc-gh-gbloom Mar 27, 2024
29994b0
delete remote only files
sfc-gh-gbloom Mar 27, 2024
f7963d8
fix relative paths
sfc-gh-gbloom Mar 27, 2024
1e82b1c
add integration test of removing remote only file
sfc-gh-gbloom Mar 27, 2024
e9c593b
add prune flag
sfc-gh-gbloom Apr 3, 2024
4031f68
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 3, 2024
a003915
default prune
sfc-gh-gbloom Apr 4, 2024
dc3ec90
move paths filter logic to manager
sfc-gh-gbloom Apr 4, 2024
28f916d
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 4, 2024
9749655
fix remote md5 map
sfc-gh-gbloom Apr 4, 2024
c60b20c
add build_md5_map test
sfc-gh-gbloom Apr 5, 2024
421607e
add no prune test
sfc-gh-gbloom Apr 5, 2024
868314d
change to relative to project root
sfc-gh-gbloom Apr 9, 2024
4c03ee3
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 9, 2024
da14615
type fix
sfc-gh-gbloom Apr 9, 2024
6392691
add integration coverage
sfc-gh-gbloom Apr 9, 2024
e162cda
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 10, 2024
1f22260
pr nits
sfc-gh-gbloom Apr 10, 2024
f4d01f5
pass created build files to find source from deploy path
sfc-gh-gbloom Apr 16, 2024
39c0d36
add recursive flag
sfc-gh-gbloom Apr 17, 2024
b6b80d2
add unit tests
sfc-gh-gbloom Apr 17, 2024
38ddfd6
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 17, 2024
3ac8f27
integration tests
sfc-gh-gbloom Apr 18, 2024
89381dc
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 18, 2024
3da37b1
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 18, 2024
488cac8
update snapshot
sfc-gh-gbloom Apr 18, 2024
0acd51b
relpath fix
sfc-gh-gbloom Apr 18, 2024
9ba1201
relpath fix
sfc-gh-gbloom Apr 18, 2024
c961ccd
fix windows relative path
sfc-gh-gbloom Apr 19, 2024
3f94f4e
resolved paths
sfc-gh-gbloom Apr 22, 2024
fead52e
resolve deploy root
sfc-gh-gbloom Apr 22, 2024
1177c8b
resolve deploy root
sfc-gh-gbloom Apr 23, 2024
e79ae17
fix test regex
sfc-gh-gbloom Apr 23, 2024
60551ef
debug
sfc-gh-gbloom Apr 23, 2024
b69921b
resolve source path
sfc-gh-gbloom Apr 23, 2024
d1e38b2
debug
sfc-gh-gbloom Apr 23, 2024
26497cb
debug
sfc-gh-gbloom Apr 23, 2024
7fff826
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 23, 2024
aecc5b4
handle windows path in integration test
sfc-gh-gbloom Apr 24, 2024
7176727
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 24, 2024
6ffbcf3
release notes
sfc-gh-gbloom Apr 24, 2024
8d89da9
move default arguments
sfc-gh-gbloom Apr 24, 2024
fc58314
pr nits
sfc-gh-gbloom Apr 24, 2024
b41ae52
pr nits
sfc-gh-gbloom Apr 25, 2024
26d0407
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 25, 2024
2dcd2d7
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 26, 2024
4d179c5
no recursive check in full deploy
sfc-gh-gbloom Apr 26, 2024
ccea424
prune test
sfc-gh-gbloom Apr 26, 2024
7ca66fa
add file not found test
sfc-gh-gbloom Apr 26, 2024
f7bc13e
fix non recursive full deploy
sfc-gh-gbloom Apr 26, 2024
b57f7d2
use different names in src/dest
sfc-gh-gbloom Apr 26, 2024
4568237
rename mapped_files
sfc-gh-gbloom Apr 29, 2024
c7be70e
rename source_path
sfc-gh-gbloom Apr 29, 2024
fbd6b04
nit
sfc-gh-gbloom Apr 29, 2024
a282972
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 29, 2024
ef3dedd
better docs
sfc-gh-gbloom Apr 29, 2024
4e4bf85
update snapshot
sfc-gh-gbloom Apr 29, 2024
b8f0650
better docs
sfc-gh-gbloom Apr 29, 2024
e1cc96d
use ClickException
sfc-gh-gbloom Apr 30, 2024
e3550de
tests
sfc-gh-gbloom Apr 30, 2024
63db862
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom Apr 30, 2024
1ea1785
add snapshot
sfc-gh-gbloom Apr 30, 2024
ece7214
match click exception output
sfc-gh-gbloom Apr 30, 2024
4e39f76
remove text assertion
sfc-gh-gbloom Apr 30, 2024
be7d3a1
doc
sfc-gh-gbloom Apr 30, 2024
19ab8a2
snapshot
sfc-gh-gbloom Apr 30, 2024
5b55216
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom May 2, 2024
dd293e9
Merge branch 'main' into gbloom-SNOW-1238239-deploy-files-argument
sfc-gh-gbloom May 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## New additions
* `snow sql` command supports now client-side templating of queries.
* New `snow app deploy` functionality:
* Passing files and directories as arguments syncs these only: `snow app deploy some-file some-dir`.
* `--recursive` syncs all files and subdirectories recursively.
* `--prune` deletes specified files from the stage if they don't exist locally.

## Fixes and improvements
* More human-friendly errors in case of corrupted `config.toml` file.
Expand Down
49 changes: 44 additions & 5 deletions src/snowflake/cli/plugins/nativeapp/artifacts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import os
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union

from click import ClickException
from click.exceptions import ClickException
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
from snowflake.cli.api.project.schemas.native_app.path_mapping import PathMapping
from snowflake.cli.api.secure_path import SecurePath
from yaml import safe_load

# Map from source directories and files in the project directory to their path in the deploy directory. Both paths are absolute.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do they need to be absolute? Intuitively, I think of source paths as relative to the project root, and dest paths as relative to the deployment directory. My guess is that without this invariant, you'd have to drag both roots around, is that it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with relative paths, but switched to absolute mostly to canonicalize windows paths (C:\Users\RUNNER~1\ -> C:\Users\runneradmin\).
It also simplifies the code, and doesn't assume that the deploy root is a subdirectory of the project root.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the deploy root could be anything, we should never assume that it's within the project directory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an ideal world, these would be relative to their respective roots, but it's documented well and if we need them we can always use relative_to.

ArtifactDeploymentMap = Dict[Path, Path]


class DeployRootError(ClickException):
"""
Expand Down Expand Up @@ -195,11 +198,14 @@ def resolve_without_follow(path: Path) -> Path:


def build_bundle(
project_root: Path, deploy_root: Path, artifacts: List[ArtifactMapping]
):
project_root: Path,
deploy_root: Path,
artifacts: List[ArtifactMapping],
) -> ArtifactDeploymentMap:
"""
Prepares a local folder (deploy_root) with configured app artifacts.
This folder can then be uploaded to a stage.
Returns a map of the copied source files, pointing to where they were copied.
"""
resolved_root = deploy_root.resolve()
if resolved_root.exists() and not resolved_root.is_dir():
Expand All @@ -217,6 +223,7 @@ def build_bundle(
if resolved_root.exists():
delete(resolved_root)

mapped_files: ArtifactDeploymentMap = {}
for artifact in artifacts:
dest_path = resolve_without_follow(Path(resolved_root, artifact.dest))
source_paths = get_source_paths(artifact, project_root)
Expand All @@ -228,7 +235,9 @@ def build_bundle(

# copy all files as children of the given destination path
for source_path in source_paths:
symlink_or_copy(source_path, dest_path / source_path.name)
dest_child_path = dest_path / source_path.name
symlink_or_copy(source_path, dest_child_path)
mapped_files[source_path.resolve()] = dest_child_path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be related to the discussion above, but why:

  1. Is source_path being resolved?
  2. Is only source_path being resolved but not dest_child_path?

Also, can you clarify the following:
If the user specified a glob pattern, then source_path will be the directories or files that qualify the source pattern. In case of directories, even though their contents are symlinked as well, mapped_files will only contain a kv pair of the directory to the dest, and not its contents.

Copy link
Contributor Author

@sfc-gh-gbloom sfc-gh-gbloom Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Both src/dest parts in ArtifactDeploymentMap are resolved, mostly to canonicalize windows paths: C:\Users\RUNNER~1\ -> C:\Users\runneradmin\
  2. dest_child_path is also resolved (dest_path is resolved a few lines above)

Yes, mapped_files won't contain the contents of symlinked directories. It's only the files that we actually copied to the deploy directory.
You can see the logic in project_path_to_deploy_path() for cases where we look for a file under a symlinked directory.

else:
# ensure we are copying into the deploy root, not replacing it!
if resolved_root not in dest_path.parents:
Expand All @@ -237,9 +246,11 @@ def build_bundle(
if len(source_paths) == 1:
# copy a single file as the given destination path
symlink_or_copy(source_paths[0], dest_path)
mapped_files[source_paths[0].resolve()] = dest_path
else:
# refuse to map multiple source files to one destination (undefined behaviour)
raise TooManyFilesError(dest_path)
return mapped_files


def find_manifest_file(deploy_root: Path) -> Path:
Expand Down Expand Up @@ -283,3 +294,31 @@ def find_version_info_in_manifest_file(
patch_name = version_info[patch_field]

return version_name, patch_name


def source_path_to_deploy_path(
source_path: Path, mapped_files: ArtifactDeploymentMap
) -> Path:
"""Returns the absolute path where the specified source path was copied to during bundle."""

source_path = source_path.resolve()

if source_path in mapped_files:
return mapped_files[source_path]

# Find the first parent directory that exists in mapped_files
common_root = source_path
while common_root:
if common_root in mapped_files:
break
elif common_root.parent != common_root:
common_root = common_root.parent
else:
raise ClickException(f"Could not find the deploy path of {source_path}")

# Construct the target deploy path
path_to_symlink = mapped_files[common_root]
relative_path_to_target = Path(source_path).relative_to(common_root)
result = Path(path_to_symlink, relative_path_to_target)

return result
34 changes: 31 additions & 3 deletions src/snowflake/cli/plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import Optional
from pathlib import Path
from typing import List, Optional

import typer
from snowflake.cli.api.cli_global_context import cli_context
Expand Down Expand Up @@ -229,17 +230,44 @@ def app_teardown(
@app.command("deploy", requires_connection=True)
@with_project_definition("native_app")
def app_deploy(
prune: Optional[bool] = typer.Option(
default=None,
help=f"""Whether to delete specified files from the stage if they don't exist locally. If set, the command deletes files that exist in the stage, but not in the local filesystem.""",
),
recursive: Optional[bool] = typer.Option(
None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I would use False as default for boolean value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed it to be None here because there is the special flow of running snow app deploy with no arguments, in which case the default value is True

"--recursive",
"-r",
help=f"""Whether to traverse and deploy files from subdirectories. If set, the command deploys all files and subdirectories; otherwise, only files in the current directory are deployed.""",
),
files: Optional[List[Path]] = typer.Argument(
default=None,
show_default=False,
help=f"""Paths, relative to the the project root, of files you want to upload to a stage. The paths must match one of the artifacts src pattern entries in snowflake.yml. If unspecified, the command syncs all local changes to the stage.""",
),
**options,
) -> CommandResult:
"""
Creates an application package in your Snowflake account and syncs the local changes to the stage without creating or updating the application.
Running this command with no arguments at all, as in `snow app deploy`, is a shorthand for `snow app deploy --prune --recursive`.
"""
if files is None:
files = []
if prune is None and recursive is None and len(files) == 0:
prune = True
recursive = True
else:
if prune is None:
prune = False
if recursive is None:
recursive = False

manager = NativeAppManager(
project_definition=cli_context.project_definition,
project_root=cli_context.project_root,
)

manager.build_bundle()
manager.deploy()
mapped_files = manager.build_bundle()
manager.deploy(prune, recursive, files, mapped_files)

return MessageResult(f"Deployed successfully.")
78 changes: 72 additions & 6 deletions src/snowflake/cli/plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
from abc import ABC, abstractmethod
from functools import cached_property
from pathlib import Path
Expand All @@ -23,8 +24,11 @@
from snowflake.cli.api.sql_execution import SqlExecutionMixin
from snowflake.cli.plugins.connection.util import make_snowsight_url
from snowflake.cli.plugins.nativeapp.artifacts import (
ArtifactDeploymentMap,
ArtifactMapping,
build_bundle,
resolve_without_follow,
source_path_to_deploy_path,
translate_artifact,
)
from snowflake.cli.plugins.nativeapp.constants import (
Expand All @@ -43,8 +47,10 @@
MissingPackageScriptError,
UnexpectedOwnerError,
)
from snowflake.cli.plugins.nativeapp.utils import verify_exists, verify_no_directories
from snowflake.cli.plugins.stage.diff import (
DiffResult,
filter_from_diff,
stage_diff,
sync_local_diff_with_stage,
)
Expand Down Expand Up @@ -100,6 +106,20 @@ def ensure_correct_owner(row: dict, role: str, obj_name: str) -> None:
raise UnexpectedOwnerError(obj_name, role, actual_owner)


def _get_paths_to_sync(paths_to_sync: List[Path], deploy_root: Path) -> List[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an internal function (so the doc string is not customer-visible)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, not visible to the customer using the CLI. But visible to the public in general since this is open source.
Regardless, good idea to still have some information on what this function does.

"""Takes a list of paths (files and directories), returning a list of all files recursively relative to the deploy root."""
paths = []
for path in paths_to_sync:
if path.is_dir():
for current_dir, _dirs, files in os.walk(path):
for file in files:
deploy_path = Path(current_dir, file).relative_to(deploy_root)
paths.append(str(deploy_path))
Comment on lines +116 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why convert back to str here? We could leave them as relative paths

Copy link
Contributor Author

@sfc-gh-gbloom sfc-gh-gbloom Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiffResult stores the paths as strings, and I convert these to a set downstream for quicker lookups

else:
paths.append(str(path.relative_to(deploy_root)))
return paths


class NativeAppCommandProcessor(ABC):
@abstractmethod
def process(self, *args, **kwargs):
Expand Down Expand Up @@ -278,13 +298,20 @@ def verify_project_distribution(
return False
return True

def build_bundle(self) -> None:
def build_bundle(self) -> ArtifactDeploymentMap:
"""
Populates the local deploy root from artifact sources.
"""
build_bundle(self.project_root, self.deploy_root, self.artifacts)

def sync_deploy_root_with_stage(self, role: str) -> DiffResult:
return build_bundle(self.project_root, self.deploy_root, self.artifacts)

def sync_deploy_root_with_stage(
self,
role: str,
prune: bool,
recursive: bool,
paths_to_sync: List[Path] = [], # relative to project root
mapped_files: Optional[ArtifactDeploymentMap] = None,
) -> DiffResult:
"""
Ensures that the files on our remote stage match the artifacts we have in
the local filesystem. Returns the DiffResult used to make changes.
Expand All @@ -310,6 +337,37 @@ def sync_deploy_root_with_stage(self, role: str) -> DiffResult:
% self.deploy_root
)
diff: DiffResult = stage_diff(self.deploy_root, self.stage_fqn)

files_not_removed = []
if len(paths_to_sync) > 0:
# Deploying specific files/directories
resolved_paths_to_sync = [resolve_without_follow(p) for p in paths_to_sync]
if not recursive:
verify_no_directories(resolved_paths_to_sync)
deploy_paths_to_sync = [
source_path_to_deploy_path(p, mapped_files)
for p in resolved_paths_to_sync
]
verify_exists(deploy_paths_to_sync)
paths_to_sync_set = set(
_get_paths_to_sync(deploy_paths_to_sync, self.deploy_root.resolve())
)
files_not_removed = filter_from_diff(diff, paths_to_sync_set, prune)
else:
# Full deploy
if not recursive:
deploy_files = os.listdir(str(self.deploy_root.resolve()))
verify_no_directories([Path(path_str) for path_str in deploy_files])
if not prune:
files_not_removed = diff.only_on_stage
diff.only_on_stage = []

if len(files_not_removed) > 0:
files_not_removed_str = "\n".join(files_not_removed)
cc.warning(
f"The following files exist only on the stage:\n{files_not_removed_str}\n\nUse the --prune flag to delete them from the stage."
)

cc.message(str(diff))

# Upload diff-ed files to application package stage
Expand Down Expand Up @@ -435,7 +493,13 @@ def _apply_package_scripts(self) -> None:
err, role=self.package_role, warehouse=self.package_warehouse
)

def deploy(self) -> DiffResult:
def deploy(
self,
prune: bool,
recursive: bool,
paths_to_sync: List[Path] = [],
mapped_files: Optional[ArtifactDeploymentMap] = None,
) -> DiffResult:
"""app deploy process"""

# 1. Create an empty application package, if none exists
Expand All @@ -446,6 +510,8 @@ def deploy(self) -> DiffResult:
self._apply_package_scripts()

# 3. Upload files from deploy root local folder to the above stage
diff = self.sync_deploy_root_with_stage(self.package_role)
diff = self.sync_deploy_root_with_stage(
self.package_role, prune, recursive, paths_to_sync, mapped_files
)

return diff
2 changes: 1 addition & 1 deletion src/snowflake/cli/plugins/nativeapp/run_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,5 +297,5 @@ def process(
)
return

diff = self.deploy()
diff = self.deploy(prune=True, recursive=True)
self._create_dev_app(diff)
18 changes: 17 additions & 1 deletion src/snowflake/cli/plugins/nativeapp/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from os import PathLike
from pathlib import Path
from sys import stdin, stdout
from typing import Optional, Union
from typing import List, Optional, Union

from click import ClickException


def needs_confirmation(needs_confirm: bool, auto_yes: bool) -> bool:
Expand Down Expand Up @@ -65,3 +67,17 @@ def shallow_git_clone(url: Union[str, PathLike], to_path: Union[str, PathLike]):
repo.close()

return repo


def verify_no_directories(paths_to_sync: List[Path]):
for path in paths_to_sync:
if path.is_dir():
raise ClickException(
f"{path} is a directory. Add the -r flag to deploy directories." #
)


def verify_exists(paths_to_sync: List[Path]):
for path in paths_to_sync:
if not path.exists():
raise ClickException(f"The following path does not exist: {path}")
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ def process(
self._apply_package_scripts()

# Upload files from deploy root local folder to the above stage
self.sync_deploy_root_with_stage(self.package_role)
self.sync_deploy_root_with_stage(
self.package_role, prune=True, recursive=True
)

# Warn if the version exists in a release directive(s)
existing_release_directives = (
Expand Down
27 changes: 24 additions & 3 deletions src/snowflake/cli/plugins/stage/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Set

from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.exceptions import (
SnowflakeSQLExecutionError,
)
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
from snowflake.connector.cursor import DictCursor

Expand Down Expand Up @@ -154,7 +156,26 @@ def build_md5_map(list_stage_cursor: DictCursor) -> Dict[str, str]:
}


def stage_diff(local_path: Path, stage_fqn: str) -> DiffResult:
def filter_from_diff(
diff: DiffResult, paths_to_sync: Set[str], prune: bool
) -> List[str]:
"""Modifies the given diff, keeping only the provided paths. If prune is false, remote-only paths will be empty and the non-removed paths will be returned."""
diff.different = [i for i in diff.different if i in paths_to_sync]
diff.only_local = [i for i in diff.only_local if i in paths_to_sync]
only_on_stage = [i for i in diff.only_on_stage if i in paths_to_sync]
files_not_removed = []
if prune:
diff.only_on_stage = only_on_stage
else:
files_not_removed = only_on_stage
diff.only_on_stage = []
return files_not_removed


def stage_diff(
local_path: Path,
stage_fqn: str,
) -> DiffResult:
"""
Diffs the files in a stage with a local folder.
"""
Expand Down
Loading
Loading