fmtr.tools 1.1.1__py3-none-any.whl → 1.4.37__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 (97) hide show
  1. fmtr/tools/__init__.py +86 -52
  2. fmtr/tools/ai_tools/__init__.py +2 -2
  3. fmtr/tools/ai_tools/agentic_tools.py +151 -32
  4. fmtr/tools/ai_tools/inference_tools.py +2 -1
  5. fmtr/tools/api_tools.py +73 -12
  6. fmtr/tools/async_tools.py +4 -0
  7. fmtr/tools/av_tools.py +7 -0
  8. fmtr/tools/caching_tools.py +101 -3
  9. fmtr/tools/constants.py +41 -0
  10. fmtr/tools/context_tools.py +23 -0
  11. fmtr/tools/data_modelling_tools.py +227 -14
  12. fmtr/tools/database_tools/__init__.py +6 -0
  13. fmtr/tools/database_tools/document.py +51 -0
  14. fmtr/tools/datatype_tools.py +22 -2
  15. fmtr/tools/datetime_tools.py +12 -0
  16. fmtr/tools/debugging_tools.py +60 -1
  17. fmtr/tools/dns_tools/__init__.py +7 -0
  18. fmtr/tools/dns_tools/client.py +97 -0
  19. fmtr/tools/dns_tools/dm.py +257 -0
  20. fmtr/tools/dns_tools/proxy.py +66 -0
  21. fmtr/tools/dns_tools/server.py +138 -0
  22. fmtr/tools/docker_tools/__init__.py +6 -0
  23. fmtr/tools/entrypoints/__init__.py +0 -0
  24. fmtr/tools/entrypoints/cache_hfh.py +3 -0
  25. fmtr/tools/entrypoints/ep_test.py +2 -0
  26. fmtr/tools/entrypoints/install_yamlscript.py +8 -0
  27. fmtr/tools/{console_script_tools.py → entrypoints/remote_debug_test.py} +1 -6
  28. fmtr/tools/entrypoints/shell_debug.py +8 -0
  29. fmtr/tools/environment_tools.py +3 -2
  30. fmtr/tools/function_tools.py +77 -1
  31. fmtr/tools/google_api_tools.py +15 -4
  32. fmtr/tools/ha_tools/__init__.py +8 -0
  33. fmtr/tools/ha_tools/constants.py +9 -0
  34. fmtr/tools/ha_tools/core.py +16 -0
  35. fmtr/tools/ha_tools/supervisor.py +16 -0
  36. fmtr/tools/ha_tools/utils.py +46 -0
  37. fmtr/tools/http_tools.py +52 -0
  38. fmtr/tools/inherit_tools.py +27 -0
  39. fmtr/tools/interface_tools/__init__.py +8 -0
  40. fmtr/tools/interface_tools/context.py +13 -0
  41. fmtr/tools/interface_tools/controls.py +354 -0
  42. fmtr/tools/interface_tools/interface_tools.py +189 -0
  43. fmtr/tools/iterator_tools.py +122 -1
  44. fmtr/tools/logging_tools.py +99 -18
  45. fmtr/tools/mqtt_tools.py +89 -0
  46. fmtr/tools/networking_tools.py +73 -0
  47. fmtr/tools/packaging_tools.py +14 -0
  48. fmtr/tools/path_tools/__init__.py +12 -0
  49. fmtr/tools/path_tools/app_path_tools.py +40 -0
  50. fmtr/tools/{path_tools.py → path_tools/path_tools.py} +217 -14
  51. fmtr/tools/path_tools/type_path_tools.py +3 -0
  52. fmtr/tools/pattern_tools.py +277 -0
  53. fmtr/tools/pdf_tools.py +39 -1
  54. fmtr/tools/settings_tools.py +27 -6
  55. fmtr/tools/setup_tools/__init__.py +8 -0
  56. fmtr/tools/setup_tools/setup_tools.py +481 -0
  57. fmtr/tools/string_tools.py +92 -13
  58. fmtr/tools/tabular_tools.py +61 -0
  59. fmtr/tools/tools.py +27 -2
  60. fmtr/tools/version +1 -1
  61. fmtr/tools/version_tools/__init__.py +12 -0
  62. fmtr/tools/version_tools/version_tools.py +51 -0
  63. fmtr/tools/webhook_tools.py +17 -0
  64. fmtr/tools/yaml_tools.py +64 -5
  65. fmtr/tools/youtube_tools.py +128 -0
  66. fmtr_tools-1.4.37.data/scripts/add-service +14 -0
  67. fmtr_tools-1.4.37.data/scripts/add-user-path +8 -0
  68. fmtr_tools-1.4.37.data/scripts/apt-headless +23 -0
  69. fmtr_tools-1.4.37.data/scripts/compose-update +10 -0
  70. fmtr_tools-1.4.37.data/scripts/docker-sandbox +43 -0
  71. fmtr_tools-1.4.37.data/scripts/docker-sandbox-init +23 -0
  72. fmtr_tools-1.4.37.data/scripts/docs-deploy +6 -0
  73. fmtr_tools-1.4.37.data/scripts/docs-serve +5 -0
  74. fmtr_tools-1.4.37.data/scripts/download +9 -0
  75. fmtr_tools-1.4.37.data/scripts/fmtr-test-script +3 -0
  76. fmtr_tools-1.4.37.data/scripts/ftu +3 -0
  77. fmtr_tools-1.4.37.data/scripts/ha-addon-launch +16 -0
  78. fmtr_tools-1.4.37.data/scripts/install-browser +8 -0
  79. fmtr_tools-1.4.37.data/scripts/parse-args +43 -0
  80. fmtr_tools-1.4.37.data/scripts/set-password +5 -0
  81. fmtr_tools-1.4.37.data/scripts/snips-install +14 -0
  82. fmtr_tools-1.4.37.data/scripts/ssh-auth +28 -0
  83. fmtr_tools-1.4.37.data/scripts/ssh-serve +15 -0
  84. fmtr_tools-1.4.37.data/scripts/vlc-tn +10 -0
  85. fmtr_tools-1.4.37.data/scripts/vm-launch +17 -0
  86. {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/METADATA +178 -54
  87. fmtr_tools-1.4.37.dist-info/RECORD +122 -0
  88. {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/WHEEL +1 -1
  89. fmtr_tools-1.4.37.dist-info/entry_points.txt +6 -0
  90. fmtr_tools-1.4.37.dist-info/top_level.txt +1 -0
  91. fmtr/tools/docker_tools.py +0 -30
  92. fmtr/tools/interface_tools.py +0 -64
  93. fmtr/tools/version_tools.py +0 -62
  94. fmtr_tools-1.1.1.dist-info/RECORD +0 -65
  95. fmtr_tools-1.1.1.dist-info/entry_points.txt +0 -3
  96. fmtr_tools-1.1.1.dist-info/top_level.txt +0 -2
  97. {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,10 @@
1
+ import cachetools
2
+ from datetime import timedelta, datetime
1
3
  from diskcache import Cache
2
4
 
3
- from fmtr.tools import logger, Path
5
+ from fmtr.tools.constants import Constants
6
+ from fmtr.tools.logging_tools import logger
7
+ from fmtr.tools.path_tools.path_tools import Path
4
8
 
5
9
 
6
10
  class Dump(dict):
@@ -31,9 +35,9 @@ class Disk(Cache):
31
35
  if not path.parent.exists():
32
36
  raise FileNotFoundError(f"Directory {path.parent=} does not exist")
33
37
  if path and not path.exists():
34
- logger.warning(f'Cache does not exist. Will be created. "{path=}"...')
38
+ logger.warning(f'Cache does not exist. Will be created. {str(path)=}...')
35
39
 
36
- logger.info(f'Initializing Disk Cache at path "{path=}"...')
40
+ logger.info(f'Initializing Disk Cache {str(path)=}...')
37
41
 
38
42
  super().__init__(directory=str(path / self.ROOT_KEY), **settings)
39
43
 
@@ -95,7 +99,101 @@ class Disk(Cache):
95
99
  return self.dump()
96
100
 
97
101
 
102
+ class TLRU(cachetools.TLRUCache):
103
+ """
104
+
105
+ Subclass to include logging and simplify global TTU
106
+
107
+ """
108
+ MASK_MAPPING = '{key} ' + Constants.ARROW + ' {value}'
109
+
110
+ def __init__(self, maxsize=1_024, timer=datetime.now, getsizeof=None, ttu_static=None, desc=None):
111
+ """
112
+
113
+ Add overridable TTU method
114
+
115
+ """
116
+ super().__init__(maxsize=maxsize, ttu=self.get_ttu, timer=timer, getsizeof=getsizeof)
117
+ self.ttu_static = ttu_static
118
+ self.desc = desc
119
+
120
+ @property
121
+ def cache_desc(self):
122
+ """
123
+
124
+ Friendly description of cache
125
+
126
+ """
127
+ desc = self.desc or self.__class__.__name__
128
+ return desc
129
+
130
+ def get_ttu(self, _key, value, now) -> float | timedelta:
131
+ """
132
+
133
+ Default implementation just adds on the static TTU
134
+
135
+ """
136
+ return now + self.ttu_static
137
+
138
+ def expire(self, time=None):
139
+ """
140
+
141
+ Log expiry
142
+
143
+ """
144
+ items = super().expire(time)
145
+ if not items:
146
+ return items
147
+
148
+ with logger.span(f'{self.desc} cache expiry {len(items)=}...'):
149
+ for key, value in items:
150
+ logger.debug(self.MASK_MAPPING.format(key=key, value=value))
151
+
152
+ return items
153
+
154
+ def popitem(self):
155
+ """
156
+
157
+ Log eviction
158
+
159
+ """
160
+ key, value = super().popitem()
161
+ logger.debug(f'{self.desc} cache eviction: {self.MASK_MAPPING.format(key=key, value=value)}')
162
+ return key, value
163
+
164
+ def dump(self):
165
+ """
166
+
167
+ Dump contents
168
+
169
+ """
170
+ data = Dump(self.items())
171
+ return data
172
+
173
+ @property
174
+ def data(self):
175
+ """
176
+
177
+ Dump as property
178
+
179
+ """
180
+ return self.dump()
181
+
182
+
183
+
98
184
  if __name__ == '__main__':
185
+ sec10 = timedelta(seconds=10)
186
+ c = TLRU(ttu_static=sec10, maxsize=2, desc='Test Data')
187
+ c['test'] = 'val'
188
+ c['test2'] = 'val2'
189
+ c['test3'] = 'val3'
190
+ c
191
+
192
+
193
+
194
+
195
+
196
+
99
197
  path_tmp_cache = Path.cwd().parent.parent / 'data' / 'cache'
100
198
  tc = Disk(path_tmp_cache)
101
199
 
fmtr/tools/constants.py CHANGED
@@ -14,6 +14,15 @@ class Constants:
14
14
  DATETIME_NOW_STR = DATETIME_NOW.strftime(DATETIME_FILENAME_FORMAT)
15
15
  SERIALIZATION_INDENT = 4
16
16
 
17
+ ENV_NESTED_DELIMITER = '__'
18
+
19
+ ARROW = '→'
20
+ ARROW_RIGHT = ARROW
21
+ ARROW_SEP = f' {ARROW} '
22
+
23
+ ARROW_LEFT = '←'
24
+
25
+ FMTR_DEV_KEY = 'FMTR_DEV'
17
26
  FMTR_LOG_LEVEL_KEY = 'FMTR_LOG_LEVEL'
18
27
  FMTR_OBS_API_KEY_KEY = 'FMTR_OBS_API_KEY'
19
28
  FMTR_OBS_HOST = 'obs.sv.fmtr.dev'
@@ -27,3 +36,35 @@ class Constants:
27
36
 
28
37
  FMTR_AI_HOST_KEY = 'FMTR_URL_HOST'
29
38
  FMTR_AI_HOST_DEFAULT = 'ai.gex.fmtr.dev'
39
+
40
+ FMTR_DEV_HOST = 'ws.gex.fmtr.dev'
41
+
42
+ FMTR_DEV_INTERFACE_URL = f'https://{FMTR_DEV_HOST}/'
43
+ FMTR_DEV_INTERFACE_SUB_URL_MASK = f'https://{{sub}}.{FMTR_DEV_HOST}/'
44
+
45
+ FILENAME_CONFIG = 'settings.yaml'
46
+ DIR_NAME_REPO = 'repo'
47
+ DIR_NAME_DATA = 'data'
48
+ DIR_NAME_CACHE = 'cache'
49
+ DIR_NAME_ARTIFACT = 'artifact'
50
+ DIR_NAME_SOURCE = 'source'
51
+ FILENAME_VERSION = 'version'
52
+ DIR_NAME_HF = 'hf'
53
+
54
+ ENTRYPOINT = 'entrypoint'
55
+ ENTRYPOINTS_DIR = f'{ENTRYPOINT}s'
56
+ ENTRYPOINT_FILE = f'{ENTRYPOINT}.py'
57
+ SCRIPTS_DIR = 'scripts'
58
+
59
+ PACKAGE_EXCLUDE_DIRS = {SCRIPTS_DIR, 'data', 'build', 'dist', '.*', '*egg-info*'}
60
+ INIT_FILENAME = '__init__.py'
61
+ DOCS_DIR = 'docs'
62
+ DOCS_CONFIG_FILENAME = 'mkdocs.yml'
63
+
64
+ DEVELOPMENT = "development"
65
+ PRODUCTION = "production"
66
+
67
+ INFRA = 'infra'
68
+
69
+ PROMPT_NONE_SPECIFIED = '[None Specified]'
70
+ WEBHOOK_URL_NOTIFY_KEY = 'WEBHOOK_URL_NOTIFY'
@@ -0,0 +1,23 @@
1
+ from contextlib import contextmanager, ExitStack
2
+
3
+
4
+ @contextmanager
5
+ def null():
6
+ """
7
+
8
+ Null context manager.
9
+
10
+ """
11
+ yield
12
+
13
+
14
+ @contextmanager
15
+ def contexts(*contexts):
16
+ """
17
+
18
+ Tee context managers.
19
+
20
+ """
21
+ with ExitStack() as stack:
22
+ resources = [stack.enter_context(context) for context in contexts]
23
+ yield resources
@@ -1,4 +1,135 @@
1
- from pydantic import BaseModel, RootModel, ConfigDict
1
+ import inspect
2
+ from functools import cached_property
3
+ from typing import ClassVar, List, Any, Dict
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic import RootModel, ConfigDict
7
+ from pydantic.fields import FieldInfo
8
+ from pydantic.json_schema import SkipJsonSchema
9
+ from pydantic_core import PydanticUndefined, PydanticUndefinedType
10
+
11
+ from fmtr.tools.datatype_tools import is_optional, none_else
12
+ from fmtr.tools.iterator_tools import get_class_lookup
13
+ from fmtr.tools.string_tools import camel_to_snake
14
+ from fmtr.tools.tools import Auto, Required, Empty
15
+
16
+
17
+ class Field(FieldInfo):
18
+ """
19
+
20
+ Allow DRYer field definitions, set annotation and defaults at the same time, easier field inheritance, etc.
21
+
22
+ """
23
+ NAME = Auto
24
+ ANNOTATION = Empty
25
+ DEFAULT = Auto
26
+ FILLS = None
27
+ DESCRIPTION = None
28
+ TITLE = Auto
29
+ SKIP_SCHEMA = False
30
+ CONFIG = None
31
+
32
+ def __init__(self, annotation=Empty, default=Empty, description=None, title=None, fills=None, skip_schema=None, **kwargs):
33
+ """
34
+
35
+ Infer default from type annotation, if enabled, use class/argument fills to create titles/descriptions, etc.
36
+
37
+ """
38
+
39
+ fills_super = getattr(super(), 'FILLS', None)
40
+ self.fills = (fills_super or {}) | (self.FILLS or {}) | (fills or {})
41
+
42
+ skip_schema = none_else(skip_schema, self.SKIP_SCHEMA)
43
+
44
+ self.annotation = self.ANNOTATION if annotation is Empty else annotation
45
+ if self.annotation is Empty:
46
+ raise ValueError("Annotation must be specified.")
47
+
48
+ if skip_schema:
49
+ self.annotation = SkipJsonSchema[self.annotation]
50
+
51
+ default = self.get_default_auto(default)
52
+ if default is Required:
53
+ default = PydanticUndefined
54
+
55
+ description = self.get_desc(description)
56
+ title = self.get_title_auto(title)
57
+ kwargs |= (self.CONFIG or {})
58
+
59
+ super().__init__(annotation=self.annotation, default=default, title=title, description=description, **kwargs)
60
+
61
+ @classmethod
62
+ def get_name_auto(cls) -> str:
63
+ """
64
+
65
+ Infer field name, if set to auto.
66
+
67
+ """
68
+ if cls.NAME is Auto:
69
+ return camel_to_snake(cls.__name__)
70
+ elif cls.NAME is None:
71
+ return cls.__name__
72
+
73
+ return cls.NAME
74
+
75
+ @cached_property
76
+ def fills(self) -> Dict[str, str]:
77
+ """
78
+
79
+ Get fills with filled title merged in
80
+
81
+ """
82
+
83
+ fills_super = getattr(super(), 'FILLS', None)
84
+
85
+ return (fills_super or {}) | (self.FILLS or {}) | dict(title=self.get_title_auto())
86
+
87
+ def get_default_auto(self, default) -> type[Any] | None | PydanticUndefinedType:
88
+ """
89
+
90
+ Infer default, if not specified.
91
+
92
+ """
93
+
94
+ if default is not Empty:
95
+ return default
96
+
97
+ if self.DEFAULT is not Auto:
98
+ return self.DEFAULT
99
+
100
+ if is_optional(self.annotation):
101
+ return None
102
+ else:
103
+ return Required
104
+
105
+ def get_title_auto(self, mask) -> str | None:
106
+ """
107
+
108
+ Get title from classname/mask
109
+
110
+ """
111
+
112
+ if not mask:
113
+ mask = self.__class__.__name__ if self.TITLE is Auto else self.TITLE
114
+
115
+ if mask:
116
+ return mask.format(**self.fills)
117
+
118
+ return None
119
+
120
+ def get_desc(self, mask) -> str | None:
121
+ """
122
+
123
+ Fill description mask, if specified
124
+
125
+ """
126
+
127
+ mask = mask or self.DESCRIPTION
128
+
129
+ if mask:
130
+ return mask.format(**self.fills)
131
+
132
+ return None
2
133
 
3
134
 
4
135
  def to_df(*objs, name_value='value'):
@@ -51,30 +182,102 @@ class MixinFromJson:
51
182
  return self
52
183
 
53
184
 
54
- class Base(BaseModel, MixinFromJson):
185
+ class CliRunMixin:
186
+ """
187
+
188
+ Mixin only so that it can also be used with Pydantic Settings.
189
+
190
+ TODO Ideally, the run method would be defined on dm.Base and the settings base would just inherit like set.Base(BaseSettings, dm.Base), but this isn't yet tested/could break Fields.
191
+
192
+ """
193
+
194
+ def run(self):
195
+ """
196
+
197
+ Behaviour when run as a CLI command, i.e. via Pydantic Settings. Run any subcommands then exit.
198
+
199
+ """
200
+
201
+ from pydantic_settings import get_subcommand
202
+
203
+ command = get_subcommand(self, is_required=False, cli_exit_on_error=False)
204
+
205
+ if not command:
206
+ return
207
+
208
+ result = command.run()
209
+ if inspect.isawaitable(result):
210
+ import asyncio
211
+ result = asyncio.run(result)
212
+
213
+ raise SystemExit(result)
214
+
215
+
216
+ class Base(BaseModel, MixinFromJson, CliRunMixin):
55
217
  """
56
218
 
57
- Base model
219
+ Base model allowing model definition via a list of custom Field objects.
58
220
 
59
221
  """
222
+ FIELDS: ClassVar[List[Field] | Dict[str, Field]] = []
60
223
 
61
- def to_df(self, name_value='value'):
224
+ def __init_subclass__(cls, **kwargs):
62
225
  """
63
226
 
64
- DataFrame representation with Fields as rows.
227
+ Fetch aggregated fields metadata from the hierarchy and set annotations and FieldInfo objects in the class.
65
228
 
66
229
  """
230
+ super().__init_subclass__(**kwargs)
67
231
 
68
- objs = []
69
- for name in self.model_fields.keys():
70
- val = getattr(self, name)
71
- objs.append(val)
232
+ fields = {}
233
+ for base in reversed(cls.__mro__):
72
234
 
73
- df = to_df(*objs, name_value=name_value)
74
- df['id'] = list(self.model_fields.keys())
75
- df = df.set_index('id', drop=True)
235
+ try:
236
+ raw = base.FIELDS
237
+ except AttributeError:
238
+ raw = {}
239
+
240
+ if isinstance(raw, dict):
241
+ fields |= raw
242
+ else:
243
+ fields |= get_class_lookup(*raw, name_function=lambda cls_field: cls_field.get_name_auto())
244
+
245
+ cls.FIELDS = fields
246
+
247
+ for name, field in fields.items():
248
+ if name in cls.__annotations__:
249
+ continue
250
+
251
+ if inspect.isclass(field):
252
+ field = field()
253
+
254
+ setattr(cls, name, field)
255
+ cls.__annotations__[name] = field.annotation
256
+
257
+ def to_df(self, **kwargs):
258
+ """
259
+
260
+ DataFrame representation of Data Model.
261
+
262
+ """
263
+ from fmtr.tools import tabular
264
+
265
+ data = self.model_dump(**kwargs)
266
+ df = tabular.pd.json_normalize(data, sep='_')
76
267
  return df
77
268
 
269
+ @classmethod
270
+ def to_df_empty(cls):
271
+ """
272
+
273
+ Empty DataFrame
274
+
275
+ """
276
+ from fmtr.tools import tabular
277
+
278
+ row = {name: None for name in cls.model_fields.keys()}
279
+ df = tabular.pd.DataFrame([row])
280
+ return df
78
281
 
79
282
  class Root(RootModel, MixinFromJson):
80
283
  """
@@ -83,11 +286,21 @@ class Root(RootModel, MixinFromJson):
83
286
 
84
287
  """
85
288
 
86
- def to_df(self):
289
+ def to_df(self, **kwargs):
87
290
  """
88
291
 
89
292
  DataFrame representation with items as rows.
90
293
 
91
294
  """
92
295
 
93
- return to_df(*self.items)
296
+ """
297
+
298
+ DataFrame representation of Data Model.
299
+
300
+ """
301
+ from fmtr.tools import tabular
302
+
303
+ data = [item.model_dump(**kwargs) for item in self.items]
304
+ dfs = [tabular.pd.json_normalize(datum, sep='_') for datum in data]
305
+ df = tabular.pd.concat(dfs, axis=tabular.CONCAT_VERTICALLY)
306
+ return df
@@ -0,0 +1,6 @@
1
+ from fmtr.tools.import_tools import MissingExtraMockModule
2
+
3
+ try:
4
+ from fmtr.tools.database_tools import document
5
+ except ModuleNotFoundError as exception:
6
+ document = MissingExtraMockModule('db.document', exception)
@@ -0,0 +1,51 @@
1
+ from functools import cached_property
2
+ from typing import List
3
+
4
+ import beanie
5
+ from beanie.odm import actions
6
+ from pymongo import AsyncMongoClient
7
+
8
+ from fmtr.tools import data_modelling_tools
9
+ from fmtr.tools.constants import Constants
10
+ from fmtr.tools.logging_tools import logger
11
+
12
+ ModifyEvents = [
13
+ actions.Insert,
14
+ actions.Replace,
15
+ actions.Save,
16
+ actions.SaveChanges,
17
+ actions.Update
18
+ ]
19
+
20
+
21
+ class Document(beanie.Document, data_modelling_tools.Base):
22
+ """
23
+
24
+ Document stub.
25
+
26
+ """
27
+
28
+
29
+ class Client:
30
+
31
+ def __init__(self, name, host=Constants.FMTR_DEV_HOST, port=27017, documents: List[beanie.Document] | None = None):
32
+ self.name = name
33
+ self.host = host
34
+ self.port = port
35
+ self.documents = documents
36
+
37
+ self.client = AsyncMongoClient(self.uri, tz_aware=True)
38
+ self.db = self.client[self.name]
39
+
40
+ @cached_property
41
+ def uri(self):
42
+ return f'mongodb://{self.host}:{self.port}'
43
+
44
+ async def connect(self):
45
+ """
46
+
47
+ Connect
48
+
49
+ """
50
+ with logger.span(f'Connecting to document database {self.uri=} {self.name=}'):
51
+ return await beanie.init_beanie(database=self.db, document_models=self.documents)
@@ -1,8 +1,9 @@
1
- from typing import Any
1
+ from types import UnionType, NoneType
2
+ from typing import Any, get_origin, get_args, Union, Annotated
2
3
 
3
4
  from fmtr.tools.tools import Raise
4
5
 
5
- TRUES = {str(True).lower(), str(1), 'y', 'yes'}
6
+ TRUES = {str(True).lower(), str(1), 'y', 'yes', 'on'}
6
7
 
7
8
 
8
9
  class TypeConversionFailed(ValueError):
@@ -84,3 +85,22 @@ def none_else(value: Any, default: Any) -> Any:
84
85
  if is_none(value):
85
86
  return default
86
87
  return value
88
+
89
+
90
+ def is_optional(annotation) -> bool:
91
+ """
92
+
93
+ Is type/annotation optional? todo should be in typing_tools?
94
+
95
+ """
96
+ if annotation is None:
97
+ return True
98
+
99
+ origin = get_origin(annotation)
100
+ args = get_args(annotation)
101
+
102
+ if origin is Annotated:
103
+ return is_optional(args[0])
104
+
105
+ is_opt = (origin is UnionType or origin is Union) and NoneType in args
106
+ return is_opt
@@ -0,0 +1,12 @@
1
+ from datetime import datetime, timezone
2
+
3
+ MIN = datetime.min.replace(tzinfo=timezone.utc)
4
+ MAX = datetime.max.replace(tzinfo=timezone.utc)
5
+
6
+ def now() -> datetime:
7
+ """
8
+
9
+ Now UTC
10
+
11
+ """
12
+ return datetime.now(tz=timezone.utc)
@@ -1,10 +1,44 @@
1
- import pydevd_pycharm
1
+ import dataclasses
2
2
 
3
3
  from fmtr.tools import environment_tools as env
4
4
  from fmtr.tools.constants import Constants
5
5
 
6
6
  MASK = 'Starting debugger at tcp://{host}:{port}...'
7
7
 
8
+
9
+ @dataclasses.dataclass
10
+ class ShellDebug:
11
+ """
12
+
13
+ Debugging information for shell commands
14
+
15
+ """
16
+ command: str
17
+ out: str
18
+ err: str
19
+ status: int
20
+
21
+ # timestamp: datetime=dataclasses.field(default_factory=datetime.now)
22
+
23
+ @classmethod
24
+ def from_path(cls, path_str):
25
+ """
26
+
27
+ Get debug info from path
28
+
29
+ """
30
+ from fmtr.tools import Path
31
+ path = Path(path_str).absolute()
32
+ data = {field.name: (path / f'{field.name}.log').read_text().strip() for field in dataclasses.fields(cls)}
33
+ self = cls(**data)
34
+ return self
35
+
36
+ @property
37
+ def env(self):
38
+ return env.get_dict()
39
+
40
+
41
+
8
42
  def trace(is_debug=None, host=None, port=None, stdoutToServer=True, stderrToServer=True, **kwargs):
9
43
  """
10
44
 
@@ -28,4 +62,29 @@ def trace(is_debug=None, host=None, port=None, stdoutToServer=True, stderrToServ
28
62
  msg = MASK.format(host=host, port=port)
29
63
  logger.info(msg)
30
64
 
65
+ import pydevd_pycharm
31
66
  pydevd_pycharm.settrace(host, port=port, stdoutToServer=stdoutToServer, stderrToServer=stderrToServer, **kwargs)
67
+
68
+
69
+ def debug_shell():
70
+ """
71
+
72
+ Starts a debug shell by initializing a `ShellDebug` object from a given path
73
+ and enabling tracing with debug mode turned on.
74
+
75
+ """
76
+ import sys
77
+ path_str = sys.argv[1]
78
+ data = ShellDebug.from_path(path_str)
79
+ trace(is_debug=True)
80
+ data
81
+
82
+
83
+ if __name__ == "__main__":
84
+ import sys
85
+
86
+ sys.argv = [
87
+ 'test.py',
88
+ './fmtr-debug/34e8d492-2f15-419a-8fcb-fe4fa0fa02bb',
89
+ ]
90
+ debug_shell()
@@ -0,0 +1,7 @@
1
+ from fmtr.tools.import_tools import MissingExtraMockModule
2
+
3
+ try:
4
+ from fmtr.tools.dns_tools import server, client, dm, proxy
5
+ import dns
6
+ except ModuleNotFoundError as exception:
7
+ dns = server = client = dm = proxy = MissingExtraMockModule('dns', exception)