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 +5 -0
- zbq/main.py +207 -0
- zbq-0.1.0.dist-info/METADATA +10 -0
- zbq-0.1.0.dist-info/RECORD +6 -0
- zbq-0.1.0.dist-info/WHEEL +4 -0
- zbq-0.1.0.dist-info/entry_points.txt +2 -0
zbq/__init__.py
ADDED
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,,
|