python-utils 3.8.1__py2.py3-none-any.whl → 3.9.0__py2.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.
python_utils/time.py CHANGED
@@ -1,3 +1,23 @@
1
+ """
2
+ This module provides utility functions for handling time-related operations.
3
+
4
+ Functions:
5
+ - timedelta_to_seconds: Convert a timedelta to seconds with microseconds as
6
+ fraction.
7
+ - delta_to_seconds: Convert a timedelta or numeric interval to seconds.
8
+ - delta_to_seconds_or_none: Convert a timedelta to seconds or return None.
9
+ - format_time: Format a timestamp (timedelta, datetime, or seconds) to a
10
+ string.
11
+ - timeout_generator: Generate items from an iterable until a timeout is
12
+ reached.
13
+ - aio_timeout_generator: Asynchronously generate items from an iterable until a
14
+ timeout is reached.
15
+ - aio_generator_timeout_detector: Detect if an async generator has not yielded
16
+ an element for a set amount of time.
17
+ - aio_generator_timeout_detector_decorator: Decorator for
18
+ aio_generator_timeout_detector.
19
+ """
20
+
1
21
  # pyright: reportUnnecessaryIsInstance=false
2
22
  import asyncio
3
23
  import datetime
@@ -18,7 +38,7 @@ epoch = datetime.datetime(year=1970, month=1, day=1)
18
38
 
19
39
 
20
40
  def timedelta_to_seconds(delta: datetime.timedelta) -> types.Number:
21
- '''Convert a timedelta to seconds with the microseconds as fraction
41
+ """Convert a timedelta to seconds with the microseconds as fraction.
22
42
 
23
43
  Note that this method has become largely obsolete with the
24
44
  `timedelta.total_seconds()` method introduced in Python 2.7.
@@ -32,7 +52,7 @@ def timedelta_to_seconds(delta: datetime.timedelta) -> types.Number:
32
52
  '1.000001'
33
53
  >>> '%.6f' % timedelta_to_seconds(timedelta(microseconds=1))
34
54
  '0.000001'
35
- '''
55
+ """
36
56
  # Only convert to float if needed
37
57
  if delta.microseconds:
38
58
  total = delta.microseconds * 1e-6
@@ -43,9 +63,9 @@ def timedelta_to_seconds(delta: datetime.timedelta) -> types.Number:
43
63
  return total
44
64
 
45
65
 
46
- def delta_to_seconds(interval: types.delta_type) -> float:
47
- '''
48
- Convert a timedelta to seconds
66
+ def delta_to_seconds(interval: types.delta_type) -> types.Number:
67
+ """
68
+ Convert a timedelta to seconds.
49
69
 
50
70
  >>> delta_to_seconds(datetime.timedelta(seconds=1))
51
71
  1
@@ -57,18 +77,19 @@ def delta_to_seconds(interval: types.delta_type) -> float:
57
77
  Traceback (most recent call last):
58
78
  ...
59
79
  TypeError: Unknown type ...
60
- '''
80
+ """
61
81
  if isinstance(interval, datetime.timedelta):
62
82
  return timedelta_to_seconds(interval)
63
83
  elif isinstance(interval, (int, float)):
64
84
  return interval
65
85
  else:
66
- raise TypeError('Unknown type %s: %r' % (type(interval), interval))
86
+ raise TypeError(f'Unknown type {type(interval)}: {interval!r}')
67
87
 
68
88
 
69
89
  def delta_to_seconds_or_none(
70
90
  interval: types.Optional[types.delta_type],
71
- ) -> types.Optional[float]:
91
+ ) -> types.Optional[types.Number]:
92
+ """Convert a timedelta to seconds or return None."""
72
93
  if interval is None:
73
94
  return None
74
95
  else:
@@ -79,7 +100,7 @@ def format_time(
79
100
  timestamp: types.timestamp_type,
80
101
  precision: datetime.timedelta = datetime.timedelta(seconds=1),
81
102
  ) -> str:
82
- '''Formats timedelta/datetime/seconds
103
+ """Formats timedelta/datetime/seconds.
83
104
 
84
105
  >>> format_time('1')
85
106
  '0:00:01'
@@ -100,7 +121,7 @@ def format_time(
100
121
  ...
101
122
  TypeError: Unknown type ...
102
123
 
103
- '''
124
+ """
104
125
  precision_seconds = precision.total_seconds()
105
126
 
106
127
  if isinstance(timestamp, str):
@@ -130,7 +151,7 @@ def format_time(
130
151
 
131
152
  try: # pragma: no cover
132
153
  dt = datetime.datetime.fromtimestamp(seconds)
133
- except ValueError: # pragma: no cover
154
+ except (ValueError, OSError): # pragma: no cover
134
155
  dt = datetime.datetime.max
135
156
  return str(dt)
136
157
  elif isinstance(timestamp, datetime.date):
@@ -138,7 +159,38 @@ def format_time(
138
159
  elif timestamp is None:
139
160
  return '--:--:--'
140
161
  else:
141
- raise TypeError('Unknown type %s: %r' % (type(timestamp), timestamp))
162
+ raise TypeError(f'Unknown type {type(timestamp)}: {timestamp!r}')
163
+
164
+
165
+ @types.overload
166
+ def _to_iterable(
167
+ iterable: types.Union[
168
+ types.Callable[[], types.AsyncIterable[_T]],
169
+ types.AsyncIterable[_T],
170
+ ],
171
+ ) -> types.AsyncIterable[_T]: ...
172
+
173
+
174
+ @types.overload
175
+ def _to_iterable(
176
+ iterable: types.Union[
177
+ types.Callable[[], types.Iterable[_T]], types.Iterable[_T]
178
+ ],
179
+ ) -> types.Iterable[_T]: ...
180
+
181
+
182
+ def _to_iterable(
183
+ iterable: types.Union[
184
+ types.Iterable[_T],
185
+ types.Callable[[], types.Iterable[_T]],
186
+ types.AsyncIterable[_T],
187
+ types.Callable[[], types.AsyncIterable[_T]],
188
+ ],
189
+ ) -> types.Union[types.Iterable[_T], types.AsyncIterable[_T]]:
190
+ if callable(iterable):
191
+ return iterable()
192
+ else:
193
+ return iterable
142
194
 
143
195
 
144
196
  def timeout_generator(
@@ -146,16 +198,21 @@ def timeout_generator(
146
198
  interval: types.delta_type = datetime.timedelta(seconds=1),
147
199
  iterable: types.Union[
148
200
  types.Iterable[_T], types.Callable[[], types.Iterable[_T]]
149
- ] = itertools.count, # type: ignore
201
+ ] = itertools.count, # type: ignore[assignment]
150
202
  interval_multiplier: float = 1.0,
151
203
  maximum_interval: types.Optional[types.delta_type] = None,
152
- ):
153
- '''
204
+ ) -> types.Iterable[_T]:
205
+ """
154
206
  Generator that walks through the given iterable (a counter by default)
155
207
  until the float_timeout is reached with a configurable float_interval
156
- between items
208
+ between items.
209
+
210
+ This can be used to limit the time spent on a slow operation. This can be
211
+ useful for testing slow APIs so you get a small sample of the data in a
212
+ reasonable amount of time.
157
213
 
158
214
  >>> for i in timeout_generator(0.1, 0.06):
215
+ ... # Put your slow code here
159
216
  ... print(i)
160
217
  0
161
218
  1
@@ -179,20 +236,14 @@ def timeout_generator(
179
236
  0
180
237
  1
181
238
  2
182
- '''
183
- float_timeout: float = delta_to_seconds(timeout)
239
+ """
184
240
  float_interval: float = delta_to_seconds(interval)
185
241
  float_maximum_interval: types.Optional[float] = delta_to_seconds_or_none(
186
242
  maximum_interval
187
243
  )
244
+ iterable_ = _to_iterable(iterable)
188
245
 
189
- iterable_: types.Iterable[_T]
190
- if callable(iterable):
191
- iterable_ = iterable()
192
- else:
193
- iterable_ = iterable
194
-
195
- end = float_timeout + time.perf_counter()
246
+ end = delta_to_seconds(timeout) + time.perf_counter()
196
247
  for item in iterable_:
197
248
  yield item
198
249
 
@@ -201,13 +252,13 @@ def timeout_generator(
201
252
 
202
253
  time.sleep(float_interval)
203
254
 
204
- interval *= interval_multiplier
255
+ float_interval *= interval_multiplier
205
256
  if float_maximum_interval:
206
257
  float_interval = min(float_interval, float_maximum_interval)
207
258
 
208
259
 
209
260
  async def aio_timeout_generator(
210
- timeout: types.delta_type,
261
+ timeout: types.delta_type, # noqa: ASYNC109
211
262
  interval: types.delta_type = datetime.timedelta(seconds=1),
212
263
  iterable: types.Union[
213
264
  types.AsyncIterable[_T], types.Callable[..., types.AsyncIterable[_T]]
@@ -215,10 +266,10 @@ async def aio_timeout_generator(
215
266
  interval_multiplier: float = 1.0,
216
267
  maximum_interval: types.Optional[types.delta_type] = None,
217
268
  ) -> types.AsyncGenerator[_T, None]:
218
- '''
269
+ """
219
270
  Async generator that walks through the given async iterable (a counter by
220
271
  default) until the float_timeout is reached with a configurable
221
- float_interval between items
272
+ float_interval between items.
222
273
 
223
274
  The interval_exponent automatically increases the float_timeout with each
224
275
  run. Note that if the float_interval is less than 1, 1/interval_exponent
@@ -228,20 +279,14 @@ async def aio_timeout_generator(
228
279
  Doctests and asyncio are not friends, so no examples. But this function is
229
280
  effectively the same as the `timeout_generator` but it uses `async for`
230
281
  instead.
231
- '''
232
- float_timeout: float = delta_to_seconds(timeout)
282
+ """
233
283
  float_interval: float = delta_to_seconds(interval)
234
284
  float_maximum_interval: types.Optional[float] = delta_to_seconds_or_none(
235
285
  maximum_interval
236
286
  )
287
+ iterable_ = _to_iterable(iterable)
237
288
 
238
- iterable_: types.AsyncIterable[_T]
239
- if callable(iterable):
240
- iterable_ = iterable()
241
- else:
242
- iterable_ = iterable
243
-
244
- end = float_timeout + time.perf_counter()
289
+ end = delta_to_seconds(timeout) + time.perf_counter()
245
290
  async for item in iterable_: # pragma: no branch
246
291
  yield item
247
292
 
@@ -257,7 +302,7 @@ async def aio_timeout_generator(
257
302
 
258
303
  async def aio_generator_timeout_detector(
259
304
  generator: types.AsyncGenerator[_T, None],
260
- timeout: types.Optional[types.delta_type] = None,
305
+ timeout: types.Optional[types.delta_type] = None, # noqa: ASYNC109
261
306
  total_timeout: types.Optional[types.delta_type] = None,
262
307
  on_timeout: types.Optional[
263
308
  types.Callable[
@@ -272,7 +317,7 @@ async def aio_generator_timeout_detector(
272
317
  ] = exceptions.reraise,
273
318
  **on_timeout_kwargs: types.Mapping[types.Text, types.Any],
274
319
  ) -> types.AsyncGenerator[_T, None]:
275
- '''
320
+ """
276
321
  This function is used to detect if an asyncio generator has not yielded
277
322
  an element for a set amount of time.
278
323
 
@@ -282,7 +327,7 @@ async def aio_generator_timeout_detector(
282
327
  If `on_timeout` is not specified, the exception is reraised.
283
328
  If `on_timeout` is `None`, the exception is silently ignored and the
284
329
  generator will finish as normal.
285
- '''
330
+ """
286
331
  if total_timeout is None:
287
332
  total_timeout_end = None
288
333
  else:
@@ -295,14 +340,16 @@ async def aio_generator_timeout_detector(
295
340
  while True:
296
341
  try:
297
342
  if total_timeout_end and time.perf_counter() >= total_timeout_end:
298
- raise asyncio.TimeoutError('Total timeout reached')
343
+ raise asyncio.TimeoutError( # noqa: TRY301
344
+ 'Total timeout reached'
345
+ )
299
346
 
300
347
  if timeout_s:
301
348
  yield await asyncio.wait_for(generator.__anext__(), timeout_s)
302
349
  else:
303
350
  yield await generator.__anext__()
304
351
 
305
- except asyncio.TimeoutError as exception:
352
+ except asyncio.TimeoutError as exception: # noqa: PERF203
306
353
  if on_timeout is not None:
307
354
  await on_timeout(
308
355
  generator,
@@ -332,21 +379,21 @@ def aio_generator_timeout_detector_decorator(
332
379
  ]
333
380
  ] = exceptions.reraise,
334
381
  **on_timeout_kwargs: types.Mapping[types.Text, types.Any],
335
- ):
336
- '''
337
- A decorator wrapper for aio_generator_timeout_detector.
338
- '''
382
+ ) -> types.Callable[
383
+ [types.Callable[_P, types.AsyncGenerator[_T, None]]],
384
+ types.Callable[_P, types.AsyncGenerator[_T, None]],
385
+ ]:
386
+ """A decorator wrapper for aio_generator_timeout_detector."""
339
387
 
340
388
  def _timeout_detector_decorator(
341
- generator: types.Callable[_P, types.AsyncGenerator[_T, None]]
389
+ generator: types.Callable[_P, types.AsyncGenerator[_T, None]],
342
390
  ) -> types.Callable[_P, types.AsyncGenerator[_T, None]]:
343
- '''
344
- The decorator itself.
345
- '''
391
+ """The decorator itself."""
346
392
 
347
393
  @functools.wraps(generator)
348
394
  def wrapper(
349
- *args: _P.args, **kwargs: _P.kwargs
395
+ *args: _P.args,
396
+ **kwargs: _P.kwargs,
350
397
  ) -> types.AsyncGenerator[_T, None]:
351
398
  return aio_generator_timeout_detector(
352
399
  generator(*args, **kwargs),
python_utils/types.py CHANGED
@@ -1,21 +1,38 @@
1
+ """
2
+ This module provides type definitions and utility functions for type hinting.
3
+
4
+ It includes:
5
+ - Shorthand for commonly used types such as Optional and Union.
6
+ - Type aliases for various data structures and common types.
7
+ - Importing all types from the `typing` and `typing_extensions` modules.
8
+ - Importing specific types from the `types` module.
9
+
10
+ The module also configures Pyright to ignore wildcard import warnings.
11
+ """
1
12
  # pyright: reportWildcardImportFromLibrary=false
13
+ # ruff: noqa: F405
14
+
2
15
  import datetime
3
16
  import decimal
4
- from typing_extensions import * # type: ignore # noqa: F403
5
- from typing import * # type: ignore # pragma: no cover # noqa: F403
6
- from types import * # type: ignore # pragma: no cover # noqa: F403
17
+ from re import Match, Pattern
18
+ from types import * # pragma: no cover # noqa: F403
19
+ from typing import * # pragma: no cover # noqa: F403
7
20
 
8
21
  # import * does not import these in all Python versions
9
- from typing import Pattern, BinaryIO, IO, TextIO, Match
10
-
11
22
  # Quickhand for optional because it gets so much use. If only Python had
12
23
  # support for an optional type shorthand such as `SomeType?` instead of
13
24
  # `Optional[SomeType]`.
14
- from typing import Optional as O # noqa
15
-
16
25
  # Since the Union operator is only supported for Python 3.10, we'll create a
17
26
  # shorthand for it.
18
- from typing import Union as U # noqa
27
+ from typing import (
28
+ IO,
29
+ BinaryIO,
30
+ Optional as O, # noqa: N817
31
+ TextIO,
32
+ Union as U, # noqa: N817
33
+ )
34
+
35
+ from typing_extensions import * # type: ignore[no-redef,assignment] # noqa: F403
19
36
 
20
37
  Scope = Dict[str, Any]
21
38
  OptionalScope = O[Scope]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-utils
3
- Version: 3.8.1
3
+ Version: 3.9.0
4
4
  Summary: Python Utils is a module with some convenient utilities not included with the standard Python install
5
5
  Home-page: https://github.com/WoLpH/python-utils
6
6
  Author: Rick van Hattem
7
7
  Author-email: Wolph@wol.ph
8
8
  License: BSD
9
9
  Classifier: License :: OSI Approved :: BSD License
10
- Requires-Python: >3.8.0
10
+ Requires-Python: >3.9.0
11
11
  License-File: LICENSE
12
12
  Requires-Dist: typing-extensions >3.10.0.2
13
13
  Provides-Extra: docs
@@ -17,7 +17,8 @@ Requires-Dist: python-utils ; extra == 'docs'
17
17
  Provides-Extra: loguru
18
18
  Requires-Dist: loguru ; extra == 'loguru'
19
19
  Provides-Extra: tests
20
- Requires-Dist: flake8 ; extra == 'tests'
20
+ Requires-Dist: ruff ; extra == 'tests'
21
+ Requires-Dist: pyright ; extra == 'tests'
21
22
  Requires-Dist: pytest ; extra == 'tests'
22
23
  Requires-Dist: pytest-cov ; extra == 'tests'
23
24
  Requires-Dist: pytest-mypy ; extra == 'tests'
@@ -25,6 +26,9 @@ Requires-Dist: pytest-asyncio ; extra == 'tests'
25
26
  Requires-Dist: sphinx ; extra == 'tests'
26
27
  Requires-Dist: types-setuptools ; extra == 'tests'
27
28
  Requires-Dist: loguru ; extra == 'tests'
29
+ Requires-Dist: loguru-mypy ; extra == 'tests'
30
+ Requires-Dist: mypy-ipython ; extra == 'tests'
31
+ Requires-Dist: blessings ; extra == 'tests'
28
32
 
29
33
  Useful Python Utils
30
34
  ==============================================================================
@@ -0,0 +1,21 @@
1
+ python_utils/__about__.py,sha256=1Hb1SMxdrOMubt1ZyHT01jFkGgI-E1kDZt4R0W9_YME,804
2
+ python_utils/__init__.py,sha256=4s6dczKPdx5mI-cUFb_8GNP3XSlVKCaXkuW1zU2bSl0,2490
3
+ python_utils/aio.py,sha256=9xoMe_MEtDErbEBBi8_7jVw54-zY6wv1hgGXzEL4_ww,2923
4
+ python_utils/containers.py,sha256=SNHXfDvbt5MNL4WZc9Nj7JxyAfEV5cHba6VR5pH-K2Y,19099
5
+ python_utils/converters.py,sha256=HyvQZzIFXuVdwGl20woiW_X_ri5PagULJOqV4jcjKUY,13906
6
+ python_utils/decorators.py,sha256=60uq2L-2vME6TZ5AquCDrbMnFgDlZN-gQvJ8ak8kxbY,5991
7
+ python_utils/exceptions.py,sha256=KSGLr1M3IolDaM_fp5iTsC8pek2O8lOpjIpc_5gWJnw,1135
8
+ python_utils/formatters.py,sha256=5LqSlVAQSZQRzqUe84aYuePZS8ozp5AwS7mOaim1ZSU,5702
9
+ python_utils/generators.py,sha256=aR9iuUZcHGElSScTtLrBMBEd1-oYgG64UMyispDwNYY,3808
10
+ python_utils/import_.py,sha256=RO_zcslYCq19OopzZtqSugdFF5S0sP-s5FScpfjNEN4,3838
11
+ python_utils/logger.py,sha256=BD7W1R9jEBO4_Ik28mPG5AgajOFBkJDPqJfQQQNtSn8,9984
12
+ python_utils/loguru.py,sha256=oRuG6CHR5UvrvSdLoqrvwJLBqBbDWXZ4bvvI7Xoufh0,1357
13
+ python_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ python_utils/terminal.py,sha256=iv72wsIHjqX7g18qpdbn56aGHVY4WHCRiv7O6nYdCTs,5519
15
+ python_utils/time.py,sha256=Ad44NddQQYMNJnpvzeqIHfu-SIGgUoyUsTNJK3kkwkY,13135
16
+ python_utils/types.py,sha256=nV_6YvNAVaTwIZHKO_G2mBo7qsyoRQ1a0vuZtR0x-SI,4096
17
+ python_utils-3.9.0.dist-info/LICENSE,sha256=_Zrs9NZu-G6_aZT2g3Jv5_JREJQfhRNBC61EH6WDH-o,1501
18
+ python_utils-3.9.0.dist-info/METADATA,sha256=4fEmiraBMeIaaLopsSPUVIW1pmba9nrUhr9nPpMoRVw,9827
19
+ python_utils-3.9.0.dist-info/WHEEL,sha256=AHX6tWk3qWuce7vKLrj7lnulVHEdWoltgauo8bgCXgU,109
20
+ python_utils-3.9.0.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
21
+ python_utils-3.9.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
python_utils/compat.py DELETED
File without changes
@@ -1,22 +0,0 @@
1
- python_utils/__about__.py,sha256=B-Z6aSb4uA_SEoLmYE5XeTDruTNa5wLWBsMuOIQthSk,385
2
- python_utils/__init__.py,sha256=HGz7pZnS6QesK0SomLOgHnA2ldA8yFZoV3m8qwlPBks,1705
3
- python_utils/aio.py,sha256=HCcfWm82qzQvwMGbO5MwCiLZMW1WLT28TksLV-45d78,1291
4
- python_utils/compat.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- python_utils/containers.py,sha256=QBO0GNAJ-W_Ox0ElAjK49Rs3OtB3MAyV-Nq5dpALLKU,10802
6
- python_utils/converters.py,sha256=4yainms6NUWITUxq-ajJUenT5I0zN1G9XZlxpGsrUVc,11404
7
- python_utils/decorators.py,sha256=L6BdW94YdW4Wfgwa5aRlqYRpcAxq3Olo4mutexbKkuY,5390
8
- python_utils/exceptions.py,sha256=iUOxIz5OGPRJcqr6afgwuIZ-i_expS5UyMxpVs8WP3I,617
9
- python_utils/formatters.py,sha256=Qy7w3zua3e2UBfmkfKEMYhGBGbmYm1gZVnKjt-ZT1Wc,5015
10
- python_utils/generators.py,sha256=oux5qLeWdC0x8nHvF7CZ-IL7gU_gPRA06KYkvXpmWfY,2670
11
- python_utils/import_.py,sha256=5fTkTyX6UYVjy1Tc8R9adBmHZZEoX_ynET044B9OsPg,3165
12
- python_utils/logger.py,sha256=hnog07avWLkAIfMGbaP5ri_YAuv00psj-z73ha5zrH8,6371
13
- python_utils/loguru.py,sha256=jbAr6SVavrsH4nAicJstLtLTs50S7w5HWeWHzIOLw54,358
14
- python_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- python_utils/terminal.py,sha256=FbuYoWohCTllBheaTjOVvkbkj9gFbXF0-Zq4Z0updKA,4711
16
- python_utils/time.py,sha256=xc6D9MLy10tRWBof6koePIMNs7eRTOVauyitWwsOkXI,11304
17
- python_utils/types.py,sha256=75p6yXn771yfHHzbBWGrPCYmqqrme1beRurYWh4ltsg,3652
18
- python_utils-3.8.1.dist-info/LICENSE,sha256=_Zrs9NZu-G6_aZT2g3Jv5_JREJQfhRNBC61EH6WDH-o,1501
19
- python_utils-3.8.1.dist-info/METADATA,sha256=Txz6iPgUeWnC5HJWQuc6wwJCCBk3EBFrVvNs1jkQ54k,9650
20
- python_utils-3.8.1.dist-info/WHEEL,sha256=iYlv5fX357PQyRT2o6tw1bN-YcKFFHKqB_LwHO5wP-g,110
21
- python_utils-3.8.1.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
22
- python_utils-3.8.1.dist-info/RECORD,,