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
engforge/reporting.py ADDED
@@ -0,0 +1,595 @@
1
+ """The set of reporters define an interface to report plots or tables"""
2
+
3
+ from engforge.configuration import Configuration, forge
4
+ from engforge.typing import Options
5
+ from engforge.logging import LoggingMixin
6
+ import attrs
7
+ import abc
8
+
9
+ import os, pathlib
10
+ import datetime
11
+
12
+
13
+ def path_exist_validator(inst, attr, value):
14
+ if not isinstance(value, str):
15
+ raise ValueError(f"{attr.name} isnt a string")
16
+ if not os.path.exists(value):
17
+ raise FileExistsError(f"{value} does not exist")
18
+
19
+
20
+ # BASE CLASSSES (PLOT + TABLE)
21
+ @attrs.define
22
+ class Reporter(LoggingMixin):
23
+ """A mixin intended"""
24
+
25
+ name: str = attrs.field(default="reporter")
26
+
27
+ def check_config(self):
28
+ """a test to see if the reporter should be used"""
29
+ return True
30
+
31
+ @classmethod
32
+ def subclasses(cls, out=None):
33
+ """get all reporters of this type"""
34
+ if out is None:
35
+ out = set()
36
+
37
+ for scls in cls.__subclasses__():
38
+ out.add(scls)
39
+ scls.subclasses(scls)
40
+
41
+ def upload(self, analysis: "Analysis"):
42
+ self.info(f"uploading {analysis.identity} to {self.identity}")
43
+ raise NotImplemented()
44
+
45
+
46
+ # Functionality Mixins
47
+ @attrs.define
48
+ class TemporalReporterMixin(Reporter):
49
+ """Provide single or periodic keys"""
50
+
51
+ # report_mode: Options("single", "daily", "monthly") #choose wisely
52
+
53
+ @property
54
+ def report_root(self):
55
+ raise NotImplemented()
56
+
57
+ @property
58
+ def date_key(self):
59
+ d = datetime.datetime.utcnow().date()
60
+ return f"{d.year}.{d.month:02}.{d.day:02}"
61
+
62
+ @property
63
+ def month_key(self):
64
+ d = datetime.datetime.utcnow().date()
65
+ return f"{d.year}.{d.month:02}"
66
+
67
+ @property
68
+ def report_root(self):
69
+ """define your keys!"""
70
+ raise NotImplemented()
71
+
72
+
73
+ @attrs.define
74
+ class DiskReporterMixin(TemporalReporterMixin):
75
+ path: str = attrs.field(default=None, validator=path_exist_validator)
76
+
77
+ # whats our /report/<key> gonna be
78
+ report_mode = Options("single", "daily", "monthly")
79
+
80
+ @property
81
+ def report_root(self):
82
+ if self.report_mode == "single":
83
+ return self.path
84
+ elif self.report_mode == "daily":
85
+ return os.path.join(self.path, self.date_key)
86
+ elif self.report_mode == "monthly":
87
+ return os.path.join(self.path, self.month_key)
88
+
89
+ raise ValueError(f"options failed!")
90
+
91
+ def ensure_exists(self):
92
+ if not os.path.exists(self.report_root):
93
+ pth = pathlib.Path(self.report_root)
94
+ pth.mkdir(parents=True, exist_ok=True)
95
+
96
+ def ensure_path(self, file_path, is_dir=False):
97
+ pth = pathlib.Path(file_path)
98
+ diri = pth if is_dir else pth.parent
99
+ if not diri.exists():
100
+ diri.mkdir(parents=True, exist_ok=True)
101
+
102
+
103
+ # Basic Reporter Types
104
+ @attrs.define
105
+ class TableReporter(Reporter):
106
+ """A reporter to upload dataframes to a table store"""
107
+
108
+ name: str = attrs.field(default="table_reporter")
109
+
110
+
111
+ @attrs.define
112
+ class PlotReporter(Reporter):
113
+ """A reporter to upload plots to a file store"""
114
+
115
+ name: str = attrs.field(default="plot_reporter")
116
+
117
+ # FIXME: make attrs work... layout class conflict
118
+ # ext = Options('png','jpg','gif')
119
+ # ext: str = attrs.field(default='png')
120
+ ext = "png"
121
+
122
+ # def upload(self, analysis):
123
+ # for figkey, fig in analysis.stored_plots.items():
124
+ # try:
125
+ # filname = os.path.join(self.report_root, f"{figkey}.{self.ext}")
126
+ # self.ensure_path(filname)
127
+ # self.info(f"saving {figkey} to {filname}")
128
+ # fig.savefig(filname)
129
+ #
130
+ # except Exception as e:
131
+ # self.error(e, f"issue showing {figkey}")
132
+ def upload(self, analyis):
133
+ raise NotImplemented(f"plot reporter subclass needs an upload method")
134
+
135
+
136
+ # Table Reporters
137
+ @attrs.define
138
+ class CSVReporter(TableReporter, DiskReporterMixin):
139
+ name: str = attrs.field(default="CSV")
140
+
141
+ def upload(self, analysis: "Analysis"):
142
+ self.info(f"uploading {analysis.identity} to {self.identity}")
143
+ self.ensure_exists()
144
+
145
+ system = analysis.system
146
+
147
+ # Make System Dataframe
148
+ af = os.path.join(self.report_root, f"system_{system.name}.csv")
149
+ self.ensure_path(af)
150
+ self.info(f"saving system to: {af}")
151
+ system.dataframe.to_csv(af)
152
+
153
+
154
+ # # Make Analysis Dataframe
155
+ # af = os.path.join(self.report_root, f"analysis_{analysis.name}.csv")
156
+ # self.ensure_path(af)
157
+ # self.info(f"saving analysis to: {af}")
158
+ # analysis.dataframe.to_csv(af)
159
+ #
160
+ # for ckey, comp in system.comp_references().items():
161
+ # af = os.path.join(self.report_root, f"{ckey}_{comp.name}.csv")
162
+ # self.ensure_path(af)
163
+ # self.info(f"saving {ckey} to: {af}")
164
+ # comp.dataframe.to_csv(af)
165
+
166
+
167
+ @attrs.define
168
+ class ExcelReporter(TableReporter, DiskReporterMixin):
169
+ name: str = attrs.field(default="EXCEL")
170
+
171
+
172
+ @attrs.define
173
+ class GsheetsReporter(TableReporter, TemporalReporterMixin):
174
+ name: str = attrs.field(default="GSHEETS")
175
+
176
+
177
+ # @attrs.define
178
+ # class SQLReporter(TableReporter):
179
+ # name: str = attrs.field(default="SQL")
180
+ #
181
+ #
182
+ # @attrs.define
183
+ # class ArangoReporter(TableReporter):
184
+ # name: str = attrs.field(default="SQL")
185
+
186
+
187
+ # PLOT Reporters
188
+ @attrs.define
189
+ class DiskPlotReporter(PlotReporter, DiskReporterMixin):
190
+ name: str = attrs.field(default="DrivePlots")
191
+
192
+ def upload(self, analysis):
193
+ for figkey, fig in analysis.stored_plots.items():
194
+ try:
195
+ filname = os.path.join(self.report_root, f"{figkey}.{self.ext}")
196
+ self.ensure_path(filname)
197
+ self.info(f"saving {figkey} to {filname}")
198
+ fig.savefig(filname)
199
+
200
+ except Exception as e:
201
+ self.error(e, f"issue showing {figkey}")
202
+
203
+
204
+ @attrs.define
205
+ class GdriveReporter(PlotReporter, TemporalReporterMixin):
206
+ name: str = attrs.field(default="Gdrive")
207
+ share_drive: str = attrs.field(
208
+ default=None, validator=attrs.validators.instance_of(str)
209
+ )
210
+
211
+
212
+ # TODO: move to analysis
213
+ # def split_dataframe_by_colmum(self,df,max_columns=10):
214
+ # df_cols = list(df.columns)
215
+ # if len(df_cols) < max_columns:
216
+ # return [df]
217
+ #
218
+ # col_chunks = list(chunks(df_cols,max_columns))
219
+ # dat_chunks = [df[colck].copy() for colck in col_chunks]
220
+ #
221
+ # return dat_chunks
222
+
223
+ # def cleanup_dataframe(self,df,badwords=('min','max')):
224
+ # tl_df = self.static_dataframe
225
+ # to_drop = [cl for cl in tl_df.columns if any([bw in cl.lower() for bw in badwords])]
226
+ # out_df = tl_df.drop(columns=to_drop)
227
+ # return out_df
228
+
229
+
230
+ # @solver_cached
231
+ # def variable_dataframe(self):
232
+ # vals = list(zip(*list((self.variable_data_dict.values()))))
233
+ # cols = list((self.variable_data_dict.keys()))
234
+ # if vals:
235
+ # return pandas.DataFrame(data = vals, columns=cols, copy = True)
236
+ # else:
237
+ # return None
238
+ #
239
+ # @solver_cached
240
+ # def static_dataframe(self):
241
+ # vals = [list((self.static_data_dict.values()))]
242
+ # cols = list((self.static_data_dict.keys()))
243
+ # if vals:
244
+ # return pandas.DataFrame(data = vals, columns=cols, copy = True)
245
+ # else:
246
+ # return None
247
+
248
+ # Clearance Methods
249
+ # table property internal variables
250
+ # __store_options = ('csv','excel','gsheets')#,'db','gsheets','excel','json')
251
+ # _store_types = None
252
+ # _store_level:int = -1
253
+
254
+ # @property
255
+ # def table_iterator(self):
256
+ # '''Checks data to see what type of table it is, type 1 is a single row
257
+ # whereas type 2 & 3 have several rows, but type 2 has some values that are singular
258
+ #
259
+ # iterates table_type,values,label'''
260
+ #
261
+ # if not self.TABLE:
262
+ # return 0,[],[]
263
+ # if len(self.TABLE) <= 1:
264
+ # yield 1,self.TABLE[1],self.data_label
265
+ # else:
266
+ # #wide outerloop over vars
267
+ # for label in set.union(*[set(tuple(row.keys())) for i,row in self.TABLE.items()]):
268
+ # #inner loop over data
269
+ # col = [row[label] if label in row else None for inx,row in self.TABLE.items()]
270
+ #
271
+ # if all([isinstance(cvar,TABLE_TYPES) for cvar in col]): #do a type check
272
+ # if len(set(col)) <= 1: #All Values Are Similar
273
+ # yield 2, col[0], label #Can assume that first value is equal to all
274
+ # else:
275
+ # yield 3, col, label
276
+ #
277
+ # def recursive_data_structure(self,levels_to_descend = -1, parent_level=0):
278
+ # '''Returns the static and variable data from each configuration to grab defined by the
279
+ # recursive commands input in this function
280
+ #
281
+ # data is stored like: output[level]=[{static,variable},{static,variable},...]'''
282
+ #
283
+ # output = {}
284
+ #
285
+ # for level,conf in self.go_through_configurations(0,levels_to_descend,parent_level):
286
+ # if level in output:
287
+ # output[level].append({'static':conf.static_dataframe,\
288
+ # 'variable':conf.variable_dataframe,'conf':conf})
289
+ # else:
290
+ # output[level] = [{'static':conf.static_dataframe,\
291
+ # 'variable':conf.variable_dataframe,\
292
+ # 'conf':conf}]
293
+ #
294
+ # return output
295
+
296
+ # Multi-Component Table Combination Methods
297
+ # @solver_cached
298
+ # def static_data_dict(self):
299
+ # '''returns key-value pairs in table that are single valued, all in type 1, some in type 2'''
300
+ #
301
+ # output = {}
302
+ # for tab_type,value,label in self.table_iterator:
303
+ # if tab_type == 1:
304
+ # return {l:v for l,v in zip(label,value)}
305
+ # elif tab_type == 2:
306
+ # output[label] = value
307
+ # return output
308
+ #
309
+ # @solver_cached
310
+ # def variable_data_dict(self):
311
+ # '''returns a dictionary of key value pairs where values list is not the same all of type 3 and some of type 2'''
312
+ # output = {}
313
+ # for tab_type,value,label in self.table_iterator:
314
+ # if tab_type == 3:
315
+ # output[label] = value
316
+ # return output
317
+ #
318
+ #
319
+ # #Multi-Component Table Lookups
320
+ # @solver_cached
321
+ # def toplevel_static(self):
322
+ # out_df = self.cleanup_dataframe( self.static_dataframe )
323
+ # df_list = self.split_dataframe_by_colmum(out_df,self.max_col_width_static)
324
+ #
325
+ # return {'conf':self,'dfs': df_list }
326
+ #
327
+ # @solver_cached
328
+ # def other_static_tables(self):
329
+ # rds = self.recursive_data_structure(self.store_level)
330
+ # output = []
331
+ # for index, components in rds.items():
332
+ # if index > 0:
333
+ # for comp in components:
334
+ # if comp['static'] is not None:
335
+ # df_list = self.split_dataframe_by_colmum(comp['static'],self.max_col_width_static)
336
+ # output.append({'conf':comp['conf'],'dfs': df_list})
337
+ # return output
338
+ #
339
+ # @solver_cached
340
+ # def variable_tables(self):
341
+ # '''Grabs all valid variable dataframes and puts them in a list'''
342
+ # rds = self.recursive_data_structure(self.store_level)
343
+ #
344
+ # output = []
345
+ # for index, components in rds.items():
346
+ # for comp in components:
347
+ # if comp['variable'] is not None:
348
+ # output.append({'conf':comp['conf'],'df':comp['variable']})
349
+ #
350
+ # return output
351
+ #
352
+ # @solver_cached
353
+ # def joined_dataframe(self):
354
+ # '''this is a high level data frame with all data that changes in the system'''
355
+ # if self.variable_tables:
356
+ # return pandas.concat([ vt['df'] for vt in list(reversed(self.variable_tables))],axis=1)
357
+ # else:
358
+ # return None
359
+ #
360
+ # @property
361
+ # def complete_dataframe(self):
362
+ # '''this is a high level data frame with all data in the system'''
363
+ #
364
+ # rds = self.recursive_data_structure()
365
+ # if rds:
366
+ # dataframes = []
367
+ # stat_dataframes = []
368
+ # for lvl, comps in rds.items():
369
+ # for comp in comps:
370
+ # df = comp['conf'].dataframe
371
+ # if not comp['conf'] is self:
372
+ # nm = comp['conf'].name.lower()
373
+ # if nm == 'default':
374
+ # nm = comp['conf'].__class__.__name__.lower()
375
+ # nm = nm.replace(' ','_')
376
+ # strv=f'{nm}_'
377
+ # strv+='{}'
378
+ # else:
379
+ # nm = ''
380
+ # strv='{}'
381
+ # df.rename(strv.format, axis=1, inplace=True)
382
+ #
383
+ # dataframes.append(df)
384
+ # return pandas.concat(dataframes,join='outer',axis=1)
385
+ #
386
+ # else:
387
+ # return None
388
+
389
+ # Saving & Data Acces Methods
390
+ # def get_field_from_table(self,field,check_type=None,check_value:Callable = None):
391
+ # '''Converts Pandas To Numpy Array By Key, also handles poorly formated fields
392
+ # :param check_type: use a type or tuple of types to validate if the field is of type table
393
+ # :param check_value: use a function to check each value to ensure its valid for return, check type take priority'''
394
+ # if self.joined_dataframe is None:
395
+ # return numpy.array([])
396
+ # elif field in self.joined_dataframe:
397
+ # table = self.joined_dataframe[field]
398
+ # elif field.title() in self.joined_dataframe:
399
+ # table = self.joined_dataframe[field.title()]
400
+ # else:
401
+ # raise Exception('No Field Named {}'.format(field))
402
+ #
403
+ # #Remove Infinity
404
+ # table = table.replace([numpy.inf, -numpy.inf], numpy.nan)
405
+ #
406
+ # if check_type is not None:
407
+ # if all([isinstance(v,check_type) for v in table]):
408
+ # return table.to_numpy(copy=True)
409
+ # return None
410
+ # elif check_value is not None:
411
+ # if all([check_value(v) for v in table]):
412
+ # return table.to_numpy(copy=True)
413
+ # return None
414
+ #
415
+ # return table.to_numpy(dtype=float,copy=True)
416
+
417
+
418
+ # Save functionality
419
+ # TODO: get me outta hur
420
+ # def save_table(self,dataframe=None,filename=None,meta_tags=None,*args,**kwargs):
421
+ # '''Header method to save the config in many different formats
422
+ # :param meta_tags: a dictionary with headers being column names, and the value as the item to fill that column'''
423
+ # if dataframe is None:
424
+ # dataframe = self.dataframe
425
+ #
426
+ # self.info('saving gsheets...')
427
+ #
428
+ # if meta_tags is not None and type(meta_tags) is dict:
429
+ # for tag,value in meta_tags.items():
430
+ # dataframe[tag] = value
431
+ #
432
+ #
433
+ # for save_format in self.store_types:
434
+ # try:
435
+ # if save_format == 'csv':
436
+ # self.save_csv(dataframe,filename,*args,**kwargs)
437
+ # elif save_format == 'excel':
438
+ # self.save_excel(dataframe,filename,*args,**kwargs)
439
+ # elif save_format == 'gsheets':
440
+ # self.save_gsheets(dataframe,filename,*args,**kwargs)
441
+ # except Exception as e:
442
+ # self.error(e,'Issue Saving Tables:')
443
+
444
+ # def save_csv(self,dataframe,filename=None,*args,**kwargs):
445
+ # if self.TABLE:
446
+ # if filename is None:
447
+ # filename = '{}.csv'.format(self.filename)
448
+ # if type(filename) is str and not filename.endswith('.csv'):
449
+ # filename += '.csv'
450
+ # filepath = os.path.join(self.config_path_daily,filename)
451
+ # dataframe.to_csv(path_or_buf=filepath,index=False,*args,**kwargs)
452
+ #
453
+ # def save_excel(self,dataframe,filename=None,*args,**kwargs):
454
+ # if self.TABLE:
455
+ # if filename is None:
456
+ # filename = '{}.xlsx'.format(self.filename)
457
+ # if type(filename) is str and not filename.endswith('.xlsx'):
458
+ # filename += '.xlsx'
459
+ # filepath = os.path.join(self.config_path_daily,filename)
460
+ # dataframe.to_excel(path_or_buf=filepath,*args,**kwargs)
461
+
462
+ # @property
463
+ # def store_level(self):
464
+ # return self._store_level
465
+ #
466
+ # @store_level.setter
467
+ # def store_level(self,new_level:int):
468
+ # assert isinstance(new_level,(int))
469
+ # self._store_level = new_level
470
+ #
471
+ # @property
472
+ # def store_types(self):
473
+ # if self._store_types is None:
474
+ # self._store_types = [] #initalization
475
+ # return self._store_types
476
+
477
+ # @store_types.setter
478
+ # def store_types(self, new_type_or_list):
479
+ # '''If you add a list or iterable, and each value is a valid output option we will assign it
480
+ # otherwise if its a value in the valid options it will be added.
481
+ # '''
482
+ # if isinstance(new_type_or_list,(list,tuple)):
483
+ # assert all([val in self.__store_options for val in new_type_or_list])
484
+ # self._store_types = list(new_type_or_list)
485
+ #
486
+ # elif new_type_or_list in self.__store_options:
487
+ # self._store_types.append(new_type_or_list)
488
+ #
489
+ # else:
490
+ # self.warning('store types input not valid {}'.format(new_type_or_list))
491
+
492
+
493
+ # def save_to_worksheet(self,worksheet:'pygsheets.Worksheet'):
494
+ # '''Saves to a gsheets via pygsheets adds static and regular data'''
495
+ #
496
+ # title = self.identity.replace('_',' ').title()
497
+ #
498
+ # self.info('saving worksheet as {}'.format(title))
499
+ # wksh = worksheet
500
+ #
501
+ # wksh.clear()
502
+ #
503
+ # #Static data
504
+ # start = pygsheets.Address((2,2))
505
+ #
506
+ # tld = self.toplevel_static
507
+ # sdf = tld['dfs']
508
+ #
509
+ # cur_index = start + (1,0)
510
+ # for i,df in enumerate(sdf):
511
+ # self.debug('saving dataframe {}'.format(df))
512
+ # wksh.update_value(start.label,self.identity)
513
+ # wksh.cell(start.label).set_text_format('bold',True)
514
+ # wksh.set_dataframe(df,cur_index.label , extend=True)
515
+ # cur_index += (2,0)
516
+ #
517
+ # cur_index += (3,0)
518
+ #
519
+ # var_index = pygsheets.Address(cur_index.label)
520
+ #
521
+ # max_row = 0
522
+ #
523
+ # vrt = self.variable_tables
524
+ # self.info('saving {} other static tables'.format(len(vrt)))
525
+ #
526
+ # for dfpack in vrt:
527
+ # conf = dfpack['conf']
528
+ # df = dfpack['df']
529
+ # self.debug('saving dataframe {}'.format(df))
530
+ #
531
+ # (num_rows,num_cols) = df.shape
532
+ # max_row = max(max_row,num_rows)
533
+ #
534
+ # wksh.update_value((var_index-(1,0)).label,conf.classname)
535
+ # wksh.cell((var_index-(1,0)).label).set_text_format('bold',True)
536
+ #
537
+ # wksh.set_dataframe(df,start=var_index.label,extend=True)
538
+ #
539
+ # var_index += (0,num_cols)
540
+ #
541
+ # cur_index += (3+max_row,0)
542
+ #
543
+ # ost = self.other_static_tables
544
+ #
545
+ # self.info('saving {} other static tables'.format(len(ost)))
546
+ #
547
+ # for dfpack in ost:
548
+ # conf = dfpack['conf']
549
+ # sdf = dfpack['dfs']
550
+ #
551
+ # wksh.update_value((cur_index-(1,0)).label,conf.identity)
552
+ # wksh.cell((cur_index-(1,0)).label).set_text_format('bold',True)
553
+ #
554
+ # for i,df in enumerate(sdf):
555
+ #
556
+ # self.debug('saving {} dataframe {}'.format(conf.identity, df))
557
+ # wksh.set_dataframe(df,start=cur_index.label ,extend=True)
558
+ # cur_index += (2,0)
559
+ #
560
+ # cur_index += (3,0)
561
+
562
+
563
+ # def save_gsheets(self,dataframe,filename=None,index=False,*args,**kwargs):
564
+ # '''A function to save the table to google sheets
565
+ # :param filename: the filepath on google drive - be careful!
566
+ # '''
567
+ # with self.drive.context(filepath_root=self.local_sync_path, sync_root=self.cloud_sync_path) as gdrive:
568
+ # with gdrive.rate_limit_manager( self.save_gsheets,6,dataframe,filename=filename,*args,**kwargs) as tdrive:
569
+ #
570
+ # old_sleep = tdrive._sleep_time
571
+ # tdrive.reset_sleep_time( max(old_sleep,2.5) )
572
+ #
573
+ # gpath = tdrive.sync_path(self.local_sync_path)
574
+ # self.info(f'saving as gsheets in dir {self.local_sync_path} -> {gpath}')
575
+ # parent_id = gdrive.get_gpath_id(gpath)
576
+ # #TODO: delete old file if exists
577
+ # tdrive.sleep(2*(1+tdrive.time_fuzz*random.random()))
578
+ # if tdrive and tdrive.gsheets:
579
+ # sht = tdrive.gsheets.create(filename,folder=parent_id)
580
+ #
581
+ # tdrive.sleep(2*(1+tdrive.time_fuzz*random.random()))
582
+ # tdrive.cache_directory(parent_id)
583
+ #
584
+ # wk = sht.sheet1
585
+ #
586
+ # wk.rows = dataframe.shape[0]
587
+ # gdrive.sleep(2*(1+tdrive.time_fuzz*random.random()))
588
+ #
589
+ # wk.set_dataframe(dataframe,start='A1',fit=True)
590
+ # gdrive.sleep(2*(1+tdrive.time_fuzz*random.random()))
591
+ #
592
+ # #TODO: add in dataframe dict with schema sheename: {dataframe,**other_args}
593
+ # self.info('gsheet saved -> {}'.format(os.path.join(gpath,filename)))
594
+ #
595
+ # tdrive.reset_sleep_time( old_sleep )