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
@@ -0,0 +1,97 @@
1
+ from dataclasses import dataclass
2
+ from dns import query as dnspython_query, message as dnspython_message, rdatatype as dnspython_rdatatype, rcode as dnspython_rcode
3
+ from functools import cached_property
4
+ from httpx_retries import Retry, RetryTransport
5
+ from typing import Optional
6
+
7
+ from fmtr.tools import http_tools as http
8
+ from fmtr.tools.dns_tools.dm import Exchange, Response
9
+ from fmtr.tools.logging_tools import logger
10
+
11
+ RETRY_STRATEGY = Retry(
12
+ total=2, # initial + 1 retry
13
+ allowed_methods={"GET", "POST"},
14
+ status_forcelist={502, 503, 504},
15
+ retry_on_exceptions=None, # defaults to httpx.TransportError etc.
16
+ backoff_factor=0.25, # short backoff (e.g. 0.25s, 0.5s)
17
+ max_backoff_wait=0.75, # max total delay before giving up
18
+ backoff_jitter=0.1, # small jitter to avoid retry bursts
19
+ respect_retry_after_header=False, # DoH resolvers probably won't set this
20
+ )
21
+
22
+
23
+ class HTTPClientDoH(http.Client):
24
+ """
25
+
26
+ Base HTTP client for DoH-appropriate retry strategy.
27
+
28
+ """
29
+ TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
30
+
31
+
32
+ @dataclass
33
+ class Plain:
34
+ """
35
+
36
+ Plain DNS
37
+
38
+ """
39
+ host: str
40
+ port: int = 53
41
+ ttl_min: Optional[int] = None
42
+
43
+ def resolve(self, exchange: Exchange):
44
+
45
+ with logger.span(f'UDP {self.host}:{self.port}'):
46
+ response_plain = dnspython_query.udp(q=exchange.query_last, where=self.host, port=self.port)
47
+ response = Response.from_message(response_plain)
48
+ for answer in response.message.answer:
49
+ answer.ttl = max(answer.ttl, self.ttl_min or answer.ttl)
50
+
51
+ exchange.response = response
52
+
53
+
54
+ @dataclass
55
+ class HTTP:
56
+ """
57
+
58
+ DNS over HTTP
59
+
60
+ """
61
+
62
+ HEADERS = {"Content-Type": "application/dns-message"}
63
+ CLIENT = HTTPClientDoH()
64
+ BOOTSTRAP = Plain('8.8.8.8')
65
+
66
+ host: str
67
+ url: str
68
+
69
+
70
+ @cached_property
71
+ def ip(self):
72
+ message = dnspython_message.make_query(self.host, dnspython_rdatatype.A, flags=0)
73
+ exchange = Exchange.from_wire(message.to_wire(), ip=None, port=None)
74
+ self.BOOTSTRAP.resolve(exchange)
75
+ ip = next(iter(exchange.response.answer.items.keys())).address
76
+ return ip
77
+
78
+ def resolve(self, exchange: Exchange):
79
+ """
80
+
81
+ Resolve via DoH
82
+
83
+ """
84
+
85
+ headers = self.HEADERS | dict(Host=self.host)
86
+ url = self.url.format(host=self.ip)
87
+
88
+ try:
89
+ response_doh = self.CLIENT.post(url, headers=headers, content=exchange.query_last.to_wire())
90
+ response_doh.raise_for_status()
91
+ response = Response.from_http(response_doh)
92
+ exchange.response = response
93
+
94
+ except Exception as exception:
95
+ exchange.response.message.set_rcode(dnspython_rcode.SERVFAIL)
96
+ exchange.response.is_complete = True
97
+ logger.exception(exception)
@@ -0,0 +1,257 @@
1
+ import dns
2
+ import httpx
3
+ from dataclasses import dataclass, field
4
+ from dns import rcode as dnspython_rcode, reversename as dnspython_reversename
5
+ from dns.message import Message, QueryMessage
6
+ from dns.rrset import RRset
7
+ from functools import cached_property
8
+ from typing import Self, Optional, List
9
+
10
+ from fmtr.tools.string_tools import join
11
+
12
+ TTL_CODE_DEFAULTS = {
13
+ dnspython_rcode.NOERROR: 300, # Successful query
14
+ dnspython_rcode.FORMERR: 60, # Format error
15
+ dnspython_rcode.SERVFAIL: 10, # Server failure
16
+ dnspython_rcode.NXDOMAIN: 60 * 60, # Non-existent domain
17
+ dnspython_rcode.NOTIMP: 60, # Not implemented
18
+ dnspython_rcode.REFUSED: 60, # Refused
19
+ dnspython_rcode.YXDOMAIN: 600, # Name exists when it should not
20
+ dnspython_rcode.YXRRSET: 600, # RR Set exists when it should not
21
+ dnspython_rcode.NXRRSET: 300, # RR Set that should exist does not
22
+ dnspython_rcode.NOTAUTH: 60, # Not authorized
23
+ dnspython_rcode.NOTZONE: 60 # Name not contained in zone
24
+ }
25
+
26
+ @dataclass
27
+ class BaseDNSData:
28
+ """
29
+
30
+ DNS response object.
31
+
32
+ """
33
+ wire: bytes
34
+
35
+ @cached_property
36
+ def message(self) -> Message:
37
+ return dns.message.from_wire(self.wire)
38
+
39
+ @classmethod
40
+ def from_message(cls, message: Message) -> Self:
41
+ return cls(message.to_wire())
42
+
43
+ @dataclass
44
+ class Response(BaseDNSData):
45
+ """
46
+
47
+ DNS response object.
48
+
49
+ """
50
+
51
+ http: Optional[httpx.Response] = None
52
+ blocked_by: Optional[str] = None
53
+
54
+ @classmethod
55
+ def from_http(cls, response: httpx.Response) -> Self:
56
+ """
57
+
58
+ Initialise from an HTTP response.
59
+
60
+ """
61
+ self = cls(response.content, http=response)
62
+ return self
63
+
64
+ @property
65
+ def answer(self) -> Optional[RRset]:
66
+ """
67
+
68
+ Get the latest answer, if one exists.
69
+
70
+ """
71
+ if not self.message.answer:
72
+ return None
73
+ return self.message.answer[-1]
74
+
75
+ @property
76
+ def rcode(self) -> dnspython_rcode.Rcode:
77
+ return self.message.rcode()
78
+
79
+ @property
80
+ def rcode_text(self) -> str:
81
+ return dnspython_rcode.to_text(self.rcode)
82
+
83
+ @property
84
+ def ttl(self) -> int:
85
+ """
86
+
87
+ Get median TTL from answers, falling back to authority, then to error-code defaults.
88
+
89
+ """
90
+ answers = self.message.answer or self.message.authority
91
+ if answers:
92
+ ttls = [answer.ttl for answer in answers]
93
+ ttl = min(ttls)
94
+ return ttl
95
+
96
+ ttl = TTL_CODE_DEFAULTS.get(self.rcode, dnspython_rcode.NXDOMAIN)
97
+ return ttl
98
+
99
+
100
+
101
+ def __str__(self):
102
+ """
103
+
104
+ Put answer and ID text in string representation.
105
+
106
+ """
107
+ answer = self.answer
108
+
109
+ if answer:
110
+ answer = join(answer.to_text().splitlines(), sep=', ')
111
+
112
+ string = join([answer, self.message.flags], sep=', ')
113
+ string = f'{self.__class__.__name__}({string})'
114
+ return string
115
+
116
+
117
+
118
+ @dataclass
119
+ class Request(BaseDNSData):
120
+ """
121
+
122
+ DNS request object.
123
+
124
+ """
125
+ wire: bytes
126
+
127
+ @cached_property
128
+ def question(self) -> RRset:
129
+ return self.message.question[0]
130
+
131
+ @cached_property
132
+ def is_valid(self):
133
+ return len(self.message.question) != 0
134
+
135
+ @cached_property
136
+ def type(self):
137
+ return self.question.rdtype
138
+
139
+ @cached_property
140
+ def type_text(self):
141
+ return dns.rdatatype.to_text(self.type)
142
+
143
+ @cached_property
144
+ def name(self):
145
+ return self.question.name
146
+
147
+ @cached_property
148
+ def name_text(self):
149
+ return self.name.to_text()
150
+
151
+ def get_response_template(self):
152
+ message = dns.message.make_response(self.message)
153
+ message.flags |= dns.flags.RA
154
+ return message
155
+
156
+ @cached_property
157
+ def blackhole(self) -> Response:
158
+ blackhole = self.get_response_template()
159
+ blackhole.set_rcode(dns.rcode.NXDOMAIN)
160
+ response = Response.from_message(blackhole)
161
+ return response
162
+
163
+
164
+ @dataclass
165
+ class Exchange:
166
+ """
167
+
168
+ Entire DNS exchange for a DNS Proxy: request -> upstream response -> response
169
+
170
+ """
171
+ ip: str
172
+ port: int
173
+
174
+ request: Request
175
+ response: Optional[Response] = None
176
+ answers_pre: List[RRset] = field(default_factory=list)
177
+ is_internal: bool = False
178
+ client_name: Optional[str] = None
179
+ is_complete: bool = False
180
+
181
+ @property
182
+ def addr(self):
183
+ return self.ip, self.port
184
+
185
+ @classmethod
186
+ def from_wire(cls, wire: bytes, **kwargs) -> Self:
187
+ request = Request(wire)
188
+ response = Response.from_message(request.get_response_template())
189
+ return cls(request=request, response=response, **kwargs)
190
+
191
+ @cached_property
192
+ def client(self):
193
+ return f'{self.ip}:{self.port}'
194
+
195
+ @property
196
+ def question_last(self) -> RRset:
197
+ """
198
+
199
+ Create an RRset surrogate representing the latest/current question.
200
+ This can be the original question - or a hybrid one if we've injected our own answers into the Exchange.
201
+ If there's a response, use its answers, else fall back to answers_pre, else to the original question.
202
+
203
+ """
204
+ answers = self.answers_pre
205
+ if self.response:
206
+ answers = self.response.message.answer or answers
207
+
208
+ if answers:
209
+ rrset = answers[-1]
210
+ rdtype = self.request.type
211
+ ttl = self.request.question.ttl
212
+ rdclass = self.request.question.rdclass
213
+ name = next(iter(rrset.items.keys())).to_text()
214
+ rrset_surrogate = dns.rrset.from_text(
215
+ name=name,
216
+ ttl=ttl,
217
+ rdtype=rdtype,
218
+ rdclass=rdclass,
219
+ )
220
+
221
+ return rrset_surrogate
222
+ else:
223
+ return self.request.question
224
+
225
+ @property
226
+ def query_last(self) -> QueryMessage:
227
+ """
228
+
229
+ Create a query (e.g. for use by upstream) based on the last question.
230
+
231
+ """
232
+
233
+ question_last = self.question_last
234
+ query = dns.message.make_query(qname=question_last.name, rdclass=question_last.rdclass, rdtype=question_last.rdtype, id=self.request.message.id)
235
+ return query
236
+
237
+ @property
238
+ def key(self):
239
+ """
240
+
241
+ Hashable key for caching
242
+
243
+ """
244
+ data = tuple(self.request.question.to_text().split())
245
+ return data
246
+
247
+ @cached_property
248
+ def reverse(self) -> Self:
249
+ """
250
+
251
+ Create an Exchange for a reverse lookup of this Exchange's client IP.
252
+
253
+ """
254
+ name = dnspython_reversename.from_address(self.ip)
255
+ query = dns.message.make_query(name, dns.rdatatype.PTR)
256
+ exchange = self.__class__.from_wire(query.to_wire(), ip=self.ip, port=self.port, is_internal=True)
257
+ return exchange
@@ -0,0 +1,66 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fmtr.tools.dns_tools import server, client
4
+ from fmtr.tools.dns_tools.dm import Exchange
5
+ from fmtr.tools.logging_tools import logger
6
+
7
+
8
+ @dataclass(kw_only=True, eq=False)
9
+ class Proxy(server.Plain):
10
+ """
11
+
12
+ Base for a DNS Proxy server (plain server) TODO: Allow subclassing of any server type.
13
+
14
+ """
15
+
16
+ client: client.HTTP
17
+
18
+ def process_question(self, exchange: Exchange):
19
+ """
20
+
21
+ Modify exchange based on initial question.
22
+
23
+ """
24
+ return
25
+
26
+ def process_upstream(self, exchange: Exchange):
27
+ """
28
+
29
+ Modify exchange after upstream response.
30
+
31
+ """
32
+ return
33
+
34
+ def finalize(self, exchange: Exchange):
35
+ """
36
+
37
+ Finalize a still open exchange.
38
+
39
+ """
40
+ exchange.is_complete = True
41
+
42
+ async def resolve(self, exchange: Exchange) -> Exchange:
43
+ """
44
+
45
+ Resolve a request, processing each stage, initial question, upstream response etc.
46
+ Subclasses can override the relevant processing methods to implement custom behaviour.
47
+
48
+ """
49
+ with logger.span(f'Processing question...'):
50
+ self.process_question(exchange)
51
+ if exchange.is_complete:
52
+ return exchange
53
+
54
+ with logger.span(f'Making upstream request...'):
55
+ self.client.resolve(exchange)
56
+ if exchange.is_complete:
57
+ return exchange
58
+
59
+ with logger.span(f'Processing upstream response...'):
60
+ self.process_upstream(exchange)
61
+ if exchange.is_complete:
62
+ return exchange
63
+
64
+ self.finalize(exchange)
65
+
66
+ return exchange
@@ -0,0 +1,138 @@
1
+ import asyncio
2
+ from dataclasses import dataclass, field
3
+ from datetime import timedelta
4
+ from dns import rcode as dnspython_rcode
5
+ from functools import cached_property
6
+ from typing import Optional
7
+
8
+ from fmtr.tools import caching_tools as caching
9
+ from fmtr.tools.dns_tools.dm import Exchange
10
+ from fmtr.tools.logging_tools import logger
11
+
12
+
13
+ @dataclass(kw_only=True, eq=False)
14
+ class Plain(asyncio.DatagramProtocol):
15
+ """
16
+
17
+ Async base class for a plain DNS server using asyncio DatagramProtocol.
18
+ """
19
+
20
+ host: str
21
+ port: int
22
+ transport: Optional[asyncio.DatagramTransport] = field(default=None, init=False)
23
+
24
+ @cached_property
25
+ def loop(self):
26
+ return asyncio.get_event_loop()
27
+
28
+
29
+ @cached_property
30
+ def cache(self):
31
+ """
32
+
33
+ Overridable cache.
34
+ """
35
+ cache = caching.TLRU(maxsize=1_024, ttu_static=timedelta(hours=1), desc='DNS Request')
36
+ return cache
37
+
38
+ def connection_made(self, transport: asyncio.DatagramTransport):
39
+ self.transport = transport
40
+ logger.info(f'Listening on {self.host}:{self.port}')
41
+
42
+ def datagram_received(self, data: bytes, addr):
43
+ ip, port = addr
44
+ exchange = Exchange.from_wire(data, ip=ip, port=port)
45
+ asyncio.create_task(self.handle(exchange))
46
+
47
+ async def start(self):
48
+ """
49
+
50
+ Start the async UDP server.
51
+ """
52
+
53
+ logger.info(f'Starting async DNS server on {self.host}:{self.port}...')
54
+ await self.loop.create_datagram_endpoint(
55
+ lambda: self,
56
+ local_addr=(self.host, self.port)
57
+ )
58
+ await asyncio.Future() # Prevent exit by blocking forever
59
+
60
+ async def resolve(self, exchange: Exchange) -> Exchange:
61
+ """
62
+
63
+ To be defined in subclasses.
64
+
65
+ """
66
+ raise NotImplementedError
67
+
68
+ def check_cache(self, exchange: Exchange):
69
+ if exchange.key in self.cache:
70
+ logger.info(f'Request found in cache.')
71
+ exchange.response = self.cache[exchange.key]
72
+ exchange.response.message.id = exchange.request.message.id
73
+ exchange.is_complete = True
74
+
75
+ def get_span(self, exchange: Exchange):
76
+ """
77
+
78
+ Get handling span
79
+
80
+ """
81
+ request = exchange.request
82
+ span = logger.span(
83
+ f'Handling request {exchange.client_name=} {request.message.id=} {request.type_text} {request.name_text} {request.question=}...'
84
+ )
85
+ return span
86
+
87
+ def log_response(self, exchange: Exchange):
88
+ """
89
+
90
+ Log when resolution complete
91
+
92
+ """
93
+ request = exchange.request
94
+ response = exchange.response
95
+ logger.info(
96
+ f'Resolution complete {exchange.client_name=} {request.message.id=} {request.type_text} {request.name_text} {request.question=} {exchange.is_complete=} {response.rcode=} {response.rcode_text=} {response.answer=} {response.blocked_by=}...'
97
+ )
98
+
99
+ def log_dns_errors(self, exchange: Exchange):
100
+ """
101
+
102
+ Warn about any errors
103
+
104
+ """
105
+ if exchange.response.rcode != dnspython_rcode.NOERROR:
106
+ logger.warning(f'Error {exchange.response.rcode_text=}')
107
+
108
+ async def handle(self, exchange: Exchange):
109
+ """
110
+
111
+ Warn about any errors
112
+
113
+ """
114
+ if not exchange.request.is_valid:
115
+ raise ValueError(f'Only one question per request is supported. Got {len(exchange.request.question)} questions.')
116
+
117
+ if not exchange.is_internal:
118
+ await self.handle(exchange.reverse)
119
+ client_name = exchange.reverse.question_last.name.to_text()
120
+ if not exchange.reverse.response.answer:
121
+ logger.warning(f'Client name could not be resolved {client_name=}.')
122
+ exchange.client_name = client_name
123
+
124
+ with self.get_span(exchange):
125
+ with logger.span(f'Checking cache...'):
126
+ self.check_cache(exchange)
127
+
128
+ if not exchange.is_complete:
129
+ exchange = await self.resolve(exchange)
130
+ self.cache[exchange.key] = exchange.response
131
+
132
+ self.log_dns_errors(exchange)
133
+ self.log_response(exchange)
134
+
135
+ if exchange.is_internal:
136
+ return
137
+
138
+ self.transport.sendto(exchange.response.message.to_wire(), exchange.addr)
@@ -0,0 +1,6 @@
1
+ from fmtr.tools.import_tools import MissingExtraMockModule
2
+
3
+ try:
4
+ from python_on_whales import DockerClient
5
+ except ModuleNotFoundError as exception:
6
+ DockerClient = MissingExtraMockModule('docker.client', exception)
File without changes
@@ -0,0 +1,3 @@
1
+ def main():
2
+ from fmtr import tools
3
+ tools.hfh.main()
@@ -0,0 +1,2 @@
1
+ def main():
2
+ print('Ran test entrypoint.')
@@ -0,0 +1,8 @@
1
+ def main():
2
+ """
3
+
4
+ Ensure YS binary is installed.
5
+
6
+ """
7
+ from fmtr.tools import yaml
8
+ yaml.install()
@@ -1,9 +1,4 @@
1
- def cache_hfh():
2
- from fmtr import tools
3
- tools.hfh.main()
4
-
5
-
6
- def remote_debug_test():
1
+ def main():
7
2
  """
8
3
 
9
4
  Test debugger connection
@@ -0,0 +1,8 @@
1
+ def main():
2
+ """
3
+
4
+ Start debugger
5
+
6
+ """
7
+ from fmtr.tools import debug
8
+ debug.debug_shell()
@@ -28,7 +28,7 @@ def get_dict() -> Dict[str, str]:
28
28
  Return environment variables as a standard dictionary.
29
29
 
30
30
  """
31
- environment_dict = dict(os.environ)
31
+ environment_dict = dict(sorted(dict(os.environ).items()))
32
32
  return environment_dict
33
33
 
34
34
 
@@ -76,4 +76,5 @@ get_date = get_getter(date.fromisoformat)
76
76
  get_datetime = get_getter(datetime.fromisoformat)
77
77
  get_path = get_getter(Path)
78
78
 
79
- IS_DEBUG = get(Constants.FMTR_LOG_LEVEL_KEY, None, converter=str.upper) == 'DEBUG'
79
+ IS_DEV = get_bool(Constants.FMTR_DEV_KEY, default=False)
80
+ CHANNEL = Constants.DEVELOPMENT if IS_DEV else Constants.PRODUCTION