fmtr.tools 1.3.0__tar.gz → 1.3.2__tar.gz

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.

Potentially problematic release.


This version of fmtr.tools might be problematic. Click here for more details.

Files changed (91) hide show
  1. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/PKG-INFO +44 -44
  2. fmtr_tools-1.3.2/fmtr/tools/dns_tools/__init__.py +6 -0
  3. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/dns_tools/client.py +28 -19
  4. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/dns_tools/dm.py +59 -10
  5. fmtr_tools-1.3.0/fmtr/tools/dns_tools/server.py → fmtr_tools-1.3.2/fmtr/tools/dns_tools/proxy.py +17 -38
  6. fmtr_tools-1.3.2/fmtr/tools/dns_tools/server.py +38 -0
  7. fmtr_tools-1.3.2/fmtr/tools/version +1 -0
  8. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr.tools.egg-info/PKG-INFO +44 -44
  9. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr.tools.egg-info/SOURCES.txt +2 -0
  10. fmtr_tools-1.3.2/pyproject.toml +3 -0
  11. fmtr_tools-1.3.0/fmtr/tools/dns_tools/__init__.py +0 -6
  12. fmtr_tools-1.3.0/fmtr/tools/version +0 -1
  13. fmtr_tools-1.3.0/pyproject.toml +0 -3
  14. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/LICENSE +0 -0
  15. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/README.md +0 -0
  16. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/__init__.py +0 -0
  17. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/ai_tools/__init__.py +0 -0
  18. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/ai_tools/agentic_tools.py +0 -0
  19. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/ai_tools/inference_tools.py +0 -0
  20. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/api_tools.py +0 -0
  21. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/async_tools.py +0 -0
  22. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/augmentation_tools.py +0 -0
  23. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/caching_tools.py +0 -0
  24. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/constants.py +0 -0
  25. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/data_modelling_tools.py +0 -0
  26. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/dataclass_tools.py +0 -0
  27. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/datatype_tools.py +0 -0
  28. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/debugging_tools.py +0 -0
  29. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/docker_tools.py +0 -0
  30. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/entrypoints/__init__.py +0 -0
  31. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/entrypoints/cache_hfh.py +0 -0
  32. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/entrypoints/ep_test.py +0 -0
  33. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/entrypoints/remote_debug_test.py +0 -0
  34. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/entrypoints/shell_debug.py +0 -0
  35. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/environment_tools.py +0 -0
  36. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/function_tools.py +0 -0
  37. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/google_api_tools.py +0 -0
  38. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/hash_tools.py +0 -0
  39. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/hfh_tools.py +0 -0
  40. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/html_tools.py +0 -0
  41. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/http_tools.py +0 -0
  42. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/import_tools.py +0 -0
  43. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/inspection_tools.py +0 -0
  44. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/interface_tools.py +0 -0
  45. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/iterator_tools.py +0 -0
  46. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/json_fix_tools.py +0 -0
  47. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/json_tools.py +0 -0
  48. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/logging_tools.py +0 -0
  49. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/merging_tools.py +0 -0
  50. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/metric_tools.py +0 -0
  51. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/name_tools.py +0 -0
  52. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/netrc_tools.py +0 -0
  53. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/openai_tools.py +0 -0
  54. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/packaging_tools.py +0 -0
  55. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/parallel_tools.py +0 -0
  56. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/path_tools/__init__.py +0 -0
  57. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/path_tools/app_path_tools.py +0 -0
  58. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/path_tools/path_tools.py +0 -0
  59. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/path_tools/type_path_tools.py +0 -0
  60. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/pattern_tools.py +0 -0
  61. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/pdf_tools.py +0 -0
  62. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/platform_tools.py +0 -0
  63. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/process_tools.py +0 -0
  64. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/profiling_tools.py +0 -0
  65. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/random_tools.py +0 -0
  66. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/semantic_tools.py +0 -0
  67. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/settings_tools.py +0 -0
  68. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/setup_tools/__init__.py +0 -0
  69. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/setup_tools/setup_tools.py +0 -0
  70. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/spaces_tools.py +0 -0
  71. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/string_tools.py +0 -0
  72. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tabular_tools.py +0 -0
  73. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/__init__.py +0 -0
  74. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/conftest.py +0 -0
  75. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/helpers.py +0 -0
  76. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/test_datatype.py +0 -0
  77. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/test_environment.py +0 -0
  78. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/test_json.py +0 -0
  79. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/test_path.py +0 -0
  80. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tests/test_yaml.py +0 -0
  81. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tokenization_tools.py +0 -0
  82. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/tools.py +0 -0
  83. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/unicode_tools.py +0 -0
  84. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/version_tools.py +0 -0
  85. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr/tools/yaml_tools.py +0 -0
  86. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr.tools.egg-info/dependency_links.txt +0 -0
  87. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr.tools.egg-info/entry_points.txt +0 -0
  88. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr.tools.egg-info/requires.txt +42 -42
  89. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/fmtr.tools.egg-info/top_level.txt +0 -0
  90. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/setup.cfg +0 -0
  91. {fmtr_tools-1.3.0 → fmtr_tools-1.3.2}/setup.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
5
- Home-page: https://github.com/fmtr/tools
5
+ Home-page: https://github.com/fmtr/fmtr.tools
6
6
  Author: Frontmatter
7
7
  Author-email: innovative.fowler@mask.pro.fmtr.dev
8
8
  License: Copyright © 2025 Frontmatter. All rights reserved.
@@ -130,60 +130,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
130
130
  Provides-Extra: setup
131
131
  Requires-Dist: setuptools; extra == "setup"
132
132
  Provides-Extra: all
133
- Requires-Dist: torchaudio; extra == "all"
134
- Requires-Dist: httpx; extra == "all"
133
+ Requires-Dist: uvicorn[standard]; extra == "all"
134
+ Requires-Dist: openpyxl; extra == "all"
135
+ Requires-Dist: sentence_transformers; extra == "all"
136
+ Requires-Dist: peft; extra == "all"
137
+ Requires-Dist: httpx_retries; extra == "all"
138
+ Requires-Dist: sre_yield; extra == "all"
139
+ Requires-Dist: faker; extra == "all"
140
+ Requires-Dist: ollama; extra == "all"
141
+ Requires-Dist: logfire; extra == "all"
142
+ Requires-Dist: bokeh; extra == "all"
143
+ Requires-Dist: setuptools; extra == "all"
135
144
  Requires-Dist: dask[bag]; extra == "all"
136
- Requires-Dist: docker; extra == "all"
137
145
  Requires-Dist: diskcache; extra == "all"
138
- Requires-Dist: ollama; extra == "all"
139
- Requires-Dist: pyyaml; extra == "all"
146
+ Requires-Dist: torchaudio; extra == "all"
147
+ Requires-Dist: tokenizers; extra == "all"
140
148
  Requires-Dist: pydantic; extra == "all"
141
- Requires-Dist: google-auth-httplib2; extra == "all"
142
- Requires-Dist: semver; extra == "all"
143
- Requires-Dist: pymupdf4llm; extra == "all"
149
+ Requires-Dist: pandas; extra == "all"
144
150
  Requires-Dist: flet-video; extra == "all"
145
- Requires-Dist: appdirs; extra == "all"
146
- Requires-Dist: logfire; extra == "all"
147
- Requires-Dist: json_repair; extra == "all"
148
- Requires-Dist: pydevd-pycharm; extra == "all"
149
- Requires-Dist: torchvision; extra == "all"
150
151
  Requires-Dist: Unidecode; extra == "all"
151
- Requires-Dist: contexttimer; extra == "all"
152
- Requires-Dist: faker; extra == "all"
153
- Requires-Dist: regex; extra == "all"
154
- Requires-Dist: setuptools; extra == "all"
155
- Requires-Dist: flet-webview; extra == "all"
156
- Requires-Dist: tinynetrc; extra == "all"
157
- Requires-Dist: uvicorn[standard]; extra == "all"
158
- Requires-Dist: logfire[fastapi]; extra == "all"
159
- Requires-Dist: pymupdf; extra == "all"
160
- Requires-Dist: tokenizers; extra == "all"
161
- Requires-Dist: distributed; extra == "all"
162
- Requires-Dist: filetype; extra == "all"
163
- Requires-Dist: yamlscript; extra == "all"
152
+ Requires-Dist: pydantic-settings; extra == "all"
164
153
  Requires-Dist: dnspython[doh]; extra == "all"
165
- Requires-Dist: google-api-python-client; extra == "all"
166
- Requires-Dist: pandas; extra == "all"
167
154
  Requires-Dist: logfire[httpx]; extra == "all"
168
- Requires-Dist: httpx_retries; extra == "all"
155
+ Requires-Dist: yamlscript; extra == "all"
156
+ Requires-Dist: fastapi; extra == "all"
169
157
  Requires-Dist: openai; extra == "all"
170
- Requires-Dist: bokeh; extra == "all"
171
- Requires-Dist: flet[all]; extra == "all"
172
- Requires-Dist: sentence_transformers; extra == "all"
173
- Requires-Dist: google-auth; extra == "all"
174
- Requires-Dist: transformers[sentencepiece]; extra == "all"
175
- Requires-Dist: peft; extra == "all"
158
+ Requires-Dist: deepmerge; extra == "all"
159
+ Requires-Dist: html2text; extra == "all"
176
160
  Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
161
+ Requires-Dist: contexttimer; extra == "all"
162
+ Requires-Dist: flet[all]; extra == "all"
163
+ Requires-Dist: logfire[fastapi]; extra == "all"
164
+ Requires-Dist: regex; extra == "all"
165
+ Requires-Dist: docker; extra == "all"
166
+ Requires-Dist: filetype; extra == "all"
177
167
  Requires-Dist: google-auth-oauthlib; extra == "all"
168
+ Requires-Dist: tabulate; extra == "all"
178
169
  Requires-Dist: pytest-cov; extra == "all"
179
- Requires-Dist: fastapi; extra == "all"
170
+ Requires-Dist: pyyaml; extra == "all"
171
+ Requires-Dist: transformers[sentencepiece]; extra == "all"
172
+ Requires-Dist: pymupdf; extra == "all"
173
+ Requires-Dist: pydevd-pycharm; extra == "all"
174
+ Requires-Dist: torchvision; extra == "all"
175
+ Requires-Dist: google-auth; extra == "all"
176
+ Requires-Dist: flet-webview; extra == "all"
180
177
  Requires-Dist: huggingface_hub; extra == "all"
181
- Requires-Dist: tabulate; extra == "all"
182
- Requires-Dist: pydantic-settings; extra == "all"
183
- Requires-Dist: openpyxl; extra == "all"
184
- Requires-Dist: deepmerge; extra == "all"
185
- Requires-Dist: sre_yield; extra == "all"
186
- Requires-Dist: html2text; extra == "all"
178
+ Requires-Dist: httpx; extra == "all"
179
+ Requires-Dist: semver; extra == "all"
180
+ Requires-Dist: json_repair; extra == "all"
181
+ Requires-Dist: tinynetrc; extra == "all"
182
+ Requires-Dist: google-auth-httplib2; extra == "all"
183
+ Requires-Dist: appdirs; extra == "all"
184
+ Requires-Dist: pymupdf4llm; extra == "all"
185
+ Requires-Dist: distributed; extra == "all"
186
+ Requires-Dist: google-api-python-client; extra == "all"
187
187
  Dynamic: author
188
188
  Dynamic: author-email
189
189
  Dynamic: description
@@ -0,0 +1,6 @@
1
+ from fmtr.tools.import_tools import MissingExtraMockModule
2
+
3
+ try:
4
+ from fmtr.tools.dns_tools import server, client, dm, proxy
5
+ except ImportError as exception:
6
+ server = client = dm = proxy = MissingExtraMockModule('dns', exception)
@@ -1,11 +1,12 @@
1
- import dns
2
1
  from dataclasses import dataclass
3
- from dns import query
4
2
  from functools import cached_property
3
+
4
+ import dns as dnspython
5
+ from dns import query
5
6
  from httpx_retries import Retry, RetryTransport
6
7
 
7
8
  from fmtr.tools import http_tools as http
8
- from fmtr.tools.dns_tools.dm import Exchange, Response, Request
9
+ from fmtr.tools.dns_tools.dm import Exchange, Response
9
10
  from fmtr.tools.logging_tools import logger
10
11
 
11
12
  RETRY_STRATEGY = Retry(
@@ -29,28 +30,36 @@ class HTTPClientDoH(http.Client):
29
30
  TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
30
31
 
31
32
 
32
- class ClientBasePlain:
33
- def __init__(self, host, port=53):
34
- self.host = host
35
- self.port = port
33
+ @dataclass
34
+ class Plain:
35
+ """
36
+
37
+ Plain DNS
38
+
39
+ """
40
+ host: str
41
+ port: int = 53
36
42
 
37
43
  def resolve(self, exchange: Exchange):
44
+
38
45
  with logger.span(f'UDP {self.host}:{self.port}'):
39
- response = query.udp(q=exchange.request.message, where=self.host, port=self.port)
40
- exchange.response_upstream = Response.from_message(response)
46
+ response_plain = query.udp(q=exchange.query_last, where=self.host, port=self.port)
47
+ response = Response.from_message(response_plain)
48
+
49
+ exchange.response.message.answer += response.message.answer
41
50
 
42
51
 
43
52
  @dataclass
44
- class ClientDoH:
53
+ class HTTP:
45
54
  """
46
55
 
47
- Base DoH client.
56
+ DNS over HTTP
48
57
 
49
58
  """
50
59
 
51
60
  HEADERS = {"Content-Type": "application/dns-message"}
52
61
  CLIENT = HTTPClientDoH()
53
- BOOTSTRAP = ClientBasePlain('8.8.8.8')
62
+ BOOTSTRAP = Plain('8.8.8.8')
54
63
 
55
64
  host: str
56
65
  url: str
@@ -58,11 +67,10 @@ class ClientDoH:
58
67
 
59
68
  @cached_property
60
69
  def ip(self):
61
- message = dns.message.make_query(self.host, dns.rdatatype.A, flags=0)
62
- request = Request.from_message(message)
63
- exchange = Exchange(request=request, ip=None, port=None)
70
+ message = dnspython.message.make_query(self.host, dnspython.rdatatype.A, flags=0)
71
+ exchange = Exchange.from_wire(message.to_wire(), ip=None, port=None)
64
72
  self.BOOTSTRAP.resolve(exchange)
65
- ip = next(iter(exchange.response_upstream.answer.items.keys())).address
73
+ ip = next(iter(exchange.response.answer.items.keys())).address
66
74
  return ip
67
75
 
68
76
  def resolve(self, exchange: Exchange):
@@ -71,10 +79,11 @@ class ClientDoH:
71
79
  Resolve via DoH
72
80
 
73
81
  """
74
- request = exchange.request
82
+
75
83
  headers = self.HEADERS | dict(Host=self.host)
76
84
  url = self.url.format(host=self.ip)
77
- response_doh = self.CLIENT.post(url, headers=headers, content=request.wire)
85
+ response_doh = self.CLIENT.post(url, headers=headers, content=exchange.query_last.to_wire())
78
86
  response_doh.raise_for_status()
79
87
  response = Response.from_http(response_doh)
80
- exchange.response_upstream = response
88
+
89
+ exchange.response.message.answer += response.message.answer
@@ -1,10 +1,12 @@
1
- import dns
2
- import httpx
3
1
  from dataclasses import dataclass
4
- from dns.message import Message
5
2
  from functools import cached_property
6
3
  from typing import Self, Optional
7
4
 
5
+ import dns
6
+ import httpx
7
+ from dns.message import Message, QueryMessage
8
+ from dns.rrset import RRset
9
+
8
10
 
9
11
  @dataclass
10
12
  class BaseDNSData:
@@ -33,14 +35,17 @@ class Response(BaseDNSData):
33
35
  """
34
36
 
35
37
  http: Optional[httpx.Response] = None
38
+ is_complete: bool = False
36
39
 
37
40
  @classmethod
38
41
  def from_http(cls, response: httpx.Response) -> Self:
39
42
  self = cls(response.content, http=response)
40
43
  return self
41
44
 
42
- @cached_property
43
- def answer(self):
45
+ @property
46
+ def answer(self) -> Optional[RRset]:
47
+ if not self.message.answer:
48
+ return None
44
49
  return self.message.answer[-1]
45
50
 
46
51
 
@@ -54,7 +59,7 @@ class Request(BaseDNSData):
54
59
  wire: bytes
55
60
 
56
61
  @cached_property
57
- def question(self):
62
+ def question(self) -> RRset:
58
63
  return self.message.question[0]
59
64
 
60
65
  @cached_property
@@ -77,10 +82,14 @@ class Request(BaseDNSData):
77
82
  def name_text(self):
78
83
  return self.name.to_text()
79
84
 
85
+ def get_response_template(self):
86
+ message = dns.message.make_response(self.message)
87
+ message.flags |= dns.flags.RA
88
+ return message
89
+
80
90
  @cached_property
81
91
  def blackhole(self) -> Response:
82
- blackhole = dns.message.make_response(self.message)
83
- blackhole.flags |= dns.flags.RA
92
+ blackhole = self.get_response_template()
84
93
  blackhole.set_rcode(dns.rcode.NXDOMAIN)
85
94
  response = Response.from_message(blackhole)
86
95
  return response
@@ -98,13 +107,53 @@ class Exchange:
98
107
 
99
108
  request: Request
100
109
  response: Optional[Response] = None
101
- response_upstream: Optional[Response] = None
110
+
102
111
 
103
112
  @classmethod
104
113
  def from_wire(cls, wire: bytes, ip: str, port: int) -> Self:
105
114
  request = Request(wire)
106
- return cls(request=request, ip=ip, port=port)
115
+ response = Response.from_message(request.get_response_template())
116
+
117
+ return cls(request=request, response=response, ip=ip, port=port)
107
118
 
108
119
  @cached_property
109
120
  def client(self):
110
121
  return f'{self.ip}:{self.port}'
122
+
123
+ @property
124
+ def question_last(self) -> RRset:
125
+ """
126
+
127
+ Contrive an RRset representing the latest/current question. This can be the original question - or a hybrid one if we've injected our own answers into the exchange.
128
+
129
+ """
130
+ if self.response.answer:
131
+ rrset = self.response.answer
132
+ ty = self.request.type
133
+ ttl = self.request.question.ttl
134
+ rdclass = self.request.question.rdclass
135
+ name = next(iter(rrset.items.keys())).to_text()
136
+
137
+ rrset_contrived = dns.rrset.from_text(
138
+ name=name,
139
+ ttl=ttl,
140
+ rdtype=ty,
141
+ rdclass=rdclass,
142
+
143
+ )
144
+
145
+ return rrset_contrived
146
+ else:
147
+ return self.request.question # Solves the issue of digging out the name.
148
+
149
+ @property
150
+ def query_last(self) -> QueryMessage:
151
+ """
152
+
153
+ Create a query (e.g. for use by upstream) based on the last question.
154
+
155
+ """
156
+
157
+ question_last = self.question_last
158
+ query = dns.message.make_query(qname=question_last.name, rdclass=question_last.rdclass, rdtype=question_last.rdtype)
159
+ return query
@@ -1,59 +1,34 @@
1
- import socket
2
1
  from dataclasses import dataclass
3
2
 
4
3
  from fmtr.tools import logger
5
- from fmtr.tools.dns_tools.client import ClientDoH
4
+ from fmtr.tools.dns_tools import server, client
6
5
  from fmtr.tools.dns_tools.dm import Exchange
7
6
 
8
7
 
9
8
  @dataclass
10
- class ServerBasePlain:
9
+ class Proxy(server.Plain):
11
10
  """
12
11
 
13
- Base for starting a plain DNS server
12
+ Base for a DNS Proxy server (plain server) TODO: Allow subclassing of any server type.
14
13
 
15
14
  """
16
15
 
17
- host: str
18
- port: int
16
+ client: client.HTTP
19
17
 
20
- def __post_init__(self):
21
-
22
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
23
-
24
- def resolve(self, exchange: Exchange):
25
- raise NotImplemented
26
-
27
- def start(self):
18
+ def process_question(self, exchange: Exchange):
28
19
  """
29
20
 
30
- Listen and resolve via overridden resolve method.
21
+ Modify exchange based on initial question.
31
22
 
32
23
  """
33
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
34
- sock.bind((self.host, self.port))
35
- print(f"Listening on {self.host}:{self.port}")
36
- while True:
37
- data, (ip, port) = sock.recvfrom(512)
38
- exchange = Exchange.from_wire(data, ip=ip, port=port)
39
- self.resolve(exchange)
40
- sock.sendto(exchange.response.wire, (ip, port))
41
-
42
-
43
- @dataclass
44
- class ServerBaseDoHProxy(ServerBasePlain):
45
- """
46
-
47
- Base for a DNS Proxy server
48
-
49
- """
50
-
51
- client: ClientDoH
52
-
53
- def process_question(self, exchange: Exchange):
54
24
  return
55
25
 
56
26
  def process_upstream(self, exchange: Exchange):
27
+ """
28
+
29
+ Modify exchange after upstream response.
30
+
31
+ """
57
32
  return
58
33
 
59
34
  def resolve(self, exchange: Exchange):
@@ -73,17 +48,21 @@ class ServerBaseDoHProxy(ServerBasePlain):
73
48
 
74
49
  with logger.span(f'Processing question...'):
75
50
  self.process_question(exchange)
76
- if exchange.response:
51
+ if exchange.response.is_complete:
77
52
  return
78
53
 
79
54
  with logger.span(f'Making upstream request for {request.name_text}...'):
80
55
  self.client.resolve(exchange)
56
+ if exchange.response.is_complete:
57
+ return
81
58
 
82
59
  with logger.span(f'Processing upstream response...'):
83
60
  self.process_upstream(exchange)
61
+ if exchange.response.is_complete:
62
+ return
84
63
 
85
64
  if exchange.response:
86
65
  return
87
66
 
88
- exchange.response = exchange.response_upstream
67
+ exchange.response.is_complete = True
89
68
  return
@@ -0,0 +1,38 @@
1
+ import socket
2
+ from dataclasses import dataclass
3
+
4
+ from fmtr.tools.dns_tools.dm import Exchange
5
+
6
+
7
+ @dataclass
8
+ class Plain:
9
+ """
10
+
11
+ Base for starting a plain DNS server
12
+
13
+ """
14
+
15
+ host: str
16
+ port: int
17
+
18
+ def __post_init__(self):
19
+
20
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
21
+
22
+ def resolve(self, exchange: Exchange):
23
+ raise NotImplemented
24
+
25
+ def start(self):
26
+ """
27
+
28
+ Listen and resolve via overridden resolve method.
29
+
30
+ """
31
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
32
+ sock.bind((self.host, self.port))
33
+ print(f"Listening on {self.host}:{self.port}")
34
+ while True:
35
+ data, (ip, port) = sock.recvfrom(512)
36
+ exchange = Exchange.from_wire(data, ip=ip, port=port)
37
+ self.resolve(exchange)
38
+ sock.sendto(exchange.response.message.to_wire(), (ip, port))
@@ -0,0 +1 @@
1
+ 1.3.2
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
5
- Home-page: https://github.com/fmtr/tools
5
+ Home-page: https://github.com/fmtr/fmtr.tools
6
6
  Author: Frontmatter
7
7
  Author-email: innovative.fowler@mask.pro.fmtr.dev
8
8
  License: Copyright © 2025 Frontmatter. All rights reserved.
@@ -130,60 +130,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
130
130
  Provides-Extra: setup
131
131
  Requires-Dist: setuptools; extra == "setup"
132
132
  Provides-Extra: all
133
- Requires-Dist: torchaudio; extra == "all"
134
- Requires-Dist: httpx; extra == "all"
133
+ Requires-Dist: uvicorn[standard]; extra == "all"
134
+ Requires-Dist: openpyxl; extra == "all"
135
+ Requires-Dist: sentence_transformers; extra == "all"
136
+ Requires-Dist: peft; extra == "all"
137
+ Requires-Dist: httpx_retries; extra == "all"
138
+ Requires-Dist: sre_yield; extra == "all"
139
+ Requires-Dist: faker; extra == "all"
140
+ Requires-Dist: ollama; extra == "all"
141
+ Requires-Dist: logfire; extra == "all"
142
+ Requires-Dist: bokeh; extra == "all"
143
+ Requires-Dist: setuptools; extra == "all"
135
144
  Requires-Dist: dask[bag]; extra == "all"
136
- Requires-Dist: docker; extra == "all"
137
145
  Requires-Dist: diskcache; extra == "all"
138
- Requires-Dist: ollama; extra == "all"
139
- Requires-Dist: pyyaml; extra == "all"
146
+ Requires-Dist: torchaudio; extra == "all"
147
+ Requires-Dist: tokenizers; extra == "all"
140
148
  Requires-Dist: pydantic; extra == "all"
141
- Requires-Dist: google-auth-httplib2; extra == "all"
142
- Requires-Dist: semver; extra == "all"
143
- Requires-Dist: pymupdf4llm; extra == "all"
149
+ Requires-Dist: pandas; extra == "all"
144
150
  Requires-Dist: flet-video; extra == "all"
145
- Requires-Dist: appdirs; extra == "all"
146
- Requires-Dist: logfire; extra == "all"
147
- Requires-Dist: json_repair; extra == "all"
148
- Requires-Dist: pydevd-pycharm; extra == "all"
149
- Requires-Dist: torchvision; extra == "all"
150
151
  Requires-Dist: Unidecode; extra == "all"
151
- Requires-Dist: contexttimer; extra == "all"
152
- Requires-Dist: faker; extra == "all"
153
- Requires-Dist: regex; extra == "all"
154
- Requires-Dist: setuptools; extra == "all"
155
- Requires-Dist: flet-webview; extra == "all"
156
- Requires-Dist: tinynetrc; extra == "all"
157
- Requires-Dist: uvicorn[standard]; extra == "all"
158
- Requires-Dist: logfire[fastapi]; extra == "all"
159
- Requires-Dist: pymupdf; extra == "all"
160
- Requires-Dist: tokenizers; extra == "all"
161
- Requires-Dist: distributed; extra == "all"
162
- Requires-Dist: filetype; extra == "all"
163
- Requires-Dist: yamlscript; extra == "all"
152
+ Requires-Dist: pydantic-settings; extra == "all"
164
153
  Requires-Dist: dnspython[doh]; extra == "all"
165
- Requires-Dist: google-api-python-client; extra == "all"
166
- Requires-Dist: pandas; extra == "all"
167
154
  Requires-Dist: logfire[httpx]; extra == "all"
168
- Requires-Dist: httpx_retries; extra == "all"
155
+ Requires-Dist: yamlscript; extra == "all"
156
+ Requires-Dist: fastapi; extra == "all"
169
157
  Requires-Dist: openai; extra == "all"
170
- Requires-Dist: bokeh; extra == "all"
171
- Requires-Dist: flet[all]; extra == "all"
172
- Requires-Dist: sentence_transformers; extra == "all"
173
- Requires-Dist: google-auth; extra == "all"
174
- Requires-Dist: transformers[sentencepiece]; extra == "all"
175
- Requires-Dist: peft; extra == "all"
158
+ Requires-Dist: deepmerge; extra == "all"
159
+ Requires-Dist: html2text; extra == "all"
176
160
  Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
161
+ Requires-Dist: contexttimer; extra == "all"
162
+ Requires-Dist: flet[all]; extra == "all"
163
+ Requires-Dist: logfire[fastapi]; extra == "all"
164
+ Requires-Dist: regex; extra == "all"
165
+ Requires-Dist: docker; extra == "all"
166
+ Requires-Dist: filetype; extra == "all"
177
167
  Requires-Dist: google-auth-oauthlib; extra == "all"
168
+ Requires-Dist: tabulate; extra == "all"
178
169
  Requires-Dist: pytest-cov; extra == "all"
179
- Requires-Dist: fastapi; extra == "all"
170
+ Requires-Dist: pyyaml; extra == "all"
171
+ Requires-Dist: transformers[sentencepiece]; extra == "all"
172
+ Requires-Dist: pymupdf; extra == "all"
173
+ Requires-Dist: pydevd-pycharm; extra == "all"
174
+ Requires-Dist: torchvision; extra == "all"
175
+ Requires-Dist: google-auth; extra == "all"
176
+ Requires-Dist: flet-webview; extra == "all"
180
177
  Requires-Dist: huggingface_hub; extra == "all"
181
- Requires-Dist: tabulate; extra == "all"
182
- Requires-Dist: pydantic-settings; extra == "all"
183
- Requires-Dist: openpyxl; extra == "all"
184
- Requires-Dist: deepmerge; extra == "all"
185
- Requires-Dist: sre_yield; extra == "all"
186
- Requires-Dist: html2text; extra == "all"
178
+ Requires-Dist: httpx; extra == "all"
179
+ Requires-Dist: semver; extra == "all"
180
+ Requires-Dist: json_repair; extra == "all"
181
+ Requires-Dist: tinynetrc; extra == "all"
182
+ Requires-Dist: google-auth-httplib2; extra == "all"
183
+ Requires-Dist: appdirs; extra == "all"
184
+ Requires-Dist: pymupdf4llm; extra == "all"
185
+ Requires-Dist: distributed; extra == "all"
186
+ Requires-Dist: google-api-python-client; extra == "all"
187
187
  Dynamic: author
188
188
  Dynamic: author-email
189
189
  Dynamic: description
@@ -57,6 +57,7 @@ setup.py
57
57
  ./fmtr/tools/dns_tools/__init__.py
58
58
  ./fmtr/tools/dns_tools/client.py
59
59
  ./fmtr/tools/dns_tools/dm.py
60
+ ./fmtr/tools/dns_tools/proxy.py
60
61
  ./fmtr/tools/dns_tools/server.py
61
62
  ./fmtr/tools/entrypoints/__init__.py
62
63
  ./fmtr/tools/entrypoints/cache_hfh.py
@@ -138,6 +139,7 @@ fmtr/tools/ai_tools/inference_tools.py
138
139
  fmtr/tools/dns_tools/__init__.py
139
140
  fmtr/tools/dns_tools/client.py
140
141
  fmtr/tools/dns_tools/dm.py
142
+ fmtr/tools/dns_tools/proxy.py
141
143
  fmtr/tools/dns_tools/server.py
142
144
  fmtr/tools/entrypoints/__init__.py
143
145
  fmtr/tools/entrypoints/cache_hfh.py
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["fmtr.tools[setup]==1.3.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -1,6 +0,0 @@
1
- from fmtr.tools.import_tools import MissingExtraMockModule
2
-
3
- try:
4
- from fmtr.tools.dns_tools import server, client, dm
5
- except ImportError as exception:
6
- server = client = dm = MissingExtraMockModule('dns', exception)
@@ -1 +0,0 @@
1
- 1.3.0
@@ -1,3 +0,0 @@
1
- [build-system]
2
- requires = ["fmtr.tools[setup]==1.2.0"]
3
- build-backend = "setuptools.build_meta"
File without changes
File without changes
@@ -15,60 +15,60 @@ pydantic-ai[logfire,openai]
15
15
  ollama
16
16
 
17
17
  [all]
18
- torchaudio
19
- httpx
18
+ uvicorn[standard]
19
+ openpyxl
20
+ sentence_transformers
21
+ peft
22
+ httpx_retries
23
+ sre_yield
24
+ faker
25
+ ollama
26
+ logfire
27
+ bokeh
28
+ setuptools
20
29
  dask[bag]
21
- docker
22
30
  diskcache
23
- ollama
24
- pyyaml
31
+ torchaudio
32
+ tokenizers
25
33
  pydantic
26
- google-auth-httplib2
27
- semver
28
- pymupdf4llm
34
+ pandas
29
35
  flet-video
30
- appdirs
31
- logfire
32
- json_repair
33
- pydevd-pycharm
34
- torchvision
35
36
  Unidecode
36
- contexttimer
37
- faker
38
- regex
39
- setuptools
40
- flet-webview
41
- tinynetrc
42
- uvicorn[standard]
43
- logfire[fastapi]
44
- pymupdf
45
- tokenizers
46
- distributed
47
- filetype
48
- yamlscript
37
+ pydantic-settings
49
38
  dnspython[doh]
50
- google-api-python-client
51
- pandas
52
39
  logfire[httpx]
53
- httpx_retries
40
+ yamlscript
41
+ fastapi
54
42
  openai
55
- bokeh
56
- flet[all]
57
- sentence_transformers
58
- google-auth
59
- transformers[sentencepiece]
60
- peft
43
+ deepmerge
44
+ html2text
61
45
  pydantic-ai[logfire,openai]
46
+ contexttimer
47
+ flet[all]
48
+ logfire[fastapi]
49
+ regex
50
+ docker
51
+ filetype
62
52
  google-auth-oauthlib
53
+ tabulate
63
54
  pytest-cov
64
- fastapi
55
+ pyyaml
56
+ transformers[sentencepiece]
57
+ pymupdf
58
+ pydevd-pycharm
59
+ torchvision
60
+ google-auth
61
+ flet-webview
65
62
  huggingface_hub
66
- tabulate
67
- pydantic-settings
68
- openpyxl
69
- deepmerge
70
- sre_yield
71
- html2text
63
+ httpx
64
+ semver
65
+ json_repair
66
+ tinynetrc
67
+ google-auth-httplib2
68
+ appdirs
69
+ pymupdf4llm
70
+ distributed
71
+ google-api-python-client
72
72
 
73
73
  [api]
74
74
  fastapi
File without changes
File without changes