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/env_var.py ADDED
@@ -0,0 +1,214 @@
1
+ """Defines a class called `EnvVariable` that defines an interface for env variables with an option to obscure and convert variables, as well as provide a default option.
2
+
3
+ A global record of variables is kept for informational purposes in keeping track of progam variables
4
+
5
+ To prevent storage of env vars in program memory, access to the os env variables is provided on access of the `secret` variable. It is advisable to use the result of this as directly as possible when dealing with actual secrets.
6
+
7
+ For example add: `db_driver(DB_HOST.secret,DB_PASSWORD.secret,...)
8
+ """
9
+
10
+ import os
11
+ from engforge.logging import LoggingMixin
12
+ from typing import Any
13
+ import socket
14
+ import inspect
15
+
16
+ global warned
17
+ warned = set() # a nice global variable to hold any warnings
18
+
19
+ FALSE_VALUES = (False, "false", "no", "n")
20
+ TRUE_VALUES = (True, "checked", "true", "yes", "y")
21
+
22
+
23
+ def parse_bool(input: str):
24
+ if isinstance(input, str):
25
+ input = input.lower()
26
+
27
+ if not input:
28
+ return False
29
+ elif input in TRUE_VALUES:
30
+ return True
31
+ elif input in FALSE_VALUES:
32
+ return False
33
+ return False
34
+
35
+
36
+ DEFAULT_CONVERTERS = {bool: parse_bool}
37
+
38
+
39
+ class EnvVariable(LoggingMixin):
40
+ """A method to wrap SECRETS and in application with a way to get the value using self.secret
41
+ Do not store values from self.secret to ensure security
42
+
43
+ You can override the secret with _override"""
44
+
45
+ var_name: str = None
46
+ type_conv: Any = None
47
+ default: Any = None
48
+ obscure: bool = True
49
+ _override: str
50
+ _secrets = {} # its class based so like a singleton
51
+ _replaced = set()
52
+ fail_on_missing: bool
53
+ desc: str = None
54
+ _upgrd_warn: bool = False
55
+ _dontovrride: bool = False
56
+
57
+ def __init__(
58
+ self,
59
+ secret_var_name,
60
+ type_conv=None,
61
+ default=None,
62
+ obscure=False,
63
+ fail_on_missing=False,
64
+ desc: str = None,
65
+ dontovrride=False,
66
+ ):
67
+ """pass arguments to SecretVariable to have it look up information at runtime from envargs, but not store it in memory.
68
+ :param secret_var_name: the enviornmental variable
69
+ :param type_conv: the data from env vars will be converted with this function
70
+ :param default: the value to use if the secret_var_name doesn't exist in enviornmental variables
71
+ :param obscure: default True, will prevent the result being printed by str(self)
72
+ :param fail_on_missing: if the secret env variable is not found, and default is None
73
+ :param desc: a description of the purpose of the variable
74
+ """
75
+ self.var_name = secret_var_name
76
+ self.type_conv = (
77
+ type_conv
78
+ if type_conv not in DEFAULT_CONVERTERS
79
+ else DEFAULT_CONVERTERS[type_conv]
80
+ )
81
+ self._dontovrride = dontovrride
82
+
83
+ if default is not None:
84
+ self.default = default
85
+ self.obscure = obscure
86
+ # UserString.__init__(self,f'[SECRET:{secret_var_name}]')
87
+
88
+ self.fail_on_missing = fail_on_missing
89
+
90
+ # record env vars
91
+ if secret_var_name in self.__class__._secrets:
92
+ cur = self.__class__._secrets[secret_var_name]
93
+ if dontovrride:
94
+ self.debug(f"not replacing: {cur}->{self}")
95
+ self.__dict__ = cur.__dict__
96
+ else:
97
+ self.info(f"replacing {cur}->{self}")
98
+ self._replaced.add(cur)
99
+ self.__class__._secrets[secret_var_name] = self
100
+ else:
101
+ self.__class__._secrets[secret_var_name] = self
102
+
103
+ # FIXME: prevent engforge var from replacing other module instnace
104
+ # not possible to locate where other instances
105
+ # if secret_var_name in self.__class__._secrets:
106
+ # cur = self.__class__._secrets[secret_var_name]
107
+ # if cur != self and self not in self._replaced:
108
+ # self._replaced.add(cur)
109
+ # self.info(f'replacing {cur}->{self}')
110
+ # self.__class__._secrets[secret_var_name] = self
111
+ # elif self in self._replaced:
112
+ # self.info(f'skipping replaced readd {self}')
113
+ # #self.__class__._secrets[secret_var_name] = self
114
+ # else:
115
+ # self.__class__._secrets[secret_var_name] = self
116
+
117
+ def __str__(self):
118
+ if self.obscure:
119
+ return f"{self.obscured_name:<40} = XXXXXX"
120
+ return f"{self.obscured_name:<40} = {self.secret}"
121
+
122
+ def __add__(self, other) -> str:
123
+ return str(str.__add__(str(self), other))
124
+
125
+ def __radd__(self, other) -> str:
126
+ return str(str.__add__(other, str(self)))
127
+
128
+ @property
129
+ def obscured_name(self) -> str:
130
+ if hasattr(self, "_override"):
131
+ return f"SECRETS[OVERRIDE]"
132
+ return f"SECRETS[{self.var_name}]"
133
+
134
+ @property
135
+ def secret(self):
136
+ # Check if this secret is the one in the secrets registry
137
+ sec = self.__class__._secrets[self.var_name]
138
+ if sec is not self and not sec._dontovrride:
139
+ # Provide warning that the secret is being replaced
140
+ if not self._upgrd_warn:
141
+ self._upgrd_warn = True
142
+ self.info(f"upgrading: {self.var_name} from {id(self)}->{id(sec)}")
143
+
144
+ # Monkeypatch dictionary
145
+ self.__dict__ = sec.__dict__
146
+
147
+ if hasattr(self, "_override"):
148
+ return self._override
149
+
150
+ if self.var_name in os.environ:
151
+ secval = os.environ[self.var_name]
152
+ elif self.default is not None:
153
+ if self.var_name not in warned:
154
+ if self.obscure:
155
+ dflt = "XXXXXXX"
156
+ else:
157
+ dflt = self.default
158
+
159
+ self.debug(f"Env Var: {self.var_name} Not Found! Using: {dflt}")
160
+ warned.add(self.var_name)
161
+
162
+ secval = self.default
163
+ else:
164
+ if self.fail_on_missing:
165
+ raise FileNotFoundError(f"Could Not Find Env Variable {self.var_name}")
166
+ else:
167
+ if self.var_name not in warned:
168
+ self.debug(f"Env Var: {self.var_name} Not Found!")
169
+ warned.add(self.var_name)
170
+ return None
171
+
172
+ if self.type_conv is None:
173
+ return secval
174
+ else:
175
+ return self.type_conv(secval)
176
+
177
+ @property
178
+ def in_env(self):
179
+ return self.var_name in os.environ
180
+
181
+ def remove(self):
182
+ """removes this secret from the record"""
183
+ if self in self.__class__._secrets:
184
+ self.__class__._secrets.remove(self)
185
+
186
+ @classmethod
187
+ def load_env_vars(self):
188
+ for s in EnvVariable._secrets.values():
189
+ str(s)
190
+
191
+ @classmethod
192
+ def print_env_vars(cls):
193
+ """prints env vars in memory"""
194
+ # preload
195
+ cls.load_env_vars()
196
+ for var, s in sorted(
197
+ EnvVariable._secrets.items(), key=lambda kv: kv[1].var_name
198
+ ):
199
+ print(f"{s.var_name:<40}|{s}")
200
+
201
+
202
+ # DEFAULT ENV VARIABLES
203
+ try:
204
+ # This should always work unless we don't have privideges (rare assumed)
205
+ host = socket.gethostname().upper()
206
+ except:
207
+ host = "MASTER"
208
+
209
+ global HOSTNAME, SLACK_WEBHOOK
210
+
211
+ HOSTNAME = EnvVariable("FORGE_HOSTNAME", default=host, obscure=False, dontovrride=True)
212
+ SLACK_WEBHOOK = EnvVariable(
213
+ "FORGE_SLACK_LOG_WEBHOOK", default=None, obscure=False, dontovrride=True
214
+ )
engforge/locations.py ADDED
@@ -0,0 +1,18 @@
1
+ from engforge.env_var import EnvVariable
2
+ import os
3
+
4
+ FORGE_ROOT_VAR = EnvVariable(
5
+ "FORGE_ROOT", default=None, dontovrride=True, type_conv=os.path.expanduser
6
+ )
7
+
8
+
9
+ def client_path(alternate_path=None, **kw):
10
+ path = FORGE_ROOT_VAR.secret
11
+ if path is None:
12
+ if alternate_path is None:
13
+ raise KeyError(
14
+ f"no `FORGE_ROOT` set and no alternate path in client_path call"
15
+ )
16
+ return alternate_path
17
+ else:
18
+ return path
engforge/logging.py ADDED
@@ -0,0 +1,290 @@
1
+ import logging
2
+ import logging
3
+ import traceback
4
+ import sys, os
5
+ from pyee import EventEmitter
6
+ from termcolor import colored
7
+ import requests
8
+ import json
9
+ import uuid
10
+
11
+ global log_change_emitter
12
+ log_change_emitter = EventEmitter()
13
+
14
+
15
+ BASIC_LOG_FMT = "[%(name)-24s]%(message)s"
16
+
17
+ global LOG_LEVEL
18
+ LOG_LEVEL = logging.INFO
19
+
20
+
21
+ def change_all_log_levels(new_log_level: int = 20, inst=None, check_function=None):
22
+ """Changes All Log Levels With pyee broadcast before reactor is running
23
+ :param new_log_level: int - changes unit level log level (10-msg,20-debug,30-info,40-warning,50-error,60-crit)
24
+ :param check_function: callable -> bool - (optional) if provided if check_function(unit) is true then the new_log_level is applied
25
+ """
26
+ if isinstance(new_log_level, float):
27
+ new_log_level = int(new_log_level) # Float Case Is Handled
28
+
29
+ assert (
30
+ isinstance(new_log_level, int) and new_log_level >= 1 and new_log_level <= 100
31
+ )
32
+
33
+ global LOG_LEVEL
34
+ LOG_LEVEL = new_log_level
35
+
36
+ if LoggingMixin.log_level != new_log_level:
37
+ print(f"changing log levels to {new_log_level}...")
38
+ log.info(f"Changing All Logging Units To Level {new_log_level}")
39
+ log_change_emitter.emit("change_level", new_log_level, check_function)
40
+ LoggingMixin.log_level = new_log_level
41
+ if inst:
42
+ inst.log_level = new_log_level
43
+
44
+
45
+ class LoggingMixin(logging.Filter):
46
+ """Class to include easy formatting in subclasses"""
47
+
48
+ log_level = LOG_LEVEL
49
+ _log = None
50
+
51
+ log_on = True
52
+
53
+ log_fmt = "[%(name)-24s]%(message)s"
54
+
55
+ slack_webhook_url = None
56
+ # log_silo = False
57
+
58
+ change_all_log_lvl = lambda s, *a, **kw: change_all_log_levels(*a, inst=s, **kw)
59
+
60
+ @property
61
+ def logger(self):
62
+ global LOG_LEVEL
63
+ if self._log is None:
64
+ inst_log_name = "engforgelog_" + self.identity + "_" + str(uuid.uuid4())
65
+ self._log = logging.getLogger(inst_log_name)
66
+ self._log.setLevel(level=LOG_LEVEL)
67
+
68
+ # Apply Filter Info
69
+ self._log.addFilter(self)
70
+ self.installSTDLogger()
71
+ from engforge.env_var import EnvVariable, SLACK_WEBHOOK
72
+
73
+ # Hot Patch Class (EnvVar is logging mixin... soo... here we are)
74
+ if LoggingMixin.slack_webhook_url is None:
75
+ # Do this on the fly since we SecretVariable is a log component
76
+ LoggingMixin.slack_webhook_url = SLACK_WEBHOOK
77
+
78
+ if not hasattr(self, "_f_change_log"):
79
+ # change log level listener
80
+ def _change_log(new_level, check_function=None):
81
+ if new_level != self.log_level:
82
+ if check_function is None or check_function(self):
83
+ msg = f"changing {self.identity} log level: {self.log_level} -> {new_level}"
84
+ self.__class__.log_level = new_level
85
+ self.info(msg)
86
+ self._log.setLevel(new_level)
87
+ self.log_level = new_level
88
+ self.resetLog()
89
+
90
+ # our emitter gets set with a callback
91
+ log_change_emitter.add_listener("change_level", _change_log)
92
+
93
+ self._f_change_log = _change_log
94
+
95
+ return self._log
96
+
97
+ def resetLog(self):
98
+ """reset log"""
99
+ self._log = None
100
+ self.debug(f"reset!")
101
+
102
+ def resetSystemLogs(self, reseted=None):
103
+ """resets log on all internal instance LoggingMixins"""
104
+ self.resetLog()
105
+ self.debug(f"reset!")
106
+ if reseted is None:
107
+ reseted = set()
108
+ for k, v in self.__dict__.items():
109
+ if isinstance(v, LoggingMixin) and id(v) not in reseted:
110
+ reseted.add(id(v))
111
+ v.resetSystemLogs(reseted)
112
+
113
+ def installSTDLogger(self):
114
+ """We only want std logging to start"""
115
+ sh = logging.StreamHandler(sys.stdout)
116
+ peerlog = logging.Formatter(self.log_fmt)
117
+ sh.setFormatter(peerlog)
118
+ self._log.addHandler(sh)
119
+
120
+ def add_fields(self, record):
121
+ """Overwrite this to modify logging fields"""
122
+ pass
123
+
124
+ def filter(self, record):
125
+ """This acts as the interface for `logging.Filter`
126
+ Don't overwrite this, use `add_fields` instead."""
127
+ record.name = self.identity.lower()[:24]
128
+ self.add_fields(record)
129
+ return True
130
+
131
+ def msg(self, *args, lvl=5):
132
+ """Writes to log... this should be for raw data or something... least priorty"""
133
+ if self.log_on:
134
+ self.logger.log(
135
+ lvl,
136
+ self.message_with_identiy(self.extract_message(args), "blue"),
137
+ )
138
+
139
+ def debug(self, *args):
140
+ """Writes at a low level to the log file... usually this should
141
+ be detailed messages about what exactly is going on"""
142
+ if self.log_on:
143
+ self.logger.debug(
144
+ self.message_with_identiy(self.extract_message(args), "cyan")
145
+ )
146
+
147
+ def info(self, *args):
148
+ """Writes to log but with info category, these are important typically
149
+ and inform about progress of process in general"""
150
+ if self.log_on:
151
+ self.logger.info(
152
+ self.message_with_identiy(self.extract_message(args), "white")
153
+ )
154
+
155
+ def warning(self, *args):
156
+ """Writes to log as a warning"""
157
+ self.logger.warning(
158
+ self.message_with_identiy("WARN: " + self.extract_message(args), "yellow")
159
+ )
160
+
161
+ def error(self, error, msg=""):
162
+ """Writes to log as a error"""
163
+
164
+ # fmt = 'ERROR: {msg!r}|{err!r}'
165
+
166
+ tb = error.__traceback__
167
+ fmt = "ERROR:{msg}->{err}"
168
+
169
+ tb = "\n".join(traceback.format_exception(error, value=error, tb=tb))
170
+ msgfmt = ("\n" + " " * 51 + "|").join(str(msg).split("\n"))
171
+
172
+ # tbcl = colored(tb, "red")
173
+ # self.logger.exception( fmt.format(msg=msgfmt,err=tbcl))
174
+ # self.logger.exception( msgfmt)
175
+
176
+ m = colored(fmt.format(msg=msgfmt, err=tb), "red")
177
+ self.logger.error(m)
178
+
179
+ def critical(self, *args):
180
+ """A routine to communicate to the root of the server network that there is an issue"""
181
+ msg = self.extract_message(args)
182
+ msg = self.message_with_identiy(msg, "magenta")
183
+ self.logger.critical(msg)
184
+
185
+ # FIXME: setup slack notificatinos with env var
186
+ self.slack_notification(self.identity.title(), msg)
187
+
188
+ def slack_notification(self, category, message):
189
+ from engforge.env_var import SLACK_WEBHOOK, HOSTNAME
190
+
191
+ if SLACK_WEBHOOK.var_name in os.environ:
192
+ self.info("getting slack webhook")
193
+ url = SLACK_WEBHOOK.secret
194
+ else:
195
+ return
196
+ stage = HOSTNAME.secret
197
+ headers = {"Content-type": "application/json"}
198
+ data = {
199
+ "text": "{category} on {stage}:\n```{message}```".format(
200
+ category=category.upper(), stage=stage, message=message
201
+ )
202
+ }
203
+ self.info(f"Slack Notification : {url}:{category},{message}")
204
+ slack_note = requests.post(
205
+ url, data=json.dumps(data).encode("ascii"), headers=headers
206
+ )
207
+
208
+ def message_with_identiy(self, message: str, color=None):
209
+ """converts to color and string via the termcolor library
210
+ :param message: a string convertable entity
211
+ :param color: a color in [grey,red,green,yellow,blue,magenta,cyan,white]
212
+ """
213
+ if color != None:
214
+ return colored(str(message), color)
215
+ return str(message)
216
+
217
+ def extract_message(self, args):
218
+ for arg in args:
219
+ if type(arg) is str:
220
+ return arg
221
+ if self.log_level < 0:
222
+ print(f"no string found for {args}")
223
+ return ""
224
+
225
+ @property
226
+ def identity(self):
227
+ return type(self).__name__
228
+
229
+ def __getstate__(self):
230
+ d = dict(self.__dict__)
231
+ d["_f_change_log"] = None
232
+ return d
233
+
234
+
235
+ class Log(LoggingMixin):
236
+ pass
237
+
238
+
239
+ log = Log()
240
+
241
+
242
+ # try:
243
+ # logging.getLogger('parso.cache').disabled=True
244
+ # logging.getLogger('parso.cache.pickle').disabled=True
245
+ # logging.getLogger('parso.python.diff').disabled=True
246
+ #
247
+ # except Exception as e:
248
+ # log.warning(f'could not diable parso {e}')
249
+ # def installGELFLogger():
250
+ # '''Installs GELF Logger'''
251
+ # # self.gelf = graypy.GELFTLSHandler(GELF_HOST,GELF_PORT, validate=True,\
252
+ # # ca_certs=credfile('graylog-clients-ca.crt'),\
253
+ # # certfile = credfile('test-client.crt'),
254
+ # # keyfile = credfile('test-client.key')
255
+ # # )
256
+ # log = logging.getLogger('')
257
+ # gelf = graypy.GELFUDPHandler(host=GELF_HOST,port=12203, extra_fields=True)
258
+ # log.addHandler(gelf)
259
+
260
+
261
+ # def installSTDLogger(fmt = BASIC_LOG_FMT):
262
+ # '''We only want std logging to start'''
263
+ # log = logging.getLogger('')
264
+ # sh = logging.StreamHandler(sys.stdout)
265
+ # peerlog = logging.Formatter()
266
+ # sh.setFormatter(peerlog)
267
+ # log.addHandler( sh )
268
+ #
269
+ #
270
+ # def set_all_loggers_to(level,set_stdout=False,all_loggers=False):
271
+ # global LOG_LEVEL
272
+ # LOG_LEVEL = level
273
+ #
274
+ # if set_stdout: installSTDLogger()
275
+ #
276
+ # logging.basicConfig(level = LOG_LEVEL) #basic config
277
+ #
278
+ # log = logging.getLogger()
279
+ # log.setLevel(LOG_LEVEL)# Set Root Logger
280
+ #
281
+ # log.setLevel(level) #root
282
+ #
283
+ # loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
284
+ # for logger in loggers:
285
+ # if logger.__class__.__name__.lower().startswith('engforge'):
286
+ # logger.log(LOG_LEVEL,'setting log level: {}'.format(LOG_LEVEL))
287
+ # logger.setLevel(LOG_LEVEL)
288
+ # elif all_loggers:
289
+ # logger.log(LOG_LEVEL,'setting log level: {}'.format(LOG_LEVEL))
290
+ # logger.setLevel(LOG_LEVEL)