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.
Files changed (48) hide show
  1. {snowlib-0.4.0 → snowlib-0.4.2}/PKG-INFO +1 -1
  2. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/__init__.py +5 -1
  3. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/context.py +3 -1
  4. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/__init__.py +5 -1
  5. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/schema.py +12 -1
  6. snowlib-0.4.2/src/snowlib/models/stage.py +178 -0
  7. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/__init__.py +2 -1
  8. snowlib-0.4.2/src/snowlib/models/table/table.py +425 -0
  9. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/result.py +23 -2
  10. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/session.py +25 -5
  11. snowlib-0.4.2/src/snowlib/utils/schema.py +215 -0
  12. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/PKG-INFO +1 -1
  13. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/SOURCES.txt +3 -1
  14. snowlib-0.4.0/src/snowlib/models/table/table.py +0 -272
  15. {snowlib-0.4.0 → snowlib-0.4.2}/LICENSE +0 -0
  16. {snowlib-0.4.0 → snowlib-0.4.2}/README.md +0 -0
  17. {snowlib-0.4.0 → snowlib-0.4.2}/licenses/THIRD_PARTY.md +0 -0
  18. {snowlib-0.4.0 → snowlib-0.4.2}/pyproject.toml +0 -0
  19. {snowlib-0.4.0 → snowlib-0.4.2}/setup.cfg +0 -0
  20. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/_data/connections.toml.example +0 -0
  21. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/_data/test_config.toml.example +0 -0
  22. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/__init__.py +0 -0
  23. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/base.py +0 -0
  24. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/connection.py +0 -0
  25. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/paths.py +0 -0
  26. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/connection/profiles.py +0 -0
  27. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/__init__.py +0 -0
  28. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/core.py +0 -0
  29. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/fqn.py +0 -0
  30. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/schema_child.py +0 -0
  31. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/base/show.py +0 -0
  32. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/column.py +0 -0
  33. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/database.py +0 -0
  34. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/base.py +0 -0
  35. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/dynamic_table.py +0 -0
  36. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/materialized_view.py +0 -0
  37. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/models/table/view.py +0 -0
  38. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/__init__.py +0 -0
  39. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/async_query.py +0 -0
  40. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/primitives/execute.py +0 -0
  41. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/py.typed +0 -0
  42. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/sqlalchemy.py +0 -0
  43. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/utils/identifiers.py +0 -0
  44. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/utils/json_columns.py +0 -0
  45. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib/utils/query.py +0 -0
  46. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/dependency_links.txt +0 -0
  47. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/requires.txt +0 -0
  48. {snowlib-0.4.0 → snowlib-0.4.2}/src/snowlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowlib
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Python-Snowflake utilities
5
5
  Author: Rashad Aziz
6
6
  Maintainer: Rashad Aziz
@@ -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.0"
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
- result = self._cursor.execute(query).fetchone()
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",