fmtr.tools 1.3.8__py3-none-any.whl → 1.3.9__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.

@@ -2,7 +2,9 @@ import cachetools
2
2
  from datetime import timedelta, datetime
3
3
  from diskcache import Cache
4
4
 
5
- from fmtr.tools import logger, Path, Constants
5
+ from fmtr.tools.constants import Constants
6
+ from fmtr.tools.logging_tools import logger
7
+ from fmtr.tools.path_tools.path_tools import Path
6
8
 
7
9
 
8
10
  class Dump(dict):
@@ -1,11 +1,12 @@
1
- from dataclasses import dataclass
2
- from functools import cached_property
3
- from typing import Self, Optional
4
-
5
1
  import dns
6
2
  import httpx
3
+ from dataclasses import dataclass
7
4
  from dns.message import Message, QueryMessage
8
5
  from dns.rrset import RRset
6
+ from functools import cached_property
7
+ from typing import Self, Optional
8
+
9
+ from fmtr.tools.string_tools import join
9
10
 
10
11
 
11
12
  @dataclass
@@ -25,7 +26,6 @@ class BaseDNSData:
25
26
  def from_message(cls, message: Message) -> Self:
26
27
  return cls(message.to_wire())
27
28
 
28
-
29
29
  @dataclass
30
30
  class Response(BaseDNSData):
31
31
  """
@@ -39,15 +39,41 @@ class Response(BaseDNSData):
39
39
 
40
40
  @classmethod
41
41
  def from_http(cls, response: httpx.Response) -> Self:
42
+ """
43
+
44
+ Initialise from an HTTP response.
45
+
46
+ """
42
47
  self = cls(response.content, http=response)
43
48
  return self
44
49
 
45
50
  @property
46
51
  def answer(self) -> Optional[RRset]:
52
+ """
53
+
54
+ Get the latest answer, if one exists.
55
+
56
+ """
47
57
  if not self.message.answer:
48
58
  return None
49
59
  return self.message.answer[-1]
50
60
 
61
+ def __str__(self):
62
+ """
63
+
64
+ Put answer and ID text in string representation.
65
+
66
+ """
67
+ answer = self.answer
68
+
69
+ if answer:
70
+ answer = join(answer.to_text().splitlines(), sep=', ')
71
+
72
+ string = join([answer, self.message.flags], sep=', ')
73
+ string = f'{self.__class__.__name__}({string})'
74
+ return string
75
+
76
+
51
77
 
52
78
  @dataclass
53
79
  class Request(BaseDNSData):
@@ -157,3 +183,13 @@ class Exchange:
157
183
  question_last = self.question_last
158
184
  query = dns.message.make_query(qname=question_last.name, rdclass=question_last.rdclass, rdtype=question_last.rdtype)
159
185
  return query
186
+
187
+ @property
188
+ def key(self):
189
+ """
190
+
191
+ Hashable key for caching
192
+
193
+ """
194
+ data = tuple(self.request.question.to_text().split())
195
+ return data
@@ -1,11 +1,11 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from fmtr.tools import logger
4
3
  from fmtr.tools.dns_tools import server, client
5
4
  from fmtr.tools.dns_tools.dm import Exchange
5
+ from fmtr.tools.logging_tools import logger
6
6
 
7
7
 
8
- @dataclass
8
+ @dataclass(kw_only=True, eq=False)
9
9
  class Proxy(server.Plain):
10
10
  """
11
11
 
@@ -31,7 +31,7 @@ class Proxy(server.Plain):
31
31
  """
32
32
  return
33
33
 
34
- def resolve(self, exchange: Exchange):
34
+ def resolve(self, exchange: Exchange) -> Exchange:
35
35
  """
36
36
 
37
37
  Resolve a request, processing each stage, initial question, upstream response etc.
@@ -39,29 +39,19 @@ class Proxy(server.Plain):
39
39
 
40
40
  """
41
41
 
42
- request = exchange.request
42
+ with logger.span(f'Processing question...'):
43
+ self.process_question(exchange)
44
+ if exchange.response.is_complete:
45
+ return exchange
43
46
 
44
- with logger.span(f'Handling request {request.message.id=} {request.question=} {exchange.client=}...'):
47
+ with logger.span(f'Making upstream request...'):
48
+ self.client.resolve(exchange)
49
+ if exchange.response.is_complete:
50
+ return exchange
45
51
 
46
- if not request.is_valid:
47
- raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
52
+ with logger.span(f'Processing upstream response...'):
53
+ self.process_upstream(exchange)
54
+ if exchange.response.is_complete:
55
+ return exchange
48
56
 
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...'):
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
- exchange.response.is_complete = True
65
-
66
- logger.info(f'Resolution complete {request.message.id=} {exchange.response.answer=}')
67
- return
57
+ return exchange
@@ -1,11 +1,14 @@
1
1
  import socket
2
2
  from dataclasses import dataclass
3
+ from datetime import timedelta
4
+ from functools import cached_property
3
5
 
6
+ from fmtr.tools import caching_tools as caching
4
7
  from fmtr.tools.dns_tools.dm import Exchange
5
8
  from fmtr.tools.logging_tools import logger
6
9
 
7
10
 
8
- @dataclass
11
+ @dataclass(kw_only=True, eq=False)
9
12
  class Plain:
10
13
  """
11
14
 
@@ -16,12 +19,19 @@ class Plain:
16
19
  host: str
17
20
  port: int
18
21
 
19
- def __post_init__(self):
22
+ @cached_property
23
+ def sock(self):
24
+ return socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
20
25
 
21
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
26
+ @cached_property
27
+ def cache(self):
28
+ """
22
29
 
23
- def resolve(self, exchange: Exchange):
24
- raise NotImplemented
30
+ Overridable cache.
31
+
32
+ """
33
+ cache = caching.TLRU(maxsize=1_024, ttu_static=timedelta(hours=1), desc='DNS Request')
34
+ return cache
25
35
 
26
36
  def start(self):
27
37
  """
@@ -35,5 +45,50 @@ class Plain:
35
45
  while True:
36
46
  data, (ip, port) = sock.recvfrom(512)
37
47
  exchange = Exchange.from_wire(data, ip=ip, port=port)
38
- self.resolve(exchange)
48
+ self.handle(exchange)
39
49
  sock.sendto(exchange.response.message.to_wire(), (ip, port))
50
+
51
+ def resolve(self, exchange: Exchange) -> Exchange:
52
+ """
53
+
54
+ Defined in subclasses
55
+
56
+ """
57
+ raise NotImplemented
58
+
59
+ def check_cache(self, exchange: Exchange):
60
+ """
61
+
62
+ Check cache, patch in in new ID and mark complete
63
+
64
+ """
65
+ if exchange.key in self.cache:
66
+ logger.info(f'Request found in cache.')
67
+ exchange.response = self.cache[exchange.key]
68
+ exchange.response.message.id = exchange.request.message.id
69
+ exchange.response.is_complete = True
70
+
71
+ def handle(self, exchange: Exchange):
72
+ """
73
+
74
+ Check validity of request, presence in cache and resolve.
75
+
76
+ """
77
+ request = exchange.request
78
+
79
+ if not request.is_valid:
80
+ raise ValueError(f'Only one question per request is supported. Got {len(request.question)} questions.')
81
+
82
+ with logger.span(f'Handling request {request.message.id=} {request.question=} {exchange.client=}...'):
83
+
84
+ with logger.span(f'Checking cache...'):
85
+ self.check_cache(exchange)
86
+
87
+ if not exchange.response.is_complete:
88
+ exchange = self.resolve(exchange)
89
+ exchange.response.is_complete = True
90
+
91
+ self.cache[exchange.key] = exchange.response
92
+
93
+ logger.info(f'Resolution complete {request.message.id=} {exchange.response.answer=}')
94
+ return exchange
@@ -92,7 +92,8 @@ class Item:
92
92
  source: Key
93
93
  target: Key
94
94
 
95
- @dataclass
95
+
96
+ @dataclass(kw_only=True)
96
97
  class Transformer:
97
98
  """
98
99
 
fmtr/tools/version CHANGED
@@ -1 +1 @@
1
- 1.3.8
1
+ 1.3.9
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmtr.tools
3
- Version: 1.3.8
3
+ Version: 1.3.9
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
@@ -131,61 +131,61 @@ Requires-Dist: logfire[httpx]; extra == "http"
131
131
  Provides-Extra: setup
132
132
  Requires-Dist: setuptools; extra == "setup"
133
133
  Provides-Extra: all
134
- Requires-Dist: html2text; extra == "all"
135
- Requires-Dist: appdirs; extra == "all"
136
- Requires-Dist: httpx_retries; extra == "all"
137
- Requires-Dist: diskcache; extra == "all"
138
- Requires-Dist: google-auth; extra == "all"
139
- Requires-Dist: docker; extra == "all"
140
- Requires-Dist: faker; extra == "all"
134
+ Requires-Dist: regex; extra == "all"
135
+ Requires-Dist: pyyaml; extra == "all"
136
+ Requires-Dist: pydevd-pycharm~=251.25410.159; extra == "all"
141
137
  Requires-Dist: pydantic-ai[logfire,openai]; extra == "all"
142
- Requires-Dist: sre_yield; extra == "all"
143
- Requires-Dist: fastapi; extra == "all"
144
- Requires-Dist: bokeh; extra == "all"
145
- Requires-Dist: huggingface_hub; extra == "all"
146
- Requires-Dist: pymupdf; extra == "all"
147
- Requires-Dist: google-api-python-client; extra == "all"
148
- Requires-Dist: pydantic; extra == "all"
149
138
  Requires-Dist: dnspython[doh]; extra == "all"
150
- Requires-Dist: pandas; extra == "all"
151
- Requires-Dist: yamlscript; extra == "all"
152
- Requires-Dist: openai; extra == "all"
153
- Requires-Dist: deepmerge; extra == "all"
154
- Requires-Dist: filetype; extra == "all"
155
- Requires-Dist: regex; extra == "all"
156
- Requires-Dist: peft; extra == "all"
157
- Requires-Dist: transformers[sentencepiece]; extra == "all"
158
- Requires-Dist: tinynetrc; extra == "all"
159
- Requires-Dist: flet-video; extra == "all"
160
- Requires-Dist: logfire; extra == "all"
161
139
  Requires-Dist: google-auth-oauthlib; extra == "all"
162
- Requires-Dist: logfire[fastapi]; extra == "all"
163
- Requires-Dist: httpx; extra == "all"
164
- Requires-Dist: pytest-cov; extra == "all"
165
- Requires-Dist: pydevd-pycharm~=251.25410.159; extra == "all"
166
- Requires-Dist: distributed; extra == "all"
167
- Requires-Dist: contexttimer; extra == "all"
168
140
  Requires-Dist: pymupdf4llm; extra == "all"
169
- Requires-Dist: setuptools; extra == "all"
141
+ Requires-Dist: docker; extra == "all"
142
+ Requires-Dist: distributed; extra == "all"
143
+ Requires-Dist: transformers[sentencepiece]; extra == "all"
144
+ Requires-Dist: yamlscript; extra == "all"
145
+ Requires-Dist: faker; extra == "all"
170
146
  Requires-Dist: uvicorn[standard]; extra == "all"
171
- Requires-Dist: logfire[httpx]; extra == "all"
172
- Requires-Dist: json_repair; extra == "all"
173
- Requires-Dist: pyyaml; extra == "all"
174
- Requires-Dist: openpyxl; extra == "all"
175
- Requires-Dist: semver; extra == "all"
176
- Requires-Dist: torchaudio; extra == "all"
177
- Requires-Dist: pydantic-settings; extra == "all"
178
- Requires-Dist: dask[bag]; extra == "all"
179
- Requires-Dist: tokenizers; extra == "all"
147
+ Requires-Dist: logfire; extra == "all"
180
148
  Requires-Dist: tabulate; extra == "all"
181
149
  Requires-Dist: cachetools; extra == "all"
150
+ Requires-Dist: pandas; extra == "all"
151
+ Requires-Dist: httpx; extra == "all"
152
+ Requires-Dist: httpx_retries; extra == "all"
153
+ Requires-Dist: deepmerge; extra == "all"
154
+ Requires-Dist: flet-video; extra == "all"
182
155
  Requires-Dist: torchvision; extra == "all"
183
- Requires-Dist: Unidecode; extra == "all"
184
- Requires-Dist: sentence_transformers; extra == "all"
185
- Requires-Dist: google-auth-httplib2; extra == "all"
186
156
  Requires-Dist: flet[all]; extra == "all"
187
157
  Requires-Dist: flet-webview; extra == "all"
158
+ Requires-Dist: bokeh; extra == "all"
159
+ Requires-Dist: torchaudio; extra == "all"
160
+ Requires-Dist: filetype; extra == "all"
161
+ Requires-Dist: Unidecode; extra == "all"
162
+ Requires-Dist: pytest-cov; extra == "all"
163
+ Requires-Dist: openpyxl; extra == "all"
164
+ Requires-Dist: pydantic-settings; extra == "all"
165
+ Requires-Dist: logfire[httpx]; extra == "all"
166
+ Requires-Dist: logfire[fastapi]; extra == "all"
167
+ Requires-Dist: openai; extra == "all"
188
168
  Requires-Dist: ollama; extra == "all"
169
+ Requires-Dist: google-api-python-client; extra == "all"
170
+ Requires-Dist: diskcache; extra == "all"
171
+ Requires-Dist: tokenizers; extra == "all"
172
+ Requires-Dist: fastapi; extra == "all"
173
+ Requires-Dist: sentence_transformers; extra == "all"
174
+ Requires-Dist: sre_yield; extra == "all"
175
+ Requires-Dist: peft; extra == "all"
176
+ Requires-Dist: huggingface_hub; extra == "all"
177
+ Requires-Dist: json_repair; extra == "all"
178
+ Requires-Dist: appdirs; extra == "all"
179
+ Requires-Dist: setuptools; extra == "all"
180
+ Requires-Dist: pydantic; extra == "all"
181
+ Requires-Dist: semver; extra == "all"
182
+ Requires-Dist: pymupdf; extra == "all"
183
+ Requires-Dist: html2text; extra == "all"
184
+ Requires-Dist: tinynetrc; extra == "all"
185
+ Requires-Dist: google-auth; extra == "all"
186
+ Requires-Dist: google-auth-httplib2; extra == "all"
187
+ Requires-Dist: dask[bag]; extra == "all"
188
+ Requires-Dist: contexttimer; extra == "all"
189
189
  Dynamic: author
190
190
  Dynamic: author-email
191
191
  Dynamic: description
@@ -2,7 +2,7 @@ fmtr/tools/__init__.py,sha256=vCxKR0SymygpO-EtCGPmGZEQv5O1goM7Jr8P4m-eJsQ,5676
2
2
  fmtr/tools/api_tools.py,sha256=w8Zrp_EwN5KlUghwLoTCXo4z1irg5tAsReCqDLjASfE,2133
3
3
  fmtr/tools/async_tools.py,sha256=ewz757WcveQJd-G5SVr2JDOQVbdLGecCgl-tsBGVZz4,284
4
4
  fmtr/tools/augmentation_tools.py,sha256=-6ESbO4CDlKqVOV1J1V6qBeoBMzbFIinkDHRHnCBej0,55
5
- fmtr/tools/caching_tools.py,sha256=BZ5HSzcyCSnvJ6h5xtu5jELPSPDazVsMU7q7OX_oKBE,4886
5
+ fmtr/tools/caching_tools.py,sha256=74p7m2GLFfeP41LX69wxgfkilxEAoWkMIfFMjKsYpyg,4976
6
6
  fmtr/tools/constants.py,sha256=QHJWu2UZwdV1ceGKT-pq-eYGPw4t7aFxb_W8r4G9VVk,1429
7
7
  fmtr/tools/data_modelling_tools.py,sha256=0BFm-F_cYzVTxftWQwORkPd0FM2BTLVh9-s0-rTTFoo,1744
8
8
  fmtr/tools/dataclass_tools.py,sha256=0Gt6KeLhtPgubo_2tYkIVqB8oQ91Qzag8OAGZDdjvMU,1209
@@ -30,7 +30,7 @@ fmtr/tools/netrc_tools.py,sha256=PpNpz_mWlQi6VHGromKwFfTyLpHUXsd4LY6-OKLCbeI,376
30
30
  fmtr/tools/openai_tools.py,sha256=6SUgejgzUzmlKKct2_ePXntvMegu3FJgfk9x7aqtqYc,742
31
31
  fmtr/tools/packaging_tools.py,sha256=FlgOTnDRHZWQL2iR-wucTsyGEHRE-MlddKL30MPmUqE,253
32
32
  fmtr/tools/parallel_tools.py,sha256=QEb_gN1StkxsqYaH4HSjiJX8Y3gpb2uKNsOzG4uFpaM,3071
33
- fmtr/tools/pattern_tools.py,sha256=Rmengjn_Ya246mlkAVR9fxVxXJUqEHWdDYu8Hsl9pXw,6003
33
+ fmtr/tools/pattern_tools.py,sha256=DlEKzNJKhwFmU3-awoGkN5Xy-yLF_bsoj8eoSMCEytE,6018
34
34
  fmtr/tools/pdf_tools.py,sha256=xvv9B84uAF81rFJRnXhSsxYuP42vY9ZdPVFrSMVe8G8,4069
35
35
  fmtr/tools/platform_tools.py,sha256=7p69CmAHe_sF68Fx9uVhns1k5EewTHTWgUYzkl6ZQKA,308
36
36
  fmtr/tools/process_tools.py,sha256=Ysh5Dk2QFBhXQerArjKdt7xZd3JrN5Ho02AaOjH0Nnw,1425
@@ -44,7 +44,7 @@ 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=TcdU53CiqvG9NX8A4sH_s0eyfztzmjd5EWmCXTCwpQE,5
47
+ fmtr/tools/version,sha256=rYr-rxNeNGGn5FNcTOuOuB3sW8Jcy6nF6Mog8vY8Ivg,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
@@ -52,9 +52,9 @@ fmtr/tools/ai_tools/agentic_tools.py,sha256=acSEPFS-aguDXanWGs3fAAlRyJSYPZW7L-Kb
52
52
  fmtr/tools/ai_tools/inference_tools.py,sha256=2UP2gXEyOJUjyyV6zmFIYmIxUsh1rXkRH0IbFvr2bRs,11908
53
53
  fmtr/tools/dns_tools/__init__.py,sha256=PjD3Og6D5yvDVpKmsUsrnSpz_rjXpl4zBtvMqm8xKWU,237
54
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=L0utKw8D1JXtAW9IfXcBdNMb-AyPFgW5XGiPnWuk7D8,1878
57
- fmtr/tools/dns_tools/server.py,sha256=t38TmSpra3FRoMk351eHzOOPlSuaCyjxbvPwX9o15ak,942
55
+ fmtr/tools/dns_tools/dm.py,sha256=QqEYebamLWMzr1KM1x4P4dUvPZL_zvymZrNj6ghw0rg,4445
56
+ fmtr/tools/dns_tools/proxy.py,sha256=b3TdSwRO7IwcNjrWg1e8jVQb-YxJhT377rdVkeDc8_I,1466
57
+ fmtr/tools/dns_tools/server.py,sha256=FK5S9r34aWapD3tv7rYG1dvHZrfCgg0hL9yGpZkZHkc,2623
58
58
  fmtr/tools/entrypoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  fmtr/tools/entrypoints/cache_hfh.py,sha256=fQNs4J9twQuZH_Yj98-oOvEX7-LrSUP3kO8nzw2HrHs,60
60
60
  fmtr/tools/entrypoints/ep_test.py,sha256=B8HfWISfSgw_xVX475CbJGh_QnpOe9MH65H8qGjTWbY,46
@@ -75,9 +75,9 @@ fmtr/tools/tests/test_environment.py,sha256=iHaiMQfECYZPkPKwfuIZV9uHuWe3aE-p_dN_
75
75
  fmtr/tools/tests/test_json.py,sha256=IeSP4ziPvRcmS8kq7k9tHonC9rN5YYq9GSNT2ul6Msk,287
76
76
  fmtr/tools/tests/test_path.py,sha256=AkZQa6_8BQ-VaCyL_J-iKmdf2ZaM-xFYR37Kun3k4_g,2188
77
77
  fmtr/tools/tests/test_yaml.py,sha256=jc0TwwKu9eC0LvFGNMERdgBue591xwLxYXFbtsRwXVM,287
78
- fmtr_tools-1.3.8.dist-info/licenses/LICENSE,sha256=FW9aa6vVN5IjRQWLT43hs4_koYSmpcbIovlKeAJ0_cI,10757
79
- fmtr_tools-1.3.8.dist-info/METADATA,sha256=m6mbZy-IYtew40xeEmDdyBYhxDLdfiLg_hhR_H2xMEc,16148
80
- fmtr_tools-1.3.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
81
- fmtr_tools-1.3.8.dist-info/entry_points.txt,sha256=h-r__Xh5njtFqreMLg6cGuTFS4Qh-QqJPU1HB-_BS-Q,357
82
- fmtr_tools-1.3.8.dist-info/top_level.txt,sha256=LXem9xCgNOD72tE2gRKESdiQTL902mfFkwWb6-dlwEE,5
83
- fmtr_tools-1.3.8.dist-info/RECORD,,
78
+ fmtr_tools-1.3.9.dist-info/licenses/LICENSE,sha256=FW9aa6vVN5IjRQWLT43hs4_koYSmpcbIovlKeAJ0_cI,10757
79
+ fmtr_tools-1.3.9.dist-info/METADATA,sha256=AD-iCYJ7Y80H55Q7R61JUA5NHnLPvuIGfD5PGA0zLcw,16148
80
+ fmtr_tools-1.3.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
81
+ fmtr_tools-1.3.9.dist-info/entry_points.txt,sha256=h-r__Xh5njtFqreMLg6cGuTFS4Qh-QqJPU1HB-_BS-Q,357
82
+ fmtr_tools-1.3.9.dist-info/top_level.txt,sha256=LXem9xCgNOD72tE2gRKESdiQTL902mfFkwWb6-dlwEE,5
83
+ fmtr_tools-1.3.9.dist-info/RECORD,,