omdev 0.0.0.dev10__py3-none-any.whl → 0.0.0.dev12__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 omdev might be problematic. Click here for more details.

@@ -0,0 +1,105 @@
1
+ # !/usr/bin/env python3
2
+ """
3
+ https://github.com/umlet/pwk/blob/dc23b3400108a71947a695f1fa1df0f514b42528/pwk
4
+ """
5
+ import io
6
+ import tokenize
7
+
8
+
9
+ def translate_brace_python(
10
+ s: str,
11
+ *,
12
+ indent_width: int = 4,
13
+ ) -> str:
14
+ lt = tokenize.tokenize(io.BytesIO(s.encode('utf-8')).readline)
15
+
16
+ ret = io.StringIO()
17
+
18
+ indent = 0
19
+ open_braces = 0
20
+
21
+ newline = False
22
+ indent_up = False
23
+ indent_down = False
24
+ skip = False
25
+
26
+ while True:
27
+ try:
28
+ t = next(lt)
29
+
30
+ if t.type == tokenize.ENCODING:
31
+ last_t = t
32
+ continue
33
+
34
+ if t.type == tokenize.OP and t.string == ';':
35
+ newline = True
36
+
37
+ elif t.type == tokenize.OP and t.string == ':':
38
+ if open_braces == 0:
39
+ newline = True
40
+ indent_up = True
41
+
42
+ elif t.type == tokenize.OP and t.string == '{':
43
+ if last_t.type == tokenize.OP and last_t.string == ':': # noqa
44
+ skip = True
45
+ else:
46
+ open_braces += 1
47
+
48
+ elif t.type == tokenize.OP and t.string == '}':
49
+ if open_braces > 0:
50
+ open_braces -= 1
51
+ elif open_braces == 0:
52
+ if indent > 0:
53
+ newline = True
54
+ indent_down = True
55
+ skip = True
56
+ else:
57
+ raise Exception('Too many closing braces')
58
+
59
+ if indent_up:
60
+ indent += indent_width
61
+ elif indent_down:
62
+ indent -= indent_width
63
+
64
+ if newline and indent_up:
65
+ ret.write(':\n' + ' ' * indent)
66
+ elif newline:
67
+ ret.write('\n' + ' ' * indent)
68
+ elif not skip:
69
+ ret.write(t.string)
70
+ ret.write(' ')
71
+
72
+ newline = False
73
+ indent_up = False
74
+ indent_down = False
75
+ skip = False
76
+ last_t = t
77
+
78
+ except StopIteration:
79
+ break
80
+ except tokenize.TokenError:
81
+ continue
82
+
83
+ ret.write('\n')
84
+ return ret.getvalue()
85
+
86
+
87
+ def _main(argv=None) -> None:
88
+ import argparse
89
+
90
+ parser = argparse.ArgumentParser()
91
+ parser.add_argument('-x', '--exec', action='store_true')
92
+ parser.add_argument('cmd')
93
+
94
+ args = parser.parse_args(argv)
95
+
96
+ src = translate_brace_python(args.cmd)
97
+
98
+ if args.exec:
99
+ exec(src)
100
+ else:
101
+ print(src)
102
+
103
+
104
+ if __name__ == '__main__':
105
+ _main()
@@ -28,8 +28,11 @@ def find_magic(
28
28
  continue
29
29
 
30
30
  fp = os.path.join(dp, fn)
31
- with open(fp) as f:
32
- src = f.read()
31
+ try:
32
+ with open(fp) as f:
33
+ src = f.read()
34
+ except UnicodeDecodeError:
35
+ continue
33
36
 
34
37
  if not any(
35
38
  any(pat.fullmatch(l) for pat in pats)
omdev/scripts/interp.py CHANGED
@@ -1669,8 +1669,10 @@ class Pyenv:
1669
1669
  return os.path.join(check_not_none(self.root()), 'bin', 'pyenv')
1670
1670
 
1671
1671
  def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
1672
+ if (root := self.root()) is None:
1673
+ return []
1672
1674
  ret = []
1673
- vp = os.path.join(self.root(), 'versions')
1675
+ vp = os.path.join(root, 'versions')
1674
1676
  for dn in os.listdir(vp):
1675
1677
  ep = os.path.join(vp, dn, 'bin', 'python')
1676
1678
  if not os.path.isfile(ep):
@@ -1679,6 +1681,8 @@ class Pyenv:
1679
1681
  return ret
1680
1682
 
1681
1683
  def installable_versions(self) -> ta.List[str]:
1684
+ if self.root() is None:
1685
+ return []
1682
1686
  ret = []
1683
1687
  s = subprocess_check_output_str(self.exe(), 'install', '--list')
1684
1688
  for l in s.splitlines():
@@ -39,6 +39,7 @@ import inspect
39
39
  import itertools
40
40
  import json
41
41
  import logging
42
+ import multiprocessing as mp
42
43
  import os
43
44
  import os.path
44
45
  import re
@@ -3225,8 +3226,10 @@ class Pyenv:
3225
3226
  return os.path.join(check_not_none(self.root()), 'bin', 'pyenv')
3226
3227
 
3227
3228
  def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
3229
+ if (root := self.root()) is None:
3230
+ return []
3228
3231
  ret = []
3229
- vp = os.path.join(self.root(), 'versions')
3232
+ vp = os.path.join(root, 'versions')
3230
3233
  for dn in os.listdir(vp):
3231
3234
  ep = os.path.join(vp, dn, 'bin', 'python')
3232
3235
  if not os.path.isfile(ep):
@@ -3235,6 +3238,8 @@ class Pyenv:
3235
3238
  return ret
3236
3239
 
3237
3240
  def installable_versions(self) -> ta.List[str]:
3241
+ if self.root() is None:
3242
+ return []
3238
3243
  ret = []
3239
3244
  s = subprocess_check_output_str(self.exe(), 'install', '--list')
3240
3245
  for l in s.splitlines():
@@ -3895,13 +3900,12 @@ def _venv_cmd(args) -> None:
3895
3900
  )
3896
3901
  return
3897
3902
 
3898
- venv.create()
3899
-
3900
3903
  cmd = args.cmd
3901
3904
  if not cmd:
3902
- pass
3905
+ venv.create()
3903
3906
 
3904
3907
  elif cmd == 'python':
3908
+ venv.create()
3905
3909
  os.execl(
3906
3910
  (exe := venv.exe()),
3907
3911
  exe,
@@ -3909,10 +3913,12 @@ def _venv_cmd(args) -> None:
3909
3913
  )
3910
3914
 
3911
3915
  elif cmd == 'exe':
3916
+ venv.create()
3912
3917
  check_not(args.args)
3913
3918
  print(venv.exe())
3914
3919
 
3915
3920
  elif cmd == 'run':
3921
+ venv.create()
3916
3922
  sh = check_not_none(shutil.which('bash'))
3917
3923
  script = ' '.join(args.args)
3918
3924
  if not script:
@@ -3929,6 +3935,7 @@ def _venv_cmd(args) -> None:
3929
3935
  print('\n'.join(venv.srcs()))
3930
3936
 
3931
3937
  elif cmd == 'test':
3938
+ venv.create()
3932
3939
  subprocess_check_call(venv.exe(), '-m', 'pytest', *(args.args or []), *venv.srcs())
3933
3940
 
3934
3941
  else:
@@ -3954,11 +3961,10 @@ def _pkg_cmd(args) -> None:
3954
3961
  build_output_dir = 'dist'
3955
3962
  run_build = bool(args.build)
3956
3963
 
3957
- num_threads = 8
3958
-
3959
3964
  if run_build:
3960
3965
  os.makedirs(build_output_dir, exist_ok=True)
3961
3966
 
3967
+ num_threads = max(mp.cpu_count() // 2, 1)
3962
3968
  with cf.ThreadPoolExecutor(num_threads) as ex:
3963
3969
  futs = [
3964
3970
  ex.submit(functools.partial(
@@ -0,0 +1,22 @@
1
+ import subprocess
2
+
3
+ from omlish import argparse as ap
4
+ from omlish import logs
5
+
6
+
7
+ class Cli(ap.Cli):
8
+ @ap.command()
9
+ def blob_sizes(self) -> None:
10
+ # https://stackoverflow.com/a/42544963
11
+ subprocess.check_call( # noqa
12
+ "git rev-list --objects --all | "
13
+ "git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | "
14
+ "sed -n 's/^blob //p' | "
15
+ "sort --numeric-sort --key=2",
16
+ shell=True,
17
+ )
18
+
19
+
20
+ if __name__ == '__main__':
21
+ logs.configure_standard_logging('INFO')
22
+ Cli()()
@@ -0,0 +1,173 @@
1
+ """
2
+ TODO:
3
+ - omlish-lite, move to pyproject/
4
+ - vendor-lite wheel.wheelfile
5
+ """
6
+ # ruff: noqa: TCH003 UP006 UP007
7
+ # @omlish-lite
8
+ import argparse
9
+ import io
10
+ import os.path
11
+ import subprocess
12
+ import tarfile
13
+ import typing as ta
14
+ import zipfile
15
+
16
+ from omlish.lite.logs import configure_standard_logging
17
+ from omlish.lite.logs import log
18
+
19
+ from ..wheelfile import WheelFile
20
+
21
+
22
+ class RevisionAdder:
23
+ def __init__(
24
+ self,
25
+ revision: str,
26
+ output_suffix: ta.Optional[str] = None,
27
+ ) -> None:
28
+ super().__init__()
29
+ self._revision = revision
30
+ self._output_suffix = output_suffix
31
+
32
+ REVISION_ATTR = '__revision__'
33
+
34
+ def add_to_contents(self, dct: ta.Dict[str, bytes]) -> bool:
35
+ changed = False
36
+ for n in dct:
37
+ if not n.endswith('__about__.py'):
38
+ continue
39
+ src = dct[n].decode('utf-8')
40
+ lines = src.splitlines(keepends=True)
41
+ for i, l in enumerate(lines):
42
+ if l != f'{self.REVISION_ATTR} = None\n':
43
+ continue
44
+ lines[i] = f"{self.REVISION_ATTR} = '{self._revision}'\n"
45
+ changed = True
46
+ dct[n] = ''.join(lines).encode('utf-8')
47
+ return changed
48
+
49
+ def add_to_wheel(self, f: str) -> None:
50
+ if not f.endswith('.whl'):
51
+ raise Exception(f)
52
+ log.info('Scanning wheel %s', f)
53
+
54
+ zis: ta.Dict[str, zipfile.ZipInfo] = {}
55
+ dct: ta.Dict[str, bytes] = {}
56
+ with WheelFile(f) as wf:
57
+ for zi in wf.filelist:
58
+ if zi.filename == wf.record_path:
59
+ continue
60
+ zis[zi.filename] = zi
61
+ dct[zi.filename] = wf.read(zi.filename)
62
+
63
+ if self.add_to_contents(dct):
64
+ of = f[:-4] + (self._output_suffix or '') + '.whl'
65
+ log.info('Repacking wheel %s', of)
66
+ with WheelFile(of, 'w') as wf:
67
+ for n, d in dct.items():
68
+ log.info('Adding zipinfo %s', n)
69
+ wf.writestr(zis[n], d)
70
+
71
+ def add_to_tgz(self, f: str) -> None:
72
+ if not f.endswith('.tar.gz'):
73
+ raise Exception(f)
74
+ log.info('Scanning tgz %s', f)
75
+
76
+ tis: ta.Dict[str, tarfile.TarInfo] = {}
77
+ dct: ta.Dict[str, bytes] = {}
78
+ with tarfile.open(f, 'r:gz') as tf:
79
+ for ti in tf:
80
+ tis[ti.name] = ti
81
+ if ti.type == tarfile.REGTYPE:
82
+ with tf.extractfile(ti.name) as tif: # type: ignore
83
+ dct[ti.name] = tif.read()
84
+
85
+ if self.add_to_contents(dct):
86
+ of = f[:-7] + (self._output_suffix or '') + '.tar.gz'
87
+ log.info('Repacking tgz %s', of)
88
+ with tarfile.open(of, 'w:gz') as tf:
89
+ for n, ti in tis.items():
90
+ log.info('Adding tarinfo %s', n)
91
+ if n in dct:
92
+ data = dct[n]
93
+ ti.size = len(data)
94
+ fo = io.BytesIO(data)
95
+ else:
96
+ fo = None
97
+ tf.addfile(ti, fileobj=fo)
98
+
99
+ EXTS = ('.tar.gz', '.whl')
100
+
101
+ def add_to_file(self, f: str) -> None:
102
+ if f.endswith('.whl'):
103
+ self.add_to_wheel(f)
104
+
105
+ elif f.endswith('.tar.gz'):
106
+ self.add_to_tgz(f)
107
+
108
+ def add_to(self, tgt: str) -> None:
109
+ if os.path.isfile(tgt):
110
+ self.add_to_file(tgt)
111
+
112
+ elif os.path.isdir(tgt):
113
+ for dp, dns, fns in os.walk(tgt): # noqa
114
+ for f in fns:
115
+ if any(f.endswith(ext) for ext in self.EXTS):
116
+ self.add_to_file(os.path.join(dp, f))
117
+
118
+
119
+ #
120
+
121
+
122
+ def get_revision() -> str:
123
+ return subprocess.check_output([
124
+ 'git',
125
+ 'describe',
126
+ '--match=NeVeRmAtCh',
127
+ '--always',
128
+ '--abbrev=40',
129
+ '--dirty',
130
+ ]).decode().strip()
131
+
132
+
133
+ #
134
+
135
+
136
+ def _add_cmd(args) -> None:
137
+ if (revision := args.revision) is None:
138
+ revision = get_revision()
139
+ log.info('Using revision %s', revision)
140
+
141
+ if not args.targets:
142
+ raise Exception('must specify targets')
143
+
144
+ ra = RevisionAdder(
145
+ revision,
146
+ output_suffix=args.suffix,
147
+ )
148
+ for tgt in args.targets:
149
+ ra.add_to(tgt)
150
+
151
+
152
+ def _main(argv=None) -> None:
153
+ configure_standard_logging('INFO')
154
+
155
+ parser = argparse.ArgumentParser()
156
+
157
+ subparsers = parser.add_subparsers()
158
+
159
+ parser_add = subparsers.add_parser('add')
160
+ parser_add.add_argument('-r', '--revision')
161
+ parser_add.add_argument('-s', '--suffix')
162
+ parser_add.add_argument('targets', nargs='*')
163
+ parser_add.set_defaults(func=_add_cmd)
164
+
165
+ args = parser.parse_args(argv)
166
+ if not getattr(args, 'func', None):
167
+ parser.print_help()
168
+ else:
169
+ args.func(args)
170
+
171
+
172
+ if __name__ == '__main__':
173
+ _main()
omdev/wheelfile.py ADDED
@@ -0,0 +1,246 @@
1
+ # @omlish-lite
2
+ # ruff: noqa: UP006 UP007
3
+ # https://github.com/pypa/wheel/blob/7bb46d7727e6e89fe56b3c78297b3af2672bbbe2/src/wheel/wheelfile.py
4
+ # MIT License
5
+ #
6
+ # Copyright (c) 2012 Daniel Holth <dholth@fastmail.fm> and contributors
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
9
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
10
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
11
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
14
+ # Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
17
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ import base64
21
+ import csv
22
+ import hashlib
23
+ import io
24
+ import os.path
25
+ import re
26
+ import stat
27
+ import time
28
+ import typing as ta
29
+ import zipfile
30
+
31
+
32
+ class WheelError(Exception):
33
+ pass
34
+
35
+
36
+ # Non-greedy matching of an optional build number may be too clever (more invalid wheel filenames will match). Separate
37
+ # regex for .dist-info?
38
+ WHEEL_INFO_RE = re.compile(
39
+ r'^'
40
+ r'(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]+?))'
41
+ r'(-(?P<build>\d[^\s-]*))?-'
42
+ r'(?P<pyver>[^\s-]+?)-'
43
+ r'(?P<abi>[^\s-]+?)-'
44
+ r'(?P<plat>\S+)'
45
+ r'\.whl$',
46
+ re.VERBOSE,
47
+ )
48
+
49
+
50
+ class WheelFile(zipfile.ZipFile):
51
+ """
52
+ A ZipFile derivative class that also reads SHA-256 hashes from .dist-info/RECORD and checks any read files against
53
+ those.
54
+ """
55
+
56
+ _default_algorithm = hashlib.sha256
57
+
58
+ def __init__(
59
+ self,
60
+ file: str,
61
+ mode: str = 'r', # ta.Literal["r", "w", "x", "a"]
62
+ compression: int = zipfile.ZIP_DEFLATED,
63
+ ) -> None:
64
+ basename = os.path.basename(file)
65
+ self.parsed_filename = WHEEL_INFO_RE.match(basename)
66
+ if not basename.endswith('.whl') or self.parsed_filename is None:
67
+ raise WheelError(f'Bad wheel filename {basename!r}')
68
+
69
+ super().__init__( # type: ignore
70
+ file,
71
+ mode,
72
+ compression=compression,
73
+ allowZip64=True,
74
+ )
75
+
76
+ self.dist_info_path = '{}.dist-info'.format(self.parsed_filename.group('namever'))
77
+ self.record_path = self.dist_info_path + '/RECORD'
78
+ self._file_hashes: ta.Dict[str, ta.Union[ta.Tuple[None, None], ta.Tuple[int, bytes]]] = {}
79
+ self._file_sizes: ta.Dict[str, int] = {}
80
+
81
+ if mode == 'r':
82
+ # Ignore RECORD and any embedded wheel signatures
83
+ self._file_hashes[self.record_path] = None, None
84
+ self._file_hashes[self.record_path + '.jws'] = None, None
85
+ self._file_hashes[self.record_path + '.p7s'] = None, None
86
+
87
+ # Fill in the expected hashes by reading them from RECORD
88
+ try:
89
+ record = self.open(self.record_path)
90
+ except KeyError:
91
+ raise WheelError(f'Missing {self.record_path} file') from None
92
+
93
+ with record:
94
+ for line in csv.reader(io.TextIOWrapper(record, newline='', encoding='utf-8')):
95
+ path, hash_sum, size = line
96
+ if not hash_sum:
97
+ continue
98
+
99
+ algorithm, hash_sum = hash_sum.split('=')
100
+ try:
101
+ hashlib.new(algorithm)
102
+ except ValueError:
103
+ raise WheelError(f'Unsupported hash algorithm: {algorithm}') from None
104
+
105
+ if algorithm.lower() in {'md5', 'sha1'}:
106
+ raise WheelError(f'Weak hash algorithm ({algorithm}) is not permitted by PEP 427')
107
+
108
+ self._file_hashes[path] = ( # type: ignore
109
+ algorithm,
110
+ self._urlsafe_b64decode(hash_sum.encode('ascii')),
111
+ )
112
+
113
+ @staticmethod
114
+ def _urlsafe_b64encode(data: bytes) -> bytes:
115
+ """urlsafe_b64encode without padding"""
116
+ return base64.urlsafe_b64encode(data).rstrip(b'=')
117
+
118
+ @staticmethod
119
+ def _urlsafe_b64decode(data: bytes) -> bytes:
120
+ """urlsafe_b64decode without padding"""
121
+ pad = b'=' * (4 - (len(data) & 3))
122
+ return base64.urlsafe_b64decode(data + pad)
123
+
124
+ def open( # type: ignore # noqa
125
+ self,
126
+ name_or_info: ta.Union[str, zipfile.ZipInfo],
127
+ mode: str = 'r', # ta.Literal["r", "w"]
128
+ pwd: ta.Optional[bytes] = None,
129
+ ) -> ta.IO[bytes]:
130
+ def _update_crc(newdata: bytes) -> None:
131
+ eof = ef._eof # type: ignore # noqa
132
+ update_crc_orig(newdata)
133
+ running_hash.update(newdata)
134
+ if eof and running_hash.digest() != expected_hash:
135
+ raise WheelError(f"Hash mismatch for file '{ef_name}'")
136
+
137
+ ef_name = name_or_info.filename if isinstance(name_or_info, zipfile.ZipInfo) else name_or_info
138
+ if (
139
+ mode == 'r'
140
+ and not ef_name.endswith('/')
141
+ and ef_name not in self._file_hashes
142
+ ):
143
+ raise WheelError(f"No hash found for file '{ef_name}'")
144
+
145
+ ef = super().open(name_or_info, mode, pwd) # noqa
146
+ if mode == 'r' and not ef_name.endswith('/'):
147
+ algorithm, expected_hash = self._file_hashes[ef_name]
148
+ if expected_hash is not None:
149
+ # Monkey patch the _update_crc method to also check for the hash from RECORD
150
+ running_hash = hashlib.new(algorithm) # type: ignore
151
+ update_crc_orig, ef._update_crc = ef._update_crc, _update_crc # type: ignore # noqa
152
+
153
+ return ef
154
+
155
+ def write_files(self, base_dir: str) -> None:
156
+ deferred: list[tuple[str, str]] = []
157
+ for root, dirnames, filenames in os.walk(base_dir):
158
+ # Sort the directory names so that `os.walk` will walk them in a defined order on the next iteration.
159
+ dirnames.sort()
160
+ for name in sorted(filenames):
161
+ path = os.path.normpath(os.path.join(root, name))
162
+ if os.path.isfile(path):
163
+ arcname = os.path.relpath(path, base_dir).replace(os.path.sep, '/')
164
+ if arcname == self.record_path:
165
+ pass
166
+ elif root.endswith('.dist-info'):
167
+ deferred.append((path, arcname))
168
+ else:
169
+ self.write(path, arcname)
170
+
171
+ deferred.sort()
172
+ for path, arcname in deferred:
173
+ self.write(path, arcname)
174
+
175
+ def write( # type: ignore # noqa
176
+ self,
177
+ filename: str,
178
+ arcname: ta.Optional[str] = None,
179
+ compress_type: ta.Optional[int] = None,
180
+ ) -> None:
181
+ with open(filename, 'rb') as f:
182
+ st = os.fstat(f.fileno())
183
+ data = f.read()
184
+
185
+ zinfo = zipfile.ZipInfo(
186
+ arcname or filename,
187
+ date_time=self._get_zipinfo_datetime(st.st_mtime),
188
+ )
189
+ zinfo.external_attr = (stat.S_IMODE(st.st_mode) | stat.S_IFMT(st.st_mode)) << 16
190
+ zinfo.compress_type = compress_type or self.compression
191
+ self.writestr(zinfo, data, compress_type)
192
+
193
+ _MINIMUM_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC
194
+
195
+ @classmethod
196
+ def _get_zipinfo_datetime(cls, timestamp: ta.Optional[float] = None) -> ta.Any:
197
+ # Some applications need reproducible .whl files, but they can't do this without forcing the timestamp of the
198
+ # individual ZipInfo objects. See issue #143.
199
+ timestamp = int(os.environ.get('SOURCE_DATE_EPOCH', timestamp or time.time()))
200
+ timestamp = max(timestamp, cls._MINIMUM_TIMESTAMP)
201
+ return time.gmtime(timestamp)[0:6]
202
+
203
+ def writestr( # type: ignore # noqa
204
+ self,
205
+ zinfo_or_arcname: ta.Union[str, zipfile.ZipInfo],
206
+ data: ta.Any, # SizedBuffer | str,
207
+ compress_type: ta.Optional[int] = None,
208
+ ) -> None:
209
+ if isinstance(zinfo_or_arcname, str):
210
+ zinfo_or_arcname = zipfile.ZipInfo(
211
+ zinfo_or_arcname,
212
+ date_time=self._get_zipinfo_datetime(),
213
+ )
214
+ zinfo_or_arcname.compress_type = self.compression
215
+ zinfo_or_arcname.external_attr = (0o664 | stat.S_IFREG) << 16
216
+
217
+ if isinstance(data, str):
218
+ data = data.encode('utf-8')
219
+
220
+ super().writestr(zinfo_or_arcname, data, compress_type)
221
+ fname = (
222
+ zinfo_or_arcname.filename
223
+ if isinstance(zinfo_or_arcname, zipfile.ZipInfo)
224
+ else zinfo_or_arcname
225
+ )
226
+ if fname != self.record_path:
227
+ hash_ = self._default_algorithm(data) # type: ignore
228
+ self._file_hashes[fname] = ( # type: ignore
229
+ hash_.name,
230
+ self._urlsafe_b64encode(hash_.digest()).decode('ascii'),
231
+ )
232
+ self._file_sizes[fname] = len(data)
233
+
234
+ def close(self) -> None:
235
+ # Write RECORD
236
+ if self.fp is not None and self.mode == 'w' and self._file_hashes:
237
+ data = io.StringIO()
238
+ writer = csv.writer(data, delimiter=',', quotechar='"', lineterminator='\n')
239
+ writer.writerows((
240
+ (fname, algorithm + '=' + hash_, self._file_sizes[fname]) # type: ignore
241
+ for fname, (algorithm, hash_) in self._file_hashes.items()
242
+ ))
243
+ writer.writerow((format(self.record_path), '', ''))
244
+ self.writestr(self.record_path, data.getvalue())
245
+
246
+ super().close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omdev
3
- Version: 0.0.0.dev10
3
+ Version: 0.0.0.dev12
4
4
  Summary: omdev
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,13 +12,14 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omlish ==0.0.0.dev10
15
+ Requires-Dist: omlish ==0.0.0.dev12
16
16
  Provides-Extra: all
17
17
  Requires-Dist: pycparser >=2.22 ; extra == 'all'
18
18
  Requires-Dist: cffi >=1.17 ; extra == 'all'
19
19
  Requires-Dist: pcpp >=1.30 ; extra == 'all'
20
20
  Requires-Dist: mypy >=1.11 ; extra == 'all'
21
21
  Requires-Dist: tokenize-rt >=6 ; extra == 'all'
22
+ Requires-Dist: wheel >=0.44 ; extra == 'all'
22
23
  Provides-Extra: c
23
24
  Requires-Dist: pycparser >=2.22 ; extra == 'c'
24
25
  Requires-Dist: cffi >=1.17 ; extra == 'c'
@@ -27,4 +28,6 @@ Provides-Extra: mypy
27
28
  Requires-Dist: mypy >=1.11 ; extra == 'mypy'
28
29
  Provides-Extra: tokens
29
30
  Requires-Dist: tokenize-rt >=6 ; extra == 'tokens'
31
+ Provides-Extra: wheel
32
+ Requires-Dist: wheel >=0.44 ; extra == 'wheel'
30
33