snowlib 0.4.0__tar.gz → 0.4.2__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.
- {snowlib-0.4.0 → snowlib-0.4.2}/PKG-INFO +1 -1
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/__init__.py +5 -1
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/context.py +3 -1
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/__init__.py +5 -1
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/schema.py +12 -1
- snowlib-0.4.2/src/snowlib/models/stage.py +178 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/__init__.py +2 -1
- snowlib-0.4.2/src/snowlib/models/table/table.py +425 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/result.py +23 -2
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/session.py +25 -5
- snowlib-0.4.2/src/snowlib/utils/schema.py +215 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/PKG-INFO +1 -1
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/SOURCES.txt +3 -1
- snowlib-0.4.0/src/snowlib/models/table/table.py +0 -272
- {snowlib-0.4.0 → snowlib-0.4.2}/LICENSE +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/README.md +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/licenses/THIRD_PARTY.md +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/pyproject.toml +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/setup.cfg +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/_data/connections.toml.example +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/_data/test_config.toml.example +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/__init__.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/base.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/connection.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/paths.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/profiles.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/__init__.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/core.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/fqn.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/schema_child.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/show.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/column.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/database.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/base.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/dynamic_table.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/materialized_view.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/view.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/__init__.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/async_query.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/execute.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/py.typed +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/sqlalchemy.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/utils/identifiers.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/utils/json_columns.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/utils/query.py +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/dependency_links.txt +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/requires.txt +0 -0
- {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/top_level.txt +0 -0
|
@@ -35,13 +35,15 @@ from snowlib.models import (
|
|
|
35
35
|
MaterializedView,
|
|
36
36
|
DynamicTable,
|
|
37
37
|
Column,
|
|
38
|
+
Stage,
|
|
39
|
+
StageObject,
|
|
38
40
|
Show,
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
# Session: Context-bound convenience layer
|
|
42
44
|
from snowlib.session import Session, create_session
|
|
43
45
|
|
|
44
|
-
__version__ = "0.4.
|
|
46
|
+
__version__ = "0.4.2"
|
|
45
47
|
__all__ = [
|
|
46
48
|
# Layer 1: Configuration & Connection
|
|
47
49
|
"load_profile",
|
|
@@ -65,6 +67,8 @@ __all__ = [
|
|
|
65
67
|
"MaterializedView",
|
|
66
68
|
"DynamicTable",
|
|
67
69
|
"Column",
|
|
70
|
+
"Stage",
|
|
71
|
+
"StageObject",
|
|
68
72
|
"Show",
|
|
69
73
|
# Session
|
|
70
74
|
"Session",
|
|
@@ -78,7 +78,9 @@ class SnowflakeContext:
|
|
|
78
78
|
for config_key, query, friendly_name in validations:
|
|
79
79
|
declared = cfg.get(config_key)
|
|
80
80
|
if declared:
|
|
81
|
-
|
|
81
|
+
assert self._cursor is not None
|
|
82
|
+
self._cursor.execute(query)
|
|
83
|
+
result = self._cursor.fetchone()
|
|
82
84
|
actual = result[0] if result and result[0] else None
|
|
83
85
|
|
|
84
86
|
if actual is None:
|
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from .database import Database
|
|
4
4
|
from .schema import Schema
|
|
5
|
-
from .table import Table, View, MaterializedView, DynamicTable, TableLike
|
|
5
|
+
from .table import Table, View, MaterializedView, DynamicTable, TableLike, WriteMethod
|
|
6
6
|
from .column import Column
|
|
7
|
+
from .stage import Stage, StageObject
|
|
7
8
|
from .base.show import Show
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"Database",
|
|
11
12
|
"Schema",
|
|
12
13
|
"Table",
|
|
14
|
+
"WriteMethod",
|
|
13
15
|
"View",
|
|
14
16
|
"MaterializedView",
|
|
15
17
|
"DynamicTable",
|
|
16
18
|
"Column",
|
|
17
19
|
"TableLike",
|
|
20
|
+
"Stage",
|
|
21
|
+
"StageObject",
|
|
18
22
|
"Show",
|
|
19
23
|
]
|
|
@@ -9,6 +9,7 @@ from .base import Container, FQN
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from .database import Database
|
|
11
11
|
from .table import Table, View
|
|
12
|
+
from .stage import Stage
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class Schema(Container):
|
|
@@ -89,6 +90,11 @@ class Schema(Container):
|
|
|
89
90
|
from .table import View
|
|
90
91
|
return View(self._database_name, self._schema_name, name, self._context)
|
|
91
92
|
|
|
93
|
+
def stage(self, name: str) -> 'Stage':
|
|
94
|
+
"""Get specific stage by name"""
|
|
95
|
+
from .stage import Stage
|
|
96
|
+
return Stage(self._database_name, self._schema_name, name, self._context)
|
|
97
|
+
|
|
92
98
|
def has_table(self, name: str) -> bool:
|
|
93
99
|
"""Check if table exists in schema"""
|
|
94
100
|
from .table import Table
|
|
@@ -97,4 +103,9 @@ class Schema(Container):
|
|
|
97
103
|
def has_view(self, name: str) -> bool:
|
|
98
104
|
"""Check if view exists in schema"""
|
|
99
105
|
from .table import View
|
|
100
|
-
return self._show_instance.exists(View, name, container=self)
|
|
106
|
+
return self._show_instance.exists(View, name, container=self)
|
|
107
|
+
|
|
108
|
+
def has_stage(self, name: str) -> bool:
|
|
109
|
+
"""Check if stage exists in schema"""
|
|
110
|
+
from .stage import Stage
|
|
111
|
+
return self._show_instance.exists(Stage, name, container=self)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Stage object class for managing Snowflake stages"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import ClassVar, Optional, Any, cast
|
|
9
|
+
|
|
10
|
+
from snowlib.context import SnowflakeContext
|
|
11
|
+
from snowlib.models.base import SchemaChild
|
|
12
|
+
from snowlib.primitives import execute_sql, query
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
HAS_TQDM = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
tqdm = None # type: ignore
|
|
19
|
+
HAS_TQDM = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class StageObject:
|
|
24
|
+
"""Represents a file or directory in a Snowflake stage"""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
size: int
|
|
28
|
+
md5: Optional[str]
|
|
29
|
+
last_modified: datetime
|
|
30
|
+
_stage: Stage
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def path(self) -> str:
|
|
34
|
+
"""Full path within the stage (e.g., @DB.SCHEMA.STAGE/path/to/file.csv)"""
|
|
35
|
+
return f"@{self._stage.fqn}/{self.name}"
|
|
36
|
+
|
|
37
|
+
def exists(self) -> bool:
|
|
38
|
+
"""Check if this object still exists in the stage"""
|
|
39
|
+
df = query(f"LIST {self.path}", self._stage._context)
|
|
40
|
+
return len(df) > 0
|
|
41
|
+
|
|
42
|
+
def delete(self) -> dict[str, Any]:
|
|
43
|
+
"""Delete this object from the stage, returns the result info"""
|
|
44
|
+
result = execute_sql(f"REMOVE {self.path}", self._stage._context)
|
|
45
|
+
df = result.to_df()
|
|
46
|
+
if len(df) > 0:
|
|
47
|
+
return cast(dict[str, Any], df.iloc[0].to_dict())
|
|
48
|
+
return {"status": "no result"}
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"StageObject({self.name!r}, size={self.size})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Stage(SchemaChild):
|
|
55
|
+
"""Represents a Snowflake internal named stage"""
|
|
56
|
+
|
|
57
|
+
SHOW_PLURAL: ClassVar[str] = "STAGES"
|
|
58
|
+
SHOW_NAME_COLUMN: ClassVar[str] = "name"
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
database: str,
|
|
63
|
+
schema: str,
|
|
64
|
+
name: str,
|
|
65
|
+
context: SnowflakeContext
|
|
66
|
+
):
|
|
67
|
+
"""Initialize stage object"""
|
|
68
|
+
super().__init__(database, schema, name, context)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def stage_path(self) -> str:
|
|
72
|
+
"""Stage path with @ prefix for use in SQL"""
|
|
73
|
+
return f"@{self.fqn}"
|
|
74
|
+
|
|
75
|
+
def create(self, if_not_exists: bool = True) -> Stage:
|
|
76
|
+
"""Create the stage in Snowflake"""
|
|
77
|
+
sql = "CREATE STAGE"
|
|
78
|
+
if if_not_exists:
|
|
79
|
+
sql += " IF NOT EXISTS"
|
|
80
|
+
sql += f" {self.fqn}"
|
|
81
|
+
execute_sql(sql, self._context)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def drop(self, if_exists: bool = False) -> None:
|
|
85
|
+
"""Drop the stage"""
|
|
86
|
+
sql = "DROP STAGE"
|
|
87
|
+
if if_exists:
|
|
88
|
+
sql += " IF EXISTS"
|
|
89
|
+
sql += f" {self.fqn}"
|
|
90
|
+
execute_sql(sql, self._context)
|
|
91
|
+
|
|
92
|
+
def list(self, pattern: Optional[str] = None) -> list[StageObject]:
|
|
93
|
+
"""List all objects in the stage"""
|
|
94
|
+
sql = f"LIST {self.stage_path}"
|
|
95
|
+
if pattern:
|
|
96
|
+
sql += f" PATTERN = '{pattern}'"
|
|
97
|
+
|
|
98
|
+
df = query(sql, self._context)
|
|
99
|
+
|
|
100
|
+
objects = []
|
|
101
|
+
for _, row in df.iterrows():
|
|
102
|
+
obj = StageObject(
|
|
103
|
+
name=row['name'].replace(self.stage_path + '/', ''),
|
|
104
|
+
size=row['size'],
|
|
105
|
+
md5=row.get('md5'),
|
|
106
|
+
last_modified=row['last_modified'],
|
|
107
|
+
_stage=self,
|
|
108
|
+
)
|
|
109
|
+
objects.append(obj)
|
|
110
|
+
|
|
111
|
+
return objects
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def objects(self) -> list[StageObject]:
|
|
115
|
+
"""Get all objects in the stage"""
|
|
116
|
+
return self.list()
|
|
117
|
+
|
|
118
|
+
def clear(self) -> list[dict[str, Any]]:
|
|
119
|
+
"""Remove all files from the stage"""
|
|
120
|
+
result = execute_sql(f"REMOVE {self.stage_path}", self._context)
|
|
121
|
+
df = result.to_df()
|
|
122
|
+
return cast(list[dict[str, Any]], df.to_dict('records')) if len(df) > 0 else []
|
|
123
|
+
|
|
124
|
+
def load(
|
|
125
|
+
self,
|
|
126
|
+
files: list[Path],
|
|
127
|
+
auto_compress: bool = True,
|
|
128
|
+
overwrite: bool = False,
|
|
129
|
+
show_progress: bool = True,
|
|
130
|
+
) -> list[dict[str, Any]]:
|
|
131
|
+
"""Upload local files to the stage with progress bar"""
|
|
132
|
+
results = []
|
|
133
|
+
|
|
134
|
+
iterator = tqdm(files, desc="Uploading") if (show_progress and HAS_TQDM and tqdm is not None) else files
|
|
135
|
+
|
|
136
|
+
for file_path in iterator:
|
|
137
|
+
if not file_path.exists():
|
|
138
|
+
results.append({
|
|
139
|
+
"source": str(file_path),
|
|
140
|
+
"status": "ERROR",
|
|
141
|
+
"message": "File not found",
|
|
142
|
+
})
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
sql = f"PUT 'file://{file_path.as_posix()}' {self.stage_path}"
|
|
146
|
+
sql += f" AUTO_COMPRESS = {str(auto_compress).upper()}"
|
|
147
|
+
sql += f" OVERWRITE = {str(overwrite).upper()}"
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
result = execute_sql(sql, self._context)
|
|
151
|
+
df = result.to_df()
|
|
152
|
+
if len(df) > 0:
|
|
153
|
+
row_dict = cast(dict[str, Any], df.iloc[0].to_dict())
|
|
154
|
+
row_dict["query_id"] = result.query_id
|
|
155
|
+
results.append(row_dict)
|
|
156
|
+
else:
|
|
157
|
+
results.append({
|
|
158
|
+
"source": str(file_path),
|
|
159
|
+
"status": "UNKNOWN",
|
|
160
|
+
"query_id": result.query_id,
|
|
161
|
+
})
|
|
162
|
+
except Exception as e:
|
|
163
|
+
results.append({
|
|
164
|
+
"source": str(file_path),
|
|
165
|
+
"status": "ERROR",
|
|
166
|
+
"message": str(e),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
# Print summary
|
|
170
|
+
uploaded = sum(1 for r in results if r.get("status") == "UPLOADED")
|
|
171
|
+
skipped = sum(1 for r in results if r.get("status") == "SKIPPED")
|
|
172
|
+
failed = sum(1 for r in results if r.get("status") in ("ERROR", "UNKNOWN"))
|
|
173
|
+
print(f"Upload complete: {uploaded} uploaded, {skipped} skipped, {failed} failed")
|
|
174
|
+
|
|
175
|
+
return results
|
|
176
|
+
|
|
177
|
+
def __repr__(self) -> str:
|
|
178
|
+
return f"Stage({self.fqn!r})"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Table-like Snowflake objects (tables, views, materialized views, dynamic tables)."""
|
|
2
2
|
|
|
3
3
|
from snowlib.models.table.base import TableLike
|
|
4
|
-
from snowlib.models.table.table import Table
|
|
4
|
+
from snowlib.models.table.table import Table, WriteMethod
|
|
5
5
|
from snowlib.models.table.view import View
|
|
6
6
|
from snowlib.models.table.materialized_view import MaterializedView
|
|
7
7
|
from snowlib.models.table.dynamic_table import DynamicTable
|
|
@@ -9,6 +9,7 @@ from snowlib.models.table.dynamic_table import DynamicTable
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"TableLike",
|
|
11
11
|
"Table",
|
|
12
|
+
"WriteMethod",
|
|
12
13
|
"View",
|
|
13
14
|
"MaterializedView",
|
|
14
15
|
"DynamicTable",
|