fmtr.tools 1.3.1__py3-none-any.whl → 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  from fmtr.tools.import_tools import MissingExtraMockModule
2
2
 
3
3
  try:
4
- from fmtr.tools.dns_tools import server, client, dm
4
+ from fmtr.tools.dns_tools import server, client, dm, proxy
5
5
  except ImportError as exception:
6
- server = client = dm = MissingExtraMockModule('dns', exception)
6
+ server = client = dm = proxy = MissingExtraMockModule('dns', exception)
@@ -1,11 +1,12 @@
1
- import dns
2
1
  from dataclasses import dataclass
3
- from dns import query
4
2
  from functools import cached_property
3
+
4
+ import dns as dnspython
5
+ from dns import query
5
6
  from httpx_retries import Retry, RetryTransport
6
7
 
7
8
  from fmtr.tools import http_tools as http
8
- from fmtr.tools.dns_tools.dm import Exchange, Response, Request
9
+ from fmtr.tools.dns_tools.dm import Exchange, Response
9
10
  from fmtr.tools.logging_tools import logger
10
11
 
11
12
  RETRY_STRATEGY = Retry(
@@ -29,28 +30,36 @@ class HTTPClientDoH(http.Client):
29
30
  TRANSPORT = RetryTransport(retry=RETRY_STRATEGY)
30
31
 
31
32
 
32
- class ClientBasePlain:
33
- def __init__(self, host, port=53):
34
- self.host = host
35
- self.port = port
33
+ @dataclass
34
+ class Plain:
35
+ """
36
+
37
+ Plain DNS
38
+
39
+ """
40
+ host: str
41
+ port: int = 53
36
42
 
37
43
  def resolve(self, exchange: Exchange):
44
+
38
45
  with logger.span(f'UDP {self.host}:{self.port}'):
39
- response = query.udp(q=exchange.request.message, where=self.host, port=self.port)
40
- exchange.response_upstream = Response.from_message(response)
46
+ response_plain = query.udp(q=exchange.query_last, where=self.host, port=self.port)
47
+ response = Response.from_message(response_plain)
48
+
49
+ exchange.response.message.answer += response.message.answer
41
50
 
42
51
 
43
52
  @dataclass
44
- class ClientDoH:
53
+ class HTTP:
45
54
  """
46
55
 
47
- Base DoH client.
56
+ DNS over HTTP
48
57
 
49
58
  """
50
59
 
51
60
  HEADERS = {"Content-Type": "application/dns-message"}
52
61
  CLIENT = HTTPClientDoH()
53
- BOOTSTRAP = ClientBasePlain('8.8.8.8')
62
+ BOOTSTRAP = Plain('8.8.8.8')
54
63
 
55
64
  host: str
56
65
  url: str
@@ -58,11 +67,10 @@ class ClientDoH:
58
67
 
59
68
  @cached_property
60
69
  def ip(self):
61
- message = dns.message.make_query(self.host, dns.rdatatype.A, flags=0)
62
- request = Request.from_message(message)
63
- exchange = Exchange(request=request, ip=None, port=None)
70
+ message = dnspython.message.make_query(self.host, dnspython.rdatatype.A, flags=0)
71
+ exchange = Exchange.from_wire(message.to_wire(), ip=None, port=None)
64
72
  self.BOOTSTRAP.resolve(exchange)
65
- ip = next(iter(exchange.response_upstream.answer.items.keys())).address
73
+ ip = next(iter(exchange.response.answer.items.keys())).address
66
74
  return ip
67
75
 
68
76
  def resolve(self, exchange: Exchange):
@@ -71,10 +79,11 @@ class ClientDoH:
71
79
  Resolve via DoH
72
80
 
73
81
  """
74
- request = exchange.request
82
+
75
83
  headers = self.HEADERS | dict(Host=self.host)
76
84
  url = self.url.format(host=self.ip)
77
- response_doh = self.CLIENT.post(url, headers=headers, content=request.wire)
85
+ response_doh = self.CLIENT.post(url, headers=headers, content=exchange.query_last.to_wire())
78
86
  response_doh.raise_for_status()
79
87
  response = Response.from_http(response_doh)
80
- exchange.response_upstream = response
88
+
89
+ exchange.response.message.answer += response.message.answer
@@ -1,10 +1,12 @@
1
- import dns
2
- import httpx
3
1
  from dataclasses import dataclass
4
- from dns.message import Message
5
2
  from functools import cached_property
6
3
  from typing import Self, Optional
7
4
 
5
+ import dns
6
+ import httpx
7
+ from dns.message import Message, QueryMessage
8
+ from dns.rrset import RRset
9
+
8
10
 
9
11
  @dataclass
10
12
  class BaseDNSData:
@@ -33,14 +35,17 @@ class Response(BaseDNSData):
33
35
  """
34
36
 
35
37
  http: Optional[httpx.Response] = None
38
+ is_complete: bool = False
36
39
 
37
40
  @classmethod
38
41
  def from_http(cls, response: httpx.Response) -> Self:
39
42
  self = cls(response.content, http=response)
40
43
  return self
41
44
 
42
- @cached_property
43
- def answer(self):
45
+ @property
46
+ def answer(self) -> Optional[RRset]:
47
+ if not self.message.answer:
48
+ return None
44
49
  return self.message.answer[-1]
45
50
 
46
51
 
@@ -54,7 +59,7 @@ class Request(BaseDNSData):
54
59
  wire: bytes
55
60
 
56
61
  @cached_property
57
- def question(self):
62
+ def question(self) -> RRset:
58
63
  return self.message.question[0]
59
64
 
60
65
  @cached_property
@@ -77,10 +82,14 @@ class Request(BaseDNSData):
77
82
  def name_text(self):
78
83
  return self.name.to_text()
79
84
 
85
+ def get_response_template(self):
86
+ message = dns.message.make_response(self.message)
87
+ message.flags |= dns.flags.RA
88
+ return message
89
+
80
90
  @cached_property
81
91
  def blackhole(self) -> Response:
82
- blackhole = dns.message.make_response(self.message)
83
- blackhole.flags |= dns.flags.RA
92
+ blackhole = self.get_response_template()
84
93
  blackhole.set_rcode(dns.rcode.NXDOMAIN)
85
94
  response = Response.from_message(blackhole)
86
95
  return response
@@ -98,13 +107,53 @@ class Exchange:
98
107
 
99
108
  request: Request
100
109
  response: Optional[Response] = None
101
- response_upstream: Optional[Response] = None
110
+
102
111
 
103
112
  @classmethod
104
113
  def from_wire(cls, wire: bytes, ip: str, port: int) -> Self:
105
114
  request = Request(wire)
106
- return cls(request=request, ip=ip, port=port)
115
+ response = Response.from_message(request.get_response_template())
116
+
117
+ return cls(request=request, response=response, ip=ip, port=port)
107
118
 
108
119
  @cached_property
109
120
  def client(self):
110
121
  return f'{self.ip}:{self.port}'
122
+
123
+ @property
124
+ def question_last(self) -> RRset:
125
+ """
126
+
127
+ Contrive an RRset representing the latest/current question. This can be the original question - or a hybrid one if we've injected our own answers into the exchange.
128
+
129
+ """
130
+ if self.response.answer:
131
+ rrset = self.response.answer
132
+ ty = self.request.type
133
+ ttl = self.request.question.ttl
134
+ rdclass = self.request.question.rdclass
135
+ name = next(iter(rrset.items.keys())).to_text()
136
+
137
+ rrset_contrived = dns.rrset.from_text(
138
+ name=name,
139
+ ttl=ttl,
140
+ rdtype=ty,
141
+ rdclass=rdclass,
142
+
143
+ )
144
+
145
+ return rrset_contrived
146
+ else:
147
+ return self.request.question # Solves the issue of digging out the name.
148
+
149
+ @property
150
+ def query_last(self) -> QueryMessage:
151
+ """
152
+
153
+ Create a query (e.g. for use by upstream) based on the last question.
154
+
155
+ """
156
+
157
+ question_last = self.question_last
158
+ query = dns.message.make_query(qname=question_last.name, rdclass=question_last.rdclass, rdtype=question_last.rdtype)
159
+ return query
@@ -0,0 +1,68 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fmtr.tools import logger
4
+ from fmtr.tools.dns_tools import server, client
5
+ from fmtr.tools.dns_tools.dm import Exchange
6
+
7
+
8
+ @dataclass
9
+ class Proxy(server.Plain):
10
+ """
11
+
12
+ Base for a DNS Proxy server (plain server) TODO: Allow subclassing of any server type.
13
+
14
+ """
15
+
16
+ client: client.HTTP
17
+
18
+ def process_question(self, exchange: Exchange):
19
+ """
20
+
21
+ Modify exchange based on initial question.
22
+
23
+ """
24
+ return
25
+
26
+ def process_upstream(self, exchange: Exchange):
27
+ """
28
+
29
+ Modify exchange after upstream response.
30
+
31
+ """
32
+ return
33
+
34
+ def resolve(self, exchange: Exchange):
35
+ """
36
+
37
+ Resolve a request, processing each stage, initial question, upstream response etc.
38
+ Subclasses can override the relevant processing methods to implement custom behaviour.
39
+
40
+ """
41
+
42
+ request = exchange.request
43
+
44
+ with logger.span(f'Handling request ID {request.message.id} for {request.name_text} from {exchange.client}...'):
45
+
46
+ if not request.is_valid:
47
+ raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
48
+
49
+ with logger.span(f'Processing question...'):
50
+ self.process_question(exchange)
51
+ if exchange.response.is_complete:
52
+ return
53
+
54
+ with logger.span(f'Making upstream request for {request.name_text}...'):
55
+ self.client.resolve(exchange)
56
+ if exchange.response.is_complete:
57
+ return
58
+
59
+ with logger.span(f'Processing upstream response...'):
60
+ self.process_upstream(exchange)
61
+ if exchange.response.is_complete:
62
+ return
63
+
64
+ if exchange.response:
65
+ return
66
+
67
+ exchange.response.is_complete = True
68
+ return
@@ -1,13 +1,11 @@
1
1
  import socket
2
2
  from dataclasses import dataclass
3
3
 
4
- from fmtr.tools import logger
5
- from fmtr.tools.dns_tools.client import ClientDoH
6
4
  from fmtr.tools.dns_tools.dm import Exchange
7
5
 
8
6
 
9
7
  @dataclass
10
- class ServerBasePlain:
8
+ class Plain:
11
9
  """
12
10
 
13
11
  Base for starting a plain DNS server
@@ -37,53 +35,4 @@ class ServerBasePlain:
37
35
  data, (ip, port) = sock.recvfrom(512)
38
36
  exchange = Exchange.from_wire(data, ip=ip, port=port)
39
37
  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 ID {request.message.id} 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
38
+ sock.sendto(exchange.response.message.to_wire(), (ip, port))
fmtr/tools/version CHANGED
@@ -1 +1 @@
1
- 1.3.1
1
+ 1.3.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
5
5
  Home-page: https://github.com/fmtr/fmtr.tools
6
6
  Author: Frontmatter
@@ -130,60 +130,60 @@ Requires-Dist: logfire[httpx]; extra == "http"
130
130
  Provides-Extra: setup
131
131
  Requires-Dist: setuptools; extra == "setup"
132
132
  Provides-Extra: all
133
- Requires-Dist: openai; extra == "all"
134
133
  Requires-Dist: google-auth-oauthlib; extra == "all"
135
- Requires-Dist: torchvision; extra == "all"
136
- Requires-Dist: logfire[fastapi]; extra == "all"
137
- Requires-Dist: distributed; extra == "all"
138
- Requires-Dist: pymupdf4llm; extra == "all"
139
- Requires-Dist: pandas; extra == "all"
140
- Requires-Dist: tinynetrc; extra == "all"
141
- Requires-Dist: google-api-python-client; extra == "all"
142
- Requires-Dist: Unidecode; extra == "all"
143
- Requires-Dist: flet[all]; extra == "all"
144
- Requires-Dist: google-auth-httplib2; extra == "all"
145
- Requires-Dist: pydevd-pycharm; extra == "all"
134
+ Requires-Dist: dask[bag]; extra == "all"
146
135
  Requires-Dist: tabulate; extra == "all"
136
+ Requires-Dist: flet[all]; extra == "all"
137
+ Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
138
+ Requires-Dist: tinynetrc; extra == "all"
139
+ Requires-Dist: html2text; extra == "all"
140
+ Requires-Dist: deepmerge; extra == "all"
141
+ Requires-Dist: pytest-cov; extra == "all"
142
+ Requires-Dist: logfire[fastapi]; extra == "all"
143
+ Requires-Dist: dnspython[doh]; extra == "all"
144
+ Requires-Dist: setuptools; extra == "all"
145
+ Requires-Dist: peft; extra == "all"
146
+ Requires-Dist: filetype; extra == "all"
147
+ Requires-Dist: flet-webview; extra == "all"
148
+ Requires-Dist: flet-video; extra == "all"
147
149
  Requires-Dist: tokenizers; extra == "all"
148
- Requires-Dist: json_repair; extra == "all"
149
- Requires-Dist: yamlscript; extra == "all"
150
- Requires-Dist: dask[bag]; extra == "all"
151
- Requires-Dist: bokeh; extra == "all"
152
150
  Requires-Dist: sre_yield; extra == "all"
153
- Requires-Dist: dnspython[doh]; extra == "all"
154
- Requires-Dist: huggingface_hub; extra == "all"
155
- Requires-Dist: pyyaml; extra == "all"
156
- Requires-Dist: html2text; extra == "all"
157
- Requires-Dist: faker; extra == "all"
158
- Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
159
- Requires-Dist: pydantic-settings; extra == "all"
151
+ Requires-Dist: semver; extra == "all"
152
+ Requires-Dist: httpx; extra == "all"
153
+ Requires-Dist: google-auth; extra == "all"
160
154
  Requires-Dist: openpyxl; extra == "all"
161
- Requires-Dist: diskcache; extra == "all"
162
- Requires-Dist: httpx_retries; extra == "all"
163
- Requires-Dist: pymupdf; extra == "all"
155
+ Requires-Dist: pymupdf4llm; extra == "all"
164
156
  Requires-Dist: appdirs; extra == "all"
165
- Requires-Dist: flet-video; extra == "all"
166
- Requires-Dist: filetype; extra == "all"
167
- Requires-Dist: logfire[httpx]; extra == "all"
168
- Requires-Dist: torchaudio; extra == "all"
169
- Requires-Dist: pydantic; extra == "all"
157
+ Requires-Dist: pyyaml; extra == "all"
170
158
  Requires-Dist: uvicorn[standard]; extra == "all"
171
- Requires-Dist: contexttimer; extra == "all"
172
- Requires-Dist: setuptools; extra == "all"
173
- Requires-Dist: httpx; extra == "all"
159
+ Requires-Dist: httpx_retries; extra == "all"
160
+ Requires-Dist: pydantic; extra == "all"
174
161
  Requires-Dist: sentence_transformers; extra == "all"
175
- Requires-Dist: deepmerge; extra == "all"
176
- Requires-Dist: google-auth; extra == "all"
177
- Requires-Dist: regex; extra == "all"
178
- Requires-Dist: docker; extra == "all"
179
- Requires-Dist: semver; extra == "all"
180
- Requires-Dist: peft; extra == "all"
181
- Requires-Dist: logfire; extra == "all"
182
162
  Requires-Dist: ollama; extra == "all"
163
+ Requires-Dist: distributed; extra == "all"
164
+ Requires-Dist: huggingface_hub; extra == "all"
165
+ Requires-Dist: logfire[httpx]; extra == "all"
166
+ Requires-Dist: torchaudio; extra == "all"
167
+ Requires-Dist: docker; extra == "all"
168
+ Requires-Dist: openai; extra == "all"
169
+ Requires-Dist: yamlscript; extra == "all"
170
+ Requires-Dist: google-auth-httplib2; extra == "all"
171
+ Requires-Dist: Unidecode; extra == "all"
183
172
  Requires-Dist: transformers[sentencepiece]; extra == "all"
184
- Requires-Dist: pytest-cov; extra == "all"
185
- Requires-Dist: flet-webview; extra == "all"
173
+ Requires-Dist: pandas; extra == "all"
174
+ Requires-Dist: bokeh; extra == "all"
175
+ Requires-Dist: diskcache; extra == "all"
176
+ Requires-Dist: json_repair; extra == "all"
177
+ Requires-Dist: logfire; extra == "all"
178
+ Requires-Dist: regex; extra == "all"
186
179
  Requires-Dist: fastapi; extra == "all"
180
+ Requires-Dist: contexttimer; extra == "all"
181
+ Requires-Dist: pydevd-pycharm; extra == "all"
182
+ Requires-Dist: faker; extra == "all"
183
+ Requires-Dist: pymupdf; extra == "all"
184
+ Requires-Dist: google-api-python-client; extra == "all"
185
+ Requires-Dist: torchvision; extra == "all"
186
+ Requires-Dist: pydantic-settings; extra == "all"
187
187
  Dynamic: author
188
188
  Dynamic: author-email
189
189
  Dynamic: description
@@ -44,16 +44,17 @@ fmtr/tools/tabular_tools.py,sha256=tpIpZzYku1HcJrHZJL6BC39LmN3WUWVhFbK2N7nDVmE,1
44
44
  fmtr/tools/tokenization_tools.py,sha256=me-IBzSLyNYejLybwjO9CNB6Mj2NYfKPaOVThXyaGNg,4268
45
45
  fmtr/tools/tools.py,sha256=CAsApa1YwVdNE6H66Vjivs_mXYvOas3rh7fPELAnTpk,795
46
46
  fmtr/tools/unicode_tools.py,sha256=yS_9wpu8ogNoiIL7s1G_8bETFFO_YQlo4LNPv1NLDeY,52
47
- fmtr/tools/version,sha256=BcZMtqlHZBH1wb4kRct-FjtXUmLPdsHBFjSDNdT5aG4,5
47
+ fmtr/tools/version,sha256=exPSrztkeQgMZ8VTDFGwb8f-z8T_NbtvtmOtm7IiqvA,5
48
48
  fmtr/tools/version_tools.py,sha256=yNs_CGqWpqE4jbK9wsPIi14peJVXYbhIcMqHAFOw3yE,1480
49
49
  fmtr/tools/yaml_tools.py,sha256=9kuYChqJelWQIjGlSnK4iDdOWWH06P0gp9jIcRrC3UI,1903
50
50
  fmtr/tools/ai_tools/__init__.py,sha256=JZrLuOFNV1A3wvJgonxOgz_4WS-7MfCuowGWA5uYCjs,372
51
51
  fmtr/tools/ai_tools/agentic_tools.py,sha256=acSEPFS-aguDXanWGs3fAAlRyJSYPZW7L-Kb2qDLm-I,4300
52
52
  fmtr/tools/ai_tools/inference_tools.py,sha256=2UP2gXEyOJUjyyV6zmFIYmIxUsh1rXkRH0IbFvr2bRs,11908
53
- fmtr/tools/dns_tools/__init__.py,sha256=PwHxnpiy6_isQfUmz_5V1hL0dcPaA6ItqvoGWW8MOfk,222
54
- fmtr/tools/dns_tools/client.py,sha256=omHdk9bA_8u2-VMQhh0c9r9e-oG4mZ0MWA9lfbtSEIc,2371
55
- fmtr/tools/dns_tools/dm.py,sha256=mvXacq6QJ86G0S0tkzJFhU7bOaSJytvsMNlxs5X9hfE,2236
56
- fmtr/tools/dns_tools/server.py,sha256=X6-4pC8u5BG_BnOHTPcmkT3455Cd10CddwrDUYrGUOw,2353
53
+ fmtr/tools/dns_tools/__init__.py,sha256=PjD3Og6D5yvDVpKmsUsrnSpz_rjXpl4zBtvMqm8xKWU,237
54
+ fmtr/tools/dns_tools/client.py,sha256=c2vzWBDZSxijwL1KvWUoDGc8wqk_KTAFxCr0P1rNjy8,2367
55
+ fmtr/tools/dns_tools/dm.py,sha256=KWQeeZhsDyrKoXzAD5zEOoHH3aiD4uKRqwnD8fFP1nI,3725
56
+ fmtr/tools/dns_tools/proxy.py,sha256=0TULVnD3FlZNAsNINy8bcbimTNvmkwvzXeMnOkdUqvw,1879
57
+ fmtr/tools/dns_tools/server.py,sha256=0RLsyVH8C-15PAJgqMNqxbHTPbTWkq_thh8nnS1clAg,892
57
58
  fmtr/tools/entrypoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
59
  fmtr/tools/entrypoints/cache_hfh.py,sha256=fQNs4J9twQuZH_Yj98-oOvEX7-LrSUP3kO8nzw2HrHs,60
59
60
  fmtr/tools/entrypoints/ep_test.py,sha256=B8HfWISfSgw_xVX475CbJGh_QnpOe9MH65H8qGjTWbY,46
@@ -73,9 +74,9 @@ fmtr/tools/tests/test_environment.py,sha256=iHaiMQfECYZPkPKwfuIZV9uHuWe3aE-p_dN_
73
74
  fmtr/tools/tests/test_json.py,sha256=IeSP4ziPvRcmS8kq7k9tHonC9rN5YYq9GSNT2ul6Msk,287
74
75
  fmtr/tools/tests/test_path.py,sha256=AkZQa6_8BQ-VaCyL_J-iKmdf2ZaM-xFYR37Kun3k4_g,2188
75
76
  fmtr/tools/tests/test_yaml.py,sha256=jc0TwwKu9eC0LvFGNMERdgBue591xwLxYXFbtsRwXVM,287
76
- fmtr_tools-1.3.1.dist-info/licenses/LICENSE,sha256=FW9aa6vVN5IjRQWLT43hs4_koYSmpcbIovlKeAJ0_cI,10757
77
- fmtr_tools-1.3.1.dist-info/METADATA,sha256=GGUnLRCnkgzFvhy9nRRjxMAP76MrO7APhIMdueIst2Q,16030
78
- fmtr_tools-1.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
- fmtr_tools-1.3.1.dist-info/entry_points.txt,sha256=fSQrDGNctdQXbUxpMWYVfVQ0mhZMDyaEDG3D3a0zOSc,278
80
- fmtr_tools-1.3.1.dist-info/top_level.txt,sha256=LXem9xCgNOD72tE2gRKESdiQTL902mfFkwWb6-dlwEE,5
81
- fmtr_tools-1.3.1.dist-info/RECORD,,
77
+ fmtr_tools-1.3.2.dist-info/licenses/LICENSE,sha256=FW9aa6vVN5IjRQWLT43hs4_koYSmpcbIovlKeAJ0_cI,10757
78
+ fmtr_tools-1.3.2.dist-info/METADATA,sha256=KlWOZL7_0eJCCM3bZtnujinv89YPLmtU4LcDg7JZx9I,16030
79
+ fmtr_tools-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
+ fmtr_tools-1.3.2.dist-info/entry_points.txt,sha256=fSQrDGNctdQXbUxpMWYVfVQ0mhZMDyaEDG3D3a0zOSc,278
81
+ fmtr_tools-1.3.2.dist-info/top_level.txt,sha256=LXem9xCgNOD72tE2gRKESdiQTL902mfFkwWb6-dlwEE,5
82
+ fmtr_tools-1.3.2.dist-info/RECORD,,