Package gensaschema :: Module _table
[frames] | no frames]

Source Code for Module gensaschema._table

  1  # -*- coding: ascii -*- 
  2  r""" 
  3  ===================================== 
  4   Table inspection and representation 
  5  ===================================== 
  6   
  7  Table inspection and representation 
  8   
  9  :Copyright: 
 10   
 11   Copyright 2010 - 2017 
 12   Andr\xe9 Malo or his licensors, as applicable 
 13   
 14  :License: 
 15   
 16   Licensed under the Apache License, Version 2.0 (the "License"); 
 17   you may not use this file except in compliance with the License. 
 18   You may obtain a copy of the License at 
 19   
 20       http://www.apache.org/licenses/LICENSE-2.0 
 21   
 22   Unless required by applicable law or agreed to in writing, software 
 23   distributed under the License is distributed on an "AS IS" BASIS, 
 24   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 25   See the License for the specific language governing permissions and 
 26   limitations under the License. 
 27   
 28  """ 
 29  if __doc__:  # pragma: no branch 
 30      # pylint: disable = redefined-builtin 
 31      __doc__ = __doc__.encode('ascii').decode('unicode_escape') 
 32  __author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape') 
 33  __docformat__ = "restructuredtext en" 
 34   
 35  import logging as _logging 
 36  import operator as _op 
 37  import re as _re 
 38  import warnings as _warnings 
 39   
 40  import sqlalchemy as _sa 
 41   
 42  from . import _column 
 43  from . import _constraint 
 44  from . import _util 
 45   
 46  logger = _logging.getLogger(__name__) 
47 48 49 -class Table(object):
50 """ 51 Reflected table 52 53 :CVariables: 54 `is_reference` : ``bool`` 55 Is it a table reference or a table? 56 57 :IVariables: 58 `varname` : ``str`` 59 Variable name 60 61 `sa_table` : ``sqlalchemy.Table`` 62 Table 63 64 `constraints` : ``list`` 65 Constraint list 66 67 `_symbols` : `Symbols` 68 Symbol table 69 """ 70 is_reference = False 71
72 - def __new__(cls, varname, table, schemas, symbols):
73 """ 74 Construct 75 76 This might actually return a table reference 77 78 :Parameters: 79 `varname` : ``str`` 80 Variable name 81 82 `table` : ``sqlalchemy.Table`` 83 Table 84 85 `schemas` : ``dict`` 86 Schema -> module mapping 87 88 `symbols` : `Symbols` 89 Symbol table 90 91 :Return: `Table` or `TableReference` instance 92 :Rtype: ``Table`` or ``TableReference`` 93 """ 94 if table.schema in schemas: 95 return TableReference( 96 varname, table, schemas[table.schema], symbols 97 ) 98 return super(Table, cls).__new__(cls)
99
100 - def __init__(self, varname, table, schemas, symbols):
101 """ 102 Initialization 103 104 :Parameters: 105 `varname` : ``str`` 106 Variable name 107 108 `table` : ``sqlalchemy.Table`` 109 Table 110 111 `schemas` : ``dict`` 112 Schema -> module mapping 113 114 `symbols` : `Symbols` 115 Symbol table 116 """ 117 # pylint: disable = unused-argument 118 119 symbols[u'table_%s' % table.name] = varname 120 self._symbols = symbols 121 self.varname = varname 122 self.sa_table = table 123 self.constraints = list(filter(None, [_constraint.Constraint( 124 con, self.varname, self._symbols, 125 ) for con in table.constraints]))
126 127 @classmethod
128 - def by_name(cls, name, varname, metadata, schemas, symbols, types=None):
129 """ 130 Construct by name 131 132 :Parameters: 133 `name` : ``str`` 134 Table name (possibly qualified) 135 136 `varname` : ``str`` 137 Variable name of the table 138 139 `metadata` : SA (bound) metadata 140 Metadata container 141 142 `schemas` : ``dict`` 143 Schema -> module mapping 144 145 `symbols` : `Symbols` 146 Symbol table 147 148 `types` : callable 149 Extra type loader. If the type reflection fails, because 150 SQLAlchemy cannot resolve it, the type loader will be called with 151 the type name, (bound) metadata and the symbol table. It is 152 responsible for modifying the symbols and imports *and* the 153 dialect's ``ischema_names``. If omitted or ``None``, the reflector 154 will always fail on unknown types. 155 156 :Return: New Table instance 157 :Rtype: `Table` 158 """ 159 kwargs = {} 160 if '.' in name: 161 schema, name = name.split('.') 162 kwargs['schema'] = schema 163 else: 164 schema = None 165 166 tmatch = _re.compile(u"^Did not recognize type (.+) of column").match 167 def type_name(e): 168 """ Extract type name from exception """ 169 match = tmatch(e.args[0]) 170 if match: 171 type_name = match.group(1).strip() 172 if type_name.startswith(('"', "'")): 173 type_name = type_name[1:-1] 174 return type_name or None
175 176 with _warnings.catch_warnings(): 177 _warnings.filterwarnings('error', category=_sa.exc.SAWarning, 178 message=r'^Did not recognize type ') 179 _warnings.filterwarnings('error', category=_sa.exc.SAWarning, 180 message=r'^Unknown column definition ') 181 _warnings.filterwarnings('error', category=_sa.exc.SAWarning, 182 message=r'^Incomplete reflection of ' 183 r'column definition') 184 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning, 185 message=r'^Could not instantiate type ') 186 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning, 187 message=r'^Skipped unsupported ' 188 r'reflection of expression-based' 189 r' index ') 190 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning, 191 message=r'^Predicate of partial index ') 192 193 seen = set() 194 while True: 195 try: 196 table = _sa.Table(name, metadata, autoload=True, **kwargs) 197 except _sa.exc.SAWarning as e: 198 if types is not None: 199 tname = type_name(e) 200 if tname and tname not in seen: 201 stack = [tname] 202 while stack: 203 try: 204 types(stack[-1], metadata, symbols) 205 except _sa.exc.SAWarning as e: 206 tname = type_name(e) 207 if tname and tname not in stack and \ 208 tname not in seen: 209 stack.append(tname) 210 continue 211 raise 212 else: 213 seen.add(stack.pop()) 214 continue 215 raise 216 else: 217 break 218 219 return cls(varname, table, schemas, symbols)
220
221 - def __repr__(self):
222 """ 223 Make string representation 224 225 :Return: The string representation 226 :Rtype: ``str`` 227 """ 228 args = [ 229 repr(_column.Column.from_sa(col, self._symbols)) 230 for col in self.sa_table.columns 231 ] 232 if self.sa_table.schema is not None: 233 args.append('schema=%r' % (_util.unicode(self.sa_table.schema),)) 234 235 args = ',\n '.join(args) 236 if args: 237 args = ',\n %s,\n' % args 238 result = "%s(%r, %s%s)" % ( 239 self._symbols['table'], 240 _util.unicode(self.sa_table.name), 241 self._symbols['meta'], 242 args, 243 ) 244 if self.constraints: 245 result = "\n".join(( 246 result, '\n'.join(map(repr, sorted(self.constraints))) 247 )) 248 return result
249
250 251 -class TableReference(object):
252 """ Referenced table """ 253 is_reference = True 254
255 - def __init__(self, varname, table, schema, symbols):
256 """ 257 Initialization 258 259 :Parameters: 260 `varname` : ``str`` 261 Variable name 262 263 `table` : ``sqlalchemy.Table`` 264 Table 265 266 `symbols` : `Symbols` 267 Symbol table 268 """ 269 self.varname = varname 270 self.sa_table = table 271 self.constraints = [] 272 pkg, mod = schema.rsplit('.', 1) 273 if not mod.startswith('_'): 274 modas = '_' + mod 275 symbols.imports[schema] = 'from %s import %s as %s' % ( 276 pkg, mod, modas 277 ) 278 mod = modas 279 else: 280 symbols.imports[schema] = 'from %s import %s' % (pkg, mod) 281 symbols[u'table_%s' % table.name] = "%s.%s" % (mod, varname)
282
283 284 -class TableCollection(tuple):
285 """ Table collection """ 286 287 @classmethod
288 - def by_names(cls, metadata, names, schemas, symbols, types=None):
289 """ 290 Construct by table names 291 292 :Parameters: 293 `metadata` : ``sqlalchemy.MetaData`` 294 Metadata 295 296 `names` : iterable 297 Name list (list of tuples (varname, name)) 298 299 `symbols` : `Symbols` 300 Symbol table 301 302 `types` : callable 303 Extra type loader. If the type reflection fails, because 304 SQLAlchemy cannot resolve it, the type loader will be called with 305 the type name, (bound) metadata and the symbol table. It is 306 responsible for modifying the symbols and imports *and* the 307 dialect's ``ischema_names``. If omitted or ``None``, the reflector 308 will always fail on unknown types. 309 310 :Return: New table collection instance 311 :Rtype: `TableCollection` 312 """ 313 objects = dict((table.sa_table.key, table) for table in [ 314 Table.by_name(name, varname, metadata, schemas, symbols, 315 types=types) 316 for varname, name in names 317 ]) 318 319 def map_table(sa_table): 320 """ Map SA table to table object """ 321 if sa_table.key not in objects: 322 varname = sa_table.name 323 if _util.py2 and \ 324 isinstance(varname, 325 _util.unicode): # pragma: no cover 326 varname = varname.encode('ascii') 327 objects[sa_table.key] = Table( 328 varname, sa_table, schemas, symbols 329 ) 330 return objects[sa_table.key]
331 332 tables = list(map(map_table, metadata.tables.values())) 333 tables.sort(key=lambda x: (not(x.is_reference), x.varname)) 334 335 _break_cycles(metadata) 336 seen = set() 337 338 for table in tables: 339 seen.add(table.sa_table.key) 340 for con in table.constraints: 341 # pylint: disable = unidiomatic-typecheck 342 if type(con) == _constraint.ForeignKeyConstraint: 343 if con.options == 'seen': 344 continue 345 346 remote_key = con.constraint.elements[0].column.table.key 347 if remote_key not in seen: 348 con.options = 'unseen: %s' % ( 349 objects[remote_key].varname, 350 ) 351 remote_con = con.copy() 352 remote_con.options = 'seen: %s' % (table.varname,) 353 objects[remote_key].constraints.append(remote_con) 354 355 return cls(tables)
356
357 358 -def _break_cycles(metadata):
359 """ 360 Find foreign key cycles and break them apart 361 362 :Parameters: 363 `metadata` : ``sqlalchemy.MetaData`` 364 Metadata 365 """ 366 def break_cycle(e): 367 """ Break foreign key cycle """ 368 cycle_keys = set(map(_op.attrgetter('key'), e.cycles)) 369 cycle_path = [ 370 (parent, child) 371 for parent, child in e.edges 372 if parent.key in cycle_keys and child.key in cycle_keys 373 ] 374 deps = [cycle_path.pop()] 375 while cycle_path: 376 tmp = [] 377 for parent, child in cycle_path: 378 if parent == deps[-1][1]: 379 deps.append((parent, child)) 380 else: 381 tmp.append((parent, child)) 382 if len(tmp) == len(cycle_path): 383 raise AssertionError("Could not construct sorted cycle path") 384 cycle_path = tmp 385 if deps[0][0].key != deps[-1][1].key: 386 raise AssertionError("Could not construct sorted cycle path") 387 388 deps = list(map(_op.itemgetter(0), deps)) 389 first_dep = list(sorted(deps, key=_op.attrgetter('name')))[0] 390 while first_dep != deps[-1]: 391 deps = [deps[-1]] + deps[:-1] 392 deps.reverse() 393 logger.debug("Found foreign key cycle: %s", " -> ".join([ 394 repr(table.name) for table in deps + [deps[0]] 395 ])) 396 397 def visit_foreign_key(fkey): 398 """ Visit foreign key """ 399 if fkey.column.table == deps[1]: 400 fkey.use_alter = True 401 fkey.constraint.use_alter = True
402 403 _sa.sql.visitors.traverse(deps[0], dict(schema_visitor=True), dict( 404 foreign_key=visit_foreign_key, 405 )) 406 407 while True: 408 try: 409 metadata.sorted_tables 410 except _sa.exc.CircularDependencyError as e: 411 break_cycle(e) 412 else: 413 break 414