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.
- fmtr/tools/__init__.py +86 -52
- fmtr/tools/ai_tools/__init__.py +2 -2
- fmtr/tools/ai_tools/agentic_tools.py +151 -32
- fmtr/tools/ai_tools/inference_tools.py +2 -1
- fmtr/tools/api_tools.py +73 -12
- fmtr/tools/async_tools.py +4 -0
- fmtr/tools/av_tools.py +7 -0
- fmtr/tools/caching_tools.py +101 -3
- fmtr/tools/constants.py +41 -0
- fmtr/tools/context_tools.py +23 -0
- fmtr/tools/data_modelling_tools.py +227 -14
- fmtr/tools/database_tools/__init__.py +6 -0
- fmtr/tools/database_tools/document.py +51 -0
- fmtr/tools/datatype_tools.py +22 -2
- fmtr/tools/datetime_tools.py +12 -0
- fmtr/tools/debugging_tools.py +60 -1
- fmtr/tools/dns_tools/__init__.py +7 -0
- fmtr/tools/dns_tools/client.py +97 -0
- fmtr/tools/dns_tools/dm.py +257 -0
- fmtr/tools/dns_tools/proxy.py +66 -0
- fmtr/tools/dns_tools/server.py +138 -0
- fmtr/tools/docker_tools/__init__.py +6 -0
- fmtr/tools/entrypoints/__init__.py +0 -0
- fmtr/tools/entrypoints/cache_hfh.py +3 -0
- fmtr/tools/entrypoints/ep_test.py +2 -0
- fmtr/tools/entrypoints/install_yamlscript.py +8 -0
- fmtr/tools/{console_script_tools.py → entrypoints/remote_debug_test.py} +1 -6
- fmtr/tools/entrypoints/shell_debug.py +8 -0
- fmtr/tools/environment_tools.py +3 -2
- fmtr/tools/function_tools.py +77 -1
- fmtr/tools/google_api_tools.py +15 -4
- fmtr/tools/ha_tools/__init__.py +8 -0
- fmtr/tools/ha_tools/constants.py +9 -0
- fmtr/tools/ha_tools/core.py +16 -0
- fmtr/tools/ha_tools/supervisor.py +16 -0
- fmtr/tools/ha_tools/utils.py +46 -0
- fmtr/tools/http_tools.py +52 -0
- fmtr/tools/inherit_tools.py +27 -0
- fmtr/tools/interface_tools/__init__.py +8 -0
- fmtr/tools/interface_tools/context.py +13 -0
- fmtr/tools/interface_tools/controls.py +354 -0
- fmtr/tools/interface_tools/interface_tools.py +189 -0
- fmtr/tools/iterator_tools.py +122 -1
- fmtr/tools/logging_tools.py +99 -18
- fmtr/tools/mqtt_tools.py +89 -0
- fmtr/tools/networking_tools.py +73 -0
- fmtr/tools/packaging_tools.py +14 -0
- fmtr/tools/path_tools/__init__.py +12 -0
- fmtr/tools/path_tools/app_path_tools.py +40 -0
- fmtr/tools/{path_tools.py → path_tools/path_tools.py} +217 -14
- fmtr/tools/path_tools/type_path_tools.py +3 -0
- fmtr/tools/pattern_tools.py +277 -0
- fmtr/tools/pdf_tools.py +39 -1
- fmtr/tools/settings_tools.py +27 -6
- fmtr/tools/setup_tools/__init__.py +8 -0
- fmtr/tools/setup_tools/setup_tools.py +481 -0
- fmtr/tools/string_tools.py +92 -13
- fmtr/tools/tabular_tools.py +61 -0
- fmtr/tools/tools.py +27 -2
- fmtr/tools/version +1 -1
- fmtr/tools/version_tools/__init__.py +12 -0
- fmtr/tools/version_tools/version_tools.py +51 -0
- fmtr/tools/webhook_tools.py +17 -0
- fmtr/tools/yaml_tools.py +64 -5
- fmtr/tools/youtube_tools.py +128 -0
- fmtr_tools-1.4.37.data/scripts/add-service +14 -0
- fmtr_tools-1.4.37.data/scripts/add-user-path +8 -0
- fmtr_tools-1.4.37.data/scripts/apt-headless +23 -0
- fmtr_tools-1.4.37.data/scripts/compose-update +10 -0
- fmtr_tools-1.4.37.data/scripts/docker-sandbox +43 -0
- fmtr_tools-1.4.37.data/scripts/docker-sandbox-init +23 -0
- fmtr_tools-1.4.37.data/scripts/docs-deploy +6 -0
- fmtr_tools-1.4.37.data/scripts/docs-serve +5 -0
- fmtr_tools-1.4.37.data/scripts/download +9 -0
- fmtr_tools-1.4.37.data/scripts/fmtr-test-script +3 -0
- fmtr_tools-1.4.37.data/scripts/ftu +3 -0
- fmtr_tools-1.4.37.data/scripts/ha-addon-launch +16 -0
- fmtr_tools-1.4.37.data/scripts/install-browser +8 -0
- fmtr_tools-1.4.37.data/scripts/parse-args +43 -0
- fmtr_tools-1.4.37.data/scripts/set-password +5 -0
- fmtr_tools-1.4.37.data/scripts/snips-install +14 -0
- fmtr_tools-1.4.37.data/scripts/ssh-auth +28 -0
- fmtr_tools-1.4.37.data/scripts/ssh-serve +15 -0
- fmtr_tools-1.4.37.data/scripts/vlc-tn +10 -0
- fmtr_tools-1.4.37.data/scripts/vm-launch +17 -0
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/METADATA +178 -54
- fmtr_tools-1.4.37.dist-info/RECORD +122 -0
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/WHEEL +1 -1
- fmtr_tools-1.4.37.dist-info/entry_points.txt +6 -0
- fmtr_tools-1.4.37.dist-info/top_level.txt +1 -0
- fmtr/tools/docker_tools.py +0 -30
- fmtr/tools/interface_tools.py +0 -64
- fmtr/tools/version_tools.py +0 -62
- fmtr_tools-1.1.1.dist-info/RECORD +0 -65
- fmtr_tools-1.1.1.dist-info/entry_points.txt +0 -3
- fmtr_tools-1.1.1.dist-info/top_level.txt +0 -2
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/licenses/LICENSE +0 -0
fmtr/tools/caching_tools.py
CHANGED
|
@@ -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
|
|
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.
|
|
38
|
+
logger.warning(f'Cache does not exist. Will be created. {str(path)=}...')
|
|
35
39
|
|
|
36
|
-
logger.info(f'Initializing Disk Cache
|
|
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
|
-
|
|
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
|
|
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
|
|
224
|
+
def __init_subclass__(cls, **kwargs):
|
|
62
225
|
"""
|
|
63
226
|
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
for
|
|
70
|
-
val = getattr(self, name)
|
|
71
|
-
objs.append(val)
|
|
232
|
+
fields = {}
|
|
233
|
+
for base in reversed(cls.__mro__):
|
|
72
234
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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,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)
|
fmtr/tools/datatype_tools.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from
|
|
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
|
fmtr/tools/debugging_tools.py
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
|
-
import
|
|
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()
|