velocity-python 0.0.66__py3-none-any.whl → 0.0.68__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.
- velocity/__init__.py +1 -1
- velocity/db/core/transaction.py +1 -0
- velocity/db/servers/postgres/__init__.py +9 -9
- velocity/db/servers/postgres/sql.py +139 -90
- velocity/db/servers/tablehelper.py +45 -14
- velocity/misc/tools.py +39 -0
- {velocity_python-0.0.66.dist-info → velocity_python-0.0.68.dist-info}/METADATA +1 -1
- {velocity_python-0.0.66.dist-info → velocity_python-0.0.68.dist-info}/RECORD +11 -10
- {velocity_python-0.0.66.dist-info → velocity_python-0.0.68.dist-info}/WHEEL +1 -1
- {velocity_python-0.0.66.dist-info → velocity_python-0.0.68.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.66.dist-info → velocity_python-0.0.68.dist-info}/top_level.txt +0 -0
velocity/__init__.py
CHANGED
velocity/db/core/transaction.py
CHANGED
|
@@ -3,17 +3,17 @@ import psycopg2
|
|
|
3
3
|
from .sql import SQL
|
|
4
4
|
from velocity.db.core import engine
|
|
5
5
|
|
|
6
|
-
default_config = {
|
|
7
|
-
"database": os.environ["DBDatabase"],
|
|
8
|
-
"host": os.environ["DBHost"],
|
|
9
|
-
"port": os.environ["DBPort"],
|
|
10
|
-
"user": os.environ["DBUser"],
|
|
11
|
-
"password": os.environ["DBPassword"],
|
|
12
|
-
}
|
|
13
|
-
|
|
14
6
|
|
|
15
7
|
def initialize(config=None, **kwargs):
|
|
16
8
|
if not config:
|
|
17
|
-
config
|
|
9
|
+
# Keep the default config inside this function.
|
|
10
|
+
config = {
|
|
11
|
+
"database": os.environ["DBDatabase"],
|
|
12
|
+
"host": os.environ["DBHost"],
|
|
13
|
+
"port": os.environ["DBPort"],
|
|
14
|
+
"user": os.environ["DBUser"],
|
|
15
|
+
"password": os.environ["DBPassword"],
|
|
16
|
+
}
|
|
18
17
|
config.update(kwargs)
|
|
18
|
+
print(config)
|
|
19
19
|
return engine.Engine(psycopg2, config, SQL)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import hashlib
|
|
3
3
|
import sqlparse
|
|
4
|
-
|
|
4
|
+
from psycopg2 import sql
|
|
5
5
|
|
|
6
6
|
from velocity.db.core import exceptions
|
|
7
7
|
|
|
@@ -285,6 +285,20 @@ class SQL:
|
|
|
285
285
|
|
|
286
286
|
@classmethod
|
|
287
287
|
def update(cls, tx, table, data, where=None, pk=None, excluded=False):
|
|
288
|
+
"""
|
|
289
|
+
Generate a Postgres UPDATE statement, handling the WHERE clause logic similar
|
|
290
|
+
to how the SELECT statement does. If you want to do an ON CONFLICT ... DO UPDATE,
|
|
291
|
+
that logic should generally live in `merge(...)` rather than here.
|
|
292
|
+
|
|
293
|
+
:param tx: Database/transaction context object (used by TableHelper)
|
|
294
|
+
:param table: Table name
|
|
295
|
+
:param data: Dictionary of columns to update
|
|
296
|
+
:param where: WHERE clause conditions (dict, list of tuples, or string)
|
|
297
|
+
:param pk: Primary key dict to merge with `where`
|
|
298
|
+
:param excluded: If True, creates `col = EXCLUDED.col` expressions (used in upsert)
|
|
299
|
+
:return: (sql_string, params_tuple)
|
|
300
|
+
"""
|
|
301
|
+
|
|
288
302
|
if not table:
|
|
289
303
|
raise ValueError("Table name is required.")
|
|
290
304
|
if not pk and not where:
|
|
@@ -293,102 +307,110 @@ class SQL:
|
|
|
293
307
|
raise ValueError("data must be a non-empty mapping of column-value pairs.")
|
|
294
308
|
|
|
295
309
|
th = TableHelper(tx, table)
|
|
296
|
-
|
|
297
310
|
set_clauses = []
|
|
298
311
|
vals = []
|
|
299
312
|
|
|
313
|
+
# Merge pk into where if pk is provided
|
|
300
314
|
if pk:
|
|
301
315
|
if where:
|
|
302
|
-
where
|
|
316
|
+
# If where is a dict, update it; otherwise raise error
|
|
317
|
+
if isinstance(where, Mapping):
|
|
318
|
+
where = dict(where) # copy to avoid mutation
|
|
319
|
+
where.update(pk)
|
|
320
|
+
else:
|
|
321
|
+
raise ValueError(
|
|
322
|
+
"Cannot combine 'pk' with a non-dict 'where' clause."
|
|
323
|
+
)
|
|
303
324
|
else:
|
|
304
325
|
where = pk
|
|
305
326
|
|
|
306
|
-
#
|
|
327
|
+
# Build SET clauses
|
|
307
328
|
for col, val in data.items():
|
|
308
|
-
|
|
329
|
+
col_quoted = th.resolve_references(
|
|
309
330
|
col, options={"alias_column": False, "alias_table": False}
|
|
310
331
|
)
|
|
311
|
-
|
|
312
|
-
# Normal column
|
|
313
332
|
if excluded:
|
|
314
|
-
|
|
333
|
+
# For ON CONFLICT DO UPDATE statements, use the EXCLUDED value
|
|
334
|
+
set_clauses.append(f"{col_quoted} = EXCLUDED.{col_quoted}")
|
|
315
335
|
else:
|
|
316
|
-
set_clauses.append(f"{
|
|
336
|
+
set_clauses.append(f"{col_quoted} = %s")
|
|
317
337
|
vals.append(val)
|
|
318
338
|
|
|
319
|
-
#
|
|
339
|
+
# Build WHERE clauses for a normal update (ignored when excluded is True)
|
|
320
340
|
where_clauses = []
|
|
321
341
|
if not excluded:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
342
|
+
if where:
|
|
343
|
+
if isinstance(where, Mapping):
|
|
344
|
+
new_where = []
|
|
345
|
+
for key, val in where.items():
|
|
346
|
+
new_where.append(th.make_predicate(key, val))
|
|
347
|
+
where = new_where
|
|
348
|
+
if isinstance(where, str):
|
|
349
|
+
where_clauses.append(where)
|
|
350
|
+
else:
|
|
351
|
+
for pred, value in where:
|
|
352
|
+
where_clauses.append(pred)
|
|
353
|
+
if value is None:
|
|
354
|
+
pass
|
|
355
|
+
elif isinstance(value, tuple):
|
|
356
|
+
vals.extend(value)
|
|
357
|
+
else:
|
|
358
|
+
vals.append(value)
|
|
359
|
+
if not where_clauses:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
"No WHERE clause could be constructed. Update would affect all rows."
|
|
339
362
|
)
|
|
340
|
-
else:
|
|
341
|
-
sql.append(TableHelper.quote(table))
|
|
342
|
-
sql.append("SET " + ", ".join(set_clauses))
|
|
343
|
-
if not excluded:
|
|
344
|
-
if th.foreign_keys:
|
|
345
|
-
for key, ref_info in th.foreign_keys.items():
|
|
346
|
-
ref_table = ref_info["ref_table"]
|
|
347
|
-
sql.append(
|
|
348
|
-
f"LEFT JOIN {th.quote(ref_table)} AS {th.quote(ref_info['alias'])} "
|
|
349
|
-
)
|
|
350
|
-
where_clauses.append(
|
|
351
|
-
f"{th.quote(th.get_table_alias('current_table'))}.{th.quote(ref_info['local_column'])} = {th.quote(ref_info['alias'])}.{th.quote(ref_info['ref_column'])}"
|
|
352
|
-
)
|
|
353
363
|
|
|
354
|
-
|
|
364
|
+
# Construct final SQL
|
|
365
|
+
if excluded:
|
|
366
|
+
# For an upsert's DO UPDATE, only return the SET clause (no table name, no WHERE)
|
|
367
|
+
sql_parts = []
|
|
368
|
+
sql_parts.append("UPDATE")
|
|
369
|
+
sql_parts.append("SET " + ", ".join(set_clauses))
|
|
370
|
+
final_sql = sqlparse.format(
|
|
371
|
+
" ".join(sql_parts), reindent=True, keyword_case="upper"
|
|
372
|
+
)
|
|
373
|
+
return final_sql, tuple(vals)
|
|
374
|
+
else:
|
|
375
|
+
sql_parts = []
|
|
376
|
+
sql_parts.append("UPDATE")
|
|
377
|
+
sql_parts.append(th.quote(table))
|
|
378
|
+
sql_parts.append("SET " + ", ".join(set_clauses))
|
|
355
379
|
if where_clauses:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
raise ValueError(
|
|
362
|
-
"No WHERE clause could be constructed. Update would affect all rows."
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
# Final assembled query
|
|
366
|
-
sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
367
|
-
return sql, tuple(vals)
|
|
380
|
+
sql_parts.append("WHERE " + " AND ".join(where_clauses))
|
|
381
|
+
final_sql = sqlparse.format(
|
|
382
|
+
" ".join(sql_parts), reindent=True, keyword_case="upper"
|
|
383
|
+
)
|
|
384
|
+
return final_sql, tuple(vals)
|
|
368
385
|
|
|
369
386
|
@classmethod
|
|
370
387
|
def insert(cls, table, data):
|
|
388
|
+
"""
|
|
389
|
+
Generate an INSERT statement.
|
|
390
|
+
"""
|
|
391
|
+
|
|
371
392
|
keys = []
|
|
372
|
-
|
|
393
|
+
vals_placeholders = []
|
|
373
394
|
args = []
|
|
374
395
|
for key, val in data.items():
|
|
375
396
|
keys.append(TableHelper.quote(key.lower()))
|
|
376
397
|
if isinstance(val, str) and len(val) > 2 and val[:2] == "@@" and val[2:]:
|
|
377
|
-
|
|
398
|
+
vals_placeholders.append(val[2:])
|
|
378
399
|
else:
|
|
379
|
-
|
|
400
|
+
vals_placeholders.append("%s")
|
|
380
401
|
args.append(val)
|
|
381
402
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
403
|
+
sql_parts = []
|
|
404
|
+
sql_parts.append("INSERT INTO")
|
|
405
|
+
sql_parts.append(TableHelper.quote(table))
|
|
406
|
+
sql_parts.append("(")
|
|
407
|
+
sql_parts.append(",".join(keys))
|
|
408
|
+
sql_parts.append(")")
|
|
409
|
+
sql_parts.append("VALUES")
|
|
410
|
+
sql_parts.append("(")
|
|
411
|
+
sql_parts.append(",".join(vals_placeholders))
|
|
412
|
+
sql_parts.append(")")
|
|
413
|
+
sql = sqlparse.format(" ".join(sql_parts), reindent=True, keyword_case="upper")
|
|
392
414
|
return sql, tuple(args)
|
|
393
415
|
|
|
394
416
|
@classmethod
|
|
@@ -397,21 +419,23 @@ class SQL:
|
|
|
397
419
|
pkeys = tx.table(table).primary_keys()
|
|
398
420
|
if not pkeys:
|
|
399
421
|
raise ValueError("Primary key required for merge.")
|
|
400
|
-
# If there are multiple primary keys,
|
|
422
|
+
# If there are multiple primary keys, use all of them
|
|
401
423
|
if len(pkeys) > 1:
|
|
402
424
|
pk = {pk: data[pk] for pk in pkeys}
|
|
403
425
|
else:
|
|
404
426
|
pk = {pkeys[0]: data[pkeys[0]]}
|
|
405
|
-
#
|
|
427
|
+
# Remove primary keys from data; they will be used in the conflict target
|
|
406
428
|
data = {k: v for k, v in data.items() if k not in pk}
|
|
407
429
|
|
|
430
|
+
# Create a merged dictionary for insert (data + primary key columns)
|
|
408
431
|
full_data = {}
|
|
409
432
|
full_data.update(data)
|
|
410
433
|
full_data.update(pk)
|
|
411
434
|
|
|
412
435
|
sql, vals = cls.insert(table, full_data)
|
|
413
436
|
sql = [sql]
|
|
414
|
-
vals = list(vals)
|
|
437
|
+
vals = list(vals) # Convert to a mutable list
|
|
438
|
+
|
|
415
439
|
if on_conflict_do_nothing != on_conflict_update:
|
|
416
440
|
sql.append("ON CONFLICT")
|
|
417
441
|
sql.append("(")
|
|
@@ -421,15 +445,20 @@ class SQL:
|
|
|
421
445
|
if on_conflict_do_nothing:
|
|
422
446
|
sql.append("NOTHING")
|
|
423
447
|
elif on_conflict_update:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
448
|
+
# Call update() with excluded=True to produce the SET clause for the upsert.
|
|
449
|
+
sql_update, vals_update = cls.update(tx, table, data, pk, excluded=True)
|
|
450
|
+
sql.append(sql_update)
|
|
451
|
+
# Use list.extend to add the update values to vals.
|
|
452
|
+
vals.extend(vals_update)
|
|
427
453
|
else:
|
|
428
454
|
raise Exception(
|
|
429
455
|
"Update on conflict must have one and only one option to complete on conflict."
|
|
430
456
|
)
|
|
431
|
-
|
|
432
|
-
|
|
457
|
+
|
|
458
|
+
import sqlparse
|
|
459
|
+
|
|
460
|
+
final_sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
461
|
+
return final_sql, tuple(vals)
|
|
433
462
|
|
|
434
463
|
@classmethod
|
|
435
464
|
def version(cls):
|
|
@@ -513,9 +542,9 @@ class SQL:
|
|
|
513
542
|
@classmethod
|
|
514
543
|
def create_table(cls, name, columns={}, drop=False):
|
|
515
544
|
if "." in name:
|
|
516
|
-
fqtn = name
|
|
545
|
+
fqtn = TableHelper.quote(name)
|
|
517
546
|
else:
|
|
518
|
-
fqtn = f"public.{name}"
|
|
547
|
+
fqtn = f"public.{TableHelper.quote(name)}"
|
|
519
548
|
schema, table = fqtn.split(".")
|
|
520
549
|
name = fqtn.replace(".", "_")
|
|
521
550
|
sql = []
|
|
@@ -563,7 +592,7 @@ class SQL:
|
|
|
563
592
|
)
|
|
564
593
|
|
|
565
594
|
for key, val in columns.items():
|
|
566
|
-
key = re.sub("<>!=%", "", key
|
|
595
|
+
key = re.sub("<>!=%", "", key)
|
|
567
596
|
if key in system_fields:
|
|
568
597
|
continue
|
|
569
598
|
sql.append(
|
|
@@ -797,7 +826,7 @@ class SQL:
|
|
|
797
826
|
if "." not in table and schema:
|
|
798
827
|
table = f"{schema}.{table}"
|
|
799
828
|
if isinstance(columns, (list, set)):
|
|
800
|
-
columns = ",".join([TableHelper.quote(c
|
|
829
|
+
columns = ",".join([TableHelper.quote(c) for c in columns])
|
|
801
830
|
else:
|
|
802
831
|
columns = TableHelper.quote(columns)
|
|
803
832
|
sql = ["CREATE"]
|
|
@@ -858,7 +887,7 @@ class SQL:
|
|
|
858
887
|
if "." not in table and schema:
|
|
859
888
|
table = f"{schema}.{table}"
|
|
860
889
|
if isinstance(columns, (list, set)):
|
|
861
|
-
columns = ",".join([TableHelper.quote(c
|
|
890
|
+
columns = ",".join([TableHelper.quote(c) for c in columns])
|
|
862
891
|
else:
|
|
863
892
|
columns = TableHelper.quote(columns)
|
|
864
893
|
sql = ["DROP"]
|
|
@@ -880,7 +909,7 @@ class SQL:
|
|
|
880
909
|
|
|
881
910
|
@classmethod
|
|
882
911
|
def massage_data(cls, data):
|
|
883
|
-
data = {key
|
|
912
|
+
data = {key: val for key, val in data.items()}
|
|
884
913
|
primaryKey = set(cls.GetPrimaryKeyColumnNames())
|
|
885
914
|
if not primaryKey:
|
|
886
915
|
if not cls.Exists():
|
|
@@ -895,23 +924,43 @@ class SQL:
|
|
|
895
924
|
|
|
896
925
|
@classmethod
|
|
897
926
|
def alter_add(cls, table, columns, null_allowed=True):
|
|
927
|
+
"""
|
|
928
|
+
Modify the table to add new columns. If the `value` is 'now()', treat it as a
|
|
929
|
+
TIMESTAMP type (optionally with a DEFAULT now() clause).
|
|
930
|
+
"""
|
|
898
931
|
sql = []
|
|
899
|
-
|
|
932
|
+
null_clause = "NOT NULL" if not null_allowed else ""
|
|
933
|
+
|
|
900
934
|
if isinstance(columns, dict):
|
|
901
|
-
for
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
935
|
+
for col_name, val in columns.items():
|
|
936
|
+
col_name_clean = re.sub("<>!=%", "", col_name)
|
|
937
|
+
# If the user wants 'now()' to be recognized as a TIMESTAMP column:
|
|
938
|
+
if isinstance(val, str) and val.strip().lower() == "@@now()":
|
|
939
|
+
# We assume the user wants the type to be TIMESTAMP
|
|
940
|
+
# Optionally we can also add `DEFAULT now()` if desired
|
|
941
|
+
# so that newly added rows use the current timestamp
|
|
942
|
+
col_type = "TIMESTAMP"
|
|
943
|
+
sql.append(
|
|
944
|
+
f"ALTER TABLE {TableHelper.quote(table)} "
|
|
945
|
+
f"ADD {TableHelper.quote(col_name_clean)} {col_type} {null_clause};"
|
|
946
|
+
)
|
|
947
|
+
else:
|
|
948
|
+
# Normal code path: rely on your `TYPES.get_type(...)` logic
|
|
949
|
+
col_type = TYPES.get_type(val)
|
|
950
|
+
sql.append(
|
|
951
|
+
f"ALTER TABLE {TableHelper.quote(table)} "
|
|
952
|
+
f"ADD {TableHelper.quote(col_name_clean)} {col_type} {null_clause};"
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
final_sql = sqlparse.format(" ".join(sql), reindent=True, keyword_case="upper")
|
|
956
|
+
return final_sql, tuple()
|
|
908
957
|
|
|
909
958
|
@classmethod
|
|
910
959
|
def alter_drop(cls, table, columns):
|
|
911
960
|
sql = [f"ALTER TABLE {TableHelper.quote(table)} DROP COLUMN"]
|
|
912
961
|
if isinstance(columns, dict):
|
|
913
962
|
for key, val in columns.items():
|
|
914
|
-
key = re.sub("<>!=%", "", key
|
|
963
|
+
key = re.sub("<>!=%", "", key)
|
|
915
964
|
sql.append(f"{key},")
|
|
916
965
|
if sql[-1][-1] == ",":
|
|
917
966
|
sql[-1] = sql[-1][:-1]
|
|
@@ -963,7 +1012,7 @@ class SQL:
|
|
|
963
1012
|
|
|
964
1013
|
@classmethod
|
|
965
1014
|
def delete(cls, tx, table, where):
|
|
966
|
-
sql = [f"DELETE FROM {table}"]
|
|
1015
|
+
sql = [f"DELETE FROM {TableHelper.quote(table)}"]
|
|
967
1016
|
vals = []
|
|
968
1017
|
if where:
|
|
969
1018
|
s, v = TableHelper(tx, table).make_where(where)
|
|
@@ -141,27 +141,58 @@ class TableHelper:
|
|
|
141
141
|
|
|
142
142
|
def extract_column_name(self, sql_expression):
|
|
143
143
|
"""
|
|
144
|
-
Extracts the 'bare' column name from
|
|
144
|
+
Extracts the 'bare' column name from a SQL expression.
|
|
145
|
+
|
|
146
|
+
Supports:
|
|
147
|
+
- Aliases (AS ...)
|
|
148
|
+
- Window functions (OVER(... ORDER BY ...))
|
|
149
|
+
- CAST(... AS ...)
|
|
150
|
+
- CASE WHEN ... THEN ... ELSE ... END
|
|
151
|
+
- Nested function calls
|
|
152
|
+
- Grabs column from inside expressions (e.g. PLAID_ERROR from SUM(CASE...))
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
sql_expression (str): SQL expression (SELECT column) string.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
str or None: Extracted column name or None if undetectable.
|
|
145
159
|
"""
|
|
146
|
-
expr = sql_expression.replace('"', "")
|
|
160
|
+
expr = sql_expression.replace('"', "").strip()
|
|
147
161
|
|
|
148
|
-
# Remove
|
|
149
|
-
# This regex removes any trailing alias defined using "as".
|
|
162
|
+
# Remove trailing alias
|
|
150
163
|
expr = re.sub(r"(?i)\s+as\s+\w+$", "", expr).strip()
|
|
151
164
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
# If OVER clause: extract column inside ORDER BY
|
|
166
|
+
over_match = re.search(r"(?i)OVER\s*\(\s*ORDER\s+BY\s+([^\s,)]+)", expr)
|
|
167
|
+
if over_match:
|
|
168
|
+
return over_match.group(1)
|
|
169
|
+
|
|
170
|
+
# Remove CAST(... AS ...)
|
|
171
|
+
while re.search(r"(?i)CAST\s*\(([^()]+?)\s+AS\s+[^\)]+\)", expr):
|
|
172
|
+
expr = re.sub(r"(?i)CAST\s*\(([^()]+?)\s+AS\s+[^\)]+\)", r"\1", expr)
|
|
173
|
+
|
|
174
|
+
# Remove CASE WHEN ... THEN ... ELSE ... END, keep just the WHEN part
|
|
175
|
+
while re.search(
|
|
176
|
+
r"(?i)CASE\s+WHEN\s+(.+?)\s+THEN\s+.+?(?:\s+ELSE\s+.+?)?\s+END", expr
|
|
177
|
+
):
|
|
178
|
+
expr = re.sub(
|
|
179
|
+
r"(?i)CASE\s+WHEN\s+(.+?)\s+THEN\s+.+?(?:\s+ELSE\s+.+?)?\s+END",
|
|
180
|
+
r"\1",
|
|
181
|
+
expr,
|
|
182
|
+
)
|
|
155
183
|
|
|
156
|
-
#
|
|
157
|
-
while "("
|
|
158
|
-
expr = re.sub(r"\b\w+\s*\((
|
|
184
|
+
# Unwrap function calls (SUM(...), MAX(...), etc.)
|
|
185
|
+
while re.search(r"\b\w+\s*\(([^()]+)\)", expr):
|
|
186
|
+
expr = re.sub(r"\b\w+\s*\(([^()]+)\)", r"\1", expr)
|
|
159
187
|
|
|
188
|
+
# If multiple columns, take the first
|
|
160
189
|
if "," in expr:
|
|
161
190
|
expr = expr.split(",")[0].strip()
|
|
162
191
|
|
|
163
|
-
|
|
164
|
-
match = re.search(
|
|
192
|
+
# Extract column name (basic or dotted like table.col or *)
|
|
193
|
+
match = re.search(
|
|
194
|
+
r"\b([a-zA-Z_][\w]*\.\*|\*|[a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)?)\b", expr
|
|
195
|
+
)
|
|
165
196
|
return match.group(1) if match else None
|
|
166
197
|
|
|
167
198
|
def are_parentheses_balanced(self, expression):
|
|
@@ -212,8 +243,8 @@ class TableHelper:
|
|
|
212
243
|
# Lists / tuples => IN / NOT IN
|
|
213
244
|
if isinstance(val, (list, tuple)) and "><" not in key:
|
|
214
245
|
if "!" in key:
|
|
215
|
-
return f"{column}
|
|
216
|
-
return f"{column}
|
|
246
|
+
return f"{column} != ANY(%s)", list(val)
|
|
247
|
+
return f"{column} = ANY(%s)", list(val)
|
|
217
248
|
|
|
218
249
|
# "@@" => pass as literal
|
|
219
250
|
if isinstance(val, str) and val.startswith("@@") and val[2:]:
|
velocity/misc/tools.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import inspect
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_once(func, *args, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Executes 'func(*args, **kwargs)' only once across script runs.
|
|
9
|
+
|
|
10
|
+
The sentinel file name is automatically generated based on the function's
|
|
11
|
+
module and name. If the function is a lambda, a stable hash of its bytecode
|
|
12
|
+
is used in place of a function name.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def _generate_sentinel_name(_func):
|
|
16
|
+
# Use __module__ to get the name of the module where _func is defined.
|
|
17
|
+
module_name = getattr(_func, "__module__", "unknown_module")
|
|
18
|
+
# Use __name__ to get the function name; for lambdas, this is "<lambda>"
|
|
19
|
+
function_name = getattr(_func, "__name__", "unknown_func")
|
|
20
|
+
|
|
21
|
+
if function_name == "<lambda>":
|
|
22
|
+
# For lambdas, generate a stable name from a short hash of the bytecode.
|
|
23
|
+
code_hash = hashlib.md5(_func.__code__.co_code).hexdigest()
|
|
24
|
+
function_name = f"lambda_{code_hash[:8]}"
|
|
25
|
+
|
|
26
|
+
return f".has_run_once_{module_name}_{function_name}"
|
|
27
|
+
|
|
28
|
+
# Derive the sentinel filename from the function’s identity
|
|
29
|
+
sentinel_file = _generate_sentinel_name(func)
|
|
30
|
+
|
|
31
|
+
# Check if the sentinel file exists
|
|
32
|
+
if os.path.exists(sentinel_file):
|
|
33
|
+
print(f"Code in '{func.__name__}' has already been run. Skipping.")
|
|
34
|
+
else:
|
|
35
|
+
# Execute the function for the first time
|
|
36
|
+
func(*args, **kwargs)
|
|
37
|
+
# Create an empty sentinel file to mark as "done"
|
|
38
|
+
with open(sentinel_file, "w") as f:
|
|
39
|
+
f.write("")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.68
|
|
4
4
|
Summary: A rapid application development library for interfacing with data storage
|
|
5
5
|
Author-email: Paul Perez <pperez@codeclubs.org>
|
|
6
6
|
Project-URL: Homepage, https://codeclubs.org/projects/velocity
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
velocity/__init__.py,sha256=
|
|
1
|
+
velocity/__init__.py,sha256=RG-xG6jwuUUVOz7GYfyRuXiVYUh4Tkvg0MRxwhH8HZ8,88
|
|
2
2
|
velocity/aws/__init__.py,sha256=GBTEr02whnCH3TG-BWCpUC3KfHY3uNxD21g0OvsVJnc,598
|
|
3
3
|
velocity/aws/handlers/__init__.py,sha256=xnpFZJVlC2uoeeFW4zuPST8wA8ajaQDky5Y6iXZzi3A,172
|
|
4
4
|
velocity/aws/handlers/context.py,sha256=UIjNR83y2NSIyK8HMPX8t5tpJHFNabiZvNgmmdQL3HA,1822
|
|
@@ -16,7 +16,7 @@ velocity/db/core/result.py,sha256=OVqoMwlx3CHNNwr-JGWRx5I8u_YX6hlUpecx99UT5nE,61
|
|
|
16
16
|
velocity/db/core/row.py,sha256=aliLYTTFirgJsOvmUsANwJMyxaATuhpGpFJhcu_twwY,6709
|
|
17
17
|
velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
|
|
18
18
|
velocity/db/core/table.py,sha256=1zI_GgkUCCjRmM30OGiLOgJnnAeJNkriK_mYeV34lC0,35058
|
|
19
|
-
velocity/db/core/transaction.py,sha256=
|
|
19
|
+
velocity/db/core/transaction.py,sha256=IQEmrHAjCg6hqxQQOpPLWUbyLXrTjIPGLHHv7P6urKU,6589
|
|
20
20
|
velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
velocity/db/servers/mysql.py,sha256=qHwlB_Mg02R7QFjD5QvJCorYYiP50CqEiQyZVl3uYns,20914
|
|
22
22
|
velocity/db/servers/mysql_reserved.py,sha256=CYdZJBOpS-ptImaRZcmVLumdUdbFuf9Tfdzu_mUT5wY,3507
|
|
@@ -24,11 +24,11 @@ velocity/db/servers/sqlite.py,sha256=X210a5pENT9PiVK7f16fxXzFwEsq8fSe58Vouv2xqlk
|
|
|
24
24
|
velocity/db/servers/sqlite_reserved.py,sha256=-xmjl-Hgu6lKqkCAXq_6U8_aJX6gvaMgLMLdCt-Ej7o,3006
|
|
25
25
|
velocity/db/servers/sqlserver.py,sha256=0uGLEWRXiUhrOVTpEA1zvaKq1mcfiaCDp9r7gX-N71g,29914
|
|
26
26
|
velocity/db/servers/sqlserver_reserved.py,sha256=3LGQYU0qfvk6AbKety96gbzzfLbZ0dNHDPLxKGvvi4Q,4596
|
|
27
|
-
velocity/db/servers/tablehelper.py,sha256=
|
|
28
|
-
velocity/db/servers/postgres/__init__.py,sha256=
|
|
27
|
+
velocity/db/servers/tablehelper.py,sha256=Z6fpzewuCmrZ98Lk5OKu_nhpBpeX_OLnSCyqr1KdxRk,11054
|
|
28
|
+
velocity/db/servers/postgres/__init__.py,sha256=SRqTRrhHkueEzGScG82KVveXC9mfNm6t4XylSCObkXQ,546
|
|
29
29
|
velocity/db/servers/postgres/operators.py,sha256=A2T1qFwhzPl0fdXVhLZJhh5Qfx-qF8oZsDnxnq2n_V8,389
|
|
30
30
|
velocity/db/servers/postgres/reserved.py,sha256=5tKLaqFV-HrWRj-nsrxl5KGbmeM3ukn_bPZK36XEu8M,3648
|
|
31
|
-
velocity/db/servers/postgres/sql.py,sha256
|
|
31
|
+
velocity/db/servers/postgres/sql.py,sha256=mGPUrg0M5NGNkCHbMKdL0F01CEfCr883lJlB-qAG6bM,38147
|
|
32
32
|
velocity/db/servers/postgres/types.py,sha256=Wa45ppVf_pdWul-jYWFRGMl6IdSq8dAp10SKnhL7osQ,3757
|
|
33
33
|
velocity/misc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
velocity/misc/db.py,sha256=MPgt-kkukKR_Wh_S_5W-MyDgaeoZ4YLoDJ54wU2ppm4,2830
|
|
@@ -37,11 +37,12 @@ velocity/misc/format.py,sha256=fA5ai3kp-bLhPCCg7Yq9XPhUCE3In-jVZobjBGvmHDg,2649
|
|
|
37
37
|
velocity/misc/mail.py,sha256=BrxDqeVsOd0epyJKwrHA-owzs6di2oLA_qJskoTux-c,2553
|
|
38
38
|
velocity/misc/merge.py,sha256=EYtqwnckBllPO60tRALxFRuzmUQ7Wl0qZC6sCgyiZDA,1885
|
|
39
39
|
velocity/misc/timer.py,sha256=cN3aS0t6HLlhYfF2Ir6ihJehxNrWf9ebaLzXUaWRKEA,1637
|
|
40
|
+
velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
|
|
40
41
|
velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
|
|
41
42
|
velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
|
|
42
43
|
velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
|
|
43
|
-
velocity_python-0.0.
|
|
44
|
-
velocity_python-0.0.
|
|
45
|
-
velocity_python-0.0.
|
|
46
|
-
velocity_python-0.0.
|
|
47
|
-
velocity_python-0.0.
|
|
44
|
+
velocity_python-0.0.68.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
|
|
45
|
+
velocity_python-0.0.68.dist-info/METADATA,sha256=hZEB2oLJnPxM4jt42JBv2XVAIDqDY7TZnYEguFQi_DU,8541
|
|
46
|
+
velocity_python-0.0.68.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
47
|
+
velocity_python-0.0.68.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
|
|
48
|
+
velocity_python-0.0.68.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|