fmtr.tools 1.2.4__tar.gz → 1.2.6__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.6}/PKG-INFO +42 -40
  2. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/__init__.py +1 -5
  3. fmtr_tools-1.2.6/fmtr/tools/dns_tools/__init__.py +6 -0
  4. fmtr_tools-1.2.6/fmtr/tools/dns_tools/client.py +80 -0
  5. fmtr_tools-1.2.6/fmtr/tools/dns_tools/dm.py +110 -0
  6. fmtr_tools-1.2.6/fmtr/tools/dns_tools/server.py +89 -0
  7. fmtr_tools-1.2.6/fmtr/tools/version +1 -0
  8. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr.tools.egg-info/PKG-INFO +42 -40
  9. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr.tools.egg-info/SOURCES.txt +8 -2
  10. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr.tools.egg-info/requires.txt +41 -39
  11. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/setup.py +1 -1
  12. fmtr_tools-1.2.4/fmtr/tools/dns_tools.py +0 -221
  13. fmtr_tools-1.2.4/fmtr/tools/version +0 -1
  14. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/LICENSE +0 -0
  15. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/README.md +0 -0
  16. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/ai_tools/__init__.py +0 -0
  17. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/ai_tools/agentic_tools.py +0 -0
  18. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/ai_tools/inference_tools.py +0 -0
  19. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/api_tools.py +0 -0
  20. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/async_tools.py +0 -0
  21. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/augmentation_tools.py +0 -0
  22. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/caching_tools.py +0 -0
  23. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/constants.py +0 -0
  24. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/data_modelling_tools.py +0 -0
  25. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/dataclass_tools.py +0 -0
  26. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/datatype_tools.py +0 -0
  27. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/debugging_tools.py +0 -0
  28. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/docker_tools.py +0 -0
  29. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/entrypoints/__init__.py +0 -0
  30. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/entrypoints/cache_hfh.py +0 -0
  31. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/entrypoints/ep_test.py +0 -0
  32. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/entrypoints/remote_debug_test.py +0 -0
  33. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/entrypoints/shell_debug.py +0 -0
  34. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/environment_tools.py +0 -0
  35. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/function_tools.py +0 -0
  36. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/google_api_tools.py +0 -0
  37. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/hash_tools.py +0 -0
  38. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/hfh_tools.py +0 -0
  39. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/html_tools.py +0 -0
  40. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/http_tools.py +0 -0
  41. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/import_tools.py +0 -0
  42. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/inspection_tools.py +0 -0
  43. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/interface_tools.py +0 -0
  44. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/iterator_tools.py +0 -0
  45. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/json_fix_tools.py +0 -0
  46. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/json_tools.py +0 -0
  47. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/logging_tools.py +0 -0
  48. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/merging_tools.py +0 -0
  49. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/metric_tools.py +0 -0
  50. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/name_tools.py +0 -0
  51. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/netrc_tools.py +0 -0
  52. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/openai_tools.py +0 -0
  53. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/packaging_tools.py +0 -0
  54. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/parallel_tools.py +0 -0
  55. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/path_tools/__init__.py +0 -0
  56. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/path_tools/app_path_tools.py +0 -0
  57. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/path_tools/path_tools.py +0 -0
  58. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/path_tools/type_path_tools.py +0 -0
  59. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/pattern_tools.py +0 -0
  60. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/pdf_tools.py +0 -0
  61. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/platform_tools.py +0 -0
  62. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/process_tools.py +0 -0
  63. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/profiling_tools.py +0 -0
  64. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/random_tools.py +0 -0
  65. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/semantic_tools.py +0 -0
  66. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/settings_tools.py +0 -0
  67. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/setup_tools/__init__.py +0 -0
  68. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/setup_tools/setup_tools.py +0 -0
  69. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/spaces_tools.py +0 -0
  70. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/string_tools.py +0 -0
  71. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tabular_tools.py +0 -0
  72. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/__init__.py +0 -0
  73. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/conftest.py +0 -0
  74. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/helpers.py +0 -0
  75. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/test_datatype.py +0 -0
  76. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/test_environment.py +0 -0
  77. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/test_json.py +0 -0
  78. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/test_path.py +0 -0
  79. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tests/test_yaml.py +0 -0
  80. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tokenization_tools.py +0 -0
  81. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/tools.py +0 -0
  82. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/unicode_tools.py +0 -0
  83. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/version_tools.py +0 -0
  84. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr/tools/yaml_tools.py +0 -0
  85. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr.tools.egg-info/dependency_links.txt +0 -0
  86. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr.tools.egg-info/entry_points.txt +0 -0
  87. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/fmtr.tools.egg-info/top_level.txt +0 -0
  88. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/pyproject.toml +0 -0
  89. {fmtr_tools-1.2.4 → fmtr_tools-1.2.6}/setup.cfg +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.6
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
@@ -106,6 +106,8 @@ Requires-Dist: pydevd-pycharm; extra == "debug"
106
106
  Provides-Extra: sets
107
107
  Requires-Dist: pydantic-settings; extra == "sets"
108
108
  Requires-Dist: pydantic; extra == "sets"
109
+ Requires-Dist: yamlscript; extra == "sets"
110
+ Requires-Dist: pyyaml; extra == "sets"
109
111
  Provides-Extra: path-app
110
112
  Requires-Dist: appdirs; extra == "path-app"
111
113
  Provides-Extra: path-type
@@ -128,60 +130,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
128
130
  Provides-Extra: setup
129
131
  Requires-Dist: setuptools; extra == "setup"
130
132
  Provides-Extra: all
131
- Requires-Dist: httpx; extra == "all"
132
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
133
+ Requires-Dist: google-api-python-client; extra == "all"
133
134
  Requires-Dist: Unidecode; extra == "all"
135
+ Requires-Dist: uvicorn[standard]; extra == "all"
134
136
  Requires-Dist: huggingface_hub; extra == "all"
135
- Requires-Dist: faker; extra == "all"
137
+ Requires-Dist: logfire[fastapi]; extra == "all"
138
+ Requires-Dist: pymupdf4llm; extra == "all"
139
+ Requires-Dist: semver; extra == "all"
140
+ Requires-Dist: pymupdf; extra == "all"
136
141
  Requires-Dist: bokeh; extra == "all"
137
- Requires-Dist: docker; extra == "all"
138
- Requires-Dist: deepmerge; extra == "all"
139
- Requires-Dist: uvicorn[standard]; extra == "all"
140
- Requires-Dist: regex; extra == "all"
141
- Requires-Dist: tinynetrc; extra == "all"
142
- Requires-Dist: pydantic-settings; extra == "all"
143
142
  Requires-Dist: contexttimer; extra == "all"
144
- Requires-Dist: flet-webview; extra == "all"
145
- Requires-Dist: pandas; extra == "all"
146
- Requires-Dist: openai; extra == "all"
147
- Requires-Dist: pytest-cov; extra == "all"
148
- Requires-Dist: distributed; extra == "all"
149
- Requires-Dist: html2text; extra == "all"
150
143
  Requires-Dist: diskcache; extra == "all"
144
+ Requires-Dist: peft; extra == "all"
145
+ Requires-Dist: dask[bag]; extra == "all"
146
+ Requires-Dist: pydevd-pycharm; extra == "all"
147
+ Requires-Dist: transformers[sentencepiece]; extra == "all"
148
+ Requires-Dist: regex; extra == "all"
151
149
  Requires-Dist: json_repair; extra == "all"
152
- Requires-Dist: flet-video; extra == "all"
153
- Requires-Dist: pyyaml; extra == "all"
150
+ Requires-Dist: html2text; extra == "all"
151
+ Requires-Dist: faker; extra == "all"
154
152
  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"
158
- Requires-Dist: pymupdf4llm; extra == "all"
159
- Requires-Dist: dask[bag]; extra == "all"
160
- Requires-Dist: httpx_retries; extra == "all"
161
- Requires-Dist: torchvision; extra == "all"
162
- Requires-Dist: tabulate; extra == "all"
153
+ Requires-Dist: appdirs; extra == "all"
163
154
  Requires-Dist: logfire[httpx]; extra == "all"
155
+ Requires-Dist: openai; extra == "all"
156
+ Requires-Dist: sentence_transformers; extra == "all"
157
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
158
+ Requires-Dist: fastapi; extra == "all"
159
+ Requires-Dist: torchvision; extra == "all"
160
+ Requires-Dist: ollama; extra == "all"
164
161
  Requires-Dist: tokenizers; extra == "all"
165
- Requires-Dist: logfire; extra == "all"
166
- Requires-Dist: pymupdf; extra == "all"
167
162
  Requires-Dist: dnspython[doh]; extra == "all"
163
+ Requires-Dist: flet-video; extra == "all"
164
+ Requires-Dist: pandas; extra == "all"
168
165
  Requires-Dist: yamlscript; extra == "all"
169
- Requires-Dist: sentence_transformers; extra == "all"
170
- Requires-Dist: fastapi; extra == "all"
166
+ Requires-Dist: filetype; extra == "all"
167
+ Requires-Dist: logfire; extra == "all"
168
+ Requires-Dist: tabulate; extra == "all"
171
169
  Requires-Dist: google-auth-oauthlib; extra == "all"
172
- Requires-Dist: pydevd-pycharm; extra == "all"
173
- Requires-Dist: pydantic; extra == "all"
174
170
  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"
181
- Requires-Dist: transformers[sentencepiece]; extra == "all"
171
+ Requires-Dist: pydantic-settings; extra == "all"
172
+ Requires-Dist: tinynetrc; extra == "all"
173
+ Requires-Dist: deepmerge; extra == "all"
174
+ Requires-Dist: distributed; extra == "all"
175
+ Requires-Dist: flet-webview; extra == "all"
182
176
  Requires-Dist: setuptools; extra == "all"
177
+ Requires-Dist: pyyaml; extra == "all"
183
178
  Requires-Dist: sre_yield; extra == "all"
184
- Requires-Dist: peft; extra == "all"
179
+ Requires-Dist: google-auth; extra == "all"
180
+ Requires-Dist: torchaudio; extra == "all"
181
+ Requires-Dist: pydantic; extra == "all"
182
+ Requires-Dist: google-auth-httplib2; extra == "all"
183
+ Requires-Dist: docker; extra == "all"
184
+ Requires-Dist: httpx; extra == "all"
185
+ Requires-Dist: pytest-cov; extra == "all"
186
+ Requires-Dist: httpx_retries; extra == "all"
185
187
  Dynamic: author
186
188
  Dynamic: author-email
187
189
  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,80 @@
1
+ import dns
2
+ from dataclasses import dataclass
3
+ from dns import query
4
+ from functools import cached_property
5
+ from httpx_retries import Retry, RetryTransport
6
+
7
+ from fmtr.tools import http_tools as http
8
+ from fmtr.tools.dns_tools.dm import Exchange, Response, Request
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
+ class ClientBasePlain:
33
+ def __init__(self, host, port=53):
34
+ self.host = host
35
+ self.port = port
36
+
37
+ def resolve(self, exchange: Exchange):
38
+ 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)
41
+
42
+
43
+ @dataclass
44
+ class ClientDoH:
45
+ """
46
+
47
+ Base DoH client.
48
+
49
+ """
50
+
51
+ HEADERS = {"Content-Type": "application/dns-message"}
52
+ CLIENT = HTTPClientDoH()
53
+ BOOTSTRAP = ClientBasePlain('8.8.8.8')
54
+
55
+ host: str
56
+ url: str
57
+
58
+
59
+ @cached_property
60
+ 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)
64
+ self.BOOTSTRAP.resolve(exchange)
65
+ ip = next(iter(exchange.response_upstream.answer.items.keys())).address
66
+ return ip
67
+
68
+ def resolve(self, exchange: Exchange):
69
+ """
70
+
71
+ Resolve via DoH
72
+
73
+ """
74
+ request = exchange.request
75
+ headers = self.HEADERS | dict(Host=self.host)
76
+ url = self.url.format(host=self.ip)
77
+ response_doh = self.CLIENT.post(url, headers=headers, content=request.wire)
78
+ response_doh.raise_for_status()
79
+ response = Response.from_http(response_doh)
80
+ 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,89 @@
1
+ import socket
2
+ from dataclasses import dataclass
3
+
4
+ from fmtr.tools import logger
5
+ from fmtr.tools.dns_tools.client import ClientDoH
6
+ from fmtr.tools.dns_tools.dm import Exchange
7
+
8
+
9
+ @dataclass
10
+ class ServerBasePlain:
11
+ """
12
+
13
+ Base for starting a plain DNS server
14
+
15
+ """
16
+
17
+ host: str
18
+ port: int
19
+
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):
28
+ """
29
+
30
+ Listen and resolve via overridden resolve method.
31
+
32
+ """
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
+ return
55
+
56
+ def process_upstream(self, exchange: Exchange):
57
+ return
58
+
59
+ def resolve(self, exchange: Exchange):
60
+ """
61
+
62
+ Resolve a request, processing each stage, initial question, upstream response etc.
63
+ Subclasses can override the relevant processing methods to implement custom behaviour.
64
+
65
+ """
66
+
67
+ request = exchange.request
68
+
69
+ with logger.span(f'Handling request for {request.name_text} from {exchange.client}...'):
70
+
71
+ if not request.is_valid:
72
+ raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
73
+
74
+ with logger.span(f'Processing question...'):
75
+ self.process_question(exchange)
76
+ if exchange.response:
77
+ return
78
+
79
+ with logger.span(f'Making upstream request for {request.name_text}...'):
80
+ self.client.resolve(exchange)
81
+
82
+ with logger.span(f'Processing upstream response...'):
83
+ self.process_upstream(exchange)
84
+
85
+ if exchange.response:
86
+ return
87
+
88
+ exchange.response = exchange.response_upstream
89
+ return
@@ -0,0 +1 @@
1
+ 1.2.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.2.4
3
+ Version: 1.2.6
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
@@ -106,6 +106,8 @@ Requires-Dist: pydevd-pycharm; extra == "debug"
106
106
  Provides-Extra: sets
107
107
  Requires-Dist: pydantic-settings; extra == "sets"
108
108
  Requires-Dist: pydantic; extra == "sets"
109
+ Requires-Dist: yamlscript; extra == "sets"
110
+ Requires-Dist: pyyaml; extra == "sets"
109
111
  Provides-Extra: path-app
110
112
  Requires-Dist: appdirs; extra == "path-app"
111
113
  Provides-Extra: path-type
@@ -128,60 +130,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
128
130
  Provides-Extra: setup
129
131
  Requires-Dist: setuptools; extra == "setup"
130
132
  Provides-Extra: all
131
- Requires-Dist: httpx; extra == "all"
132
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
133
+ Requires-Dist: google-api-python-client; extra == "all"
133
134
  Requires-Dist: Unidecode; extra == "all"
135
+ Requires-Dist: uvicorn[standard]; extra == "all"
134
136
  Requires-Dist: huggingface_hub; extra == "all"
135
- Requires-Dist: faker; extra == "all"
137
+ Requires-Dist: logfire[fastapi]; extra == "all"
138
+ Requires-Dist: pymupdf4llm; extra == "all"
139
+ Requires-Dist: semver; extra == "all"
140
+ Requires-Dist: pymupdf; extra == "all"
136
141
  Requires-Dist: bokeh; extra == "all"
137
- Requires-Dist: docker; extra == "all"
138
- Requires-Dist: deepmerge; extra == "all"
139
- Requires-Dist: uvicorn[standard]; extra == "all"
140
- Requires-Dist: regex; extra == "all"
141
- Requires-Dist: tinynetrc; extra == "all"
142
- Requires-Dist: pydantic-settings; extra == "all"
143
142
  Requires-Dist: contexttimer; extra == "all"
144
- Requires-Dist: flet-webview; extra == "all"
145
- Requires-Dist: pandas; extra == "all"
146
- Requires-Dist: openai; extra == "all"
147
- Requires-Dist: pytest-cov; extra == "all"
148
- Requires-Dist: distributed; extra == "all"
149
- Requires-Dist: html2text; extra == "all"
150
143
  Requires-Dist: diskcache; extra == "all"
144
+ Requires-Dist: peft; extra == "all"
145
+ Requires-Dist: dask[bag]; extra == "all"
146
+ Requires-Dist: pydevd-pycharm; extra == "all"
147
+ Requires-Dist: transformers[sentencepiece]; extra == "all"
148
+ Requires-Dist: regex; extra == "all"
151
149
  Requires-Dist: json_repair; extra == "all"
152
- Requires-Dist: flet-video; extra == "all"
153
- Requires-Dist: pyyaml; extra == "all"
150
+ Requires-Dist: html2text; extra == "all"
151
+ Requires-Dist: faker; extra == "all"
154
152
  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"
158
- Requires-Dist: pymupdf4llm; extra == "all"
159
- Requires-Dist: dask[bag]; extra == "all"
160
- Requires-Dist: httpx_retries; extra == "all"
161
- Requires-Dist: torchvision; extra == "all"
162
- Requires-Dist: tabulate; extra == "all"
153
+ Requires-Dist: appdirs; extra == "all"
163
154
  Requires-Dist: logfire[httpx]; extra == "all"
155
+ Requires-Dist: openai; extra == "all"
156
+ Requires-Dist: sentence_transformers; extra == "all"
157
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
158
+ Requires-Dist: fastapi; extra == "all"
159
+ Requires-Dist: torchvision; extra == "all"
160
+ Requires-Dist: ollama; extra == "all"
164
161
  Requires-Dist: tokenizers; extra == "all"
165
- Requires-Dist: logfire; extra == "all"
166
- Requires-Dist: pymupdf; extra == "all"
167
162
  Requires-Dist: dnspython[doh]; extra == "all"
163
+ Requires-Dist: flet-video; extra == "all"
164
+ Requires-Dist: pandas; extra == "all"
168
165
  Requires-Dist: yamlscript; extra == "all"
169
- Requires-Dist: sentence_transformers; extra == "all"
170
- Requires-Dist: fastapi; extra == "all"
166
+ Requires-Dist: filetype; extra == "all"
167
+ Requires-Dist: logfire; extra == "all"
168
+ Requires-Dist: tabulate; extra == "all"
171
169
  Requires-Dist: google-auth-oauthlib; extra == "all"
172
- Requires-Dist: pydevd-pycharm; extra == "all"
173
- Requires-Dist: pydantic; extra == "all"
174
170
  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"
181
- Requires-Dist: transformers[sentencepiece]; extra == "all"
171
+ Requires-Dist: pydantic-settings; extra == "all"
172
+ Requires-Dist: tinynetrc; extra == "all"
173
+ Requires-Dist: deepmerge; extra == "all"
174
+ Requires-Dist: distributed; extra == "all"
175
+ Requires-Dist: flet-webview; extra == "all"
182
176
  Requires-Dist: setuptools; extra == "all"
177
+ Requires-Dist: pyyaml; extra == "all"
183
178
  Requires-Dist: sre_yield; extra == "all"
184
- Requires-Dist: peft; extra == "all"
179
+ Requires-Dist: google-auth; extra == "all"
180
+ Requires-Dist: torchaudio; extra == "all"
181
+ Requires-Dist: pydantic; extra == "all"
182
+ Requires-Dist: google-auth-httplib2; extra == "all"
183
+ Requires-Dist: docker; extra == "all"
184
+ Requires-Dist: httpx; extra == "all"
185
+ Requires-Dist: pytest-cov; extra == "all"
186
+ Requires-Dist: httpx_retries; extra == "all"
185
187
  Dynamic: author
186
188
  Dynamic: author-email
187
189
  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
@@ -15,60 +15,60 @@ pydantic-ai[logfire,openai]
15
15
  ollama
16
16
 
17
17
  [all]
18
- httpx
19
- pydantic-ai[logfire,openai]
18
+ google-api-python-client
20
19
  Unidecode
20
+ uvicorn[standard]
21
21
  huggingface_hub
22
- faker
22
+ logfire[fastapi]
23
+ pymupdf4llm
24
+ semver
25
+ pymupdf
23
26
  bokeh
24
- docker
25
- deepmerge
26
- uvicorn[standard]
27
- regex
28
- tinynetrc
29
- pydantic-settings
30
27
  contexttimer
31
- flet-webview
32
- pandas
33
- openai
34
- pytest-cov
35
- distributed
36
- html2text
37
28
  diskcache
29
+ peft
30
+ dask[bag]
31
+ pydevd-pycharm
32
+ transformers[sentencepiece]
33
+ regex
38
34
  json_repair
39
- flet-video
40
- pyyaml
35
+ html2text
36
+ faker
41
37
  openpyxl
42
- filetype
43
- google-api-python-client
44
- google-auth
45
- pymupdf4llm
46
- dask[bag]
47
- httpx_retries
48
- torchvision
49
- tabulate
38
+ appdirs
50
39
  logfire[httpx]
40
+ openai
41
+ sentence_transformers
42
+ pydantic-ai[logfire,openai]
43
+ fastapi
44
+ torchvision
45
+ ollama
51
46
  tokenizers
52
- logfire
53
- pymupdf
54
47
  dnspython[doh]
48
+ flet-video
49
+ pandas
55
50
  yamlscript
56
- sentence_transformers
57
- fastapi
51
+ filetype
52
+ logfire
53
+ tabulate
58
54
  google-auth-oauthlib
59
- pydevd-pycharm
60
- pydantic
61
55
  flet[all]
62
- semver
63
- logfire[fastapi]
64
- torchaudio
65
- ollama
66
- google-auth-httplib2
67
- appdirs
68
- transformers[sentencepiece]
56
+ pydantic-settings
57
+ tinynetrc
58
+ deepmerge
59
+ distributed
60
+ flet-webview
69
61
  setuptools
62
+ pyyaml
70
63
  sre_yield
71
- peft
64
+ google-auth
65
+ torchaudio
66
+ pydantic
67
+ google-auth-httplib2
68
+ docker
69
+ httpx
70
+ pytest-cov
71
+ httpx_retries
72
72
 
73
73
  [api]
74
74
  fastapi
@@ -183,6 +183,8 @@ openpyxl
183
183
  [sets]
184
184
  pydantic-settings
185
185
  pydantic
186
+ yamlscript
187
+ pyyaml
186
188
 
187
189
  [setup]
188
190
  setuptools
@@ -32,7 +32,7 @@ DEPENDENCIES = {
32
32
  'caching': ['diskcache'],
33
33
  'pdf': ['pymupdf', 'dm', 'pymupdf4llm'],
34
34
  'debug': ['pydevd-pycharm'],
35
- 'sets': ['pydantic-settings', 'dm'],
35
+ 'sets': ['pydantic-settings', 'dm', 'yaml'],
36
36
  'path.app': ['appdirs'],
37
37
  'path.type': ['filetype'],
38
38
  'dns': ['dnspython[doh]', 'http'],
@@ -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
File without changes
File without changes