fmtr.tools 1.2.3__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.3 → fmtr_tools-1.2.5}/PKG-INFO +46 -46
  2. {fmtr_tools-1.2.3 → 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.3 → fmtr_tools-1.2.5}/fmtr/tools/pattern_tools.py +6 -3
  8. fmtr_tools-1.2.5/fmtr/tools/version +1 -0
  9. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/PKG-INFO +46 -46
  10. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/SOURCES.txt +8 -2
  11. fmtr_tools-1.2.3/fmtr/tools/dns_tools.py +0 -221
  12. fmtr_tools-1.2.3/fmtr/tools/version +0 -1
  13. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/LICENSE +0 -0
  14. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/README.md +0 -0
  15. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/ai_tools/__init__.py +0 -0
  16. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/ai_tools/agentic_tools.py +0 -0
  17. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/ai_tools/inference_tools.py +0 -0
  18. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/api_tools.py +0 -0
  19. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/async_tools.py +0 -0
  20. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/augmentation_tools.py +0 -0
  21. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/caching_tools.py +0 -0
  22. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/constants.py +0 -0
  23. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/data_modelling_tools.py +0 -0
  24. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/dataclass_tools.py +0 -0
  25. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/datatype_tools.py +0 -0
  26. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/debugging_tools.py +0 -0
  27. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/docker_tools.py +0 -0
  28. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/__init__.py +0 -0
  29. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/cache_hfh.py +0 -0
  30. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/ep_test.py +0 -0
  31. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/remote_debug_test.py +0 -0
  32. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/entrypoints/shell_debug.py +0 -0
  33. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/environment_tools.py +0 -0
  34. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/function_tools.py +0 -0
  35. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/google_api_tools.py +0 -0
  36. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/hash_tools.py +0 -0
  37. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/hfh_tools.py +0 -0
  38. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/html_tools.py +0 -0
  39. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/http_tools.py +0 -0
  40. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/import_tools.py +0 -0
  41. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/inspection_tools.py +0 -0
  42. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/interface_tools.py +0 -0
  43. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/iterator_tools.py +0 -0
  44. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/json_fix_tools.py +0 -0
  45. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/json_tools.py +0 -0
  46. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/logging_tools.py +0 -0
  47. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/merging_tools.py +0 -0
  48. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/metric_tools.py +0 -0
  49. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/name_tools.py +0 -0
  50. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/netrc_tools.py +0 -0
  51. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/openai_tools.py +0 -0
  52. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/packaging_tools.py +0 -0
  53. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/parallel_tools.py +0 -0
  54. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/__init__.py +0 -0
  55. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/app_path_tools.py +0 -0
  56. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/path_tools.py +0 -0
  57. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/path_tools/type_path_tools.py +0 -0
  58. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/pdf_tools.py +0 -0
  59. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/platform_tools.py +0 -0
  60. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/process_tools.py +0 -0
  61. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/profiling_tools.py +0 -0
  62. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/random_tools.py +0 -0
  63. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/semantic_tools.py +0 -0
  64. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/settings_tools.py +0 -0
  65. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/setup_tools/__init__.py +0 -0
  66. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/setup_tools/setup_tools.py +0 -0
  67. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/spaces_tools.py +0 -0
  68. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/string_tools.py +0 -0
  69. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tabular_tools.py +0 -0
  70. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/__init__.py +0 -0
  71. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/conftest.py +0 -0
  72. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/helpers.py +0 -0
  73. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_datatype.py +0 -0
  74. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_environment.py +0 -0
  75. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_json.py +0 -0
  76. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_path.py +0 -0
  77. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tests/test_yaml.py +0 -0
  78. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tokenization_tools.py +0 -0
  79. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/tools.py +0 -0
  80. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/unicode_tools.py +0 -0
  81. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/version_tools.py +0 -0
  82. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr/tools/yaml_tools.py +0 -0
  83. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/dependency_links.txt +0 -0
  84. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/entry_points.txt +0 -0
  85. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/requires.txt +45 -45
  86. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/fmtr.tools.egg-info/top_level.txt +0 -0
  87. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/pyproject.toml +0 -0
  88. {fmtr_tools-1.2.3 → fmtr_tools-1.2.5}/setup.cfg +0 -0
  89. {fmtr_tools-1.2.3 → 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.3
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: logfire; extra == "all"
132
- Requires-Dist: tokenizers; extra == "all"
133
- Requires-Dist: appdirs; extra == "all"
134
- Requires-Dist: pydantic-settings; extra == "all"
135
- Requires-Dist: filetype; extra == "all"
136
- Requires-Dist: pymupdf; extra == "all"
137
- Requires-Dist: json_repair; extra == "all"
138
- Requires-Dist: sentence_transformers; extra == "all"
139
- Requires-Dist: fastapi; extra == "all"
140
- Requires-Dist: tabulate; extra == "all"
141
- Requires-Dist: distributed; extra == "all"
142
- Requires-Dist: faker; extra == "all"
143
- Requires-Dist: semver; extra == "all"
144
- Requires-Dist: uvicorn[standard]; extra == "all"
145
- Requires-Dist: html2text; extra == "all"
146
- Requires-Dist: google-auth-httplib2; extra == "all"
147
- Requires-Dist: bokeh; extra == "all"
148
- Requires-Dist: flet-webview; extra == "all"
149
- Requires-Dist: logfire[httpx]; extra == "all"
150
131
  Requires-Dist: torchaudio; extra == "all"
151
- Requires-Dist: Unidecode; extra == "all"
152
- Requires-Dist: flet-video; extra == "all"
153
- Requires-Dist: google-auth; extra == "all"
154
- Requires-Dist: flet[all]; extra == "all"
155
- Requires-Dist: openai; extra == "all"
156
- Requires-Dist: pymupdf4llm; extra == "all"
157
- Requires-Dist: dnspython[doh]; extra == "all"
158
- Requires-Dist: deepmerge; extra == "all"
159
- Requires-Dist: contexttimer; extra == "all"
160
- Requires-Dist: openpyxl; extra == "all"
161
- Requires-Dist: tinynetrc; extra == "all"
162
- Requires-Dist: ollama; extra == "all"
163
- Requires-Dist: huggingface_hub; extra == "all"
164
132
  Requires-Dist: google-auth-oauthlib; extra == "all"
133
+ Requires-Dist: deepmerge; extra == "all"
165
134
  Requires-Dist: httpx_retries; extra == "all"
166
- Requires-Dist: docker; extra == "all"
167
- Requires-Dist: setuptools; extra == "all"
168
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
169
- Requires-Dist: google-api-python-client; extra == "all"
170
- Requires-Dist: torchvision; extra == "all"
171
- Requires-Dist: logfire[fastapi]; extra == "all"
172
- Requires-Dist: pandas; extra == "all"
173
135
  Requires-Dist: regex; extra == "all"
174
- Requires-Dist: httpx; extra == "all"
136
+ Requires-Dist: dask[bag]; extra == "all"
137
+ Requires-Dist: pytest-cov; extra == "all"
138
+ Requires-Dist: flet-video; 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"
175
143
  Requires-Dist: sre_yield; extra == "all"
144
+ Requires-Dist: google-auth-httplib2; extra == "all"
145
+ Requires-Dist: pymupdf4llm; extra == "all"
146
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
147
+ Requires-Dist: semver; extra == "all"
148
+ Requires-Dist: yamlscript; extra == "all"
149
+ Requires-Dist: torchvision; extra == "all"
150
+ Requires-Dist: pydevd-pycharm; extra == "all"
151
+ Requires-Dist: tinynetrc; extra == "all"
176
152
  Requires-Dist: pyyaml; extra == "all"
153
+ Requires-Dist: openai; extra == "all"
154
+ Requires-Dist: sentence_transformers; extra == "all"
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"
177
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"
178
168
  Requires-Dist: pydantic; extra == "all"
179
- Requires-Dist: pytest-cov; extra == "all"
180
- Requires-Dist: yamlscript; extra == "all"
169
+ Requires-Dist: dnspython[doh]; extra == "all"
170
+ Requires-Dist: openpyxl; extra == "all"
171
+ Requires-Dist: filetype; extra == "all"
172
+ Requires-Dist: fastapi; extra == "all"
173
+ Requires-Dist: faker; extra == "all"
181
174
  Requires-Dist: transformers[sentencepiece]; extra == "all"
182
- Requires-Dist: diskcache; extra == "all"
183
- Requires-Dist: dask[bag]; extra == "all"
184
- Requires-Dist: pydevd-pycharm; 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
@@ -1,9 +1,8 @@
1
+ import regex as re
1
2
  from dataclasses import dataclass
2
3
  from functools import cached_property
3
4
  from typing import List
4
5
 
5
- import regex as re
6
-
7
6
  from fmtr.tools.logging_tools import logger
8
7
 
9
8
 
@@ -158,7 +157,11 @@ class Rewriter:
158
157
 
159
158
  previous = new
160
159
 
161
- logger.debug(f'Finished rewriting: {get_history_str()}')
160
+ if len(history) == 1:
161
+ history_str = 'No rewrites performed.'
162
+ else:
163
+ history_str = get_history_str()
164
+ logger.debug(f'Finished rewriting: {history_str}')
162
165
 
163
166
  return previous
164
167
 
@@ -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.3
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: logfire; extra == "all"
132
- Requires-Dist: tokenizers; extra == "all"
133
- Requires-Dist: appdirs; extra == "all"
134
- Requires-Dist: pydantic-settings; extra == "all"
135
- Requires-Dist: filetype; extra == "all"
136
- Requires-Dist: pymupdf; extra == "all"
137
- Requires-Dist: json_repair; extra == "all"
138
- Requires-Dist: sentence_transformers; extra == "all"
139
- Requires-Dist: fastapi; extra == "all"
140
- Requires-Dist: tabulate; extra == "all"
141
- Requires-Dist: distributed; extra == "all"
142
- Requires-Dist: faker; extra == "all"
143
- Requires-Dist: semver; extra == "all"
144
- Requires-Dist: uvicorn[standard]; extra == "all"
145
- Requires-Dist: html2text; extra == "all"
146
- Requires-Dist: google-auth-httplib2; extra == "all"
147
- Requires-Dist: bokeh; extra == "all"
148
- Requires-Dist: flet-webview; extra == "all"
149
- Requires-Dist: logfire[httpx]; extra == "all"
150
131
  Requires-Dist: torchaudio; extra == "all"
151
- Requires-Dist: Unidecode; extra == "all"
152
- Requires-Dist: flet-video; extra == "all"
153
- Requires-Dist: google-auth; extra == "all"
154
- Requires-Dist: flet[all]; extra == "all"
155
- Requires-Dist: openai; extra == "all"
156
- Requires-Dist: pymupdf4llm; extra == "all"
157
- Requires-Dist: dnspython[doh]; extra == "all"
158
- Requires-Dist: deepmerge; extra == "all"
159
- Requires-Dist: contexttimer; extra == "all"
160
- Requires-Dist: openpyxl; extra == "all"
161
- Requires-Dist: tinynetrc; extra == "all"
162
- Requires-Dist: ollama; extra == "all"
163
- Requires-Dist: huggingface_hub; extra == "all"
164
132
  Requires-Dist: google-auth-oauthlib; extra == "all"
133
+ Requires-Dist: deepmerge; extra == "all"
165
134
  Requires-Dist: httpx_retries; extra == "all"
166
- Requires-Dist: docker; extra == "all"
167
- Requires-Dist: setuptools; extra == "all"
168
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
169
- Requires-Dist: google-api-python-client; extra == "all"
170
- Requires-Dist: torchvision; extra == "all"
171
- Requires-Dist: logfire[fastapi]; extra == "all"
172
- Requires-Dist: pandas; extra == "all"
173
135
  Requires-Dist: regex; extra == "all"
174
- Requires-Dist: httpx; extra == "all"
136
+ Requires-Dist: dask[bag]; extra == "all"
137
+ Requires-Dist: pytest-cov; extra == "all"
138
+ Requires-Dist: flet-video; 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"
175
143
  Requires-Dist: sre_yield; extra == "all"
144
+ Requires-Dist: google-auth-httplib2; extra == "all"
145
+ Requires-Dist: pymupdf4llm; extra == "all"
146
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
147
+ Requires-Dist: semver; extra == "all"
148
+ Requires-Dist: yamlscript; extra == "all"
149
+ Requires-Dist: torchvision; extra == "all"
150
+ Requires-Dist: pydevd-pycharm; extra == "all"
151
+ Requires-Dist: tinynetrc; extra == "all"
176
152
  Requires-Dist: pyyaml; extra == "all"
153
+ Requires-Dist: openai; extra == "all"
154
+ Requires-Dist: sentence_transformers; extra == "all"
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"
177
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"
178
168
  Requires-Dist: pydantic; extra == "all"
179
- Requires-Dist: pytest-cov; extra == "all"
180
- Requires-Dist: yamlscript; extra == "all"
169
+ Requires-Dist: dnspython[doh]; extra == "all"
170
+ Requires-Dist: openpyxl; extra == "all"
171
+ Requires-Dist: filetype; extra == "all"
172
+ Requires-Dist: fastapi; extra == "all"
173
+ Requires-Dist: faker; extra == "all"
181
174
  Requires-Dist: transformers[sentencepiece]; extra == "all"
182
- Requires-Dist: diskcache; extra == "all"
183
- Requires-Dist: dask[bag]; extra == "all"
184
- Requires-Dist: pydevd-pycharm; 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.3
File without changes
File without changes
@@ -15,60 +15,60 @@ pydantic-ai[logfire,openai]
15
15
  ollama
16
16
 
17
17
  [all]
18
- logfire
19
- tokenizers
20
- appdirs
21
- pydantic-settings
22
- filetype
23
- pymupdf
24
- json_repair
25
- sentence_transformers
26
- fastapi
27
- tabulate
28
- distributed
29
- faker
30
- semver
31
- uvicorn[standard]
32
- html2text
33
- google-auth-httplib2
34
- bokeh
35
- flet-webview
36
- logfire[httpx]
37
18
  torchaudio
38
- Unidecode
39
- flet-video
40
- google-auth
41
- flet[all]
42
- openai
43
- pymupdf4llm
44
- dnspython[doh]
45
- deepmerge
46
- contexttimer
47
- openpyxl
48
- tinynetrc
49
- ollama
50
- huggingface_hub
51
19
  google-auth-oauthlib
20
+ deepmerge
52
21
  httpx_retries
53
- docker
54
- setuptools
55
- pydantic-ai[logfire,openai]
56
- google-api-python-client
57
- torchvision
58
- logfire[fastapi]
59
- pandas
60
22
  regex
61
- httpx
23
+ dask[bag]
24
+ pytest-cov
25
+ flet-video
26
+ uvicorn[standard]
27
+ logfire[fastapi]
28
+ logfire
29
+ bokeh
62
30
  sre_yield
31
+ google-auth-httplib2
32
+ pymupdf4llm
33
+ pydantic-ai[logfire,openai]
34
+ semver
35
+ yamlscript
36
+ torchvision
37
+ pydevd-pycharm
38
+ tinynetrc
63
39
  pyyaml
40
+ openai
41
+ sentence_transformers
42
+ pymupdf
43
+ pydantic-settings
44
+ httpx
45
+ tabulate
46
+ setuptools
47
+ diskcache
48
+ distributed
49
+ huggingface_hub
50
+ flet-webview
64
51
  peft
52
+ google-api-python-client
53
+ contexttimer
54
+ docker
65
55
  pydantic
66
- pytest-cov
67
- yamlscript
56
+ dnspython[doh]
57
+ openpyxl
58
+ filetype
59
+ fastapi
60
+ faker
68
61
  transformers[sentencepiece]
69
- diskcache
70
- dask[bag]
71
- pydevd-pycharm
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