fmtr.tools 1.1.1__py3-none-any.whl → 1.3.81__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 +68 -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 +8 -5
- fmtr/tools/caching_tools.py +101 -3
- fmtr/tools/constants.py +33 -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 +21 -1
- fmtr/tools/datetime_tools.py +12 -0
- fmtr/tools/debugging_tools.py +60 -0
- 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 +2 -2
- fmtr/tools/function_tools.py +77 -1
- fmtr/tools/google_api_tools.py +15 -4
- fmtr/tools/http_tools.py +26 -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 +29 -0
- fmtr/tools/logging_tools.py +43 -16
- 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} +156 -12
- fmtr/tools/path_tools/type_path_tools.py +3 -0
- fmtr/tools/pattern_tools.py +260 -0
- fmtr/tools/pdf_tools.py +39 -1
- fmtr/tools/settings_tools.py +23 -4
- fmtr/tools/setup_tools/__init__.py +8 -0
- fmtr/tools/setup_tools/setup_tools.py +447 -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 +66 -5
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.3.81.dist-info}/METADATA +136 -54
- fmtr_tools-1.3.81.dist-info/RECORD +93 -0
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.3.81.dist-info}/WHEEL +1 -1
- fmtr_tools-1.3.81.dist-info/entry_points.txt +6 -0
- fmtr_tools-1.3.81.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.3.81.dist-info}/licenses/LICENSE +0 -0
fmtr/tools/pdf_tools.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from typing import List, Tuple, Dict, Any, Self
|
|
2
|
+
|
|
1
3
|
import pymupdf as pm
|
|
2
4
|
import pymupdf4llm
|
|
3
|
-
from typing import List, Tuple, Dict, Any, Self
|
|
4
5
|
|
|
5
6
|
from fmtr.tools import data_modelling_tools
|
|
6
7
|
|
|
@@ -179,6 +180,43 @@ class Document(pm.Document):
|
|
|
179
180
|
"""
|
|
180
181
|
return pymupdf4llm.to_markdown(self, **kwargs)
|
|
181
182
|
|
|
183
|
+
def to_text_pages(self) -> List[str]:
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
Simple text output per-page.
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
lines = []
|
|
190
|
+
for page in self:
|
|
191
|
+
text = page.get_text()
|
|
192
|
+
lines.append(text)
|
|
193
|
+
|
|
194
|
+
return lines
|
|
195
|
+
|
|
196
|
+
def to_text(self) -> str:
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
Simple text output.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
text = '\n'.join(self.to_text_pages())
|
|
204
|
+
return text
|
|
205
|
+
|
|
206
|
+
def split(self) -> List[Self]:
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
Split pages into individual documents.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
documents = []
|
|
214
|
+
for i, page in enumerate(self, start=1):
|
|
215
|
+
document = self.__class__()
|
|
216
|
+
document.insert_pdf(self, from_page=i, to_page=i)
|
|
217
|
+
documents.append(document)
|
|
218
|
+
|
|
219
|
+
return documents
|
|
182
220
|
|
|
183
221
|
if __name__ == '__main__':
|
|
184
222
|
from fmtr.tools.path_tools import Path
|
fmtr/tools/settings_tools.py
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
from typing import ClassVar, Any
|
|
2
|
+
|
|
1
3
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, YamlConfigSettingsSource, EnvSettingsSource, CliSettingsSource
|
|
2
|
-
from typing import ClassVar
|
|
3
4
|
|
|
4
|
-
from fmtr.tools.
|
|
5
|
+
from fmtr.tools.data_modelling_tools import CliRunMixin
|
|
6
|
+
from fmtr.tools.path_tools import PackagePaths, Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class YamlScriptConfigSettingsSource(YamlConfigSettingsSource):
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
Customer source for reading YAML *Script* (as opposed to plain YAML) configuration files.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def _read_file(self, file_path: Path) -> dict[str, Any]:
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
Use our own Path class to read YAML Script.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
data = Path(file_path).read_yaml() or {}
|
|
23
|
+
return data
|
|
5
24
|
|
|
6
25
|
|
|
7
|
-
class Base(BaseSettings):
|
|
26
|
+
class Base(BaseSettings, CliRunMixin):
|
|
8
27
|
"""
|
|
9
28
|
|
|
10
29
|
Base class for settings configuration using Pydantic BaseSettings.
|
|
@@ -34,7 +53,7 @@ class Base(BaseSettings):
|
|
|
34
53
|
init_settings,
|
|
35
54
|
CliSettingsSource(settings_cls, cli_parse_args=True),
|
|
36
55
|
EnvSettingsSource(settings_cls, env_prefix=cls.get_env_prefix()),
|
|
37
|
-
|
|
56
|
+
YamlScriptConfigSettingsSource(settings_cls, yaml_file=cls.paths.settings),
|
|
38
57
|
)
|
|
39
58
|
|
|
40
59
|
return sources
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from fmtr.tools.import_tools import MissingExtraMockModule
|
|
2
|
+
|
|
3
|
+
from fmtr.tools.setup_tools.setup_tools import Setup, SetupPaths, Dependencies, Tools
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from setuptools import find_namespace_packages, find_packages, setup as setup_setuptools
|
|
7
|
+
except ModuleNotFoundError as exception:
|
|
8
|
+
find_namespace_packages = find_packages = setup_setuptools = MissingExtraMockModule('setup', exception)
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from fnmatch import fnmatch
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from itertools import chain
|
|
6
|
+
from typing import List, Dict, Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from fmtr.tools.constants import Constants
|
|
9
|
+
from fmtr.tools.path_tools import Path
|
|
10
|
+
from fmtr.tools.path_tools.path_tools import FromCallerMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SetupPaths(FromCallerMixin):
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
Canonical paths for a repo.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, path=None, org=Constants.ORG_NAME):
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
Use calling module path as default path, if not otherwise specified.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
if not path:
|
|
27
|
+
path = self.from_caller()
|
|
28
|
+
|
|
29
|
+
self.org_name = org
|
|
30
|
+
self.repo = Path(path)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def readme(self) -> Path:
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
Path of the README file.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
return self.repo / 'README.md'
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def version(self) -> Path:
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
Path of the version file
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
return self.path / Constants.FILENAME_VERSION
|
|
49
|
+
|
|
50
|
+
@cached_property
|
|
51
|
+
def path(self) -> Path:
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
Infer the package path. It should be the only non-excluded package in the repo/org Path.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if self.is_namespace:
|
|
59
|
+
base = self.org
|
|
60
|
+
else:
|
|
61
|
+
base = self.repo
|
|
62
|
+
|
|
63
|
+
packages = [
|
|
64
|
+
dir for dir in base.iterdir()
|
|
65
|
+
if (dir / Constants.INIT_FILENAME).is_file()
|
|
66
|
+
and not any(fnmatch(dir.name, pattern) for pattern in Constants.PACKAGE_EXCLUDE_DIRS)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if len(packages) != 1:
|
|
70
|
+
dirs_str = ', '.join([str(dir) for dir in packages])
|
|
71
|
+
msg = f'Expected exactly one package in {self.repo}, found {dirs_str}'
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
|
|
74
|
+
package = next(iter(packages))
|
|
75
|
+
return package
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def org(self) -> bool | Path:
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
Get the org path, i.e. the namespace parent directory.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
if not self.org_name:
|
|
85
|
+
return False
|
|
86
|
+
org = self.repo / self.org_name
|
|
87
|
+
if not org.is_dir():
|
|
88
|
+
return False
|
|
89
|
+
return org
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def entrypoint(self) -> Path:
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
Path of base entrypoint module.
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
return self.path / Constants.ENTRYPOINT_FILE
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def entrypoints(self) -> Path:
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
Path of entrypoints sub-package.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
return self.path / Constants.ENTRYPOINTS_DIR
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_namespace(self) -> bool:
|
|
111
|
+
return bool(self.org)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def name(self) -> str:
|
|
115
|
+
return self.path.stem
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Setup(FromCallerMixin):
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
Abstract canonical pacakge setup for setuptools.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
AUTHOR = 'Frontmatter'
|
|
125
|
+
AUTHOR_EMAIL = 'innovative.fowler@mask.pro.fmtr.dev'
|
|
126
|
+
|
|
127
|
+
REQUIREMENTS_ARG = 'requirements'
|
|
128
|
+
|
|
129
|
+
ENTRYPOINT_COMMAND_SEP = '-'
|
|
130
|
+
ENTRYPOINT_FUNCTION_SEP = '_'
|
|
131
|
+
ENTRYPOINT_FUNC_NAME = 'main'
|
|
132
|
+
|
|
133
|
+
def __init__(self, dependencies, paths=None, org=Constants.ORG_NAME, client=None, do_setup=True, **kwargs):
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
First check if commandline arguments for requirements output exist. If so, print them and return early.
|
|
137
|
+
Otherwise, continue generating data to pass to setuptools.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
self.kwargs = kwargs
|
|
141
|
+
|
|
142
|
+
if type(dependencies) is not Dependencies:
|
|
143
|
+
dependencies = Dependencies(**dependencies)
|
|
144
|
+
self.dependencies = dependencies
|
|
145
|
+
|
|
146
|
+
requirements_extras = self.get_requirements_extras()
|
|
147
|
+
|
|
148
|
+
if requirements_extras:
|
|
149
|
+
self.print_requirements()
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
self.org = org
|
|
153
|
+
|
|
154
|
+
if not paths:
|
|
155
|
+
paths = SetupPaths(path=self.from_caller(), org=self.org)
|
|
156
|
+
self.paths = paths
|
|
157
|
+
|
|
158
|
+
self.client = client
|
|
159
|
+
|
|
160
|
+
if do_setup:
|
|
161
|
+
self.setup()
|
|
162
|
+
|
|
163
|
+
def get_requirements_extras(self) -> Optional[List[str]]:
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
Get list of extras from command line arguments.
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if self.REQUIREMENTS_ARG not in sys.argv:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
extras_str = sys.argv[-1]
|
|
173
|
+
extras = extras_str.split(',')
|
|
174
|
+
return extras
|
|
175
|
+
|
|
176
|
+
def print_requirements(self):
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
Output flat list of requirements for specified extras
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
reqs = []
|
|
183
|
+
reqs += self.dependencies.install
|
|
184
|
+
|
|
185
|
+
for extra in sys.argv[-1].split(','):
|
|
186
|
+
reqs += self.dependencies.extras[extra]
|
|
187
|
+
reqs = '\n'.join(reqs)
|
|
188
|
+
print(reqs)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def console_scripts(self) -> List[str]:
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
Generate console scripts for the `entrypoint` module - and/or any modules in `entrypoints` sub-package.
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
if not self.paths.entrypoints.exists():
|
|
199
|
+
paths_mods = []
|
|
200
|
+
else:
|
|
201
|
+
paths_mods = list(self.paths.entrypoints.iterdir())
|
|
202
|
+
|
|
203
|
+
names_mods = [path.stem for path in paths_mods if path.is_file() and path.name != Constants.INIT_FILENAME]
|
|
204
|
+
command_suffixes = [name_mod.replace(self.ENTRYPOINT_FUNCTION_SEP, self.ENTRYPOINT_COMMAND_SEP) for name_mod in names_mods]
|
|
205
|
+
commands = [f'{self.name_command}-{command_suffix}' for command_suffix in command_suffixes]
|
|
206
|
+
paths = [f'{self.name}.{Constants.ENTRYPOINTS_DIR}.{name_mod}:{self.ENTRYPOINT_FUNC_NAME}' for name_mod in names_mods]
|
|
207
|
+
|
|
208
|
+
if self.paths.entrypoint.exists():
|
|
209
|
+
commands.append(self.name_command)
|
|
210
|
+
path = f'{self.name}.{self.paths.entrypoint.stem}:{self.ENTRYPOINT_FUNC_NAME}'
|
|
211
|
+
paths.append(path)
|
|
212
|
+
|
|
213
|
+
console_scripts = [f'{command} = {path}' for command, path in zip(commands, paths)]
|
|
214
|
+
|
|
215
|
+
return console_scripts
|
|
216
|
+
|
|
217
|
+
@cached_property
|
|
218
|
+
def name_command(self) -> str:
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
Name as a command, e.g. `fmtr-tools`
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
return self.name.replace('.', self.ENTRYPOINT_COMMAND_SEP)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def name(self) -> str:
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
Full library name
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
if self.paths.is_namespace:
|
|
234
|
+
return f'{self.paths.org_name}.{self.paths.name}'
|
|
235
|
+
return self.paths.name
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def author(self) -> str:
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
Create appropriate author string
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
if self.client:
|
|
245
|
+
return f'{self.AUTHOR} on behalf of {self.client}'
|
|
246
|
+
return self.AUTHOR
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def copyright(self) -> str:
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
Create appropriate copyright string
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
if self.client:
|
|
256
|
+
return self.client
|
|
257
|
+
return self.AUTHOR
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def long_description(self) -> str:
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
Read in README.md
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
return self.paths.readme.read_text()
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def version(self) -> str:
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
Read in the version string from file
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
return self.paths.version.read_text().strip()
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def find(self) -> Callable:
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
Use the appropriate package finding function from setuptools
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
from fmtr.tools import setup
|
|
285
|
+
|
|
286
|
+
if self.paths.is_namespace:
|
|
287
|
+
return setup.find_namespace_packages
|
|
288
|
+
else:
|
|
289
|
+
return setup.find_packages
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def packages(self) -> List[str]:
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
Fetch list of packages excluding canonical paths
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
excludes = list(Constants.PACKAGE_EXCLUDE_DIRS) + [f'{name}.*' for name in Constants.PACKAGE_EXCLUDE_DIRS if '*' not in name]
|
|
299
|
+
packages = self.find(where=str(self.paths.repo), exclude=excludes)
|
|
300
|
+
return packages
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def package_dir(self):
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
Needs to be relative apparently as absolute paths break during packaging
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
if self.paths.is_namespace:
|
|
310
|
+
return {'': '.'}
|
|
311
|
+
else:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def package_data(self):
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
Default package data is just the version file
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
return {self.name: [Constants.FILENAME_VERSION]}
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def url(self) -> str:
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
Default to GitHub URL
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
return f'https://github.com/{self.org}/{self.name}'
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def data(self) -> Dict[str, Any]:
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
Generate data for use by setuptools
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
data = dict(
|
|
340
|
+
name=self.name,
|
|
341
|
+
version=self.version,
|
|
342
|
+
author=self.author,
|
|
343
|
+
author_email=self.AUTHOR_EMAIL,
|
|
344
|
+
url=self.url,
|
|
345
|
+
license=f'Copyright © {datetime.now().year} {self.copyright}. All rights reserved.',
|
|
346
|
+
long_description=self.long_description,
|
|
347
|
+
long_description_content_type='text/markdown',
|
|
348
|
+
packages=self.packages,
|
|
349
|
+
package_dir=self.package_dir,
|
|
350
|
+
package_data=self.package_data,
|
|
351
|
+
entry_points=dict(
|
|
352
|
+
console_scripts=self.console_scripts,
|
|
353
|
+
),
|
|
354
|
+
install_requires=self.dependencies.install,
|
|
355
|
+
extras_require=self.dependencies.extras,
|
|
356
|
+
) | self.kwargs
|
|
357
|
+
return data
|
|
358
|
+
|
|
359
|
+
def setup(self):
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
Call setuptools.setup using generated data
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
from fmtr.tools import setup
|
|
367
|
+
|
|
368
|
+
return setup.setup_setuptools(**self.data)
|
|
369
|
+
|
|
370
|
+
def __repr__(self) -> str:
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
Show library name
|
|
374
|
+
|
|
375
|
+
"""
|
|
376
|
+
return f'{self.__class__.__name__}("{self.name}")'
|
|
377
|
+
|
|
378
|
+
class Tools:
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
Helper for downstream libraries to specify lists of `fmtr.tools` extras
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
MASK = f'{Constants.LIBRARY_NAME}[{{extras}}]'
|
|
385
|
+
|
|
386
|
+
def __init__(self, *extras):
|
|
387
|
+
self.extras = extras
|
|
388
|
+
|
|
389
|
+
def __str__(self):
|
|
390
|
+
extras_str = ','.join(self.extras)
|
|
391
|
+
return self.MASK.format(extras=extras_str)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class Dependencies:
|
|
396
|
+
ALL = 'all'
|
|
397
|
+
INSTALL = 'install'
|
|
398
|
+
|
|
399
|
+
def __init__(self, **kwargs):
|
|
400
|
+
self.dependencies = kwargs
|
|
401
|
+
|
|
402
|
+
def resolve_values(self, key) -> List[str]:
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
Flatten a list of dependencies.
|
|
406
|
+
|
|
407
|
+
"""
|
|
408
|
+
values_resolved = []
|
|
409
|
+
values = self.dependencies[key]
|
|
410
|
+
|
|
411
|
+
for value in values:
|
|
412
|
+
if value == key or value not in self.dependencies:
|
|
413
|
+
# Add the value directly if it references itself or is not a dependency key.
|
|
414
|
+
values_resolved.append(str(value))
|
|
415
|
+
else:
|
|
416
|
+
# Recurse into nested dependencies.
|
|
417
|
+
values_resolved += self.resolve_values(value)
|
|
418
|
+
|
|
419
|
+
return values_resolved
|
|
420
|
+
|
|
421
|
+
@cached_property
|
|
422
|
+
def extras(self) -> Dict[str, List[str]]:
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
Flatten dependencies.
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
resolved = {key: self.resolve_values(key) for key in self.dependencies.keys()}
|
|
429
|
+
resolved.pop(self.INSTALL, None)
|
|
430
|
+
resolved[self.ALL] = list(set(chain.from_iterable(resolved.values())))
|
|
431
|
+
return resolved
|
|
432
|
+
|
|
433
|
+
@cached_property
|
|
434
|
+
def install(self) -> List[str]:
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
Get install_requires
|
|
438
|
+
|
|
439
|
+
"""
|
|
440
|
+
if self.INSTALL in self.dependencies:
|
|
441
|
+
return self.resolve_values(self.INSTALL)
|
|
442
|
+
else:
|
|
443
|
+
return []
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == '__main__':
|
|
447
|
+
...
|
fmtr/tools/string_tools.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from collections import namedtuple
|
|
2
|
-
|
|
3
1
|
import re
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from dataclasses import dataclass
|
|
4
4
|
from string import Formatter
|
|
5
5
|
from textwrap import dedent
|
|
6
6
|
from typing import List
|
|
@@ -84,27 +84,80 @@ def sanitize(*strings, sep: str = '-') -> str:
|
|
|
84
84
|
return string
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
@dataclass
|
|
88
|
+
class Truncation:
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
Result type for truncation functions
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
text: str
|
|
95
|
+
text_without_sep: str | None
|
|
96
|
+
original: str
|
|
97
|
+
remainder: str | None
|
|
98
|
+
sep: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def truncate(text, length=None, sep=ELLIPSIS, return_type=str):
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
Truncate a string to length characters
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
text = flatten(text)
|
|
108
|
+
if len(text) <= length or not length:
|
|
109
|
+
return text if return_type is str else Truncation(text, text, text, None, sep)
|
|
110
|
+
|
|
111
|
+
cutoff = length - len(sep)
|
|
112
|
+
truncated = text[:cutoff] + sep
|
|
113
|
+
|
|
114
|
+
if return_type is str:
|
|
115
|
+
return truncated
|
|
116
|
+
else:
|
|
117
|
+
return Truncation(
|
|
118
|
+
text=truncated,
|
|
119
|
+
text_without_sep=text[:cutoff],
|
|
120
|
+
original=text,
|
|
121
|
+
remainder=text[cutoff:] or None,
|
|
122
|
+
sep=sep
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def truncate_mid(text, length=None, sep=ELLIPSIS, return_type=str):
|
|
88
127
|
"""
|
|
89
128
|
|
|
90
|
-
Truncate a string to `length` characters in the middle
|
|
129
|
+
Truncate a string to `length` characters in the middle.
|
|
91
130
|
|
|
92
131
|
"""
|
|
93
132
|
text = flatten(text)
|
|
94
133
|
if len(text) <= length or not length:
|
|
95
|
-
return text
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
return text if return_type is str else Truncation(text, text, text, '', sep)
|
|
135
|
+
|
|
136
|
+
half = (length - len(sep)) // 2
|
|
137
|
+
left = text[:half]
|
|
138
|
+
right = text[-half:]
|
|
139
|
+
truncated = left + sep + right
|
|
140
|
+
|
|
141
|
+
if return_type is str:
|
|
142
|
+
return truncated
|
|
143
|
+
else:
|
|
144
|
+
return Truncation(
|
|
145
|
+
text=truncated,
|
|
146
|
+
text_without_sep=None,
|
|
147
|
+
original=text,
|
|
148
|
+
remainder=None,
|
|
149
|
+
sep=sep
|
|
150
|
+
)
|
|
98
151
|
|
|
99
152
|
|
|
100
|
-
def flatten(raw):
|
|
153
|
+
def flatten(raw, sep=' '):
|
|
101
154
|
"""
|
|
102
155
|
|
|
103
156
|
Flatten a multiline string to a single line
|
|
104
157
|
|
|
105
158
|
"""
|
|
106
159
|
lines = raw.splitlines()
|
|
107
|
-
text =
|
|
160
|
+
text = sep.join(lines)
|
|
108
161
|
text = text.strip()
|
|
109
162
|
return text
|
|
110
163
|
|
|
@@ -121,6 +174,23 @@ def join(strings, sep=' '):
|
|
|
121
174
|
return text
|
|
122
175
|
|
|
123
176
|
|
|
177
|
+
def join_natural(items, sep=', ', conj='and'):
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
Natural language list
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
items = list(items)
|
|
185
|
+
if not items:
|
|
186
|
+
return ""
|
|
187
|
+
if len(items) == 1:
|
|
188
|
+
return items[0]
|
|
189
|
+
firsts, last = items[:-1], items[-1]
|
|
190
|
+
firsts_str = join(firsts, sep=sep)
|
|
191
|
+
text = f"{firsts_str} {conj} {last}"
|
|
192
|
+
return text
|
|
193
|
+
|
|
124
194
|
class Mask:
|
|
125
195
|
"""
|
|
126
196
|
|
|
@@ -172,8 +242,17 @@ def trim(text: str) -> str:
|
|
|
172
242
|
"""
|
|
173
243
|
return dedent(text).strip()
|
|
174
244
|
|
|
175
|
-
if __name__ == '__main__':
|
|
176
|
-
import numpy as np
|
|
177
245
|
|
|
178
|
-
|
|
179
|
-
|
|
246
|
+
ACRONYM_BOUNDARY = re.compile(r'([A-Z]+)([A-Z][a-z])')
|
|
247
|
+
CAMEL_BOUNDARY = re.compile(r'([a-z0-9])([A-Z])')
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def camel_to_snake(name: str) -> str:
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
Camel case to snake case
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
name = ACRONYM_BOUNDARY.sub(r'\1_\2', name)
|
|
257
|
+
name = CAMEL_BOUNDARY.sub(r'\1_\2', name)
|
|
258
|
+
return name.lower()
|