SCUDO 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.
cudo/__init__.py ADDED
File without changes
cudo/doris/__init__.py ADDED
File without changes
cudo/doris/client.py ADDED
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ '''
5
+ # Doris Flight SQL Client (ADBC)
6
+
7
+ Python client for interacting with Apache Doris using Arrow Flight SQL via ADBC.
8
+
9
+ This class provides:
10
+ - Simple connection handling
11
+ - Query execution
12
+ - Optimized DataFrame fetching (`fetch_df`)
13
+ - Optional Arrow-based and partition-based methods
14
+ - Timing and logging utilities
15
+
16
+ ## Requirements
17
+
18
+ ```bash
19
+ pip install adbc_driver_manager adbc_driver_flightsql pandas
20
+ ```
21
+
22
+ ## Example
23
+ client = DorisFlightSQLClient(
24
+ uri="grpc://127.0.0.1:9090",
25
+ username="root",
26
+ password=""
27
+ )
28
+
29
+ df = client.query("SELECT * FROM my_table LIMIT 1000;")
30
+ print(df.head())
31
+
32
+ client.close()
33
+ '''
34
+
35
+ import adbc_driver_manager
36
+ import adbc_driver_flightsql.dbapi as flight_sql
37
+ import pandas as pd
38
+ from datetime import datetime
39
+ from typing import Optional
40
+
41
+
42
+
43
+ class DorisFlightSQLClient:
44
+ '''
45
+ Apache Doris Flight SQL client using ADBC (DB-API 2.0).
46
+ This client wraps the ADBC Flight SQL driver and provides optimized
47
+ methods for querying data into pandas DataFrames.
48
+
49
+ Parameters
50
+ ----------
51
+ uri : str
52
+ Flight SQL endpoint (e.g., "grpc://127.0.0.1:9090").
53
+
54
+ username : str
55
+ Database username.
56
+
57
+ password : str
58
+ Database password.
59
+
60
+ Attributes
61
+ ----------
62
+ conn : flight_sql.Connection
63
+ Active database connection.
64
+
65
+ cursor : flight_sql.Cursor
66
+ Cursor used for executing queries.
67
+
68
+ Notes
69
+ -----
70
+ - Uses `fetch_df()` by default for best performance.
71
+ - Compatible with Apache Doris Arrow Flight SQL service.
72
+ '''
73
+
74
+ def __init__(self, uri: str, username: str, password: str) -> None:
75
+ self.uri = uri
76
+ self.db_kwargs = {
77
+ adbc_driver_manager.DatabaseOptions.USERNAME.value: username,
78
+ adbc_driver_manager.DatabaseOptions.PASSWORD.value: password,
79
+ }
80
+
81
+ self.conn: Optional[flight_sql.Connection] = None
82
+ self.cursor: Optional[flight_sql.Cursor] = None
83
+
84
+ self._connect()
85
+
86
+
87
+ # ------------------------------------------------------------------
88
+ # Connection Handling
89
+ # ------------------------------------------------------------------
90
+
91
+ def _connect(self) -> None:
92
+ '''
93
+ Establish a connection and create a cursor.
94
+ '''
95
+ try:
96
+ self.conn = flight_sql.connect(uri=self.uri, db_kwargs=self.db_kwargs)
97
+ self.cursor = self.conn.cursor()
98
+ except Exception as e:
99
+ raise RuntimeError("Connection failed") from e
100
+
101
+ def close(self) -> None:
102
+ '''
103
+ Close cursor and connection.
104
+ '''
105
+ try:
106
+ if self.cursor:
107
+ self.cursor.close()
108
+ finally:
109
+ if self.conn:
110
+ self.conn.close()
111
+
112
+
113
+ # ------------------------------------------------------------------
114
+ # Core Query Method
115
+ # ------------------------------------------------------------------
116
+
117
+ def query(self, sql: str, params: dict | None = None) -> pd.DataFrame:
118
+ '''
119
+ Execute a SQL query and return a pandas DataFrame.
120
+
121
+ This method uses `fetch_df()` (ADBC optimized path),
122
+ which is faster than converting from Arrow manually.
123
+
124
+ Parameters
125
+ ----------
126
+ sql : str
127
+ SQL query string.
128
+
129
+ params : dict, optional
130
+ Parameters to safely inject into SQL.
131
+
132
+ Returns
133
+ -------
134
+ pandas.DataFrame
135
+ Query result as a DataFrame.
136
+
137
+ Example
138
+ -------
139
+ >>> df = client.query("SELECT * FROM table LIMIT 100;")
140
+
141
+ '''
142
+ if not self.cursor:
143
+ raise RuntimeError("Cursor is not initialized.")
144
+
145
+ sql = self._render_sql(sql, params=params)
146
+
147
+ start_time = datetime.now()
148
+
149
+ try:
150
+ self.cursor.execute(sql)
151
+ df = self.cursor.fetch_df()
152
+ except Exception as e:
153
+ raise RuntimeError("Query execution failed") from e
154
+
155
+ duration = datetime.now() - start_time
156
+
157
+ print("\n##################")
158
+ print(f"Query executed in: {duration}")
159
+ print(df.info(memory_usage="deep"))
160
+
161
+ return df
162
+
163
+
164
+ # ------------------------------------------------------------------
165
+ # Alternative Methods
166
+ # ------------------------------------------------------------------
167
+
168
+ def query_arrow(self, sql: str, params: dict | None = None) -> pd.DataFrame:
169
+ '''
170
+ Execute query using Arrow fetch, then convert to pandas.
171
+
172
+ Slower than `query()` but useful for debugging or Arrow workflows.
173
+
174
+ Parameters
175
+ ----------
176
+ sql : str
177
+ SQL query.
178
+
179
+ params : dict, optional
180
+ Parameters to safely inject into SQL.
181
+
182
+ Returns
183
+ -------
184
+ pandas.DataFrame
185
+ '''
186
+ sql = self._render_sql(sql, params=params)
187
+
188
+ start_time = datetime.now()
189
+
190
+ try:
191
+ self.cursor.execute(sql)
192
+ arrow_data = self.cursor.fetchallarrow()
193
+ df = arrow_data.to_pandas()
194
+ except Exception as e:
195
+ raise RuntimeError("Arrow query execution failed") from e
196
+
197
+ duration = datetime.now() - start_time
198
+
199
+ print("\n##################")
200
+ print(f"Arrow fetch in: {duration}, bytes: {arrow_data.nbytes}")
201
+ print(df.info(memory_usage="deep"))
202
+
203
+ return df
204
+
205
+
206
+ def query_partition(self, sql: str, params: dict | None = None) -> pd.DataFrame:
207
+ '''
208
+ Execute query using partitioned reads.
209
+
210
+ Useful for parallel processing of large datasets.
211
+
212
+ Parameters
213
+ ----------
214
+ sql : str
215
+ SQL query.
216
+
217
+ params : dict, optional
218
+ Parameters to safely inject into SQL.
219
+
220
+ Returns
221
+ -------
222
+ pandas.DataFrame
223
+ '''
224
+ sql = self._render_sql(sql, params=params)
225
+
226
+ start_time = datetime.now()
227
+
228
+ try:
229
+ partitions, schema = self.cursor.adbc_execute_partitions(sql)
230
+ # Example: read first partition (extendable to parallel processing)
231
+ self.cursor.adbc_read_partition(partitions[0])
232
+ arrow_data = self.cursor.fetchallarrow()
233
+ df = arrow_data.to_pandas()
234
+ except Exception as e:
235
+ raise RuntimeError("Partition query execution failed") from e
236
+
237
+ duration = datetime.now() - start_time
238
+
239
+ print("\n##################")
240
+ print(f"Partitions: {len(partitions)}, time: {duration}")
241
+ print(df.info(memory_usage="deep"))
242
+
243
+ return df
244
+
245
+
246
+ # ------------------------------------------------------------------
247
+ # Utility Methods
248
+ # ------------------------------------------------------------------
249
+
250
+ def _render_sql(self, sql: str, params: dict | None = None) -> str:
251
+ '''
252
+ Render SQL using simple parameter substitution.
253
+
254
+ Parameters
255
+ ----------
256
+ sql : str
257
+ SQL query with `{param}` placeholders.
258
+
259
+ params : dict, optional
260
+ Dictionary of parameters.
261
+
262
+ Returns
263
+ -------
264
+ str
265
+ Rendered SQL string.
266
+ '''
267
+ if params:
268
+ safe_params = {}
269
+ for k, v in params.items():
270
+ if isinstance(v, (int, float)):
271
+ safe_params[k] = v
272
+ elif isinstance(v, str):
273
+ safe_params[k] = "'" + v.replace("'", "''") + "'"
274
+ else:
275
+ raise ValueError(f"Unsafe parameter type for {k}: {type(v)}")
276
+ try:
277
+ return sql.format(**safe_params)
278
+ except KeyError as e:
279
+ raise ValueError(f"Missing SQL parameter: {e}") from e
280
+
281
+
282
+ def execute(self, sql: str) -> None:
283
+ '''
284
+ Execute a SQL statement without returning results.
285
+
286
+ Useful for DDL or INSERT operations.
287
+
288
+ Parameters
289
+ ----------
290
+ sql : str
291
+ '''
292
+ if not self.cursor:
293
+ raise RuntimeError("Cursor is not initialized.")
294
+
295
+ print(f"\n### Executing SQL ###\n{sql}")
296
+ self.cursor.execute(sql)
297
+
298
+
299
+ def __enter__(self):
300
+ ''' Context manager entry.'''
301
+ return self
302
+
303
+
304
+ def __exit__(self, exc_type, exc_val, exc_tb):
305
+ ''' Context manager exit (auto-close).'''
306
+ if exc_type:
307
+ print(f"[DorisFlightSQLClient] Exception: {exc_val}")
308
+ self.close()
309
+ return False # always propagate errors
310
+
cudo/doris/pymysql.py ADDED
@@ -0,0 +1,380 @@
1
+ '''
2
+ Doris Client Module
3
+ ==============================
4
+
5
+
6
+ This module provides a lightweight client for interacting with **Apache Doris**
7
+ using a MySQL-compatible interface.
8
+
9
+ It encapsulates common operations required for data loading workflows:
10
+ - Establishing a connection to Doris
11
+ - Submitting LOAD jobs
12
+ - Monitoring job execution status
13
+ - Waiting for job completion
14
+
15
+ The implementation is designed to be simple, reusable, and easily integrable
16
+ into ETL/ELT pipelines.
17
+
18
+ Main Features
19
+ -------------
20
+
21
+ - Connection management using `pymysql`
22
+ - Load job submission via SQL
23
+ - Polling-based job monitoring
24
+ - Optional logging integration
25
+
26
+ Example Usage
27
+ -------------
28
+
29
+ ```python
30
+ client = DorisClient(config, logger)
31
+
32
+ client.submit_load("my_db", load_sql)
33
+ client.wait_for_load("my_label")
34
+ ```
35
+
36
+ '''
37
+
38
+ import time
39
+ import pymysql
40
+
41
+
42
+ class DorisPyMySQLClient:
43
+ '''
44
+ Client for interacting with Apache Doris.
45
+
46
+ This class provides methods to execute and monitor data load operations
47
+ in Apache Doris using its MySQL-compatible interface.
48
+
49
+ Args:
50
+ config (dict):
51
+ Configuration dictionary containing connection parameters:
52
+ - host (str): Doris FE host
53
+ - port (int): Doris FE MySQL port
54
+ - user (str): Username
55
+ - password (str): Password
56
+ - autocommit (bool): Autocommit flag
57
+
58
+ logger (logging.Logger, optional):
59
+ Logger instance for logging execution details.
60
+
61
+ Attributes:
62
+ conn (pymysql.Connection):
63
+ Active database connection.
64
+ '''
65
+ def __init__(self, config, logger=None):
66
+ self.logger = logger
67
+ self.conn = self._connect(config)
68
+
69
+ def _connect(self, config):
70
+ '''
71
+ Establish a connection to Apache Doris.
72
+
73
+ Args:
74
+ config (dict):
75
+ Dictionary containing connection parameters.
76
+
77
+ Returns:
78
+ pymysql.Connection:
79
+ Active connection to Doris.
80
+
81
+ Raises:
82
+ RuntimeError:
83
+ If the connection cannot be established.
84
+ '''
85
+ try:
86
+ conn = pymysql.connect(
87
+ host=config["host"],
88
+ port=config["port"],
89
+ user=config["user"],
90
+ password=config["password"],
91
+ autocommit=config["autocommit"]
92
+ )
93
+ return conn
94
+
95
+ except Exception as e:
96
+ if self.logger:
97
+ self.logger.error("Connection failed: %s", str(e))
98
+ raise RuntimeError("Module failed")
99
+
100
+ def close(self):
101
+ '''
102
+ Close Doris connection.
103
+ '''
104
+ if self.conn:
105
+ self.conn.close()
106
+
107
+ def submit_load(self, db_name, sql) -> None:
108
+ '''
109
+ Submit a LOAD job to Apache Doris.
110
+
111
+ This method switches to the target database and executes the provided
112
+ LOAD SQL statement.
113
+
114
+ Args:
115
+ db_name (str):
116
+ Target database name.
117
+
118
+ sql (str):
119
+ Full Doris LOAD SQL statement.
120
+
121
+ Returns:
122
+ None
123
+
124
+ Raises:
125
+ RuntimeError:
126
+ If the SQL execution fails.
127
+ '''
128
+ try:
129
+ with self.conn.cursor() as cur:
130
+ cur.execute(f"USE {db_name}")
131
+ cur.execute(sql)
132
+
133
+ if self.logger:
134
+ self.logger.info("Load submitted successfully")
135
+
136
+ except Exception as e:
137
+ if self.logger:
138
+ self.logger.error("Submit load failed: %s", str(e))
139
+ raise RuntimeError("Module failed")
140
+
141
+ def check_load_status(self, label) -> str:
142
+ '''
143
+ Check the status of a Doris LOAD job.
144
+
145
+ Args:
146
+ label (str):
147
+ Unique label identifying the load job.
148
+
149
+ Returns:
150
+ str | None:
151
+ - Job state (e.g., ``FINISHED``, ``CANCELLED``)
152
+ - ``None`` if the job is not yet visible
153
+
154
+ Raises:
155
+ RuntimeError:
156
+ If the query execution fails.
157
+ '''
158
+ try:
159
+ with self.conn.cursor() as cur:
160
+ cur.execute(f"SHOW LOAD WHERE Label = '{label}'")
161
+ result = cur.fetchone()
162
+
163
+ if not result:
164
+ return None
165
+
166
+ return result[2] # state column
167
+
168
+ except Exception as e:
169
+ if self.logger:
170
+ self.logger.error("Check load status failed: %s", str(e))
171
+ raise RuntimeError("Module failed")
172
+
173
+ def wait_for_load(self, label) -> None:
174
+ '''
175
+ Wait until a Doris LOAD job completes.
176
+
177
+ This method continuously polls the job status until:
178
+ - The job finishes successfully (``FINISHED``)
179
+ - The job fails (``CANCELLED``)
180
+
181
+ Args:
182
+ label (str):
183
+ Load job label.
184
+
185
+ Returns:
186
+ None
187
+
188
+ Raises:
189
+ RuntimeError:
190
+ If the load job fails or is cancelled.
191
+ '''
192
+ try:
193
+ while True:
194
+ state = self.check_load_status(label)
195
+
196
+ if state is None:
197
+ if self.logger:
198
+ self.logger.info("Load job not visible yet...")
199
+
200
+ elif state == "FINISHED":
201
+ if self.logger:
202
+ self.logger.info("Load job finished successfully")
203
+ return
204
+
205
+ elif state == "CANCELLED":
206
+ if self.logger:
207
+ self.logger.error("Doris load job failed")
208
+ raise RuntimeError("Doris load job failed")
209
+
210
+ else:
211
+ if self.logger:
212
+ self.logger.info(f"Current state: {state}")
213
+
214
+ time.sleep(5)
215
+
216
+ except Exception as e:
217
+ if self.logger:
218
+ self.logger.error("Wait for load failed: %s", str(e))
219
+ raise RuntimeError("Module failed")
220
+
221
+
222
+
223
+ # import time
224
+ # import pymysql
225
+
226
+
227
+ # def connect_database(config, logger=None):
228
+ # '''
229
+ # Create a database connection to Doris
230
+ # '''
231
+
232
+ # try:
233
+ # conn = pymysql.connect(
234
+ # host=config["host"],
235
+ # port=config["port"],
236
+ # user=config["user"],
237
+ # password=config["password"],
238
+ # autocommit=config["autocommit"]
239
+ # )
240
+ # return conn
241
+
242
+ # except Exception as e:
243
+ # if logger: logger.error("module failed: %s", str(e))
244
+ # raise RuntimeError("Module failed")
245
+
246
+ # # to call:
247
+ # # wait_for_parquet(client,args.bucket, args.parquet_prefix )
248
+ # #
249
+ # # def wait_for_parquet(client, bucket, prefix, timeout=600):
250
+
251
+ # # start = time.time()
252
+
253
+ # # while True:
254
+
255
+ # # objects = list(client.list_objects(bucket, prefix=prefix, recursive=True))
256
+ # # parquet_files = [o.object_name for o in objects if o.object_name.endswith(".parquet")]
257
+
258
+ # # if parquet_files:
259
+ # # print(f"Found {len(parquet_files)} parquet files")
260
+ # # return parquet_files
261
+
262
+ # # if time.time() - start > timeout:
263
+ # # raise RuntimeError("Timeout waiting for parquet files")
264
+
265
+ # # print("No parquet files yet, sleeping 10s...")
266
+ # # time.sleep(10)
267
+
268
+
269
+ # def submit_load(conn, db_name, sql, logger=None):
270
+ # '''
271
+ # Submit a LOAD job to Apache Doris.
272
+
273
+ # Args:
274
+ # conn (pymysql.Connection):
275
+ # Active MySQL-compatible connection to Doris.
276
+
277
+ # db_name (str):
278
+ # Target database name.
279
+
280
+ # sql (str):
281
+ # Complete Doris LOAD SQL statement.
282
+
283
+ # Returns:
284
+ # None
285
+
286
+ # Raises:
287
+ # pymysql.MySQLError:
288
+ # If the SQL execution fails.
289
+ # '''
290
+
291
+ # try:
292
+ # with conn.cursor() as cur:
293
+ # cur.execute(f"USE {db_name}")
294
+ # cur.execute(sql)
295
+
296
+ # except Exception as e:
297
+ # if logger: logger.error("module failed: %s", str(e))
298
+ # raise RuntimeError("Module failed")
299
+
300
+
301
+ # def check_load_status(conn, label, logger=None):
302
+ # '''
303
+ # Retrieve the current state of a Doris LOAD job.
304
+
305
+ # Args:
306
+ # conn (pymysql.Connection):
307
+ # Active MySQL-compatible connection to Doris.
308
+
309
+ # label (str):
310
+ # Load job label used to identify the job.
311
+
312
+ # Returns:
313
+ # str | None:
314
+ # Current state of the job (e.g. ``FINISHED`` or ``CANCELLED``),
315
+ # or ``None`` if the job is not yet visible.
316
+ # '''
317
+
318
+ # try:
319
+ # with conn.cursor() as cur:
320
+ # cur.execute(f"SHOW LOAD WHERE Label = '{label}'")
321
+ # result = cur.fetchone()
322
+
323
+ # if not result:
324
+ # return None
325
+
326
+ # return result[2] # state column
327
+
328
+ # except Exception as e:
329
+ # if logger: logger.error("module failed: %s", str(e))
330
+ # raise RuntimeError("Module failed")
331
+
332
+
333
+
334
+ # def wait_for_load(conn, label, logger=None):
335
+ # '''
336
+ # Wait until a Doris LOAD job finishes.
337
+
338
+ # This function repeatedly polls the job status until the load
339
+ # finishes successfully or fails.
340
+
341
+ # Args:
342
+ # conn (pymysql.Connection):
343
+ # Active MySQL-compatible connection to Doris.
344
+
345
+ # label (str):
346
+ # Label identifying the load job.
347
+
348
+ # logger (logging.Logger, optional):
349
+ # Logger used to report progress.
350
+
351
+ # Returns:
352
+ # None
353
+
354
+ # Raises:
355
+ # RuntimeError:
356
+ # If the load job is cancelled or fails.
357
+ # '''
358
+
359
+ # try:
360
+
361
+ # while True:
362
+
363
+ # state = check_load_status(conn, label)
364
+
365
+ # if state is None:
366
+ # if logger: logger.info("load job not visible yet...")
367
+ # elif state == "FINISHED":
368
+ # if logger: logger.info("load job finished successfully")
369
+ # return
370
+ # elif state == "CANCELLED":
371
+ # if logger: logger.error("Doris load job failed")
372
+ # raise RuntimeError("Doris load job failed")
373
+ # else:
374
+ # if logger: logger.info(f"Current state: {state}")
375
+
376
+ # time.sleep(5)
377
+
378
+ # except Exception as e:
379
+ # if logger: logger.error("module failed: %s", str(e))
380
+ # raise RuntimeError("Module failed")
cudo/minio/__init__.py ADDED
File without changes