fmtr.tools 1.3.81__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 +19 -1
- fmtr/tools/api_tools.py +65 -7
- fmtr/tools/async_tools.py +4 -0
- fmtr/tools/av_tools.py +7 -0
- fmtr/tools/constants.py +9 -1
- fmtr/tools/datatype_tools.py +1 -1
- fmtr/tools/debugging_tools.py +1 -2
- fmtr/tools/environment_tools.py +1 -0
- 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 +30 -4
- fmtr/tools/iterator_tools.py +93 -1
- fmtr/tools/logging_tools.py +72 -18
- fmtr/tools/mqtt_tools.py +89 -0
- fmtr/tools/networking_tools.py +73 -0
- fmtr/tools/path_tools/__init__.py +1 -1
- fmtr/tools/path_tools/path_tools.py +65 -6
- fmtr/tools/pattern_tools.py +17 -0
- fmtr/tools/settings_tools.py +4 -2
- fmtr/tools/setup_tools/setup_tools.py +35 -1
- fmtr/tools/version +1 -1
- fmtr/tools/yaml_tools.py +1 -3
- 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.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/METADATA +92 -50
- {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/RECORD +52 -23
- {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/WHEEL +0 -0
- {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/entry_points.txt +0 -0
- {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/licenses/LICENSE +0 -0
- {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
get_hostname = socket.gethostname
|
|
8
|
+
get_fqdn = socket.getfqdn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class MAC:
|
|
13
|
+
value: int = field(default_factory=uuid.getnode)
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_string(cls, s: str):
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
Initialise from a string.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
s = s.lower().replace(":", "").replace("-", "")
|
|
23
|
+
return cls(int(s, 16))
|
|
24
|
+
|
|
25
|
+
@cached_property
|
|
26
|
+
def int(self):
|
|
27
|
+
return self.value
|
|
28
|
+
|
|
29
|
+
@cached_property
|
|
30
|
+
def hex(self):
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
Plain 12-character hex string, no separators.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
return f"{self.value:012x}"
|
|
37
|
+
|
|
38
|
+
@cached_property
|
|
39
|
+
def components(self):
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
Return list of six 2-digit hex strings.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
h = self.hex
|
|
46
|
+
return [h[i:i + 2] for i in range(0, 12, 2)]
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def hex_colon(self):
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
Colon-separated hex string (default standard).
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
return self.string(":")
|
|
56
|
+
|
|
57
|
+
@cached_property
|
|
58
|
+
def is_random(self) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
Detect if this MAC is likely randomly generated by uuid.getnode().
|
|
62
|
+
Checks the "multicast" bit (bit 1 of the first byte).
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
return bool(self.value & (1 << 40))
|
|
66
|
+
|
|
67
|
+
def string(self, sep: str = ":") -> str:
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
Return MAC as a string with separator `sep`.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
return sep.join(self.components)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
1
3
|
import re
|
|
2
4
|
import subprocess
|
|
3
|
-
from pathlib import Path
|
|
4
5
|
from tempfile import gettempdir
|
|
5
|
-
from typing import Union, Any
|
|
6
|
+
from typing import Union, Any, Self
|
|
6
7
|
|
|
7
8
|
from fmtr.tools.constants import Constants
|
|
8
9
|
from fmtr.tools.platform_tools import is_wsl
|
|
@@ -204,11 +205,51 @@ class Path(type(Path())):
|
|
|
204
205
|
return kind
|
|
205
206
|
|
|
206
207
|
@property
|
|
207
|
-
def children(self):
|
|
208
|
+
def children(self) -> list[Self]:
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
Recursive children property
|
|
212
|
+
|
|
213
|
+
"""
|
|
208
214
|
if not self.is_dir():
|
|
209
215
|
return None
|
|
210
216
|
return sorted(self.iterdir(), key=lambda x: x.is_dir(), reverse=True)
|
|
211
217
|
|
|
218
|
+
@classmethod
|
|
219
|
+
def __get_pydantic_core_schema__(cls, source, handler):
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
Support Pydantic de/serialization and validation
|
|
223
|
+
|
|
224
|
+
TODO: Ideally these would be a mixin in dm, but then we'd need Pydantic to use it. Split dm module into Pydantic depts and other utils and import from there.
|
|
225
|
+
|
|
226
|
+
"""
|
|
227
|
+
from pydantic_core import core_schema
|
|
228
|
+
return core_schema.no_info_plain_validator_function(
|
|
229
|
+
cls.__deserialize_pydantic__,
|
|
230
|
+
serialization=core_schema.plain_serializer_function_ser_schema(cls.__serialize_pydantic__),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def __serialize_pydantic__(cls, self) -> str:
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
Serialize to string
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
return str(self)
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def __deserialize_pydantic__(cls, data) -> Self:
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
Deserialize from string
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
if isinstance(data, cls):
|
|
250
|
+
return data
|
|
251
|
+
return cls(data)
|
|
252
|
+
|
|
212
253
|
|
|
213
254
|
class FromCallerMixin:
|
|
214
255
|
"""
|
|
@@ -361,8 +402,6 @@ class PackagePaths(FromCallerMixin):
|
|
|
361
402
|
|
|
362
403
|
return self.data / Constants.DIR_NAME_SOURCE
|
|
363
404
|
|
|
364
|
-
|
|
365
|
-
|
|
366
405
|
@property
|
|
367
406
|
def settings(self) -> Path:
|
|
368
407
|
"""
|
|
@@ -381,6 +420,24 @@ class PackagePaths(FromCallerMixin):
|
|
|
381
420
|
"""
|
|
382
421
|
return self.artifact / Constants.DIR_NAME_HF
|
|
383
422
|
|
|
423
|
+
@property
|
|
424
|
+
def docs(self) -> Path:
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
Path of docs directory
|
|
428
|
+
|
|
429
|
+
"""
|
|
430
|
+
return self.repo / Constants.DOCS_DIR
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def docs_config(self) -> Path:
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
Path of docs config file
|
|
437
|
+
|
|
438
|
+
"""
|
|
439
|
+
return self.repo / Constants.DOCS_CONFIG_FILENAME
|
|
440
|
+
|
|
384
441
|
def __repr__(self) -> str:
|
|
385
442
|
"""
|
|
386
443
|
|
|
@@ -390,7 +447,9 @@ class PackagePaths(FromCallerMixin):
|
|
|
390
447
|
return f'{self.__class__.__name__}("{self.path}")'
|
|
391
448
|
|
|
392
449
|
|
|
450
|
+
root = Path('/')
|
|
451
|
+
|
|
393
452
|
if __name__ == "__main__":
|
|
394
453
|
path = Path('/usr/bin/bash').absolute()
|
|
395
454
|
path.type
|
|
396
|
-
path
|
|
455
|
+
path
|
fmtr/tools/pattern_tools.py
CHANGED
|
@@ -127,6 +127,23 @@ class Transformer:
|
|
|
127
127
|
is_recursive: bool = False
|
|
128
128
|
|
|
129
129
|
def __post_init__(self):
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
Compile on init
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
return self.compile(clear=False)
|
|
136
|
+
|
|
137
|
+
def compile(self, clear=True):
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
Re/compile regex pattern, invalidating existing caches if recompile.
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
if clear:
|
|
144
|
+
del self.pattern
|
|
145
|
+
del self.rx
|
|
146
|
+
|
|
130
147
|
with logger.span(f'Compiling expression {len(self.items)=}'):
|
|
131
148
|
rx = self.rx
|
|
132
149
|
logger.debug(f'Compiled successfully {rx.groups=}')
|
fmtr/tools/settings_tools.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import ClassVar, Any
|
|
|
2
2
|
|
|
3
3
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, YamlConfigSettingsSource, EnvSettingsSource, CliSettingsSource
|
|
4
4
|
|
|
5
|
+
from fmtr.tools import Constants
|
|
5
6
|
from fmtr.tools.data_modelling_tools import CliRunMixin
|
|
6
7
|
from fmtr.tools.path_tools import PackagePaths, Path
|
|
7
8
|
|
|
@@ -32,6 +33,7 @@ class Base(BaseSettings, CliRunMixin):
|
|
|
32
33
|
|
|
33
34
|
"""
|
|
34
35
|
|
|
36
|
+
ENV_NESTED_DELIMITER: ClassVar = Constants.ENV_NESTED_DELIMITER
|
|
35
37
|
paths: ClassVar = PackagePaths()
|
|
36
38
|
|
|
37
39
|
@classmethod
|
|
@@ -52,7 +54,7 @@ class Base(BaseSettings, CliRunMixin):
|
|
|
52
54
|
sources = (
|
|
53
55
|
init_settings,
|
|
54
56
|
CliSettingsSource(settings_cls, cli_parse_args=True),
|
|
55
|
-
EnvSettingsSource(settings_cls, env_prefix=cls.get_env_prefix()),
|
|
57
|
+
EnvSettingsSource(settings_cls, env_prefix=cls.get_env_prefix(), env_nested_delimiter=cls.ENV_NESTED_DELIMITER),
|
|
56
58
|
YamlScriptConfigSettingsSource(settings_cls, yaml_file=cls.paths.settings),
|
|
57
59
|
)
|
|
58
60
|
|
|
@@ -70,7 +72,7 @@ class Base(BaseSettings, CliRunMixin):
|
|
|
70
72
|
else:
|
|
71
73
|
stem = f'{cls.paths.name}'
|
|
72
74
|
|
|
73
|
-
prefix = f'{stem}
|
|
75
|
+
prefix = f'{stem}{cls.ENV_NESTED_DELIMITER}'.upper()
|
|
74
76
|
return prefix
|
|
75
77
|
|
|
76
78
|
@property
|
|
@@ -63,7 +63,7 @@ class SetupPaths(FromCallerMixin):
|
|
|
63
63
|
packages = [
|
|
64
64
|
dir for dir in base.iterdir()
|
|
65
65
|
if (dir / Constants.INIT_FILENAME).is_file()
|
|
66
|
-
and not any(fnmatch(dir.name, pattern) for pattern in Constants.PACKAGE_EXCLUDE_DIRS)
|
|
66
|
+
and not any(fnmatch(dir.name, pattern) for pattern in Constants.PACKAGE_EXCLUDE_DIRS) # todo add scripts dir
|
|
67
67
|
]
|
|
68
68
|
|
|
69
69
|
if len(packages) != 1:
|
|
@@ -106,6 +106,16 @@ class SetupPaths(FromCallerMixin):
|
|
|
106
106
|
"""
|
|
107
107
|
return self.path / Constants.ENTRYPOINTS_DIR
|
|
108
108
|
|
|
109
|
+
@property
|
|
110
|
+
def scripts(self) -> Path:
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
Paths of shell scripts
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
return self.repo / Constants.SCRIPTS_DIR
|
|
118
|
+
|
|
109
119
|
@property
|
|
110
120
|
def is_namespace(self) -> bool:
|
|
111
121
|
return bool(self.org)
|
|
@@ -159,6 +169,7 @@ class Setup(FromCallerMixin):
|
|
|
159
169
|
|
|
160
170
|
if do_setup:
|
|
161
171
|
self.setup()
|
|
172
|
+
self
|
|
162
173
|
|
|
163
174
|
def get_requirements_extras(self) -> Optional[List[str]]:
|
|
164
175
|
"""
|
|
@@ -214,6 +225,28 @@ class Setup(FromCallerMixin):
|
|
|
214
225
|
|
|
215
226
|
return console_scripts
|
|
216
227
|
|
|
228
|
+
@property
|
|
229
|
+
def scripts(self) -> List[str]:
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
Generate list of shell scripts.
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
paths = []
|
|
237
|
+
|
|
238
|
+
if not self.paths.scripts.exists():
|
|
239
|
+
return paths
|
|
240
|
+
|
|
241
|
+
for path in self.paths.scripts.iterdir():
|
|
242
|
+
if path.is_dir():
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
path_rel = path.relative_to(self.paths.repo)
|
|
246
|
+
paths.append(str(path_rel))
|
|
247
|
+
|
|
248
|
+
return paths
|
|
249
|
+
|
|
217
250
|
@cached_property
|
|
218
251
|
def name_command(self) -> str:
|
|
219
252
|
"""
|
|
@@ -353,6 +386,7 @@ class Setup(FromCallerMixin):
|
|
|
353
386
|
),
|
|
354
387
|
install_requires=self.dependencies.install,
|
|
355
388
|
extras_require=self.dependencies.extras,
|
|
389
|
+
scripts=self.scripts,
|
|
356
390
|
) | self.kwargs
|
|
357
391
|
return data
|
|
358
392
|
|
fmtr/tools/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.4.37
|
fmtr/tools/yaml_tools.py
CHANGED
|
@@ -3,8 +3,6 @@ from typing import Any
|
|
|
3
3
|
from yaml import CDumper as Dumper
|
|
4
4
|
from yaml import dump
|
|
5
5
|
|
|
6
|
-
from fmtr.tools import environment_tools as env
|
|
7
|
-
|
|
8
6
|
try:
|
|
9
7
|
import yamlscript
|
|
10
8
|
except ImportError:
|
|
@@ -29,7 +27,7 @@ def install():
|
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
@lru_cache
|
|
32
|
-
def get_module(is_auto=
|
|
30
|
+
def get_module(is_auto=True):
|
|
33
31
|
"""
|
|
34
32
|
|
|
35
33
|
Get the YAML Script runtime module, installing the runtime if specified
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from pytubefix import YouTube, Stream, extract, request
|
|
4
|
+
from pytubefix.exceptions import RegexMatchError
|
|
5
|
+
from typing import AsyncIterator, Iterator
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
|
|
8
|
+
from fmtr.tools.path_tools.path_tools import Path
|
|
9
|
+
|
|
10
|
+
Stream = Stream
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Video(YouTube):
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
Video stub
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AudioStreamDownloadError(Exception):
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
Error downloading audio stream
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AudioStreamData:
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
Audio stream download data and progress information
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
message: str | None = None
|
|
38
|
+
chunk: bytes | None = None
|
|
39
|
+
percentage: int | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AudioStreamDownloader:
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
Download the highest-bitrate audio stream and write to temp directory.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, url_or_id: str):
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
Initialise with URL or video ID
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
self.id = extract.video_id(url_or_id)
|
|
58
|
+
except RegexMatchError:
|
|
59
|
+
self.id = url_or_id
|
|
60
|
+
|
|
61
|
+
self.path = None
|
|
62
|
+
|
|
63
|
+
@cached_property
|
|
64
|
+
def url(self) -> str:
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
Get URL from ID
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return f'https://youtube.com/watch?v={self.id}'
|
|
71
|
+
|
|
72
|
+
async def download(self) -> AsyncIterator[AudioStreamData]:
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
Download the audio stream and yield chunks and progress information
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
yield AudioStreamData(message='Fetching video metadata...')
|
|
80
|
+
video = Video(self.url)
|
|
81
|
+
|
|
82
|
+
yield AudioStreamData('Finding audio streams...')
|
|
83
|
+
|
|
84
|
+
audio_streams = video.streams.filter(only_audio=True).order_by('bitrate')
|
|
85
|
+
if not audio_streams:
|
|
86
|
+
raise AudioStreamDownloadError(f'Error downloading: no audio streams found in "{video.title}"')
|
|
87
|
+
|
|
88
|
+
stream = audio_streams.last()
|
|
89
|
+
yield AudioStreamData(f'Found highest-bitrate audio stream: {stream.audio_codec}/{stream.subtype}@{stream.abr}')
|
|
90
|
+
|
|
91
|
+
self.path = Path.temp() / stream.default_filename
|
|
92
|
+
if self.path.exists():
|
|
93
|
+
self.path.unlink()
|
|
94
|
+
|
|
95
|
+
if stream.filesize == 0:
|
|
96
|
+
raise AudioStreamDownloadError(f'Error downloading: empty audio stream found in "{video.title}"')
|
|
97
|
+
|
|
98
|
+
yield AudioStreamData('Downloading...')
|
|
99
|
+
|
|
100
|
+
with self.path.open('wb') as out_file:
|
|
101
|
+
for data in self.iter_data(stream):
|
|
102
|
+
out_file.write(data.chunk)
|
|
103
|
+
yield data
|
|
104
|
+
|
|
105
|
+
def iter_data(self, stream: Stream, chunk_size: int | None = None) -> Iterator[AudioStreamData]:
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
Iterate over chunks of the specified size
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
bytes_total = bytes_remaining = stream.filesize
|
|
112
|
+
|
|
113
|
+
if chunk_size:
|
|
114
|
+
request.default_range_size = chunk_size
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
stream = request.stream(stream.url)
|
|
118
|
+
except HTTPError as e:
|
|
119
|
+
if e.code != 404:
|
|
120
|
+
raise
|
|
121
|
+
stream = request.seq_stream(stream.url)
|
|
122
|
+
|
|
123
|
+
for chunk in stream:
|
|
124
|
+
bytes_remaining -= len(chunk)
|
|
125
|
+
percentage = round(((bytes_total - bytes_remaining) / bytes_total) * 100)
|
|
126
|
+
|
|
127
|
+
data = AudioStreamData(chunk=chunk, percentage=percentage)
|
|
128
|
+
yield data
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
. parse-args "$@"
|
|
4
|
+
|
|
5
|
+
loginctl enable-linger $USER
|
|
6
|
+
|
|
7
|
+
NAME=$(basename "$FILE")
|
|
8
|
+
|
|
9
|
+
mkdir -p ~/.config/systemd/user
|
|
10
|
+
cp "$FILE" ~/.config/systemd/user/
|
|
11
|
+
systemctl --user daemon-reload
|
|
12
|
+
systemctl --user enable "$NAME"
|
|
13
|
+
systemctl --user start "$NAME"
|
|
14
|
+
systemctl --user status -l -n 50 "$NAME"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
source $SCRIPT_DIR/parse-args "$@"
|
|
7
|
+
|
|
8
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
9
|
+
apt update --yes
|
|
10
|
+
|
|
11
|
+
# if --full was passed, run full-upgrade
|
|
12
|
+
if [ "${FULL}" = "1" ]; then
|
|
13
|
+
apt --yes full-upgrade
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# if ARGS is not empty, install the packages
|
|
17
|
+
if [ ${#ARGS[@]} -gt 0 ]; then
|
|
18
|
+
apt install --yes --no-install-recommends "${ARGS[@]}"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
apt autoremove --yes
|
|
22
|
+
apt clean
|
|
23
|
+
rm -rf /var/lib/apt/lists/*
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
. parse-args "$@"
|
|
4
|
+
|
|
5
|
+
PROJECT_DIR=/opt/dev/repo/infrastructure/${CONTEXT-gex}/docker/${SERVICE}
|
|
6
|
+
CONTEXT=${CONTEXT-gex}
|
|
7
|
+
|
|
8
|
+
docker --context "$CONTEXT" compose --project-directory "$PROJECT_DIR" pull
|
|
9
|
+
docker --context "$CONTEXT" compose --project-directory "$PROJECT_DIR" up --detach
|
|
10
|
+
docker --context "$CONTEXT" compose --project-directory "$PROJECT_DIR" logs --follow
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
. parse-args "$@"
|
|
4
|
+
|
|
5
|
+
#Spin up the container in the background with sleep infinity
|
|
6
|
+
CONTAINER_ID=$(docker run -d \
|
|
7
|
+
--hostname=sandbox \
|
|
8
|
+
--user=user \
|
|
9
|
+
--volume=/opt/dev/:/opt/dev/ \
|
|
10
|
+
--publish=${PORT-9000}:${PORT-9000} \
|
|
11
|
+
${IMAGE-fmtr/python} \
|
|
12
|
+
bash -c "sleep infinity")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Capture Docker's default name
|
|
16
|
+
DEFAULT_NAME=$(docker inspect --format '{{.Name}}' $CONTAINER_ID | cut -c2-)
|
|
17
|
+
|
|
18
|
+
# Modify it dynamically (prefix or suffix)
|
|
19
|
+
MODIFIED_NAME="sandbox_${DEFAULT_NAME}"
|
|
20
|
+
docker rename $CONTAINER_ID $MODIFIED_NAME
|
|
21
|
+
|
|
22
|
+
echo "Started container $MODIFIED_NAME."
|
|
23
|
+
|
|
24
|
+
#Run the sandbox-init script as root (interactive)
|
|
25
|
+
docker exec \
|
|
26
|
+
--interactive=true \
|
|
27
|
+
--tty=true \
|
|
28
|
+
--user=root \
|
|
29
|
+
$CONTAINER_ID \
|
|
30
|
+
/opt/dev/repo/fmtr.tools/scripts/docker-sandbox-init --tools
|
|
31
|
+
|
|
32
|
+
#Start an interactive bash session as user
|
|
33
|
+
docker exec \
|
|
34
|
+
--interactive=true \
|
|
35
|
+
--tty=true \
|
|
36
|
+
--user=user \
|
|
37
|
+
--env USER=user \
|
|
38
|
+
$CONTAINER_ID \
|
|
39
|
+
bash
|
|
40
|
+
|
|
41
|
+
# Cleanup
|
|
42
|
+
docker rm -f $MODIFIED_NAME
|
|
43
|
+
echo "Container $MODIFIED_NAME removed."
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
SECURE_PATH=$PATH
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
source $SCRIPT_DIR/add-user-path --username=user --new=$SCRIPT_DIR
|
|
9
|
+
|
|
10
|
+
. parse-args "$@"
|
|
11
|
+
|
|
12
|
+
echo "Setting up sandbox..."
|
|
13
|
+
|
|
14
|
+
set-password
|
|
15
|
+
set-password --username=user
|
|
16
|
+
|
|
17
|
+
echo "user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/sandbox
|
|
18
|
+
echo "Defaults secure_path=\"${SECURE_PATH}\"" >> /etc/sudoers.d/sandbox
|
|
19
|
+
chmod 440 /etc/sudoers.d/sandbox
|
|
20
|
+
|
|
21
|
+
if [ "$TOOLS" == "1" ]; then
|
|
22
|
+
sudo -u user -H pip install --user fmtr.tools --upgrade
|
|
23
|
+
fi
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/with-contenv bashio
|
|
2
|
+
|
|
3
|
+
. parse-args "$@"
|
|
4
|
+
|
|
5
|
+
bashio::log.info "${NAME} container started."
|
|
6
|
+
|
|
7
|
+
export FMTR_DEV="$(bashio::config 'fmtr_dev')"
|
|
8
|
+
|
|
9
|
+
if bashio::var.true "${FMTR_DEV}"; then
|
|
10
|
+
bashio::log.info "Starting ${NAME} SSH development server"
|
|
11
|
+
printenv > /addon.env
|
|
12
|
+
echo "root:password" | chpasswd
|
|
13
|
+
/usr/sbin/sshd -D -o Port=22 -o PermitRootLogin=yes -o PasswordAuthentication=yes -o AllowTcpForwarding=yes -o LogLevel=VERBOSE
|
|
14
|
+
else
|
|
15
|
+
${NAME}
|
|
16
|
+
fi
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
sudo -u user /opt/dev/venv/fmtr.tools/bin/python -m pip install fmtr.tools[browsers] --upgrade
|
|
7
|
+
/opt/dev/venv/fmtr.tools/bin/python -m playwright install-deps
|
|
8
|
+
sudo -u user /opt/dev/venv/fmtr.tools/bin/python -m playwright install chromium
|