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.
Files changed (52) hide show
  1. fmtr/tools/__init__.py +19 -1
  2. fmtr/tools/api_tools.py +65 -7
  3. fmtr/tools/async_tools.py +4 -0
  4. fmtr/tools/av_tools.py +7 -0
  5. fmtr/tools/constants.py +9 -1
  6. fmtr/tools/datatype_tools.py +1 -1
  7. fmtr/tools/debugging_tools.py +1 -2
  8. fmtr/tools/environment_tools.py +1 -0
  9. fmtr/tools/ha_tools/__init__.py +8 -0
  10. fmtr/tools/ha_tools/constants.py +9 -0
  11. fmtr/tools/ha_tools/core.py +16 -0
  12. fmtr/tools/ha_tools/supervisor.py +16 -0
  13. fmtr/tools/ha_tools/utils.py +46 -0
  14. fmtr/tools/http_tools.py +30 -4
  15. fmtr/tools/iterator_tools.py +93 -1
  16. fmtr/tools/logging_tools.py +72 -18
  17. fmtr/tools/mqtt_tools.py +89 -0
  18. fmtr/tools/networking_tools.py +73 -0
  19. fmtr/tools/path_tools/__init__.py +1 -1
  20. fmtr/tools/path_tools/path_tools.py +65 -6
  21. fmtr/tools/pattern_tools.py +17 -0
  22. fmtr/tools/settings_tools.py +4 -2
  23. fmtr/tools/setup_tools/setup_tools.py +35 -1
  24. fmtr/tools/version +1 -1
  25. fmtr/tools/yaml_tools.py +1 -3
  26. fmtr/tools/youtube_tools.py +128 -0
  27. fmtr_tools-1.4.37.data/scripts/add-service +14 -0
  28. fmtr_tools-1.4.37.data/scripts/add-user-path +8 -0
  29. fmtr_tools-1.4.37.data/scripts/apt-headless +23 -0
  30. fmtr_tools-1.4.37.data/scripts/compose-update +10 -0
  31. fmtr_tools-1.4.37.data/scripts/docker-sandbox +43 -0
  32. fmtr_tools-1.4.37.data/scripts/docker-sandbox-init +23 -0
  33. fmtr_tools-1.4.37.data/scripts/docs-deploy +6 -0
  34. fmtr_tools-1.4.37.data/scripts/docs-serve +5 -0
  35. fmtr_tools-1.4.37.data/scripts/download +9 -0
  36. fmtr_tools-1.4.37.data/scripts/fmtr-test-script +3 -0
  37. fmtr_tools-1.4.37.data/scripts/ftu +3 -0
  38. fmtr_tools-1.4.37.data/scripts/ha-addon-launch +16 -0
  39. fmtr_tools-1.4.37.data/scripts/install-browser +8 -0
  40. fmtr_tools-1.4.37.data/scripts/parse-args +43 -0
  41. fmtr_tools-1.4.37.data/scripts/set-password +5 -0
  42. fmtr_tools-1.4.37.data/scripts/snips-install +14 -0
  43. fmtr_tools-1.4.37.data/scripts/ssh-auth +28 -0
  44. fmtr_tools-1.4.37.data/scripts/ssh-serve +15 -0
  45. fmtr_tools-1.4.37.data/scripts/vlc-tn +10 -0
  46. fmtr_tools-1.4.37.data/scripts/vm-launch +17 -0
  47. {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/METADATA +92 -50
  48. {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/RECORD +52 -23
  49. {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/WHEEL +0 -0
  50. {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/entry_points.txt +0 -0
  51. {fmtr_tools-1.3.81.dist-info → fmtr_tools-1.4.37.dist-info}/licenses/LICENSE +0 -0
  52. {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,5 +1,5 @@
1
1
  from fmtr.tools.import_tools import MissingExtraMockModule
2
- from fmtr.tools.path_tools.path_tools import Path, PackagePaths
2
+ from fmtr.tools.path_tools.path_tools import Path, PackagePaths, root
3
3
 
4
4
  try:
5
5
  from fmtr.tools.path_tools.app_path_tools import AppPaths
@@ -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
@@ -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=}')
@@ -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}_'.upper()
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.3.81
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=env.IS_DEV):
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,8 @@
1
+ #!/bin/bash
2
+
3
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
4
+ source $SCRIPT_DIR/parse-args "$@"
5
+
6
+ NEW=${NEW:-/home/${USERNAME}/.local/bin/}
7
+
8
+ PATH="${NEW}:${PATH}"
@@ -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,6 @@
1
+ #!/bin/bash
2
+
3
+ . parse-args "$@"
4
+
5
+ mkdocs gh-deploy --remote-branch docs --force
6
+
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+
3
+ . parse-args "$@"
4
+
5
+ mkdocs serve --dev-addr=${HOST-0.0.0.0}:${PORT-8180} --livereload
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+
3
+ . parse-args "$@"
4
+
5
+ OUT_OPT=${FILE:+-o "$FILE"}
6
+ [ -z "$FILE" ] && OUT_OPT="-O"
7
+
8
+ curl -L -C - --retry 5 --retry-delay 5 --retry-connrefused --progress-bar $OUT_OPT "$URL"
9
+
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ echo "Test Script Ran"
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ pip install fmtr.tools --upgrade
@@ -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