fmtr.tools 1.2.4__tar.gz → 1.2.5__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 (89) hide show
  1. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/PKG-INFO +45 -45
  2. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/__init__.py +1 -5
  3. fmtr_tools-1.2.5/fmtr/tools/dns_tools/__init__.py +6 -0
  4. fmtr_tools-1.2.5/fmtr/tools/dns_tools/client.py +78 -0
  5. fmtr_tools-1.2.5/fmtr/tools/dns_tools/dm.py +110 -0
  6. fmtr_tools-1.2.5/fmtr/tools/dns_tools/server.py +96 -0
  7. fmtr_tools-1.2.5/fmtr/tools/version +1 -0
  8. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/PKG-INFO +45 -45
  9. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/SOURCES.txt +8 -2
  10. fmtr_tools-1.2.4/fmtr/tools/dns_tools.py +0 -221
  11. fmtr_tools-1.2.4/fmtr/tools/version +0 -1
  12. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/LICENSE +0 -0
  13. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/README.md +0 -0
  14. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/ai_tools/__init__.py +0 -0
  15. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/ai_tools/agentic_tools.py +0 -0
  16. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/ai_tools/inference_tools.py +0 -0
  17. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/api_tools.py +0 -0
  18. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/async_tools.py +0 -0
  19. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/augmentation_tools.py +0 -0
  20. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/caching_tools.py +0 -0
  21. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/constants.py +0 -0
  22. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/data_modelling_tools.py +0 -0
  23. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/dataclass_tools.py +0 -0
  24. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/datatype_tools.py +0 -0
  25. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/debugging_tools.py +0 -0
  26. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/docker_tools.py +0 -0
  27. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/__init__.py +0 -0
  28. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/cache_hfh.py +0 -0
  29. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/ep_test.py +0 -0
  30. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/remote_debug_test.py +0 -0
  31. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/shell_debug.py +0 -0
  32. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/environment_tools.py +0 -0
  33. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/function_tools.py +0 -0
  34. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/google_api_tools.py +0 -0
  35. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/hash_tools.py +0 -0
  36. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/hfh_tools.py +0 -0
  37. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/html_tools.py +0 -0
  38. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/http_tools.py +0 -0
  39. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/import_tools.py +0 -0
  40. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/inspection_tools.py +0 -0
  41. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/interface_tools.py +0 -0
  42. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/iterator_tools.py +0 -0
  43. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/json_fix_tools.py +0 -0
  44. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/json_tools.py +0 -0
  45. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/logging_tools.py +0 -0
  46. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/merging_tools.py +0 -0
  47. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/metric_tools.py +0 -0
  48. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/name_tools.py +0 -0
  49. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/netrc_tools.py +0 -0
  50. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/openai_tools.py +0 -0
  51. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/packaging_tools.py +0 -0
  52. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/parallel_tools.py +0 -0
  53. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/__init__.py +0 -0
  54. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/app_path_tools.py +0 -0
  55. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/path_tools.py +0 -0
  56. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/type_path_tools.py +0 -0
  57. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/pattern_tools.py +0 -0
  58. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/pdf_tools.py +0 -0
  59. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/platform_tools.py +0 -0
  60. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/process_tools.py +0 -0
  61. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/profiling_tools.py +0 -0
  62. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/random_tools.py +0 -0
  63. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/semantic_tools.py +0 -0
  64. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/settings_tools.py +0 -0
  65. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/setup_tools/__init__.py +0 -0
  66. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/setup_tools/setup_tools.py +0 -0
  67. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/spaces_tools.py +0 -0
  68. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/string_tools.py +0 -0
  69. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tabular_tools.py +0 -0
  70. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/__init__.py +0 -0
  71. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/conftest.py +0 -0
  72. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/helpers.py +0 -0
  73. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_datatype.py +0 -0
  74. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_environment.py +0 -0
  75. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_json.py +0 -0
  76. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_path.py +0 -0
  77. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_yaml.py +0 -0
  78. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tokenization_tools.py +0 -0
  79. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/tools.py +0 -0
  80. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/unicode_tools.py +0 -0
  81. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/version_tools.py +0 -0
  82. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr/tools/yaml_tools.py +0 -0
  83. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/dependency_links.txt +0 -0
  84. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/entry_points.txt +0 -0
  85. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/requires.txt +44 -44
  86. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/top_level.txt +0 -0
  87. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/pyproject.toml +0 -0
  88. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/setup.cfg +0 -0
  89. {fmtr_tools-1.2.4 → fmtr_tools-1.2.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.2.4
3
+ Version: 1.2.5
4
4
  Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
5
5
  Home-page: https://github.com/fmtr/tools
6
6
  Author: Frontmatter
@@ -128,60 +128,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
128
128
  Provides-Extra: setup
129
129
  Requires-Dist: setuptools; extra == "setup"
130
130
  Provides-Extra: all
131
- Requires-Dist: httpx; extra == "all"
132
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
133
- Requires-Dist: Unidecode; extra == "all"
134
- Requires-Dist: huggingface_hub; extra == "all"
135
- Requires-Dist: faker; extra == "all"
136
- Requires-Dist: bokeh; extra == "all"
137
- Requires-Dist: docker; extra == "all"
131
+ Requires-Dist: torchaudio; extra == "all"
132
+ Requires-Dist: google-auth-oauthlib; extra == "all"
138
133
  Requires-Dist: deepmerge; extra == "all"
139
- Requires-Dist: uvicorn[standard]; extra == "all"
134
+ Requires-Dist: httpx_retries; extra == "all"
140
135
  Requires-Dist: regex; extra == "all"
141
- Requires-Dist: tinynetrc; extra == "all"
142
- Requires-Dist: pydantic-settings; extra == "all"
143
- Requires-Dist: contexttimer; extra == "all"
144
- Requires-Dist: flet-webview; extra == "all"
145
- Requires-Dist: pandas; extra == "all"
146
- Requires-Dist: openai; extra == "all"
136
+ Requires-Dist: dask[bag]; extra == "all"
147
137
  Requires-Dist: pytest-cov; extra == "all"
148
- Requires-Dist: distributed; extra == "all"
149
- Requires-Dist: html2text; extra == "all"
150
- Requires-Dist: diskcache; extra == "all"
151
- Requires-Dist: json_repair; extra == "all"
152
138
  Requires-Dist: flet-video; extra == "all"
153
- Requires-Dist: pyyaml; extra == "all"
154
- Requires-Dist: openpyxl; extra == "all"
155
- Requires-Dist: filetype; extra == "all"
156
- Requires-Dist: google-api-python-client; extra == "all"
157
- Requires-Dist: google-auth; extra == "all"
139
+ Requires-Dist: uvicorn[standard]; extra == "all"
140
+ Requires-Dist: logfire[fastapi]; extra == "all"
141
+ Requires-Dist: logfire; extra == "all"
142
+ Requires-Dist: bokeh; extra == "all"
143
+ Requires-Dist: sre_yield; extra == "all"
144
+ Requires-Dist: google-auth-httplib2; extra == "all"
158
145
  Requires-Dist: pymupdf4llm; extra == "all"
159
- Requires-Dist: dask[bag]; extra == "all"
160
- Requires-Dist: httpx_retries; extra == "all"
146
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
147
+ Requires-Dist: semver; extra == "all"
148
+ Requires-Dist: yamlscript; extra == "all"
161
149
  Requires-Dist: torchvision; extra == "all"
162
- Requires-Dist: tabulate; extra == "all"
163
- Requires-Dist: logfire[httpx]; extra == "all"
164
- Requires-Dist: tokenizers; extra == "all"
165
- Requires-Dist: logfire; extra == "all"
150
+ Requires-Dist: pydevd-pycharm; extra == "all"
151
+ Requires-Dist: tinynetrc; extra == "all"
152
+ Requires-Dist: pyyaml; extra == "all"
153
+ Requires-Dist: openai; extra == "all"
154
+ Requires-Dist: sentence_transformers; extra == "all"
166
155
  Requires-Dist: pymupdf; extra == "all"
156
+ Requires-Dist: pydantic-settings; extra == "all"
157
+ Requires-Dist: httpx; extra == "all"
158
+ Requires-Dist: tabulate; extra == "all"
159
+ Requires-Dist: setuptools; extra == "all"
160
+ Requires-Dist: diskcache; extra == "all"
161
+ Requires-Dist: distributed; extra == "all"
162
+ Requires-Dist: huggingface_hub; extra == "all"
163
+ Requires-Dist: flet-webview; extra == "all"
164
+ Requires-Dist: peft; extra == "all"
165
+ Requires-Dist: google-api-python-client; extra == "all"
166
+ Requires-Dist: contexttimer; extra == "all"
167
+ Requires-Dist: docker; extra == "all"
168
+ Requires-Dist: pydantic; extra == "all"
167
169
  Requires-Dist: dnspython[doh]; extra == "all"
168
- Requires-Dist: yamlscript; extra == "all"
169
- Requires-Dist: sentence_transformers; extra == "all"
170
+ Requires-Dist: openpyxl; extra == "all"
171
+ Requires-Dist: filetype; extra == "all"
170
172
  Requires-Dist: fastapi; extra == "all"
171
- Requires-Dist: google-auth-oauthlib; extra == "all"
172
- Requires-Dist: pydevd-pycharm; extra == "all"
173
- Requires-Dist: pydantic; extra == "all"
174
- Requires-Dist: flet[all]; extra == "all"
175
- Requires-Dist: semver; extra == "all"
176
- Requires-Dist: logfire[fastapi]; extra == "all"
177
- Requires-Dist: torchaudio; extra == "all"
178
- Requires-Dist: ollama; extra == "all"
179
- Requires-Dist: google-auth-httplib2; extra == "all"
180
- Requires-Dist: appdirs; extra == "all"
173
+ Requires-Dist: faker; extra == "all"
181
174
  Requires-Dist: transformers[sentencepiece]; extra == "all"
182
- Requires-Dist: setuptools; extra == "all"
183
- Requires-Dist: sre_yield; extra == "all"
184
- Requires-Dist: peft; extra == "all"
175
+ Requires-Dist: Unidecode; extra == "all"
176
+ Requires-Dist: appdirs; extra == "all"
177
+ Requires-Dist: logfire[httpx]; extra == "all"
178
+ Requires-Dist: tokenizers; extra == "all"
179
+ Requires-Dist: html2text; extra == "all"
180
+ Requires-Dist: ollama; extra == "all"
181
+ Requires-Dist: json_repair; extra == "all"
182
+ Requires-Dist: flet[all]; extra == "all"
183
+ Requires-Dist: google-auth; extra == "all"
184
+ Requires-Dist: pandas; extra == "all"
185
185
  Dynamic: author
186
186
  Dynamic: author-email
187
187
  Dynamic: description
@@ -29,6 +29,7 @@ from fmtr.tools.constants import Constants
29
29
  # Submodules
30
30
  from fmtr.tools.path_tools import Path, PackagePaths, AppPaths
31
31
  from fmtr.tools import ai_tools as ai
32
+ from fmtr.tools import dns_tools as dns
32
33
 
33
34
  import fmtr.tools.setup_tools as setup
34
35
  from fmtr.tools.setup_tools import Setup, SetupPaths, Dependencies, Tools
@@ -172,11 +173,6 @@ try:
172
173
  except ImportError as exception:
173
174
  patterns = MissingExtraMockModule('patterns', exception)
174
175
 
175
- try:
176
- from fmtr.tools import dns_tools as dns
177
- except ImportError as exception:
178
- dns = MissingExtraMockModule('dns', exception)
179
-
180
176
  try:
181
177
  from fmtr.tools import http_tools as http
182
178
  from fmtr.tools.http_tools import Client
@@ -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
5
+ except ImportError as exception:
6
+ server = client = dm = MissingExtraMockModule('dns', exception)
@@ -0,0 +1,78 @@
1
+ import dns
2
+ from dns import query
3
+ from functools import cached_property
4
+ from httpx_retries import Retry, RetryTransport
5
+
6
+ from fmtr.tools import http_tools as http
7
+ from fmtr.tools.dns_tools.dm import Exchange, Response, Request
8
+ from fmtr.tools.logging_tools import logger
9
+
10
+ RETRY_STRATEGY = Retry(
11
+ total=2, # initial + 1 retry
12
+ allowed_methods={"GET", "POST"},
13
+ status_forcelist={502, 503, 504},
14
+ retry_on_exceptions=None, # defaults to httpx.TransportError etc.
15
+ backoff_factor=0.25, # short backoff (e.g. 0.25s, 0.5s)
16
+ max_backoff_wait=0.75, # max total delay before giving up
17
+ backoff_jitter=0.1, # small jitter to avoid retry bursts
18
+ respect_retry_after_header=False, # DoH resolvers probably won't set this
19
+ )
20
+
21
+
22
+ class HTTPClientDoH(http.Client):
23
+ """
24
+
25
+ Base HTTP client for DoH-appropriate retry strategy.
26
+
27
+ """
28
+ TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
29
+
30
+
31
+ class ClientBasePlain:
32
+ def __init__(self, host, port=53):
33
+ self.host = host
34
+ self.port = port
35
+
36
+ def resolve(self, exchange: Exchange):
37
+ with logger.span(f'UDP {self.host}:{self.port}'):
38
+ response = query.udp(q=exchange.request.message, where=self.host, port=self.port)
39
+ exchange.response_upstream = Response.from_message(response)
40
+
41
+
42
+ class ClientDoH:
43
+ """
44
+
45
+ Base DoH client.
46
+
47
+ """
48
+
49
+ HEADERS = {"Content-Type": "application/dns-message"}
50
+ client = HTTPClientDoH()
51
+
52
+ def __init__(self, host, url):
53
+ self.host = host
54
+ self.url = url
55
+ self.bootstrap = ClientBasePlain('8.8.8.8')
56
+
57
+ @cached_property
58
+ def ip(self):
59
+ message = dns.message.make_query(self.host, dns.rdatatype.A, flags=0)
60
+ request = Request.from_message(message)
61
+ exchange = Exchange(request=request, ip=None, port=None)
62
+ self.bootstrap.resolve(exchange)
63
+ ip = next(iter(exchange.response_upstream.answer.items.keys())).address
64
+ return ip
65
+
66
+ def resolve(self, exchange: Exchange):
67
+ """
68
+
69
+ Resolve via DoH
70
+
71
+ """
72
+ request = exchange.request
73
+ headers = self.HEADERS | dict(Host=self.host)
74
+ url = self.url.format(host=self.ip)
75
+ response_doh = self.client.post(url, headers=headers, content=request.wire)
76
+ response_doh.raise_for_status()
77
+ response = Response.from_http(response_doh)
78
+ exchange.response_upstream = response
@@ -0,0 +1,110 @@
1
+ import dns
2
+ import httpx
3
+ from dataclasses import dataclass
4
+ from dns.message import Message
5
+ from functools import cached_property
6
+ from typing import Self, Optional
7
+
8
+
9
+ @dataclass
10
+ class BaseDNSData:
11
+ """
12
+
13
+ DNS response object.
14
+
15
+ """
16
+ wire: bytes
17
+
18
+ @cached_property
19
+ def message(self) -> Message:
20
+ return dns.message.from_wire(self.wire)
21
+
22
+ @classmethod
23
+ def from_message(cls, message: Message) -> Self:
24
+ return cls(message.to_wire())
25
+
26
+
27
+ @dataclass
28
+ class Response(BaseDNSData):
29
+ """
30
+
31
+ DNS response object.
32
+
33
+ """
34
+
35
+ http: Optional[httpx.Response] = None
36
+
37
+ @classmethod
38
+ def from_http(cls, response: httpx.Response) -> Self:
39
+ self = cls(response.content, http=response)
40
+ return self
41
+
42
+ @cached_property
43
+ def answer(self):
44
+ return self.message.answer[-1]
45
+
46
+
47
+ @dataclass
48
+ class Request(BaseDNSData):
49
+ """
50
+
51
+ DNS request object.
52
+
53
+ """
54
+ wire: bytes
55
+
56
+ @cached_property
57
+ def question(self):
58
+ return self.message.question[0]
59
+
60
+ @cached_property
61
+ def is_valid(self):
62
+ return len(self.message.question) != 0
63
+
64
+ @cached_property
65
+ def type(self):
66
+ return self.question.rdtype
67
+
68
+ @cached_property
69
+ def type_text(self):
70
+ return dns.rdatatype.to_text(self.type)
71
+
72
+ @cached_property
73
+ def name(self):
74
+ return self.question.name
75
+
76
+ @cached_property
77
+ def name_text(self):
78
+ return self.name.to_text()
79
+
80
+ @cached_property
81
+ def blackhole(self) -> Response:
82
+ blackhole = dns.message.make_response(self.message)
83
+ blackhole.flags |= dns.flags.RA
84
+ blackhole.set_rcode(dns.rcode.NXDOMAIN)
85
+ response = Response.from_message(blackhole)
86
+ return response
87
+
88
+
89
+ @dataclass
90
+ class Exchange:
91
+ """
92
+
93
+ Entire DNS exchange for a DNS Proxy: request -> upstream response -> response
94
+
95
+ """
96
+ ip: str
97
+ port: int
98
+
99
+ request: Request
100
+ response: Optional[Response] = None
101
+ response_upstream: Optional[Response] = None
102
+
103
+ @classmethod
104
+ def from_wire(cls, wire: bytes, ip: str, port: int) -> Self:
105
+ request = Request(wire)
106
+ return cls(request=request, ip=ip, port=port)
107
+
108
+ @cached_property
109
+ def client(self):
110
+ return f'{self.ip}:{self.port}'
@@ -0,0 +1,96 @@
1
+ import socket
2
+
3
+ from fmtr.tools import logger
4
+ from fmtr.tools.dns_tools.client import ClientDoH
5
+ from fmtr.tools.dns_tools.dm import Exchange, Response
6
+
7
+
8
+ class ServerBasePlain:
9
+ """
10
+
11
+ Base for starting a plain DNS server
12
+
13
+ """
14
+
15
+ def __init__(self, host, port):
16
+ self.host = host
17
+ self.port = port
18
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
19
+
20
+ def resolve(self, exchange: Exchange):
21
+ raise NotImplemented
22
+
23
+ def start(self):
24
+ """
25
+
26
+ Listen and resolve via overridden resolve method.
27
+
28
+ """
29
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
30
+ sock.bind((self.host, self.port))
31
+ print(f"Listening on {self.host}:{self.port}")
32
+ while True:
33
+ data, (ip, port) = sock.recvfrom(512)
34
+ exchange = Exchange.from_wire(data, ip=ip, port=port)
35
+ self.resolve(exchange)
36
+ sock.sendto(exchange.response.wire, (ip, port))
37
+
38
+
39
+ class ServerBaseDoHProxy(ServerBasePlain):
40
+ """
41
+
42
+ Base for a DNS Proxy server
43
+
44
+ """
45
+
46
+ def __init__(self, host, port, client: ClientDoH):
47
+ super().__init__(host, port)
48
+ self.client = client
49
+
50
+ def process_question(self, exchange: Exchange):
51
+ return
52
+
53
+ def process_upstream(self, exchange: Exchange):
54
+ return
55
+
56
+ def from_upstream(self, exchange: Exchange) -> Exchange:
57
+
58
+ request = exchange.request
59
+ response_doh = self.client.post(self.URL, headers=self.HEADERS, content=request.wire)
60
+ response_doh.raise_for_status()
61
+ response = Response.from_http(response_doh)
62
+ exchange.response_upstream = response
63
+
64
+ return exchange
65
+
66
+ def resolve(self, exchange: Exchange):
67
+ """
68
+
69
+ Resolve a request, processing each stage, initial question, upstream response etc.
70
+ Subclasses can override the relevant processing methods to implement custom behaviour.
71
+
72
+ """
73
+
74
+ request = exchange.request
75
+
76
+ with logger.span(f'Handling request for {request.name_text} from {exchange.client}...'):
77
+
78
+ if not request.is_valid:
79
+ raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
80
+
81
+ with logger.span(f'Processing question...'):
82
+ self.process_question(exchange)
83
+ if exchange.response:
84
+ return
85
+
86
+ with logger.span(f'Making upstream request for {request.name_text}...'):
87
+ self.client.resolve(exchange)
88
+
89
+ with logger.span(f'Processing upstream response...'):
90
+ self.process_upstream(exchange)
91
+
92
+ if exchange.response:
93
+ return
94
+
95
+ exchange.response = exchange.response_upstream
96
+ return
@@ -0,0 +1 @@
1
+ 1.2.5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.2.4
3
+ Version: 1.2.5
4
4
  Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
5
5
  Home-page: https://github.com/fmtr/tools
6
6
  Author: Frontmatter
@@ -128,60 +128,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
128
128
  Provides-Extra: setup
129
129
  Requires-Dist: setuptools; extra == "setup"
130
130
  Provides-Extra: all
131
- Requires-Dist: httpx; extra == "all"
132
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
133
- Requires-Dist: Unidecode; extra == "all"
134
- Requires-Dist: huggingface_hub; extra == "all"
135
- Requires-Dist: faker; extra == "all"
136
- Requires-Dist: bokeh; extra == "all"
137
- Requires-Dist: docker; extra == "all"
131
+ Requires-Dist: torchaudio; extra == "all"
132
+ Requires-Dist: google-auth-oauthlib; extra == "all"
138
133
  Requires-Dist: deepmerge; extra == "all"
139
- Requires-Dist: uvicorn[standard]; extra == "all"
134
+ Requires-Dist: httpx_retries; extra == "all"
140
135
  Requires-Dist: regex; extra == "all"
141
- Requires-Dist: tinynetrc; extra == "all"
142
- Requires-Dist: pydantic-settings; extra == "all"
143
- Requires-Dist: contexttimer; extra == "all"
144
- Requires-Dist: flet-webview; extra == "all"
145
- Requires-Dist: pandas; extra == "all"
146
- Requires-Dist: openai; extra == "all"
136
+ Requires-Dist: dask[bag]; extra == "all"
147
137
  Requires-Dist: pytest-cov; extra == "all"
148
- Requires-Dist: distributed; extra == "all"
149
- Requires-Dist: html2text; extra == "all"
150
- Requires-Dist: diskcache; extra == "all"
151
- Requires-Dist: json_repair; extra == "all"
152
138
  Requires-Dist: flet-video; extra == "all"
153
- Requires-Dist: pyyaml; extra == "all"
154
- Requires-Dist: openpyxl; extra == "all"
155
- Requires-Dist: filetype; extra == "all"
156
- Requires-Dist: google-api-python-client; extra == "all"
157
- Requires-Dist: google-auth; extra == "all"
139
+ Requires-Dist: uvicorn[standard]; extra == "all"
140
+ Requires-Dist: logfire[fastapi]; extra == "all"
141
+ Requires-Dist: logfire; extra == "all"
142
+ Requires-Dist: bokeh; extra == "all"
143
+ Requires-Dist: sre_yield; extra == "all"
144
+ Requires-Dist: google-auth-httplib2; extra == "all"
158
145
  Requires-Dist: pymupdf4llm; extra == "all"
159
- Requires-Dist: dask[bag]; extra == "all"
160
- Requires-Dist: httpx_retries; extra == "all"
146
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
147
+ Requires-Dist: semver; extra == "all"
148
+ Requires-Dist: yamlscript; extra == "all"
161
149
  Requires-Dist: torchvision; extra == "all"
162
- Requires-Dist: tabulate; extra == "all"
163
- Requires-Dist: logfire[httpx]; extra == "all"
164
- Requires-Dist: tokenizers; extra == "all"
165
- Requires-Dist: logfire; extra == "all"
150
+ Requires-Dist: pydevd-pycharm; extra == "all"
151
+ Requires-Dist: tinynetrc; extra == "all"
152
+ Requires-Dist: pyyaml; extra == "all"
153
+ Requires-Dist: openai; extra == "all"
154
+ Requires-Dist: sentence_transformers; extra == "all"
166
155
  Requires-Dist: pymupdf; extra == "all"
156
+ Requires-Dist: pydantic-settings; extra == "all"
157
+ Requires-Dist: httpx; extra == "all"
158
+ Requires-Dist: tabulate; extra == "all"
159
+ Requires-Dist: setuptools; extra == "all"
160
+ Requires-Dist: diskcache; extra == "all"
161
+ Requires-Dist: distributed; extra == "all"
162
+ Requires-Dist: huggingface_hub; extra == "all"
163
+ Requires-Dist: flet-webview; extra == "all"
164
+ Requires-Dist: peft; extra == "all"
165
+ Requires-Dist: google-api-python-client; extra == "all"
166
+ Requires-Dist: contexttimer; extra == "all"
167
+ Requires-Dist: docker; extra == "all"
168
+ Requires-Dist: pydantic; extra == "all"
167
169
  Requires-Dist: dnspython[doh]; extra == "all"
168
- Requires-Dist: yamlscript; extra == "all"
169
- Requires-Dist: sentence_transformers; extra == "all"
170
+ Requires-Dist: openpyxl; extra == "all"
171
+ Requires-Dist: filetype; extra == "all"
170
172
  Requires-Dist: fastapi; extra == "all"
171
- Requires-Dist: google-auth-oauthlib; extra == "all"
172
- Requires-Dist: pydevd-pycharm; extra == "all"
173
- Requires-Dist: pydantic; extra == "all"
174
- Requires-Dist: flet[all]; extra == "all"
175
- Requires-Dist: semver; extra == "all"
176
- Requires-Dist: logfire[fastapi]; extra == "all"
177
- Requires-Dist: torchaudio; extra == "all"
178
- Requires-Dist: ollama; extra == "all"
179
- Requires-Dist: google-auth-httplib2; extra == "all"
180
- Requires-Dist: appdirs; extra == "all"
173
+ Requires-Dist: faker; extra == "all"
181
174
  Requires-Dist: transformers[sentencepiece]; extra == "all"
182
- Requires-Dist: setuptools; extra == "all"
183
- Requires-Dist: sre_yield; extra == "all"
184
- Requires-Dist: peft; extra == "all"
175
+ Requires-Dist: Unidecode; extra == "all"
176
+ Requires-Dist: appdirs; extra == "all"
177
+ Requires-Dist: logfire[httpx]; extra == "all"
178
+ Requires-Dist: tokenizers; extra == "all"
179
+ Requires-Dist: html2text; extra == "all"
180
+ Requires-Dist: ollama; extra == "all"
181
+ Requires-Dist: json_repair; extra == "all"
182
+ Requires-Dist: flet[all]; extra == "all"
183
+ Requires-Dist: google-auth; extra == "all"
184
+ Requires-Dist: pandas; extra == "all"
185
185
  Dynamic: author
186
186
  Dynamic: author-email
187
187
  Dynamic: description
@@ -12,7 +12,6 @@ setup.py
12
12
  ./fmtr/tools/dataclass_tools.py
13
13
  ./fmtr/tools/datatype_tools.py
14
14
  ./fmtr/tools/debugging_tools.py
15
- ./fmtr/tools/dns_tools.py
16
15
  ./fmtr/tools/docker_tools.py
17
16
  ./fmtr/tools/environment_tools.py
18
17
  ./fmtr/tools/function_tools.py
@@ -55,6 +54,10 @@ setup.py
55
54
  ./fmtr/tools/ai_tools/__init__.py
56
55
  ./fmtr/tools/ai_tools/agentic_tools.py
57
56
  ./fmtr/tools/ai_tools/inference_tools.py
57
+ ./fmtr/tools/dns_tools/__init__.py
58
+ ./fmtr/tools/dns_tools/client.py
59
+ ./fmtr/tools/dns_tools/dm.py
60
+ ./fmtr/tools/dns_tools/server.py
58
61
  ./fmtr/tools/entrypoints/__init__.py
59
62
  ./fmtr/tools/entrypoints/cache_hfh.py
60
63
  ./fmtr/tools/entrypoints/ep_test.py
@@ -90,7 +93,6 @@ fmtr/tools/data_modelling_tools.py
90
93
  fmtr/tools/dataclass_tools.py
91
94
  fmtr/tools/datatype_tools.py
92
95
  fmtr/tools/debugging_tools.py
93
- fmtr/tools/dns_tools.py
94
96
  fmtr/tools/docker_tools.py
95
97
  fmtr/tools/environment_tools.py
96
98
  fmtr/tools/function_tools.py
@@ -133,6 +135,10 @@ fmtr/tools/yaml_tools.py
133
135
  fmtr/tools/ai_tools/__init__.py
134
136
  fmtr/tools/ai_tools/agentic_tools.py
135
137
  fmtr/tools/ai_tools/inference_tools.py
138
+ fmtr/tools/dns_tools/__init__.py
139
+ fmtr/tools/dns_tools/client.py
140
+ fmtr/tools/dns_tools/dm.py
141
+ fmtr/tools/dns_tools/server.py
136
142
  fmtr/tools/entrypoints/__init__.py
137
143
  fmtr/tools/entrypoints/cache_hfh.py
138
144
  fmtr/tools/entrypoints/ep_test.py
@@ -1,221 +0,0 @@
1
- import dns
2
- import httpx
3
- import socket
4
- from dataclasses import dataclass
5
- from dns.message import Message
6
- from functools import cached_property
7
- from httpx_retries import Retry, RetryTransport
8
- from typing import Optional, Self
9
-
10
- from fmtr.tools import Client, logger
11
-
12
- RETRY_STRATEGY = Retry(
13
- total=2, # initial + 1 retry
14
- allowed_methods={"GET", "POST"},
15
- status_forcelist={502, 503, 504},
16
- retry_on_exceptions=None, # defaults to httpx.TransportError etc.
17
- backoff_factor=0.25, # short backoff (e.g. 0.25s, 0.5s)
18
- max_backoff_wait=0.75, # max total delay before giving up
19
- backoff_jitter=0.1, # small jitter to avoid retry bursts
20
- respect_retry_after_header=False, # DoH resolvers probably won't set this
21
- )
22
-
23
-
24
- class HTTPClientDoH(Client):
25
- """
26
-
27
- Base HTTP client for DoH-appropriate retry strategy.
28
-
29
- """
30
- TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
31
-
32
-
33
- @dataclass
34
- class BaseDNSData:
35
- """
36
-
37
- DNS response object.
38
-
39
- """
40
- wire: bytes
41
-
42
- @cached_property
43
- def message(self) -> Message:
44
- return dns.message.from_wire(self.wire)
45
-
46
- @classmethod
47
- def from_message(cls, message: Message) -> Self:
48
- return cls(message.to_wire())
49
-
50
-
51
- @dataclass
52
- class Response(BaseDNSData):
53
- """
54
-
55
- DNS response object.
56
-
57
- """
58
-
59
- http: Optional[httpx.Response] = None
60
-
61
- @classmethod
62
- def from_http(cls, response: httpx.Response) -> Self:
63
- self = cls(response.content, http=response)
64
- return self
65
-
66
-
67
- @dataclass
68
- class Request(BaseDNSData):
69
- """
70
-
71
- DNS request object.
72
-
73
- """
74
- wire: bytes
75
-
76
- @cached_property
77
- def question(self):
78
- return self.message.question[0]
79
-
80
- @cached_property
81
- def is_valid(self):
82
- return len(self.message.question) != 0
83
-
84
- @cached_property
85
- def type(self):
86
- return self.question.rdtype
87
-
88
- @cached_property
89
- def type_text(self):
90
- return dns.rdatatype.to_text(self.type)
91
-
92
- @cached_property
93
- def name(self):
94
- return self.question.name
95
-
96
- @cached_property
97
- def name_text(self):
98
- return self.name.to_text()
99
-
100
- @cached_property
101
- def blackhole(self) -> Response:
102
- blackhole = dns.message.make_response(self.message)
103
- blackhole.flags |= dns.flags.RA
104
- blackhole.set_rcode(dns.rcode.NXDOMAIN)
105
- response = Response.from_message(blackhole)
106
- return response
107
-
108
-
109
- @dataclass
110
- class Exchange:
111
- """
112
-
113
- Entire DNS exchange for a DNS Proxy: request -> upstream response -> response
114
-
115
- """
116
- ip: str
117
- port: int
118
-
119
- request: Request
120
- response: Optional[Response] = None
121
- response_upstream: Optional[Response] = None
122
-
123
- @classmethod
124
- def from_wire(cls, wire: bytes, ip: str, port: int) -> Self:
125
- request = Request(wire)
126
- return cls(request=request, ip=ip, port=port)
127
-
128
- @cached_property
129
- def client(self):
130
- return f'{self.ip}:{self.port}'
131
-
132
-
133
- class BasePlain:
134
- """
135
-
136
- Base for starting a plain DNS server
137
-
138
- """
139
-
140
- def __init__(self, host, port):
141
- self.host = host
142
- self.port = port
143
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
144
-
145
- def resolve(self, exchange: Exchange):
146
- raise NotImplemented
147
-
148
- def start(self):
149
- """
150
-
151
- Listen and resolve via overridden resolve method.
152
-
153
- """
154
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
155
- sock.bind((self.host, self.port))
156
- print(f"Listening on {self.host}:{self.port}")
157
- while True:
158
- data, (ip, port) = sock.recvfrom(512)
159
- exchange = Exchange.from_wire(data, ip=ip, port=port)
160
- self.resolve(exchange)
161
- sock.sendto(exchange.response.wire, (ip, port))
162
-
163
-
164
- class BaseDoHProxy(BasePlain):
165
- """
166
-
167
- Base for a DNS Proxy server
168
-
169
- """
170
-
171
- URL = None
172
- HEADERS = {"Content-Type": "application/dns-message"}
173
- client = HTTPClientDoH()
174
-
175
- def process_question(self, exchange: Exchange):
176
- return
177
-
178
- def process_upstream(self, exchange: Exchange):
179
- return
180
-
181
- def from_upstream(self, exchange: Exchange) -> Exchange:
182
-
183
- request = exchange.request
184
- response_doh = self.client.post(self.URL, headers=self.HEADERS, content=request.wire)
185
- response_doh.raise_for_status()
186
- response = Response.from_http(response_doh)
187
- exchange.response_upstream = response
188
-
189
- return exchange
190
-
191
- def resolve(self, exchange: Exchange):
192
- """
193
-
194
- Resolve a request, processing each stage, initial question, upstream response etc.
195
- Subclasses can override the relevant processing methods to implement custom behaviour.
196
-
197
- """
198
-
199
- request = exchange.request
200
-
201
- with logger.span(f'Handling request for {request.name_text} from {exchange.client}...'):
202
-
203
- if not request.is_valid:
204
- raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
205
-
206
- with logger.span(f'Processing question...'):
207
- self.process_question(exchange)
208
- if exchange.response:
209
- return
210
-
211
- with logger.span(f'Making upstream request for {request.name_text}...'):
212
- self.from_upstream(exchange)
213
-
214
- with logger.span(f'Processing upstream response...'):
215
- self.process_upstream(exchange)
216
-
217
- if exchange.response:
218
- return
219
-
220
- exchange.response = exchange.response_upstream
221
- return
@@ -1 +0,0 @@
1
- 1.2.4
File without changes
File without changes
@@ -15,60 +15,60 @@ pydantic-ai[logfire,openai]
15
15
  ollama
16
16
 
17
17
  [all]
18
- httpx
19
- pydantic-ai[logfire,openai]
20
- Unidecode
21
- huggingface_hub
22
- faker
23
- bokeh
24
- docker
18
+ torchaudio
19
+ google-auth-oauthlib
25
20
  deepmerge
26
- uvicorn[standard]
21
+ httpx_retries
27
22
  regex
28
- tinynetrc
29
- pydantic-settings
30
- contexttimer
31
- flet-webview
32
- pandas
33
- openai
23
+ dask[bag]
34
24
  pytest-cov
35
- distributed
36
- html2text
37
- diskcache
38
- json_repair
39
25
  flet-video
40
- pyyaml
41
- openpyxl
42
- filetype
43
- google-api-python-client
44
- google-auth
26
+ uvicorn[standard]
27
+ logfire[fastapi]
28
+ logfire
29
+ bokeh
30
+ sre_yield
31
+ google-auth-httplib2
45
32
  pymupdf4llm
46
- dask[bag]
47
- httpx_retries
33
+ pydantic-ai[logfire,openai]
34
+ semver
35
+ yamlscript
48
36
  torchvision
49
- tabulate
50
- logfire[httpx]
51
- tokenizers
52
- logfire
37
+ pydevd-pycharm
38
+ tinynetrc
39
+ pyyaml
40
+ openai
41
+ sentence_transformers
53
42
  pymupdf
43
+ pydantic-settings
44
+ httpx
45
+ tabulate
46
+ setuptools
47
+ diskcache
48
+ distributed
49
+ huggingface_hub
50
+ flet-webview
51
+ peft
52
+ google-api-python-client
53
+ contexttimer
54
+ docker
55
+ pydantic
54
56
  dnspython[doh]
55
- yamlscript
56
- sentence_transformers
57
+ openpyxl
58
+ filetype
57
59
  fastapi
58
- google-auth-oauthlib
59
- pydevd-pycharm
60
- pydantic
61
- flet[all]
62
- semver
63
- logfire[fastapi]
64
- torchaudio
65
- ollama
66
- google-auth-httplib2
67
- appdirs
60
+ faker
68
61
  transformers[sentencepiece]
69
- setuptools
70
- sre_yield
71
- peft
62
+ Unidecode
63
+ appdirs
64
+ logfire[httpx]
65
+ tokenizers
66
+ html2text
67
+ ollama
68
+ json_repair
69
+ flet[all]
70
+ google-auth
71
+ pandas
72
72
 
73
73
  [api]
74
74
  fastapi
File without changes
File without changes
File without changes