velocity-python 0.0.35__py3-none-any.whl → 0.0.64__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.

Potentially problematic release.


This version of velocity-python might be problematic. Click here for more details.

@@ -1,16 +1,17 @@
1
1
  import datetime
2
- from velocity.misc.format import to_json
3
2
  import decimal
3
+ from velocity.misc.format import to_json
4
4
 
5
5
 
6
- class Result(object):
6
+ class Result:
7
+ """
8
+ Wraps a database cursor to provide various convenience transformations
9
+ (dict, list, tuple, etc.) and helps iterate over query results.
10
+ """
11
+
7
12
  def __init__(self, cursor=None, tx=None, sql=None, params=None):
8
13
  self._cursor = cursor
9
- if hasattr(cursor, "description") and cursor.description:
10
- self._headers = [x[0].lower() for x in cursor.description]
11
- else:
12
- self._headers = []
13
- self.as_dict()
14
+ self._headers = [x[0].lower() for x in getattr(cursor, "description", []) or []]
14
15
  self.__as_strings = False
15
16
  self.__enumerate = False
16
17
  self.__count = -1
@@ -18,32 +19,7 @@ class Result(object):
18
19
  self.__tx = tx
19
20
  self.__sql = sql
20
21
  self.__params = params
21
-
22
- @property
23
- def headers(self):
24
- if not self._headers:
25
- if self._cursor and hasattr(self._cursor, "description"):
26
- self._headers = [x[0].lower() for x in self._cursor.description]
27
- return self._headers
28
-
29
- @property
30
- def columns(self):
31
- if not self.__columns:
32
- if self._cursor and hasattr(self._cursor, "description"):
33
- for column in self._cursor.description:
34
- data = {
35
- "type_name": self.__tx.pg_types[column.type_code],
36
- # TBD This can be implemented if needed but turning off
37
- # since it is not complete set of all codes and could raise
38
- # exception
39
- #'pytype': types[self.__tx.pg_types[column.type_code]]
40
- }
41
- for key in dir(column):
42
- if "__" in key:
43
- continue
44
- data[key] = getattr(column, key)
45
- self.__columns[column.name] = data
46
- return self.__columns
22
+ self.transform = lambda row: dict(zip(self.headers, row)) # Default transform
47
23
 
48
24
  def __str__(self):
49
25
  return repr(self.all())
@@ -56,6 +32,9 @@ class Result(object):
56
32
  self.close()
57
33
 
58
34
  def __next__(self):
35
+ """
36
+ Iterator interface to retrieve the next row.
37
+ """
59
38
  if self._cursor:
60
39
  row = self._cursor.fetchone()
61
40
  if row:
@@ -64,11 +43,13 @@ class Result(object):
64
43
  if self.__enumerate:
65
44
  self.__count += 1
66
45
  return (self.__count, self.transform(row))
67
- else:
68
- return self.transform(row)
46
+ return self.transform(row)
69
47
  raise StopIteration
70
48
 
71
49
  def batch(self, qty=1):
50
+ """
51
+ Yields lists (batches) of rows with size = qty until no rows remain.
52
+ """
72
53
  results = []
73
54
  while True:
74
55
  try:
@@ -76,14 +57,15 @@ class Result(object):
76
57
  except StopIteration:
77
58
  if results:
78
59
  yield results
79
- results = []
80
- continue
81
- raise
60
+ break
82
61
  if len(results) == qty:
83
62
  yield results
84
63
  results = []
85
64
 
86
65
  def all(self):
66
+ """
67
+ Retrieves all rows at once into a list.
68
+ """
87
69
  results = []
88
70
  while True:
89
71
  try:
@@ -95,62 +77,124 @@ class Result(object):
95
77
  def __iter__(self):
96
78
  return self
97
79
 
80
+ @property
81
+ def headers(self):
82
+ """
83
+ Retrieves column headers from the cursor if not already set.
84
+ """
85
+ if not self._headers and self._cursor and hasattr(self._cursor, "description"):
86
+ self._headers = [x[0].lower() for x in self._cursor.description]
87
+ return self._headers
88
+
89
+ @property
90
+ def columns(self):
91
+ """
92
+ Retrieves detailed column information from the cursor.
93
+ """
94
+ if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
95
+ for column in self._cursor.description:
96
+ data = {
97
+ "type_name": self.__tx.pg_types[column.type_code],
98
+ }
99
+ for key in dir(column):
100
+ if "__" not in key:
101
+ data[key] = getattr(column, key)
102
+ self.__columns[column.name] = data
103
+ return self.__columns
104
+
98
105
  @property
99
106
  def cursor(self):
100
107
  return self._cursor
101
108
 
102
109
  def close(self):
103
- self._cursor.close()
110
+ """
111
+ Closes the underlying cursor if it exists.
112
+ """
113
+ if self._cursor:
114
+ self._cursor.close()
104
115
 
105
116
  def as_dict(self):
106
- self.transform = lambda row: dict(list(zip(self.headers, row)))
117
+ """
118
+ Transform each row into a dictionary keyed by column names.
119
+ """
120
+ self.transform = lambda row: dict(zip(self.headers, row))
107
121
  return self
108
122
 
109
123
  def as_json(self):
110
- self.transform = lambda row: to_json(dict(list(zip(self.headers, row))))
124
+ """
125
+ Transform each row into JSON (string).
126
+ """
127
+ self.transform = lambda row: to_json(dict(zip(self.headers, row)))
111
128
  return self
112
129
 
113
130
  def as_named_tuple(self):
131
+ """
132
+ Transform each row into a list of (column_name, value) pairs.
133
+ """
114
134
  self.transform = lambda row: list(zip(self.headers, row))
115
135
  return self
116
136
 
117
137
  def as_list(self):
138
+ """
139
+ Transform each row into a list of values.
140
+ """
118
141
  self.transform = lambda row: list(row)
119
142
  return self
120
143
 
121
144
  def as_tuple(self):
145
+ """
146
+ Transform each row into a tuple of values.
147
+ """
122
148
  self.transform = lambda row: row
123
149
  return self
124
150
 
125
151
  def as_simple_list(self, pos=0):
152
+ """
153
+ Transform each row into the single value at position `pos`.
154
+ """
126
155
  self.transform = lambda row: row[pos]
127
156
  return self
128
157
 
129
158
  def strings(self, as_strings=True):
159
+ """
160
+ Indicate whether retrieved rows should be coerced to string form.
161
+ """
130
162
  self.__as_strings = as_strings
131
163
  return self
132
164
 
133
165
  def scalar(self, default=None):
166
+ """
167
+ Return the first column of the first row, or `default` if no rows.
168
+ """
134
169
  if not self._cursor:
135
170
  return None
136
171
  val = self._cursor.fetchone()
172
+ # Drain any remaining rows.
137
173
  self._cursor.fetchall()
138
174
  return val[0] if val else default
139
175
 
140
176
  def one(self, default=None):
177
+ """
178
+ Return the first row or `default` if no rows.
179
+ """
141
180
  try:
142
- return next(self)
143
- except StopIteration:
144
- return default
145
- finally:
181
+ row = next(self)
182
+ # Drain remaining.
146
183
  if self._cursor:
147
184
  self._cursor.fetchall()
185
+ return row
186
+ except StopIteration:
187
+ return default
148
188
 
149
- def get_table_data(self, headers=True, strings=True):
189
+ def get_table_data(self, headers=True):
190
+ """
191
+ Builds a two-dimensional list: first row is column headers, subsequent rows are data.
192
+ """
150
193
  self.as_list()
151
194
  rows = []
152
195
  for row in self:
153
- rows.append(["" if x is None else str(x) for x in row])
196
+ row = ["" if x is None else str(x) for x in row]
197
+ rows.append(row)
154
198
  if isinstance(headers, list):
155
199
  rows.insert(0, [x.replace("_", " ").title() for x in headers])
156
200
  elif headers:
@@ -158,11 +202,12 @@ class Result(object):
158
202
  return rows
159
203
 
160
204
  def enum(self):
205
+ """
206
+ Yields each row as (row_index, transformed_row).
207
+ """
161
208
  self.__enumerate = True
162
209
  return self
163
210
 
164
- enumerate = enum
165
-
166
211
  @property
167
212
  def sql(self):
168
213
  return self.__sql
velocity/db/core/row.py CHANGED
@@ -1,11 +1,16 @@
1
1
  import pprint
2
2
 
3
3
 
4
- class Row(object):
4
+ class Row:
5
+ """
6
+ Represents a single row in a given table, identified by a primary key or a dictionary of conditions.
7
+ """
8
+
5
9
  def __init__(self, table, key, lock=None):
6
10
  if isinstance(table, str):
7
- raise Exception("table parameter of row class must `table` instance")
11
+ raise Exception("Table parameter must be a `table` instance.")
8
12
  self.table = table
13
+
9
14
  if isinstance(key, (dict, Row)):
10
15
  pk = {}
11
16
  try:
@@ -15,6 +20,7 @@ class Row(object):
15
20
  pk = key
16
21
  else:
17
22
  pk = {self.key_cols[0]: key}
23
+
18
24
  self.pk = pk
19
25
  self.cache = key
20
26
  if lock:
@@ -36,12 +42,12 @@ class Row(object):
36
42
 
37
43
  def __setitem__(self, key, val):
38
44
  if key in self.pk:
39
- raise Exception("Can't update a primary key, idiot!")
45
+ raise Exception("Cannot update a primary key.")
40
46
  self.table.upsert({key: val}, self.pk)
41
47
 
42
48
  def __delitem__(self, key):
43
49
  if key in self.pk:
44
- raise Exception("Can't delete a primary key, idiot!")
50
+ raise Exception("Cannot delete a primary key.")
45
51
  if key not in self:
46
52
  return
47
53
  self[key] = None
@@ -50,60 +56,64 @@ class Row(object):
50
56
  return key.lower() in [x.lower() for x in self.keys()]
51
57
 
52
58
  def clear(self):
59
+ """
60
+ Deletes this row from the database.
61
+ """
53
62
  self.table.delete(where=self.pk)
54
63
  return self
55
64
 
56
65
  def keys(self):
57
- return self.table.sys_columns
66
+ """
67
+ Returns the column names in the table (including sys_ columns).
68
+ """
69
+ return self.table.sys_columns()
58
70
 
59
71
  def values(self, *args):
72
+ """
73
+ Returns values from this row, optionally restricted to columns in `args`.
74
+ """
60
75
  d = self.table.select(where=self.pk).as_dict().one()
61
76
  if args:
62
- values = []
63
- for arg in args:
64
- values.append(d[arg])
65
- return values
66
- else:
67
- return list(d.values())
77
+ return [d[arg] for arg in args]
78
+ return list(d.values())
68
79
 
69
80
  def items(self):
81
+ """
82
+ Returns (key, value) pairs for all columns.
83
+ """
70
84
  d = self.table.select(where=self.pk).as_dict().one()
71
85
  return list(d.items())
72
86
 
73
87
  def get(self, key, failobj=None):
74
88
  data = self[key]
75
- if data == None:
89
+ if data is None:
76
90
  return failobj
77
91
  return data
78
92
 
79
93
  def setdefault(self, key, default=None):
80
94
  data = self[key]
81
- if data == None:
95
+ if data is None:
82
96
  self[key] = default
83
97
  return default
84
98
  return data
85
99
 
86
- def update(self, dict=None, **kwds):
100
+ def update(self, dict_=None, **kwds):
101
+ """
102
+ Updates columns in this row.
103
+ """
87
104
  data = {}
88
- if dict:
89
- data.update(dict)
105
+ if dict_:
106
+ data.update(dict_)
90
107
  if kwds:
91
108
  data.update(kwds)
92
109
  if data:
93
110
  self.table.upsert(data, self.pk)
94
111
  return self
95
112
 
96
- def iterkeys(self):
97
- return list(self.keys())
98
-
99
- def itervalues(self):
100
- return list(self.values())
101
-
102
- def iteritems(self):
103
- return list(self.items())
104
-
105
113
  def __cmp__(self, other):
106
- # zero == same (not less than or greater than other)
114
+ """
115
+ Legacy comparison method; returns 0 if self and other share keys/values, else -1.
116
+ """
107
117
  diff = -1
108
118
  if hasattr(other, "keys"):
109
119
  k1 = list(self.keys())
@@ -117,18 +127,18 @@ class Row(object):
117
127
  return diff
118
128
 
119
129
  def __bool__(self):
120
- return bool(self.__len__())
130
+ return bool(len(self))
121
131
 
122
132
  def copy(self, lock=None):
133
+ """
134
+ Makes a copy of this row with a new sys_id, dropping sys_-prefixed columns from the new dict.
135
+ """
123
136
  old = self.to_dict()
124
- for key in list(old.keys()):
125
- if "sys_" in key:
126
- old.pop(key)
137
+ for k in list(old.keys()):
138
+ if "sys_" in k:
139
+ old.pop(k)
127
140
  return self.table.new(old, lock=lock)
128
141
 
129
- # ================================================================
130
- # This stuff is not implemented
131
-
132
142
  def pop(self):
133
143
  raise NotImplementedError
134
144
 
@@ -152,9 +162,15 @@ class Row(object):
152
162
  raise NotImplementedError
153
163
 
154
164
  def to_dict(self):
165
+ """
166
+ Returns the row as a dictionary via a SELECT on self.pk.
167
+ """
155
168
  return self.table.select(where=self.pk).as_dict().one()
156
169
 
157
170
  def extract(self, *args):
171
+ """
172
+ Returns a dict containing only the specified columns from this row.
173
+ """
158
174
  data = {}
159
175
  for key in args:
160
176
  if isinstance(key, (tuple, list)):
@@ -165,54 +181,73 @@ class Row(object):
165
181
 
166
182
  @property
167
183
  def key_cols(self):
168
- # result = self.execute(*self.sql.primary_keys(self.tablename))
169
- # return [x[0] for x in result.as_tuple()]
184
+ """
185
+ Returns the primary key columns for the underlying table, defaulting to ['sys_id'] if missing.
186
+ """
170
187
  return ["sys_id"]
171
188
 
172
189
  def split(self):
190
+ """
191
+ Splits data from PK references into (non_sys_data, pk).
192
+ """
173
193
  old = self.to_dict()
174
- for key in list(old.keys()):
175
- if "sys_" in key:
176
- old.pop(key)
194
+ for k in list(old.keys()):
195
+ if "sys_" in k:
196
+ old.pop(k)
177
197
  return old, self.pk
178
198
 
179
199
  @property
180
200
  def data(self):
201
+ """
202
+ Returns the 'non-sys' data dictionary for this row.
203
+ """
181
204
  return self.split()[0]
182
205
 
183
206
  def row(self, key, lock=None):
184
- tx = self.table.tx
207
+ """
208
+ Retrieve a row from a foreign key column if present. E.g. row.row('fk_column').
209
+ """
185
210
  value = self[key]
186
211
  if value is None:
187
212
  return None
188
213
  fk = self.table.foreign_key_info(key)
189
214
  if not fk:
190
215
  raise Exception(
191
- "Column `{}` is not a foreign key in `{}`".format(key, self.table.name)
216
+ f"Column `{key}` is not a foreign key in `{self.table.name}`"
192
217
  )
193
- return tx.Row(fk["referenced_table_name"], value, lock=lock)
218
+ return self.table.tx.Row(fk["referenced_table_name"], value, lock=lock)
194
219
 
195
220
  def match(self, other):
196
- for key in other:
197
- if self[key] != other[key]:
221
+ """
222
+ Returns True if the columns in 'other' match this row's columns for the same keys.
223
+ """
224
+ for k in other:
225
+ if self[k] != other[k]:
198
226
  return False
199
227
  return True
200
228
 
201
229
  def touch(self):
230
+ """
231
+ Update sys_modified to current timestamp.
232
+ """
202
233
  self["sys_modified"] = "@@CURRENT_TIMESTAMP"
203
234
  return self
204
235
 
205
236
  delete = clear
206
237
 
207
238
  def lock(self):
239
+ """
240
+ SELECT ... FOR UPDATE on this row.
241
+ """
208
242
  self.table.select(where=self.pk, lock=True)
209
243
  return self
210
244
 
211
245
  def notBlank(self, key, failobj=None):
246
+ """
247
+ Returns the value if it is not blank, else failobj.
248
+ """
212
249
  data = self[key]
213
- if not data:
214
- return failobj
215
- return data
250
+ return data if data else failobj
216
251
 
217
252
  getBlank = notBlank
218
253
 
@@ -1,41 +1,131 @@
1
- class Sequence(object):
1
+ import psycopg2
2
+
3
+
4
+ class Sequence:
5
+ """
6
+ Represents a database sequence in PostgreSQL.
7
+ """
8
+
2
9
  def __init__(self, tx, name, start=1000):
3
10
  self.tx = tx
4
11
  self.name = name.lower()
5
12
  self.sql = tx.engine.sql
6
13
  self.start = start
7
14
 
8
- self.create()
9
-
10
15
  def __str__(self):
11
- return """
12
- Sequence: %s
13
- """ % (
14
- self.name
15
- )
16
-
17
- def create(self):
18
- sql, vals = (
19
- "CREATE SEQUENCE IF NOT EXISTS {} START {};".format(self.name, self.start),
20
- tuple(),
21
- )
16
+ return f"Sequence: {self.name} (current val: {self.current()})"
17
+
18
+ def create(self, start=None):
19
+ """
20
+ Creates the sequence if it does not already exist.
21
+ """
22
+ val = start if start is not None else self.start
23
+ sql = f"CREATE SEQUENCE IF NOT EXISTS {self.name} START {val};"
24
+ vals = ()
22
25
  return self.tx.execute(sql, vals)
23
26
 
24
27
  def next(self):
25
- sql, vals = "SELECT nextval('{}');".format(self.name), tuple()
28
+ """
29
+ Retrieves the next value in this sequence.
30
+ """
31
+ sql = f"SELECT nextval('{self.name}');"
32
+ vals = ()
26
33
  return self.tx.execute(sql, vals).scalar()
27
34
 
28
35
  def current(self):
29
- sql, vals = "SELECT currval('{}');".format(self.name), tuple()
36
+ """
37
+ Retrieves the current value of the sequence.
38
+ """
39
+ sql = f"SELECT currval('{self.name}');"
40
+ vals = ()
30
41
  return self.tx.execute(sql, vals).scalar()
31
42
 
32
- def reset(self, start=None):
33
- sql, vals = (
34
- "ALTER SEQUENCE {} RESTART WITH {};".format(self.name, start or self.start),
35
- tuple(),
36
- )
43
+ def safe_current(self):
44
+ """
45
+ Returns the current value of the sequence if one has been generated
46
+ in this session, or None otherwise.
47
+ """
48
+ sql = f"SELECT currval('{self.name}');"
49
+ try:
50
+ return self.tx.execute(sql, ()).scalar()
51
+ except psycopg2.ProgrammingError:
52
+ return None
53
+
54
+ def set_value(self, start=None):
55
+ """
56
+ Resets the sequence to the given start value (defaults to initial `self.start`).
57
+ """
58
+ val = start if start is not None else self.start
59
+ sql = f"ALTER SEQUENCE {self.name} RESTART WITH {val};"
60
+ vals = ()
37
61
  return self.tx.execute(sql, vals).scalar()
38
62
 
63
+ reset = set_value
64
+
39
65
  def drop(self):
40
- sql, vals = "DROP SEQUENCE IF EXISTS {};".format(self.name), tuple()
66
+ """
67
+ Drops the sequence if it exists.
68
+ """
69
+ sql = f"DROP SEQUENCE IF EXISTS {self.name};"
70
+ vals = ()
41
71
  return self.tx.execute(sql, vals)
72
+
73
+ def exists(self):
74
+ """
75
+ Checks whether the sequence exists in the database.
76
+ Returns True if it exists, False otherwise.
77
+ """
78
+ sql = f"""
79
+ SELECT EXISTS (
80
+ SELECT 1
81
+ FROM pg_class
82
+ WHERE relname = '{self.name}'
83
+ AND relkind = 'S'
84
+ );
85
+ """
86
+ return self.tx.execute(sql, ()).scalar()
87
+
88
+ def configure(self, increment=None, minvalue=None, maxvalue=None, cycle=None):
89
+ """
90
+ Alter the sequence with the given settings (any of which may be None to skip).
91
+ """
92
+ parts = []
93
+ if increment is not None:
94
+ parts.append(f"INCREMENT BY {increment}")
95
+ if minvalue is not None:
96
+ parts.append(f"MINVALUE {minvalue}")
97
+ if maxvalue is not None:
98
+ parts.append(f"MAXVALUE {maxvalue}")
99
+ if cycle is True:
100
+ parts.append("CYCLE")
101
+ elif cycle is False:
102
+ parts.append("NO CYCLE")
103
+
104
+ if not parts:
105
+ return None # no-op
106
+
107
+ sql = f"ALTER SEQUENCE {self.name} {' '.join(parts)};"
108
+ return self.tx.execute(sql, ()).scalar()
109
+
110
+ def info(self):
111
+ """
112
+ Returns a dictionary of metadata about the sequence, or None if it doesn't exist.
113
+ """
114
+ sql = f"""
115
+ SELECT *
116
+ FROM pg_sequences
117
+ WHERE schemaname = current_schema()
118
+ AND sequencename = '{self.name}'
119
+ """
120
+ row = self.tx.execute(sql, ()).fetchone()
121
+ if row is None:
122
+ return None
123
+ return dict(row)
124
+
125
+ def rename(self, new_name):
126
+ """
127
+ Renames this sequence to `new_name`. Updates self.name to the new name.
128
+ """
129
+ sql = f"ALTER SEQUENCE {self.name} RENAME TO {new_name.lower()};"
130
+ self.tx.execute(sql, ())
131
+ self.name = new_name.lower()