engforge 0.3.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.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,935 @@
1
+ import logging
2
+
3
+ from engforge.datastores.datastores.data import *
4
+ from engforge.tabulation import *
5
+ from engforge.configuration import *
6
+ from engforge.components import *
7
+
8
+ # from engforge.patterns import SingletonMeta
9
+
10
+ from engforge.analysis import *
11
+
12
+ import attr
13
+ import random
14
+
15
+ from sqlalchemy import ForeignKey, Column, Table, MetaData, Unicode, UnicodeText
16
+ from sqlalchemy.orm import relationship
17
+ from sqlalchemy.orm.collections import attribute_mapped_collection
18
+ from sqlalchemy.ext.associationproxy import association_proxy
19
+ from sqlalchemy.ext.declarative import declared_attr
20
+ from sqlalchemy.sql.sqltypes import (
21
+ BOOLEAN,
22
+ NUMERIC,
23
+ VARCHAR,
24
+ INTEGER,
25
+ INT,
26
+ Integer,
27
+ String,
28
+ Boolean,
29
+ Numeric,
30
+ )
31
+
32
+ from sqlalchemy.sql import func
33
+
34
+ from threading import Thread
35
+
36
+ from sqlalchemy.sql.expression import Insert
37
+ from sqlalchemy.dialects.postgresql import insert
38
+ from sqlalchemy.ext.compiler import compiles
39
+
40
+ # TODO: add schema info
41
+ ReportBase = declarative_base()
42
+
43
+ DEFAULT_STRING_LENGTH = 256
44
+
45
+ """A Module in which we reflect changing schema in components or other tabulationmixin instances when solved by an an analysis.
46
+
47
+ To use, add `ReporingMixin` to any engforge `Analysis` class
48
+
49
+ There are two major components to this module, that follow from the working plan:
50
+ 1) Investigate the existing database and create the tables nessicary
51
+ 2) upload the data to the database in the respective tables
52
+ 3) If nessicary update or create a view for each component and analysis (hard part!)
53
+
54
+
55
+ At the time of creation attributes with engforge validators will be created as columns in the component table, where as only numeric key value pairs will be added to the components Vertical Attribute Mapping table (VAM)
56
+
57
+ """
58
+
59
+ log = logging.getLogger("engforge-report")
60
+
61
+
62
+ # ignore duplicates
63
+ @compiles(Insert, "postgresql")
64
+ def ignore_duplicates(insert, compiler, **kw):
65
+ s = compiler.visit_insert(insert, **kw)
66
+ ignore = insert.kwargs.get("postgresql_ignore_duplicates", False)
67
+ return s if not ignore else s + " ON CONFLICT DO NOTHING"
68
+
69
+
70
+ Insert.argument_for("postgresql", "ignore_duplicates", None)
71
+
72
+ # TODO: make new sqalchemy base, dont mixin other bases into our special fancy db
73
+
74
+ # These Mixins & Base Table Classes Help Us By Defining Common Bases For tables
75
+
76
+
77
+ class MappedDictMixin:
78
+ """Adds obj[key] access to a mapped class.
79
+
80
+ This class basically proxies dictionary access to an attribute
81
+ called ``_proxied``. The class which inherits this class
82
+ should have an attribute called ``_proxied`` which points to a dictionary.
83
+
84
+ """
85
+
86
+ def __len__(self):
87
+ return len(self._proxied)
88
+
89
+ def __iter__(self):
90
+ return iter(self._proxied)
91
+
92
+ def __getitem__(self, key):
93
+ return self._proxied[key]
94
+
95
+ def __contains__(self, key):
96
+ return key in self._proxied
97
+
98
+ def __setitem__(self, key, value):
99
+ self._proxied[key] = value
100
+
101
+ def __delitem__(self, key):
102
+ del self._proxied[key]
103
+
104
+
105
+ # TODO: Bases for AttrTable, CompTable, AnalysisTable
106
+
107
+
108
+ class AttrBase(ReportBase):
109
+ __abstract__ = True
110
+ # id = Column(Integer(), primary_key=True)
111
+ key = Column(Unicode(64), nullable=False, primary_key=True)
112
+ value = Column(Numeric)
113
+
114
+ def __init__(self, result_id_in, key=None, value=None):
115
+ # log.debug(f'creating attr rid:{result_id_in} {self}, {key}={value}')
116
+
117
+ self.result_id = result_id_in
118
+
119
+ if key is not None and value is not None:
120
+ if isinstance(value, (float, int)):
121
+ self.key = key
122
+ self.value = float(value)
123
+
124
+ @property
125
+ def unique_keys(self):
126
+ rr = ResultsRegistry()
127
+ with rr.db.session_scope() as sesh:
128
+ results = list(set(sesh.query(self.__class__.key).all()))
129
+ return results
130
+
131
+ @classmethod
132
+ def items(cls):
133
+ rr = ResultsRegistry()
134
+ with rr.db.session_scope() as sesh:
135
+ results = list(sesh.query(cls).all())
136
+ return results
137
+
138
+
139
+ class TableBase(MappedDictMixin, ReportBase):
140
+ __abstract__ = True
141
+
142
+ check_types = {
143
+ String: (str,),
144
+ Integer: (int,),
145
+ Numeric: (float, int),
146
+ Boolean: (bool,),
147
+ }
148
+
149
+ store_type = {String: str, Integer: int, Numeric: float, Boolean: bool}
150
+
151
+ ignore_keys = ("id", "index")
152
+
153
+ attr_class = None
154
+
155
+ attr_store = None
156
+
157
+ def __init__(self, result_id_in, **kwargs):
158
+ self.attr_store = {}
159
+ if "result_id_in" in self.__dict__:
160
+ self.result_id_in = result_id_in
161
+
162
+ log.debug(f"creating TB id:{result_id_in} obj:{self}")
163
+ table = self.__class__.__table__
164
+ cols = self.dbi_columns
165
+
166
+ for key, val in kwargs.items():
167
+ key = key.lower()
168
+ vtype = type(val)
169
+
170
+ if key not in self.ignore_keys: # we will try to store the data
171
+ if key in cols: # we will map the data to the column
172
+ col = table.columns[key]
173
+ ctype = type(col.type)
174
+
175
+ # log.debug(f'{self} setting {key}|{col}|{ctype}|{vtype}')
176
+
177
+ in_stypes = any(
178
+ [
179
+ issubclass(ctype, check_type)
180
+ for check_type in self.check_types
181
+ ]
182
+ )
183
+
184
+ if in_stypes:
185
+ use_ctype = ctype
186
+ if not ctype in self.check_types:
187
+ valids = list(
188
+ filter(
189
+ lambda ct: issubclass(ctype, ct),
190
+ list(self.check_types.keys()),
191
+ )
192
+ )
193
+
194
+ if valids:
195
+ if len(valids) > 1:
196
+ self.warning(
197
+ "more than one type matched for!!! {ctype}->{valids} "
198
+ )
199
+ use_ctype = valids[0]
200
+ # log.debug(f'getting registered type {use_ctype} for {ctype}')
201
+
202
+ in_type = isinstance(val, self.check_types[use_ctype])
203
+
204
+ else:
205
+ in_type = False
206
+
207
+ if in_stypes and in_type:
208
+ setattr(self, key, self.store_type[use_ctype](val))
209
+ else:
210
+ log.debug(
211
+ f"{key}={val} {vtype}=>{use_ctype}? col-valid:{in_stypes} valid-type:{in_type} col:{col}"
212
+ )
213
+
214
+ elif isinstance(val, (float, int)): # dynamic mapping
215
+ if not numpy.isnan(val):
216
+ atobj = self.attr_class(result_id_in, key=key, value=val)
217
+ self.attr_store[key] = atobj
218
+ # self[key] = atobj #dont use orm for upload!
219
+
220
+ # else:
221
+ # log.debug(f'{key} not found for columns {cols}')
222
+
223
+ # these are breaking the analysis table creation since we can't make the attr_class before initiation when @declared_attr is made
224
+ # @declared_attr
225
+ # def dict_values(cls):
226
+ # if cls.attr_class:
227
+ # return relationship( cls.attr_class.__name__, collection_class = attribute_mapped_collection("key") )
228
+
229
+ # @declared_attr
230
+ # def _proxied(cls):
231
+ # return association_proxy( "dict_values", "value")
232
+
233
+ @property
234
+ def dbi_columns(self):
235
+ return list(self.__class__.__table__.columns.keys())
236
+
237
+ @classmethod
238
+ def db_columns(cls):
239
+ if "__table__" in cls.__dict__ and cls.__table__ is not None:
240
+ return set(list(cls.__table__.c.keys()))
241
+
242
+ @classmethod
243
+ def all_fields(cls):
244
+ if cls._mapped_component is None:
245
+ return []
246
+ all_table_fields = set(
247
+ [attr.lower() for attr in cls._mapped_component.cls_all_property_labels()]
248
+ )
249
+ all_attr_fields = set(
250
+ [
251
+ attr.lower()
252
+ for attr in cls._mapped_component.cls_all_attrs_fields().keys()
253
+ ]
254
+ )
255
+ all_fields = set.union(all_table_fields, all_attr_fields)
256
+ return set(list(all_fields))
257
+
258
+ @classmethod
259
+ def dynamic_columns(cls):
260
+ if cls._mapped_component is None:
261
+ return []
262
+
263
+ return set.difference(cls.all_fields(), cls.db_columns())
264
+
265
+ @classmethod
266
+ def items(cls):
267
+ rr = ResultsRegistry()
268
+ with rr.db.session_scope() as sesh:
269
+ results = list(sesh.query(cls).all())
270
+ return results
271
+
272
+
273
+ class ReportingMixin:
274
+ """Add to an analysis component"""
275
+
276
+ _report_db = None
277
+
278
+ @property
279
+ def report_db(self):
280
+ if self._report_db is not None:
281
+ return self._report_db
282
+ else:
283
+ self.warning("no report db connection set!")
284
+
285
+ # else: #this is alot, lets be more careful
286
+ # self.warning('autogenerating database...')
287
+ # report_db = DBConnection(database_name='reports')
288
+ # report_db.ensure_database_exists()
289
+
290
+ @report_db.setter
291
+ def report_db(self, inputval: DBConnection):
292
+ if isinstance(inputval, DBConnection):
293
+ self._report_db = inputval
294
+ else:
295
+ self.warning(f"Got bad value {inputval} of type {type(inputval)}")
296
+
297
+ def report_data(self, use_thread=False):
298
+ if self.report_db and self.solved:
299
+ try:
300
+ assert isinstance(self, Analysis)
301
+ rr = ResultsRegistry(self.report_db)
302
+ rr.ensure_analysis(self)
303
+ return rr.upload_analysis(self, use_thread=use_thread)
304
+
305
+ except Exception as e:
306
+ self.error(e, "Issue Reporting Data")
307
+
308
+ elif not self.solved:
309
+ self.warning("Analysis Not Solved, Cannot Upload")
310
+
311
+ elif not self.report_db:
312
+ self.warning("No Report Database Initated")
313
+
314
+
315
+ @forge
316
+ class ResultsRegistry(Configuration, metaclass=SingletonMeta):
317
+ """An instance to manage the various database mappings to this live code repository"""
318
+
319
+ default_args = {"keep_existing": True, "autoload": False}
320
+
321
+ db = attr.ib()
322
+ base = attr.ib(
323
+ default=ReportBase
324
+ ) # ,factory=declarative_base) #slot for sqlalchemy base
325
+
326
+ _component_cls_table_mapping = None
327
+
328
+ schema = "report"
329
+
330
+ analysis_table_abrv = ""
331
+ analysis_attr_abrv = "anly_attr_"
332
+ component_table_abrv = "comp_tbl_"
333
+ component_attr_abrv = "comp_attr_"
334
+
335
+ def __on_init__(self):
336
+ self.info("creating registry and ensuring db exists!")
337
+ self.db.ensure_database_exists(create_meta=False)
338
+
339
+ self.base.metadata.bind = self.db.engine
340
+ self.base.metadata.reflect(self.db.engine, autoload=True, keep_existing=True)
341
+
342
+ self._component_cls_table_mapping = {}
343
+ self.initalize()
344
+
345
+ def initalize(self):
346
+ AnalysisRegistry.__table__.create(self.db.engine, checkfirst=True)
347
+ ComponentRegistry.__table__.create(self.db.engine, checkfirst=True)
348
+
349
+ @property
350
+ def db_classes(self):
351
+ return list(set([val[0] for val in self.mapped_tables.values()]))
352
+
353
+ @property
354
+ def component_classes(self):
355
+ return list(set([val[1] for val in self.mapped_tables.values()]))
356
+
357
+ @property
358
+ def mapped_classes(self):
359
+ """schema class : db class"""
360
+ return {val[1]: val[0] for val in self.mapped_tables.values()}
361
+
362
+ @property
363
+ def mapped_tables(self):
364
+ return self._component_cls_table_mapping
365
+
366
+ @property
367
+ def analyses(self):
368
+ return Analysis.component_subclasses()
369
+
370
+ @property
371
+ def components(self):
372
+ comps = Component.component_subclasses()
373
+ comp_subclasses = set(comps)
374
+ analy_subclasses = set(Analysis.component_subclasses())
375
+ comp_keys = set.difference(comp_subclasses, analy_subclasses)
376
+
377
+ return {ckey: comps[ckey] for ckey in comp_keys}
378
+
379
+ def filter_by_validators(self, attr_obj):
380
+ if not attr_obj.validator:
381
+ return False
382
+
383
+ if not isinstance(attr_obj.validator, TAB_VALIDATOR_TYPE):
384
+ return False
385
+
386
+ valtype = type(attr_obj.validator.type)
387
+ if not valtype:
388
+ return False
389
+ if valtype is tuple and any(
390
+ [gtype in attr_obj.validator.type for gtype in (str, float, int)]
391
+ ):
392
+ return True
393
+ if any([gtype is attr_obj.validator.type for gtype in (str, float, int)]):
394
+ return True
395
+ return False
396
+
397
+ def validator_to_column(self, attr_obj):
398
+ if type(attr_obj.validator.type) is tuple and any(
399
+ [gtype in attr_obj.validator.type for gtype in (float, int)]
400
+ ):
401
+ return Column(Numeric, default=attr_obj.default, nullable=True)
402
+ if type(attr_obj.validator.type) is tuple and str in attr_obj.validator.type:
403
+ return Column(
404
+ String(DEFAULT_STRING_LENGTH),
405
+ default=attr_obj.default,
406
+ nullable=True,
407
+ )
408
+ if str is attr_obj.validator.type:
409
+ return Column(
410
+ String(DEFAULT_STRING_LENGTH),
411
+ default=attr_obj.default,
412
+ nullable=True,
413
+ )
414
+ return Column(Numeric, default=attr_obj.default, nullable=True)
415
+
416
+ def dict_attr(
417
+ self, comp_cls, target_table_id, results_table=None, is_analysis=False
418
+ ):
419
+ """we make the default entries for a components attribues table"""
420
+
421
+ # this modulelates names for componets / analysis tables
422
+ table_abrv = self.component_attr_abrv
423
+ if issubclass(comp_cls, Analysis):
424
+ table_abrv = self.analysis_attr_abrv
425
+ name = comp_cls.__name__.lower()
426
+
427
+ default_attr = {
428
+ "__tablename__": f"{table_abrv}{name}",
429
+ #'comp_id': Column(Integer,ForeignKey( target_table_id )),
430
+ "__table_args__": self.default_args,
431
+ "__abstract__": False,
432
+ }
433
+
434
+ if results_table is not None and not is_analysis: # analysis won't be mapped
435
+ default_attr["__tablename__"] = f"{results_table}_{table_abrv}{name}"
436
+ root_obj, _ = self.mapped_tables[results_table] # analysis won't be mapped
437
+ backref_name = f"db_comp_attr_{name}"
438
+ default_attr["result_id"] = Column(
439
+ Integer, ForeignKey(f"{results_table}.id"), primary_key=True
440
+ )
441
+ default_attr["analysis"] = relationship(
442
+ root_obj.__name__, backref=backref(backref_name, lazy="noload")
443
+ )
444
+
445
+ elif issubclass(comp_cls, Analysis) and is_analysis:
446
+ default_attr["__tablename__"] = f"{table_abrv}{name}"
447
+ backref_name = f"db_comp_attr_{name}"
448
+ default_attr["result_id"] = Column(
449
+ Integer, ForeignKey(target_table_id), primary_key=True
450
+ )
451
+ # default_attr['analysis'] = relationship(comp_cls.__name__,backref = backref(backref_name,lazy='noload'))
452
+
453
+ return default_attr
454
+
455
+ def default_attr(self, comp_cls, results_table=None):
456
+ """we make the default entries for a components primary table"""
457
+
458
+ table_abrv = self.component_table_abrv
459
+ if issubclass(comp_cls, Analysis):
460
+ table_abrv = self.analysis_table_abrv
461
+
462
+ name = comp_cls.__name__.lower()
463
+ # TODO: Add mapping __init__ method
464
+ default_attr = {
465
+ "__tablename__": f"{table_abrv}{name}",
466
+ #'id': Column(Integer, primary_key=True),
467
+ "__table_args__": self.default_args,
468
+ "__abstract__": False,
469
+ "_mapped_component": comp_cls,
470
+ "attr_store": None,
471
+ }
472
+
473
+ if results_table is not None:
474
+ tbl_name = f"{results_table}_{table_abrv}{name}"
475
+ backref_name = f"db_comp_{name}"
476
+ root_obj, _ = self.mapped_tables[results_table]
477
+ default_attr["__tablename__"] = f"{results_table}_{table_abrv}{name}"
478
+ default_attr["result_id"] = Column(
479
+ Integer, ForeignKey(f"{results_table}.id"), primary_key=True
480
+ )
481
+ default_attr["analysis"] = relationship(
482
+ root_obj.__name__, backref=backref(backref_name, lazy="noload")
483
+ )
484
+
485
+ else: # its an analysis
486
+ default_attr["created"] = Column(DateTime, server_default=func.now())
487
+ default_attr["active"] = Column(Boolean(), server_default="t")
488
+ default_attr["run_id"] = Column(
489
+ String(36)
490
+ ) # corresponds to uuid set in analysis
491
+ default_attr["id"]: Column(Integer, primary_key=True)
492
+ # default_attr['components'] = relationship(TableBase, back_populates= "analysis")
493
+
494
+ return default_attr
495
+
496
+ # These use keys of system_property or attr and lower() so no spaces or capitals
497
+ def all_possible_component_fields(self, cls):
498
+ all_table_fields = set([attr.lower() for attr in cls.cls_all_property_keys()])
499
+ all_attr_fields = set(
500
+ [attr.lower() for attr in cls.cls_all_attrs_fields().keys()]
501
+ )
502
+ all_fields = set.union(all_table_fields, all_attr_fields)
503
+ return set(list(all_fields))
504
+
505
+ def component_attr(self, comp_cls, results_table=None):
506
+ # #TODO: assign key values as attribute dict
507
+ # #TODO: check if class has a __tablename__ and if it does use that with, ensure component_table_ on front
508
+
509
+ component_attr = {
510
+ key.lower(): self.validator_to_column(field)
511
+ for key, field in comp_cls.cls_all_attrs_fields().items()
512
+ if self.filter_by_validators(field)
513
+ }
514
+
515
+ component_attr.update(
516
+ {
517
+ k.lower(): Column(k.lower(), Numeric(), nullable=True)
518
+ for k in comp_cls.cls_all_property_keys()
519
+ }
520
+ )
521
+
522
+ component_attr.update(self.default_attr(comp_cls, results_table=results_table))
523
+ return component_attr
524
+
525
+ def db_component_dict(self, comp_cls, results_table=None, is_analysis=False):
526
+ """returns the nessicary table types to make for reflection of input component class
527
+
528
+ this method represents the dynamic creation of SQLA typing and attributes via __dict__
529
+ """
530
+
531
+ assert isinstance(comp_cls, type)
532
+ assert Component in comp_cls.mro()
533
+
534
+ comp_attr = self.component_attr(comp_cls, results_table=results_table)
535
+ tablename = comp_attr["__tablename__"]
536
+
537
+ if results_table:
538
+ comp_cls_name = f"DB_{results_table}_{comp_cls.__name__}"
539
+
540
+ else:
541
+ comp_cls_name = f"DB_{comp_cls.__name__}"
542
+
543
+ if is_analysis:
544
+ results_table = comp_attr["__tablename__"] # pass reference for id!
545
+
546
+ if is_analysis: # We create the analysis first
547
+ if "id" not in comp_attr:
548
+ comp_attr["id"] = Column(Integer, primary_key=True)
549
+
550
+ comp_db_tup = (comp_cls_name, (TableBase,), comp_attr)
551
+ comp_db = self.check_or_create_db_type(
552
+ comp_attr["__tablename__"], comp_db_tup
553
+ )
554
+
555
+ dict_attr = self.dict_attr(
556
+ comp_cls,
557
+ f"{comp_attr['__tablename__']}.id",
558
+ results_table=results_table,
559
+ is_analysis=is_analysis,
560
+ )
561
+ dict_name = f"{comp_cls_name}_Dict"
562
+ dict_db_tup = (dict_name, (AttrBase,), dict_attr)
563
+ dict_db = self.check_or_create_db_type(
564
+ dict_attr["__tablename__"], dict_db_tup
565
+ )
566
+
567
+ comp_db.attr_class = dict_db # This delayed monkey patch is fine since we wont be able to try any init's with this class until
568
+
569
+ else:
570
+ dict_attr = self.dict_attr(
571
+ comp_cls,
572
+ f"{comp_attr['__tablename__']}.id",
573
+ results_table=results_table,
574
+ )
575
+ dict_name = f"{comp_cls_name}_Dict"
576
+ dict_db_tup = (dict_name, (AttrBase,), dict_attr)
577
+ dict_db = self.check_or_create_db_type(
578
+ dict_attr["__tablename__"], dict_db_tup
579
+ )
580
+
581
+ comp_attr["attr_class"] = dict_db
582
+ comp_db_tup = (comp_cls_name, (TableBase,), comp_attr)
583
+ comp_db = self.check_or_create_db_type(
584
+ comp_attr["__tablename__"], comp_db_tup
585
+ )
586
+
587
+ return {
588
+ comp_attr["__tablename__"]: comp_db,
589
+ dict_attr["__tablename__"]: dict_db,
590
+ }
591
+
592
+ def check_or_create_db_type(self, tablename, type_tuple):
593
+ cls_name, ttype, attr_dict = type_tuple
594
+
595
+ if self.db.engine.has_table(tablename):
596
+ remove = set(["__table_args__"])
597
+ for key, item in attr_dict.items():
598
+ if issubclass(type(item), Column):
599
+ remove.add(key)
600
+
601
+ for rkey in remove:
602
+ if rkey in attr_dict:
603
+ # self.debug(f'removing {rkey}')
604
+ attr_dict.pop(rkey)
605
+ else:
606
+ self.debug(f"couldnt find {rkey}")
607
+
608
+ attr_dict["__table__"] = self.base.metadata.tables[tablename]
609
+ attr_dict["__table_args__"] = {"autoload": True}
610
+
611
+ self.debug(f"making type: {cls_name,self.base,attr_dict}")
612
+ cls_db = type(cls_name, ttype, attr_dict)
613
+
614
+ self.debug(
615
+ f"mapped existing table to type {tablename} -> {cls_db.__name__}"
616
+ )
617
+ setattr(self, cls_db.__name__, cls_db)
618
+
619
+ return cls_db
620
+
621
+ else:
622
+ cls_db = type(*type_tuple)
623
+ self.debug(
624
+ f"created table type {cls_db.__name__} -> {cls_db.__tablename__}"
625
+ )
626
+
627
+ setattr(self, cls_db.__name__, cls_db)
628
+ return cls_db
629
+
630
+ def map_component(self, dbcomponent, component):
631
+ self.debug(f"mapping {dbcomponent},{component} -> {dbcomponent.__tablename__}")
632
+ # add to internal mapping
633
+ self._component_cls_table_mapping[dbcomponent.__tablename__] = (
634
+ dbcomponent,
635
+ component,
636
+ )
637
+
638
+ tablenames = self.db.engine.table_names()
639
+ atables = AnalysisRegistry.tablenames(self.db)
640
+ ctables = ComponentRegistry.tablenames(self.db)
641
+
642
+ self.debug(f"comparing {tablenames} | {atables} | {ctables}")
643
+ # create record of components and analyses
644
+ if isinstance(component, Analysis) or issubclass(component, Analysis):
645
+ if isinstance(component, Analysis):
646
+ component = component.__class__
647
+ if dbcomponent.__tablename__ not in atables:
648
+ self.info(f"adding analysis record { dbcomponent.__tablename__}")
649
+ with self.db.session_scope() as sesh:
650
+ rec = AnalysisRegistry(dbcomponent, component)
651
+ sesh.add(rec)
652
+
653
+ elif isinstance(component, Component) or issubclass(component, Component):
654
+ if isinstance(component, Component):
655
+ component = component.__class__
656
+ if dbcomponent.__tablename__ not in ctables:
657
+ self.info(f"adding comonent record { dbcomponent.__tablename__}")
658
+ with self.db.session_scope() as sesh:
659
+ rec = ComponentRegistry(dbcomponent, component)
660
+ sesh.add(rec)
661
+
662
+ def ensure_all_tables(self):
663
+ """creates a set of component tables linked to each analysis"""
664
+
665
+ for cls_name, cls in self.analyses.items():
666
+ self.debug(f"ensure-all-tables: {cls}")
667
+ self.ensure_analysis(cls)
668
+
669
+ def ensure_component_tables(self, results_table=None):
670
+ for cls_name, cls in self.components.items():
671
+ self.debug(f"ensure-comp-tables: {cls}")
672
+ self.ensure_component_table(cls, results_table=results_table)
673
+
674
+ def ensure_component_tables_in_analysis(self, analysis, results_table=None):
675
+ assert isinstance(analysis, Analysis)
676
+ for cls in analysis.unique_internal_components_classes:
677
+ self.debug(f"ensure-analyinst-tables: {cls}")
678
+ self.ensure_component_table(cls, results_table=results_table)
679
+
680
+ def ensure_component_table(self, component_cls, results_table=None):
681
+ self.debug(f"ensure-comp-table: {component_cls}")
682
+
683
+ is_analysis = False
684
+ if issubclass(component_cls, Analysis):
685
+ is_analysis = True
686
+
687
+ self.info(f"ensuring component {component_cls} analysis?:{is_analysis}")
688
+ db_component_type_dict = self.db_component_dict(
689
+ component_cls, results_table=results_table, is_analysis=is_analysis
690
+ )
691
+ for comp_tbl, compdb in db_component_type_dict.items():
692
+ if not self.db.engine.has_table(comp_tbl):
693
+ self.info(f"creating tables {comp_tbl}")
694
+ compdb.__table__.create(self.db.engine, checkfirst=True)
695
+
696
+ else:
697
+ self.debug(f"table exists already {comp_tbl}")
698
+
699
+ for comp_tbl, compdb in db_component_type_dict.items():
700
+ if not compdb.__name__.endswith("Dict"):
701
+ self.map_component(compdb, component_cls)
702
+
703
+ return db_component_type_dict
704
+
705
+ def ensure_analysis(self, analysis):
706
+ """pass a class or instance to create the tables, passing an instance also ensures its internal components are
707
+ tabulated"""
708
+ self.info(f"ensure-analysis: {analysis}")
709
+ analysis_cls = self.get_analysis_class(analysis)
710
+
711
+ db_component_type_dict = self.ensure_component_table(analysis_cls)
712
+
713
+ # return primary table name
714
+ out = [
715
+ dbckey
716
+ for dbckey in db_component_type_dict.keys()
717
+ if dbckey.startswith(self.analysis_table_abrv)
718
+ ]
719
+ if out:
720
+ analysis_table = out[0]
721
+ if analysis is analysis_cls:
722
+ self.ensure_component_tables(results_table=analysis_table)
723
+ else: # this is for the instance which has runtime components that we'll check
724
+ self.ensure_component_tables_in_analysis(
725
+ analysis, results_table=analysis_table
726
+ )
727
+ else:
728
+ self.warning("no analysis table found")
729
+
730
+ def get_analysis_class(self, analysis):
731
+ if isinstance(analysis, Analysis):
732
+ analysis_cls = analysis.__class__
733
+ elif issubclass(analysis, Analysis):
734
+ analysis_cls = analysis
735
+ else:
736
+ self.warning("ensure-analysis: analysis not mapped, using direct input")
737
+ analysis_cls = analysis
738
+ return analysis_cls
739
+
740
+ def upload_analysis(self, analysis, use_thread=False):
741
+ """a wrapper for upload analysis with a thread selection context,
742
+ :returns: a thread reference, which you can join after setting other work.
743
+ """
744
+
745
+ if use_thread:
746
+ thread = Thread(target=self._upload_analysis, args=(analysis,))
747
+ thread.start()
748
+ return thread
749
+
750
+ return self._upload_analysis(analysis)
751
+
752
+ def _upload_analysis(self, analysis):
753
+ """the general method here we're using is to precompute the analysis row id, then batch the inserts after
754
+ forcing the correct primary key"""
755
+ self.info(f"upload analysis: {analysis}")
756
+
757
+ class AvoidDuplicateAbortUpload(Exception):
758
+ pass
759
+
760
+ try:
761
+
762
+ def gen(table):
763
+ for item in table:
764
+ yield item
765
+
766
+ # analysis must be solved!
767
+ assert isinstance(analysis, Analysis)
768
+ assert analysis.solved
769
+
770
+ acmpcls = analysis.__class__
771
+ if acmpcls not in self.mapped_classes:
772
+ self.ensure_analysis(analysis)
773
+
774
+ adbcls = self.mapped_classes[acmpcls]
775
+
776
+ rds = analysis.recursive_data_structure()
777
+ data_gens = {}
778
+
779
+ main_gen = gen(analysis.TABLE)
780
+
781
+ for lvl, comps in rds.items():
782
+ if lvl > 0:
783
+ for comp in comps:
784
+ cmp = comp["conf"]
785
+ data_gens[self.mapped_classes[cmp.__class__]] = gen(cmp.TABLE)
786
+
787
+ # with self.db.scoped_session() as sesh:
788
+ any_succeeded = True
789
+ last_values = {}
790
+ inx = 0
791
+ main_result = None
792
+
793
+ while any_succeeded and inx < analysis.index:
794
+ any_succeeded = False # guilty until proven innocent!!
795
+
796
+ try:
797
+ data_dict = next(main_gen)
798
+ last_values[adbcls] = data_dict
799
+ main_result = adbcls(None, **data_dict)
800
+
801
+ except StopIteration:
802
+ data_dict = last_values[adbcls]
803
+ main_result = adbcls(None, **data_dict)
804
+
805
+ else:
806
+ any_succeeded = True # here's ur fricken evidence ur honor
807
+
808
+ with self.db.session_scope() as sesh:
809
+ sesh.add(main_result)
810
+
811
+ sesh.commit()
812
+ result_id = int(main_result.id)
813
+
814
+ main_attrs = list(main_result.attr_store.values())
815
+ for item in main_attrs:
816
+ item.result_id = result_id
817
+
818
+ others = []
819
+ for dbclass, generator in data_gens.items():
820
+ try:
821
+ data_dict = next(generator)
822
+ last_values[dbclass] = data_dict
823
+ data_dict["result_id"] = result_id
824
+ cmp_result = dbclass(result_id, **data_dict)
825
+ others.append(cmp_result)
826
+
827
+ except StopIteration:
828
+ data_dict = last_values[dbclass]
829
+
830
+ data_dict["result_id"] = result_id
831
+ cmp_result = dbclass(result_id, **data_dict)
832
+ others.append(cmp_result)
833
+
834
+ else:
835
+ any_succeeded = True # here's ur fricken evidence ur honor
836
+
837
+ # the scoped session rolls back anything created this time :)
838
+ if not any_succeeded:
839
+ raise AvoidDuplicateAbortUpload()
840
+
841
+ with self.db.session_scope() as sesh:
842
+ add_list = (
843
+ main_attrs
844
+ + others
845
+ + flatten([list(item.attr_store.values()) for item in others])
846
+ )
847
+
848
+ sesh.add_all(add_list)
849
+
850
+ inx += (
851
+ 1 # index += 1 is done at end of analysis so we should model that
852
+ )
853
+
854
+ except AvoidDuplicateAbortUpload:
855
+ pass # this is fine
856
+
857
+ except Exception as e:
858
+ self.error(e, "Issue Uploading Data")
859
+
860
+
861
+ # These items are registry related for the dynamic mapping
862
+
863
+
864
+ class MappedItem(ReportBase):
865
+ __abstract__ = True
866
+
867
+ _mapped_component = None
868
+
869
+ id = Column(Integer, primary_key=True)
870
+ tablename = Column(String(DEFAULT_STRING_LENGTH))
871
+ attrtable = Column(String(DEFAULT_STRING_LENGTH))
872
+ classname = Column(String(DEFAULT_STRING_LENGTH))
873
+ mro_inheritance = Column(String(1200))
874
+
875
+ active = Column(Boolean(), default=True)
876
+
877
+ def __init__(self, tablerep, mapped_class):
878
+ self.classname = mapped_class.__name__
879
+ self.tablename = tablerep.__tablename__
880
+
881
+ if issubclass(mapped_class, Analysis):
882
+ self.attrtable = f"anly_attr_{tablerep.__tablename__}"
883
+ else:
884
+ self.attrtable = tablerep.__tablename__.replace("tbl", "attr")
885
+
886
+ self._mapped_component = mapped_class
887
+
888
+ distinct = mapped_class.mro()
889
+ if mapped_class in distinct:
890
+ distinct.remove(mapped_class)
891
+ comp_inx = distinct.index(Component)
892
+ self.mro_inheritance = ".".join(
893
+ [cls.__name__ for cls in distinct[0 : comp_inx + 1]]
894
+ )
895
+
896
+ @classmethod
897
+ def db_columns(cls):
898
+ if "__table__" in cls.__dict__ and cls.__table__ is not None:
899
+ return set(list(cls.__table__.c.keys()))
900
+
901
+ @classmethod
902
+ def all_fields(cls):
903
+ all_table_fields = set(
904
+ [attr.lower() for attr in cls._mapped_component.cls_all_property_labels()]
905
+ )
906
+ all_attr_fields = set(
907
+ [
908
+ attr.lower()
909
+ for attr in cls._mapped_component.cls_all_attrs_fields().keys()
910
+ ]
911
+ )
912
+ all_fields = set.union(all_table_fields, all_attr_fields)
913
+ return set(list(all_fields))
914
+
915
+ @classmethod
916
+ def dynamic_columns(cls):
917
+ if cls._mapped_component is None:
918
+ return []
919
+
920
+ return set.difference(cls.all_fields(), cls.db_columns())
921
+
922
+ @classmethod
923
+ def tablenames(cls, db):
924
+ # rr = ResultsRegistry()
925
+ with db.session_scope() as sesh:
926
+ results = list(sesh.query(cls.tablename).all())
927
+ return flatten(results)
928
+
929
+
930
+ class AnalysisRegistry(MappedItem):
931
+ __tablename__ = "analysis_registry"
932
+
933
+
934
+ class ComponentRegistry(MappedItem):
935
+ __tablename__ = "component_registry"