| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
27 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -16,3 +16,5 @@ dj_local_conf.json | |||
| 16 | 16 | build/ | |
| 17 | 17 | .coverage | |
| 18 | 18 | ./tests/.coverage | |
| 19 | + ./tests/dj-store/* | ||
| 20 | + *.log | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,9 +1,12 @@ | |||
| 1 | 1 | """autopopulate containing the dj.AutoPopulate class. See `dj.AutoPopulate` for more info.""" | |
| 2 | 2 | import logging | |
| 3 | 3 | import datetime | |
| 4 | + import traceback | ||
| 4 | 5 | import random | |
| 6 | + from tqdm import tqdm | ||
| 7 | + from itertools import count | ||
| 5 | 8 | from pymysql import OperationalError | |
| 6 | - from .relational_operand import RelationalOperand, AndList | ||
| 9 | + from .relational_operand import RelationalOperand, AndList, U | ||
| 7 | 10 | from . import DataJointError | |
| 8 | 11 | from . import key as KEY | |
| 9 | 12 | from .base_relation import FreeRelation | |
@@ -18,15 +21,15 @@ class AutoPopulate: | |||
| 18 | 21 | """ | |
| 19 | 22 | AutoPopulate is a mixin class that adds the method populate() to a Relation class. | |
| 20 | 23 | Auto-populated relations must inherit from both Relation and AutoPopulate, | |
| 21 | - must define the property `key_source`, and must define the callback method _make_tuples. | ||
| 24 | + must define the property `key_source`, and must define the callback method `make`. | ||
| 22 | 25 | """ | |
| 23 | 26 | _key_source = None | |
| 24 | 27 | ||
| 25 | 28 | @property | |
| 26 | 29 | def key_source(self): | |
| 27 | 30 | """ | |
| 28 | 31 | :return: the relation whose primary key values are passed, sequentially, to the | |
| 29 | - `_make_tuples` method when populate() is called.The default value is the | ||
| 32 | + ``make`` method when populate() is called.The default value is the | ||
| 30 | 33 | join of the parent relations. Users may override to change the granularity | |
| 31 | 34 | or the scope of populate() calls. | |
| 32 | 35 | """ | |
@@ -40,13 +43,15 @@ def key_source(self): | |||
| 40 | 43 | self._key_source *= FreeRelation(self.connection, parents.pop(0)).proj() | |
| 41 | 44 | return self._key_source | |
| 42 | 45 | ||
| 43 | - def _make_tuples(self, key): | ||
| 46 | + | ||
| 47 | + def make(self, key): | ||
| 44 | 48 | """ | |
| 45 | - Derived classes must implement method _make_tuples that fetches data from tables that are | ||
| 49 | + Derived classes must implement method `make` that fetches data from tables that are | ||
| 46 | 50 | above them in the dependency hierarchy, restricting by the given key, computes dependent | |
| 47 | 51 | attributes, and inserts the new tuples into self. | |
| 48 | 52 | """ | |
| 49 | - raise NotImplementedError('Subclasses of AutoPopulate must implement the method "_make_tuples"') | ||
| 53 | + raise NotImplementedError('Subclasses of AutoPopulate must implement the method `make`') | ||
| 54 | + | ||
| 50 | 55 | ||
| 51 | 56 | @property | |
| 52 | 57 | def target(self): | |
@@ -63,16 +68,19 @@ def _job_key(self, key): | |||
| 63 | 68 | """ | |
| 64 | 69 | return key | |
| 65 | 70 | ||
| 66 | - def populate(self, *restrictions, suppress_errors=False, reserve_jobs=False, order="original", limit=None): | ||
| 71 | + def populate(self, *restrictions, suppress_errors=False, reserve_jobs=False, | ||
| 72 | + order="original", limit=None, max_calls=None, display_progress=False): | ||
| 67 | 73 | """ | |
| 68 | - rel.populate() calls rel._make_tuples(key) for every primary key in self.key_source | ||
| 74 | + rel.populate() calls rel.make(key) for every primary key in self.key_source | ||
| 69 | 75 | for which there is not already a tuple in rel. | |
| 70 | 76 | ||
| 71 | 77 | :param restrictions: a list of restrictions each restrict (rel.key_source - target.proj()) | |
| 72 | 78 | :param suppress_errors: suppresses error if true | |
| 73 | 79 | :param reserve_jobs: if true, reserves job to populate in asynchronous fashion | |
| 74 | 80 | :param order: "original"|"reverse"|"random" - the order of execution | |
| 75 | - :param limit: if not None, populates at max that many keys | ||
| 81 | + :param display_progress: if True, report progress_bar | ||
| 82 | + :param limit: if not None, checks at most that many keys | ||
| 83 | + :param max_calls: if not None, populates at max that many keys | ||
| 76 | 84 | """ | |
| 77 | 85 | if self.connection.in_transaction: | |
| 78 | 86 | raise DataJointError('Populate cannot be called during a transaction.') | |
@@ -84,11 +92,20 @@ def populate(self, *restrictions, suppress_errors=False, reserve_jobs=False, ord | |||
| 84 | 92 | todo = self.key_source | |
| 85 | 93 | if not isinstance(todo, RelationalOperand): | |
| 86 | 94 | raise DataJointError('Invalid key_source value') | |
| 87 | - todo = todo.proj() & AndList(restrictions) | ||
| 95 | + todo = (todo & AndList(restrictions)).proj() | ||
| 88 | 96 | ||
| 89 | - error_list = [] if suppress_errors else None | ||
| 97 | + # raise error if the populated target lacks any attributes from the primary key of key_source | ||
| 98 | + try: | ||
| 99 | + raise DataJointError( | ||
| 100 | + 'The populate target lacks attribute %s from the primary key of key_source' % next( | ||
| 101 | + name for name in todo.heading if name not in self.target.heading)) | ||
| 102 | + except StopIteration: | ||
| 103 | + pass | ||
| 104 | + | ||
| 105 | + todo -= self.target | ||
| 90 | 106 | ||
| 91 | - jobs = self.connection.jobs[self.target.database] if reserve_jobs else None | ||
| 107 | + error_list = [] if suppress_errors else None | ||
| 108 | + jobs = self.connection.schemas[self.target.database].jobs if reserve_jobs else None | ||
| 92 | 109 | ||
| 93 | 110 | # define and setup signal handler for SIGTERM | |
| 94 | 111 | if reserve_jobs: | |
@@ -97,15 +114,20 @@ def handler(signum, frame): | |||
| 97 | 114 | raise SystemExit('SIGTERM received') | |
| 98 | 115 | old_handler = signal.signal(signal.SIGTERM, handler) | |
| 99 | 116 | ||
| 100 | - todo -= self.target | ||
| 101 | 117 | keys = todo.fetch(KEY, limit=limit) | |
| 102 | 118 | if order == "reverse": | |
| 103 | 119 | keys.reverse() | |
| 104 | 120 | elif order == "random": | |
| 105 | 121 | random.shuffle(keys) | |
| 106 | 122 | ||
| 123 | + call_count = count() | ||
| 107 | 124 | logger.info('Found %d keys to populate' % len(keys)) | |
| 108 | - for key in keys: | ||
| 125 | + | ||
| 126 | + make = self._make_tuples if hasattr(self, '_make_tuples') else self.make | ||
| 127 | + | ||
| 128 | + for key in (tqdm(keys) if display_progress else keys): | ||
| 129 | + if max_calls is not None and call_count >= max_calls: | ||
| 130 | + break | ||
| 109 | 131 | if not reserve_jobs or jobs.reserve(self.target.table_name, self._job_key(key)): | |
| 110 | 132 | self.connection.start_transaction() | |
| 111 | 133 | if key in self.target: # already populated | |
@@ -114,8 +136,9 @@ def handler(signum, frame): | |||
| 114 | 136 | jobs.complete(self.target.table_name, self._job_key(key)) | |
| 115 | 137 | else: | |
| 116 | 138 | logger.info('Populating: ' + str(key)) | |
| 139 | + next(call_count) | ||
| 117 | 140 | try: | |
| 118 | - self._make_tuples(dict(key)) | ||
| 141 | + make(dict(key)) | ||
| 119 | 142 | except (KeyboardInterrupt, SystemExit, Exception) as error: | |
| 120 | 143 | try: | |
| 121 | 144 | self.connection.cancel_transaction() | |
@@ -124,7 +147,8 @@ def handler(signum, frame): | |||
| 124 | 147 | if reserve_jobs: | |
| 125 | 148 | # show error name and error message (if any) | |
| 126 | 149 | error_message = ': '.join([error.__class__.__name__, str(error)]).strip(': ') | |
| 127 | - jobs.error(self.target.table_name, self._job_key(key), error_message=error_message) | ||
| 150 | + jobs.error(self.target.table_name, self._job_key(key), | ||
| 151 | + error_message=error_message, error_stack=traceback.format_exc()) | ||
| 128 | 152 | ||
| 129 | 153 | if not suppress_errors or isinstance(error, SystemExit): | |
| 130 | 154 | raise | |
@@ -139,17 +163,18 @@ def handler(signum, frame): | |||
| 139 | 163 | # place back the original signal handler | |
| 140 | 164 | if reserve_jobs: | |
| 141 | 165 | signal.signal(signal.SIGTERM, old_handler) | |
| 142 | - | ||
| 143 | 166 | return error_list | |
| 144 | 167 | ||
| 145 | 168 | def progress(self, *restrictions, display=True): | |
| 146 | 169 | """ | |
| 147 | 170 | report progress of populating this table | |
| 148 | 171 | :return: remaining, total -- tuples to be populated | |
| 149 | 172 | """ | |
| 150 | - todo = self.key_source & AndList(restrictions) | ||
| 173 | + todo = (self.key_source & AndList(restrictions)).proj() | ||
| 174 | + if any(name not in self.target.heading for name in todo.heading): | ||
| 175 | + raise DataJointError('The populated target must have all the attributes of the key source') | ||
| 151 | 176 | total = len(todo) | |
| 152 | - remaining = len(todo.proj() - self.target) | ||
| 177 | + remaining = len(todo - self.target) | ||
| 153 | 178 | if display: | |
| 154 | 179 | print('%-20s' % self.__class__.__name__, | |
| 155 | 180 | 'Completed %d of %d (%2.1f%%) %s' % ( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,6 +30,7 @@ class BaseRelation(RelationalOperand): | |||
| 30 | 30 | _context = None | |
| 31 | 31 | database = None | |
| 32 | 32 | _log_ = None | |
| 33 | + _external_table = None | ||
| 33 | 34 | ||
| 34 | 35 | # -------------- required by RelationalOperand ----------------- # | |
| 35 | 36 | @property | |
@@ -50,14 +51,21 @@ def context(self): | |||
| 50 | 51 | ||
| 51 | 52 | def declare(self): | |
| 52 | 53 | """ | |
| 53 | - Loads the table heading. If the table is not declared, use self.definition to declare | ||
| 54 | + Use self.definition to declare the table in the database | ||
| 54 | 55 | """ | |
| 55 | 56 | try: | |
| 56 | - self.connection.query( | ||
| 57 | - declare(self.full_table_name, self.definition, self._context)) | ||
| 57 | + sql, uses_external = declare(self.full_table_name, self.definition, self._context) | ||
| 58 | + if uses_external: | ||
| 59 | + # trigger the creation of the external hash lookup for the current schema | ||
| 60 | + external_table = self.connection.schemas[self.database].external_table | ||
| 61 | + sql = sql.format(external_table=external_table.full_table_name) | ||
| 62 | + self.connection.query(sql) | ||
| 58 | 63 | except pymysql.OperationalError as error: | |
| 64 | + # skip if no create privilege | ||
| 59 | 65 | if error.args[0] == server_error_codes['command denied']: | |
| 60 | 66 | logger.warning(error.args[1]) | |
| 67 | + else: | ||
| 68 | + raise | ||
| 61 | 69 | else: | |
| 62 | 70 | self._log('Declared ' + self.full_table_name) | |
| 63 | 71 | ||
@@ -114,6 +122,12 @@ def _log(self): | |||
| 114 | 122 | self._log_ = Log(self.connection, database=self.database) | |
| 115 | 123 | return self._log_ | |
| 116 | 124 | ||
| 125 | + @property | ||
| 126 | + def external_table(self): | ||
| 127 | + if self._external_table is None: | ||
| 128 | + self._external_table = self.connection.schemas[self.database].external_table | ||
| 129 | + return self._external_table | ||
| 130 | + | ||
| 117 | 131 | def insert1(self, row, **kwargs): | |
| 118 | 132 | """ | |
| 119 | 133 | Insert one data record or one Mapping (like a dict). | |
@@ -142,60 +156,29 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields | |||
| 142 | 156 | warnings.warn('Use of `ignore_errors` in `insert` and `insert1` is deprecated. Use try...except... ' | |
| 143 | 157 | 'to explicitly handle any errors', stacklevel=2) | |
| 144 | 158 | ||
| 145 | - # handle query safely - if skip_duplicates=True, wraps the query with transaction and checks for warning | ||
| 146 | - def safe_query(*args, **kwargs): | ||
| 147 | - if skip_duplicates: | ||
| 148 | - # check if there is already an open transaction | ||
| 149 | - open_transaction = self.connection.in_transaction | ||
| 150 | - if not open_transaction: | ||
| 151 | - self.connection.start_transaction() | ||
| 152 | - try: | ||
| 153 | - with warnings.catch_warnings(record=True) as ws: | ||
| 154 | - warnings.simplefilter('always') | ||
| 155 | - self.connection.query(*args, suppress_warnings=False, **kwargs) | ||
| 156 | - for w in ws: | ||
| 157 | - if w.message.args[0] != server_error_codes['duplicate entry']: | ||
| 158 | - raise InternalError(w.message.args) | ||
| 159 | - except: | ||
| 160 | - if not open_transaction: | ||
| 161 | - try: | ||
| 162 | - self.connection.cancel_transaction() | ||
| 163 | - except OperationalError: | ||
| 164 | - pass | ||
| 165 | - raise | ||
| 166 | - else: | ||
| 167 | - if not open_transaction: | ||
| 168 | - self.connection.commit_transaction() | ||
| 169 | - else: | ||
| 170 | - self.connection.query(*args, **kwargs) | ||
| 171 | - | ||
| 159 | + heading = self.heading | ||
| 172 | 160 | if isinstance(rows, RelationalOperand): | |
| 173 | - # INSERT FROM SELECT - build alternate field-narrowing query (only) when needed | ||
| 174 | - if ignore_extra_fields and not all(name in self.heading.names for name in rows.heading.names): | ||
| 175 | - query = 'INSERT{ignore} INTO {table} ({fields}) SELECT {fields} FROM ({select}) as `__alias`'.format( | ||
| 176 | - ignore=" IGNORE" if skip_duplicates else "", | ||
| 177 | - table=self.full_table_name, | ||
| 178 | - fields='`'+'`,`'.join(self.heading.names)+'`', | ||
| 179 | - select=rows.make_sql()) | ||
| 180 | - else: | ||
| 181 | - query = 'INSERT{ignore} INTO {table} ({fields}) {select}'.format( | ||
| 182 | - ignore=" IGNORE" if skip_duplicates else "", | ||
| 161 | + # insert from select | ||
| 162 | + if not ignore_extra_fields: | ||
| 163 | + try: | ||
| 164 | + raise DataJointError( | ||
| 165 | + "Attribute %s not found. To ignore extra attributes in insert, set ignore_extra_fields=True." % | ||
| 166 | + next(name for name in rows.heading if name not in heading)) | ||
| 167 | + except StopIteration: | ||
| 168 | + pass | ||
| 169 | + fields = list(name for name in heading if name in rows.heading) | ||
| 170 | + | ||
| 171 | + query = '{command} INTO {table} ({fields}) {select}{duplicate}'.format( | ||
| 172 | + command='REPLACE' if replace else 'INSERT', | ||
| 173 | + fields='`' + '`,`'.join(fields) + '`', | ||
| 183 | 174 | table=self.full_table_name, | |
| 184 | - fields='`'+'`,`'.join(rows.heading.names)+'`', | ||
| 185 | - select=rows.make_sql()) | ||
| 186 | - try: | ||
| 187 | - safe_query(query) | ||
| 188 | - except (InternalError, IntegrityError) as err: | ||
| 189 | - if err.args[0] == server_error_codes['unknown column']: | ||
| 190 | - # args[1] -> Unknown column 'extra' in 'field list' | ||
| 191 | - raise DataJointError('{} : To ignore extra fields, set ignore_extra_fields=True in insert.'.format(err.args[1])) | ||
| 192 | - elif err.args[0] == server_error_codes['duplicate entry']: | ||
| 193 | - raise DataJointError('{} : To ignore duplicate entries, set skip_duplicates=True in insert.'.format(err.args[1])) | ||
| 194 | - else: | ||
| 195 | - raise | ||
| 175 | + select=rows.make_sql(select_fields=fields), | ||
| 176 | + duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`'.format(pk=self.primary_key[0]) | ||
| 177 | + if skip_duplicates else '') | ||
| 178 | + ) | ||
| 179 | + self.connection.query(query) | ||
| 196 | 180 | return | |
| 197 | 181 | ||
| 198 | - heading = self.heading | ||
| 199 | 182 | if heading.attributes is None: | |
| 200 | 183 | logger.warning('Could not access table {table}'.format(table=self.full_table_name)) | |
| 201 | 184 | return | |
@@ -218,16 +201,18 @@ def make_placeholder(name, value): | |||
| 218 | 201 | """ | |
| 219 | 202 | if ignore_extra_fields and name not in heading: | |
| 220 | 203 | return None | |
| 221 | - if heading[name].is_blob: | ||
| 222 | - value = pack(value) | ||
| 223 | - placeholder = '%s' | ||
| 204 | + if heading[name].is_external: | ||
| 205 | + placeholder, value = '%s', self.external_table.put(heading[name].type, value) | ||
| 206 | + elif heading[name].is_blob: | ||
| 207 | + if value is None: | ||
| 208 | + placeholder, value = 'NULL', None | ||
| 209 | + else: | ||
| 210 | + placeholder, value = '%s', pack(value) | ||
| 224 | 211 | elif heading[name].numeric: | |
| 225 | 212 | if value is None or value == '' or np.isnan(np.float(value)): # nans are turned into NULLs | |
| 226 | - placeholder = 'NULL' | ||
| 227 | - value = None | ||
| 213 | + placeholder, value = 'NULL', None | ||
| 228 | 214 | else: | |
| 229 | - placeholder = '%s' | ||
| 230 | - value = str(int(value) if isinstance(value, bool) else value) | ||
| 215 | + placeholder, value = '%s', (str(int(value) if isinstance(value, bool) else value)) | ||
| 231 | 216 | else: | |
| 232 | 217 | placeholder = '%s' | |
| 233 | 218 | return name, placeholder, value | |
@@ -284,13 +269,15 @@ def check_fields(fields): | |||
| 284 | 269 | rows = list(make_row_to_insert(row) for row in rows) | |
| 285 | 270 | if rows: | |
| 286 | 271 | try: | |
| 287 | - safe_query( | ||
| 288 | - "{command} INTO {destination}(`{fields}`) VALUES {placeholders}".format( | ||
| 289 | - command='REPLACE' if replace else 'INSERT IGNORE' if skip_duplicates else 'INSERT', | ||
| 290 | - destination=self.from_clause, | ||
| 291 | - fields='`,`'.join(field_list), | ||
| 292 | - placeholders=','.join('(' + ','.join(row['placeholders']) + ')' for row in rows)), | ||
| 293 | - args=list(itertools.chain.from_iterable((v for v in r['values'] if v is not None) for r in rows))) | ||
| 272 | + query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format( | ||
| 273 | + command='REPLACE' if replace else 'INSERT', | ||
| 274 | + destination=self.from_clause, | ||
| 275 | + fields='`,`'.join(field_list), | ||
| 276 | + placeholders=','.join('(' + ','.join(row['placeholders']) + ')' for row in rows), | ||
| 277 | + duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`'.format(pk=self.primary_key[0]) | ||
| 278 | + if skip_duplicates else '')) | ||
| 279 | + self.connection.query(query, args=list( | ||
| 280 | + itertools.chain.from_iterable((v for v in r['values'] if v is not None) for r in rows))) | ||
| 294 | 281 | except (OperationalError, InternalError, IntegrityError) as err: | |
| 295 | 282 | if err.args[0] == server_error_codes['command denied']: | |
| 296 | 283 | raise DataJointError('Command denied: %s' % err.args[1]) from None | |
@@ -304,7 +291,6 @@ def check_fields(fields): | |||
| 304 | 291 | else: | |
| 305 | 292 | raise | |
| 306 | 293 | ||
| 307 | - | ||
| 308 | 294 | def delete_quick(self): | |
| 309 | 295 | """ | |
| 310 | 296 | Deletes the table without cascading and without user prompt. If this table has any dependent | |
@@ -338,7 +324,7 @@ def delete(self): | |||
| 338 | 324 | # restrict by self | |
| 339 | 325 | if self.restrictions: | |
| 340 | 326 | restrict_by_me.add(self.full_table_name) | |
| 341 | - restrictions[self.full_table_name].append(self.restrictions.simplify()) # copy own restrictions | ||
| 327 | + restrictions[self.full_table_name].append(self.restrictions) # copy own restrictions | ||
| 342 | 328 | # restrict by renamed nodes | |
| 343 | 329 | restrict_by_me.update(table for table in delete_list if table.isdigit()) # restrict by all renamed nodes | |
| 344 | 330 | # restrict by tables restricted by a non-primary semijoin | |
@@ -454,7 +440,7 @@ def describe(self): | |||
| 454 | 440 | if attr.name in fk_props['referencing_attributes']: | |
| 455 | 441 | do_include = False | |
| 456 | 442 | if attributes_thus_far.issuperset(fk_props['referencing_attributes']): | |
| 457 | - # simple foreign keys | ||
| 443 | + # simple foreign key | ||
| 458 | 444 | parents.pop(parent_name) | |
| 459 | 445 | if not parent_name.isdigit(): | |
| 460 | 446 | definition += '-> {class_name}\n'.format( | |
@@ -473,9 +459,10 @@ def describe(self): | |||
| 473 | 459 | attributes_declared.update(fk_props['referencing_attributes']) | |
| 474 | 460 | if do_include: | |
| 475 | 461 | attributes_declared.add(attr.name) | |
| 462 | + name = attr.name.lstrip('_') # for external | ||
| 476 | 463 | definition += '%-20s : %-28s # %s\n' % ( | |
| 477 | - attr.name if attr.default is None else '%s=%s' % (attr.name, attr.default), | ||
| 478 | - '%s%s' % (attr.type, 'auto_increment' if attr.autoincrement else ''), attr.comment) | ||
| 464 | + name if attr.default is None else '%s=%s' % (name, attr.default), | ||
| 465 | + '%s%s' % (attr.type, ' auto_increment' if attr.autoincrement else ''), attr.comment) | ||
| 479 | 466 | print(definition) | |
| 480 | 467 | return definition | |
| 481 | 468 | ||
@@ -547,7 +534,7 @@ def lookup_class_name(name, context, depth=3): | |||
| 547 | 534 | except AttributeError: | |
| 548 | 535 | pass # not a UserRelation -- cannot have part tables. | |
| 549 | 536 | else: | |
| 550 | - for part in (getattr(member, p) for p in parts): | ||
| 537 | + for part in (getattr(member, p) for p in parts if hasattr(member, p)): | ||
| 551 | 538 | if inspect.isclass(part) and issubclass(part, BaseRelation) and part.full_table_name == name: | |
| 552 | 539 | return '.'.join([node['context_name'], member_name, part.__name__]).lstrip('.') | |
| 553 | 540 | elif node['depth'] > 0 and inspect.ismodule(member) and member.__name__ != 'datajoint': | |
| Back | FazBrowse Home | New Git URL |
0 commit comments