zbq 0.1.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.
zbq/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .main import BigQueryHandler
2
+
3
+ zclient = BigQueryHandler()
4
+
5
+ __all__ = ["zclient"]
zbq/main.py ADDED
@@ -0,0 +1,207 @@
1
+ from polars.exceptions import PolarsError
2
+ from google.cloud import bigquery
3
+ from google.auth.exceptions import DefaultCredentialsError
4
+ from google.auth import default as get_google_credentials
5
+ import polars as pl
6
+ import tempfile
7
+ import os
8
+ import subprocess
9
+
10
+
11
+ class BigQueryHandler:
12
+ def __init__(self, project_id: str = ""):
13
+ self._project_id = project_id.strip() or self._get_default_project()
14
+ self._client = None # Lazy init
15
+
16
+ def _get_default_project(self) -> str:
17
+ try:
18
+ result = subprocess.run(
19
+ ["gcloud", "config", "get", "project"],
20
+ capture_output=True,
21
+ text=True,
22
+ check=True,
23
+ )
24
+ project = result.stdout.strip()
25
+ if project:
26
+ return project
27
+ except (subprocess.CalledProcessError, FileNotFoundError):
28
+ pass
29
+
30
+ # Fallback to environment
31
+ return os.environ.get("GOOGLE_CLOUD_PROJECT", "").strip()
32
+
33
+ def _check_adc(self) -> bool:
34
+ try:
35
+ get_google_credentials()
36
+ return True
37
+ except DefaultCredentialsError:
38
+ return False
39
+
40
+ def _init_client(self):
41
+ if not self._check_adc():
42
+ raise RuntimeError(
43
+ "No Google Cloud credentials found. Run:\n"
44
+ " gcloud auth application-default login\n"
45
+ "Or set the GOOGLE_APPLICATION_CREDENTIALS environment variable."
46
+ )
47
+ if not self.project_id:
48
+ raise RuntimeError(
49
+ "No GCP project found. Set one via:\n"
50
+ " gcloud config set project YOUR_PROJECT_ID\n"
51
+ "Or set manually: zclient.project_id = 'your-project'"
52
+ )
53
+ self._client = bigquery.Client(project=self.project_id)
54
+
55
+ @property
56
+ def client(self):
57
+ if self._client is None:
58
+ self._init_client()
59
+ return self._client
60
+
61
+ def _close_client(self):
62
+ if self._client:
63
+ self._client.close()
64
+ self._client = None
65
+
66
+ @property
67
+ def project_id(self):
68
+ return self._project_id
69
+
70
+ @project_id.setter
71
+ def project_id(self, id: str):
72
+ if not isinstance(id, str):
73
+ raise ValueError("Project ID must be a string")
74
+ if id != self._project_id:
75
+ self._project_id = id
76
+ self._close_client()
77
+
78
+ def validate(self):
79
+ """Optional helper: raise if ADC or project_id not set"""
80
+ if not self._check_adc():
81
+ raise RuntimeError("Missing ADC. Run: gcloud auth application-default login")
82
+ if not self.project_id:
83
+ raise RuntimeError("Project ID not set.")
84
+
85
+ def _full_table_path(self, dataset: str, table: str) -> str:
86
+ if not isinstance(dataset, str) or not isinstance(table, str):
87
+ raise ValueError("Dataset and table must be strings")
88
+ return f"{self.project_id}.{dataset}.{table}"
89
+
90
+ def bq(
91
+ self,
92
+ action: str,
93
+ df: pl.DataFrame | None = None,
94
+ dataset: str = "",
95
+ table: str = "",
96
+ query: str = "",
97
+ write_type: str = "WRITE_APPEND",
98
+ warning: bool = True,
99
+ create_if_needed: bool = True
100
+ ):
101
+ """
102
+ Handles CRUD-style operations with BigQuery via a unified interface.
103
+
104
+ Args:
105
+ action (str): One of {"read", "write", "insert", "delete"}.
106
+ df (pl.DataFrame, optional): Polars DataFrame to write to BigQuery. Required for "write".
107
+ dataset (str, optional): Dataset name. Required for "write".
108
+ table (str, optional): Table name. Required for "write".
109
+ query (str, optional): SQL query string. Required for "read", "insert", and "delete".
110
+ write_type (str, optional): "WRITE_APPEND" or "WRITE_TRUNCATE". Default is "WRITE_APPEND".
111
+ warning (bool, optional): Whether to prompt before truncating a table. Default is True.
112
+ create_if_needed (bool, optional): Whether to create the table if it doesn't exist. Default is True.
113
+
114
+ Returns:
115
+ pl.DataFrame or str: A Polars DataFrame for "read", or a job state string for "write".
116
+
117
+ Raises:
118
+ ValueError: If required arguments are missing based on the action.
119
+ RuntimeError: If authentication or project configuration is missing.
120
+ """
121
+
122
+ match action.lower():
123
+ case "read" | "insert" | "delete":
124
+ if query:
125
+ return self._query(query)
126
+ else:
127
+ raise ValueError("Query is empty.")
128
+ case "write":
129
+ self._check_requirements(df, dataset, table)
130
+ return self._write(df, dataset, table, write_type, warning, create_if_needed)
131
+
132
+ def _check_requirements(self, **kwargs):
133
+ missing = [k for k, v in kwargs.items() if not v]
134
+ if missing:
135
+ raise ValueError(f"Missing required argument(s): {', '.join(missing)}")
136
+
137
+ def _query(self, query: str) -> pl.DataFrame:
138
+ try:
139
+ query_job = self.client.query(query)
140
+ rows = query_job.result().to_arrow(progress_bar_type="tqdm")
141
+ df = pl.from_arrow(rows)
142
+ except PolarsError as e:
143
+ print(f"PanicException: {e}")
144
+ print("Retrying with Pandas DF")
145
+ query_job = self.client.query(query)
146
+ df = query_job.result().to_dataframe(progress_bar_type="tqdm")
147
+ df = pl.from_pandas(df)
148
+ return df
149
+
150
+ def _write(
151
+ self,
152
+ df: pl.DataFrame,
153
+ dataset: str,
154
+ table: str,
155
+ write_type: str = "WRITE_APPEND",
156
+ warning: bool = True,
157
+ create_if_needed: bool = True
158
+ ):
159
+ destination = self._full_table_path(dataset, table)
160
+ temp_file = tempfile.NamedTemporaryFile(suffix=".parquet", delete=False)
161
+ temp_file_path = temp_file.name
162
+ temp_file.close()
163
+
164
+ try:
165
+ df.write_parquet(temp_file_path)
166
+
167
+ if write_type == "WRITE_TRUNCATE" and warning:
168
+ user_warning = input(
169
+ "You are about to overwrite a table. Continue? (y/n): "
170
+ )
171
+ if user_warning.lower() != "y":
172
+ return
173
+
174
+ write_disp = (
175
+ bigquery.WriteDisposition.WRITE_TRUNCATE
176
+ if write_type == "WRITE_TRUNCATE"
177
+ else bigquery.WriteDisposition.WRITE_APPEND
178
+ )
179
+
180
+ create_disp = (
181
+ bigquery.CreateDisposition.CREATE_IF_NEEDED
182
+ if create_if_needed
183
+ else bigquery.CreateDisposition.CREATE_NEVER
184
+ )
185
+
186
+ with open(temp_file_path, "rb") as source_file:
187
+ job = self.client.load_table_from_file(
188
+ source_file,
189
+ destination=destination,
190
+ project=self.project_id,
191
+ job_config=bigquery.LoadJobConfig(
192
+ source_format=bigquery.SourceFormat.PARQUET,
193
+ write_disposition=write_disp,
194
+ create_disposition=create_disp,
195
+ ),
196
+ )
197
+ return job.result().state
198
+
199
+ finally:
200
+ if os.path.exists(temp_file_path):
201
+ os.remove(temp_file_path)
202
+
203
+ def __enter__(self):
204
+ return self
205
+
206
+ def __exit__(self, exc_type, exc_val, exc_tb):
207
+ self._close_client()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: zbq
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: FrocketGaming <frocketgames@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: google-cloud-bigquery-storage>=2.32.0
8
+ Requires-Dist: google-cloud-bigquery>=3.34.0
9
+ Requires-Dist: google-cloud-storage>=3.1.1
10
+ Requires-Dist: polars>=1.31.0
@@ -0,0 +1,6 @@
1
+ zbq/__init__.py,sha256=tLyz-DxRq3JBU3DcdbsBUDFJvg1qhGFPK1uo4MuJjFs,86
2
+ zbq/main.py,sha256=rHa1jYyw6cMHKkeM64Zlb7LS6A1dF7rYcoqR_OR6i3w,7450
3
+ zbq-0.1.0.dist-info/METADATA,sha256=6qxg_iQfhjCjlZFQOY2j00sBplPQ6V9RlyhiU32Efa4,330
4
+ zbq-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ zbq-0.1.0.dist-info/entry_points.txt,sha256=y_vRj7JgXz5hp-vBa6AKyFRDroBUxagX4m9mnkrM_s8,33
6
+ zbq-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zbq = zbq:main